diff --git a/cargo/Cargo.lock b/cargo/Cargo.lock index a87f55badf..0e7b096968 100644 --- a/cargo/Cargo.lock +++ b/cargo/Cargo.lock @@ -19,19 +19,19 @@ dependencies = [ "serde-untagged", "serde-value", "thiserror", - "toml", + "toml 0.8.23", "unicode-xid", "url", ] [[package]] name = "cargo_toml" -version = "0.20.5" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88da5a13c620b4ca0078845707ea9c3faf11edbc3ffd8497d11d686211cd1ac0" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml", + "toml 0.9.12+spec-1.1.0", ] [[package]] @@ -48,7 +48,7 @@ dependencies = [ "cargo-util-schemas", "pathdiff", "semver", - "toml", + "toml 0.8.23", ] [[package]] @@ -346,6 +346,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -417,11 +426,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_edit", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.13", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -431,6 +455,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -439,10 +472,19 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.13", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.1", ] [[package]] @@ -451,6 +493,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "typeid" version = "1.0.3" @@ -496,6 +544,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" + [[package]] name = "writeable" version = "0.6.2" diff --git a/rust/private/rust.bzl b/rust/private/rust.bzl index 729e10af3c..35fb01056f 100644 --- a/rust/private/rust.bzl +++ b/rust/private/rust.bzl @@ -684,6 +684,9 @@ RUSTC_ATTRS = { "_rustc_output_diagnostics": attr.label( default = Label("//rust/settings:rustc_output_diagnostics"), ), + "_experimental_rustc_incremental": attr.label( + default = Label("//rust/settings:experimental_rustc_incremental"), + ), } _COMMON_ATTRS = { diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index 308efed899..e3fd9b2a5e 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -934,7 +934,8 @@ def construct_arguments( skip_expanding_rustc_env = False, require_explicit_unstable_features = False, always_use_param_file = False, - error_format = None): + error_format = None, + incremental_cache_base = None): """Builds an Args object containing common rustc flags Args: @@ -1000,6 +1001,9 @@ def construct_arguments( if require_explicit_unstable_features: process_wrapper_flags.add("--require-explicit-unstable-features", "true") + if incremental_cache_base: + process_wrapper_flags.add("--inject-incremental-cache", incremental_cache_base) + # Certain rust build processes expect to find files from the environment # variable `$CARGO_MANIFEST_DIR`. Examples of this include pest, tera, # asakuma. @@ -1437,6 +1441,18 @@ def rustc_compile_action( elif ctx.attr.require_explicit_unstable_features == -1: require_explicit_unstable_features = toolchain.require_explicit_unstable_features + # Check if incremental compilation is enabled. process_wrapper injects + # -Cincremental=/.rustc_incremental_cache/... when this is on. + # Excluded for cc_common.link (emits .o, incompatible) and when there is + # no process_wrapper (bootstrap rules). + use_incremental = ( + hasattr(ctx.attr, "_experimental_rustc_incremental") and + ctx.attr._experimental_rustc_incremental[BuildSettingInfo].value and + not experimental_use_cc_common_link and + ctx.executable._process_wrapper != None + ) + incremental_cache_base = ".rustc_incremental_cache" if use_incremental else None + args, env_from_args = construct_arguments( ctx = ctx, attr = attr, @@ -1461,6 +1477,7 @@ def rustc_compile_action( skip_expanding_rustc_env = skip_expanding_rustc_env, require_explicit_unstable_features = require_explicit_unstable_features, always_use_param_file = not ctx.executable._process_wrapper, + incremental_cache_base = incremental_cache_base, ) args_metadata = None @@ -1488,6 +1505,7 @@ def rustc_compile_action( use_json_output = True, build_metadata = True, require_explicit_unstable_features = require_explicit_unstable_features, + incremental_cache_base = incremental_cache_base, ) env = dict(ctx.configuration.default_shell_env) @@ -1551,7 +1569,7 @@ def rustc_compile_action( action_outputs.append(dsym_folder) if ctx.executable._process_wrapper: - # Run as normal + incr_tag = " [incr]" if use_incremental else "" ctx.actions.run( executable = ctx.executable._process_wrapper, inputs = compile_inputs, @@ -1559,11 +1577,12 @@ def rustc_compile_action( env = env, arguments = args.all, mnemonic = "Rustc", - progress_message = "Compiling Rust {} %{{label}}{} ({} file{})".format( + progress_message = "Compiling Rust {} %{{label}}{} ({} file{}){}".format( crate_info.type, formatted_version, len(srcs), "" if len(srcs) == 1 else "s", + incr_tag, ), toolchain = "@rules_rust//rust:toolchain_type", resource_set = get_rustc_resource_set(toolchain), @@ -1576,11 +1595,12 @@ def rustc_compile_action( env = env, arguments = args_metadata.all, mnemonic = "RustcMetadata", - progress_message = "Compiling Rust metadata {} %{{label}}{} ({} file{})".format( + progress_message = "Compiling Rust metadata {} %{{label}}{} ({} file{}){}".format( crate_info.type, formatted_version, len(srcs), "" if len(srcs) == 1 else "s", + incr_tag, ), toolchain = "@rules_rust//rust:toolchain_type", ) diff --git a/rust/settings/BUILD.bazel b/rust/settings/BUILD.bazel index 6f0cd9dbca..cd83c53ca2 100644 --- a/rust/settings/BUILD.bazel +++ b/rust/settings/BUILD.bazel @@ -14,6 +14,7 @@ load( "error_format", "experimental_link_std_dylib", "experimental_per_crate_rustc_flag", + "experimental_rustc_incremental", "experimental_use_allocator_libraries_with_mangled_symbols", "experimental_use_cc_common_link", "experimental_use_coverage_metadata_files", @@ -85,6 +86,8 @@ experimental_link_std_dylib() experimental_per_crate_rustc_flag() +experimental_rustc_incremental() + experimental_use_cc_common_link() experimental_use_coverage_metadata_files() diff --git a/rust/settings/settings.bzl b/rust/settings/settings.bzl index bb232bebe2..0c6a900e31 100644 --- a/rust/settings/settings.bzl +++ b/rust/settings/settings.bzl @@ -540,6 +540,77 @@ def incompatible_do_not_include_transitive_data_in_compile_inputs(): issue = "https://github.com/bazelbuild/rules_rust/issues/3915", ) +def experimental_rustc_incremental(): + """Enable rustc incremental compilation via process_wrapper injection. + + When enabled, `process_wrapper` injects `-Cincremental=` into every + rustc invocation. The cache directory lives under the Bazel output_base at + `/.rustc_incremental_cache/`, so state persists across builds + and is shared across all concurrent compilations. + + This is **non-hermetic** and **local-only**. Rustc's emitted artifacts may + vary with cache state, the cache directory is an undeclared input/output + that Bazel cannot track, and incremental output is not bit-identical to a + non-incremental compile of the same sources even for deterministic crates + (rustc raises the default `-Ccodegen-units` to 256 in incremental mode, + which changes CGU partitioning and therefore codegen). Release builds that + are sensitive to runtime performance should not enable this. + + Incompatible with `experimental_use_cc_common_link` (the object-emitting + link path is incompatible with `-Cincremental`). + + ## Recommended .bazelrc snippet + + ``` + # --config=dev-inc: local-only incremental rebuilds via rustc -Cincremental + # NOT safe for CI or remote — produces non-hermetic, perf-degraded outputs + build:dev-inc --@rules_rust//rust/settings:experimental_rustc_incremental=true + build:dev-inc --strategy=Rustc=local + build:dev-inc --strategy=RustcMetadata=local + build:dev-inc --remote_upload_local_results=false + ``` + + Then invoke: `bazel build --config=dev-inc //some:target`. + + Why each flag matters: + + - `experimental_rustc_incremental=true` turns on `-Cincremental` injection. + Because this is a `bool_flag`, toggling it produces a distinct + configuration hash, so the Bazel disk cache does not mix incremental and + non-incremental outputs and can stay enabled. + - `--strategy=Rustc=local` / `--strategy=RustcMetadata=local` are required + because the cache lives outside any sandbox; sandboxed strategies would + block writes. Both mnemonics needed when pipelined compilation is on. + - `--remote_upload_local_results=false` stops non-hermetic rlibs from + poisoning a shared remote cache. Remote reads are still fine. + + Works transparently alongside `pipelined_compilation=hollow_rlib`. + + ## Runtime-perf caveat + + `-Cincremental` raises the default `-Ccodegen-units` from 16 to 256. LLVM + loses cross-CGU inlining opportunities, and ThinLTO can only partially + recover them. For opt builds this can materially slow the resulting binary + — stick to `--config=dev-inc` on dev machines and keep CI/release builds + on the non-incremental path. + + ## Cache eviction + + The cache grows unbounded. There is no automatic eviction today. To clear + it manually: + + ``` + rm -rf "$(bazel info output_base)/.rustc_incremental_cache" + ``` + + `bazel clean --expunge` also removes it (by removing the whole output_base) + but is heavier than necessary if that is all you want to drop. + """ + bool_flag( + name = "experimental_rustc_incremental", + build_setting_default = False, + ) + def codegen_units(): """The default value for `--codegen-units` which also affects resource allocation for rustc actions. diff --git a/util/process_wrapper/incremental_cache.rs b/util/process_wrapper/incremental_cache.rs new file mode 100644 index 0000000000..e3f68b0515 --- /dev/null +++ b/util/process_wrapper/incremental_cache.rs @@ -0,0 +1,411 @@ +// Copyright 2026 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Cache-path computation and injection for `-Cincremental`. +//! +//! Given a base directory (typically `.rustc_incremental_cache` relative to the +//! Bazel output_base) and a set of rustc arguments, compute a deterministic +//! cache directory keyed on compilation-relevant inputs. A relative base is +//! resolved against the Bazel output_base when possible so that every action +//! converges on the same shared cache. +//! +//! Partition dimensions: +//! - rustc binary path (identity) +//! - target triple +//! - crate name +//! - edition +//! - action kind (full vs metadata, the latter detected via `-Zno-codegen`) +//! - stable hash of compilation-relevant flags + +use std::collections::{hash_map::DefaultHasher, HashSet}; +use std::hash::{Hash, Hasher}; +use std::path::{Path, PathBuf}; + +/// Flags whose trailing value is an output path or diagnostic formatting knob. +/// Neither the flag nor its value contributes to the cache key. +const SKIP_WITH_VALUE: &[&str] = &[ + "-o", + "--out-dir", + "--error-format", + "--json", + "--color", + "--remap-path-prefix", +]; + +/// `SKIP_WITH_VALUE` entries with a trailing `=`, pre-joined so the per-arg +/// loop can do a cheap `starts_with` without allocating a new `String`. +const SKIP_WITH_VALUE_EQ: &[&str] = &[ + "-o=", + "--out-dir=", + "--error-format=", + "--json=", + "--color=", + "--remap-path-prefix=", +]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ActionKind { + Full, + Metadata, +} + +#[derive(Debug)] +pub(crate) struct CacheKey { + pub(crate) rustc_path: String, + pub(crate) target_triple: String, + pub(crate) crate_name: String, + pub(crate) edition: String, + pub(crate) action_kind: ActionKind, + pub(crate) args_hash: u64, +} + +/// Hash a single string with the same hasher we use for the args stream, so +/// callers that want a short directory-friendly digest of a path get something +/// consistent with the rest of the module. +fn stable_hash(s: &str) -> u64 { + let mut h = DefaultHasher::new(); + s.hash(&mut h); + h.finish() +} + +impl CacheKey { + /// Extract a cache key from the rustc executable path and rustc arguments. + pub(crate) fn from_rustc_args(rustc_path: &str, args: &[&str]) -> Self { + let mut target_triple = String::new(); + let mut crate_name = String::new(); + let mut edition = String::new(); + let mut action_kind = ActionKind::Full; + + let mut hasher = DefaultHasher::new(); + let mut skip_next = false; + + for (i, arg) in args.iter().enumerate() { + if skip_next { + skip_next = false; + continue; + } + + if *arg == "--target" { + if let Some(val) = args.get(i + 1) { + target_triple = (*val).to_string(); + } + } else if let Some(val) = arg.strip_prefix("--target=") { + target_triple = val.to_string(); + } else if *arg == "--crate-name" { + if let Some(val) = args.get(i + 1) { + crate_name = (*val).to_string(); + } + } else if let Some(val) = arg.strip_prefix("--crate-name=") { + crate_name = val.to_string(); + } else if *arg == "--edition" { + if let Some(val) = args.get(i + 1) { + edition = (*val).to_string(); + } + } else if let Some(val) = arg.strip_prefix("--edition=") { + edition = val.to_string(); + } else if *arg == "-Zno-codegen" { + action_kind = ActionKind::Metadata; + } + + if SKIP_WITH_VALUE.contains(arg) { + skip_next = true; + continue; + } + if SKIP_WITH_VALUE_EQ.iter().any(|p| arg.starts_with(p)) { + continue; + } + // Source file positional args are excluded: Bazel's action inputs + // already capture them and including their paths destabilises the + // key across workspace moves. + if arg.ends_with(".rs") && !arg.starts_with('-') { + continue; + } + + arg.hash(&mut hasher); + } + + CacheKey { + rustc_path: rustc_path.to_string(), + target_triple, + crate_name, + edition, + action_kind, + args_hash: hasher.finish(), + } + } + + /// Compute the full cache directory under `base`. + /// + /// Layout: `///--//` + pub(crate) fn cache_dir(&self, base: impl AsRef) -> PathBuf { + let rustc_hash = stable_hash(&self.rustc_path); + + let kind_str = match self.action_kind { + ActionKind::Full => "full", + ActionKind::Metadata => "meta", + }; + let target = if self.target_triple.is_empty() { + "host" + } else { + &self.target_triple + }; + let crate_part = if self.crate_name.is_empty() { + "unknown" + } else { + &self.crate_name + }; + let edition_part = if self.edition.is_empty() { + "default" + } else { + &self.edition + }; + + base.as_ref() + .join(format!("{rustc_hash:016x}")) + .join(target) + .join(format!("{crate_part}-{edition_part}-{kind_str}")) + .join(format!("{:016x}", self.args_hash)) + } +} + +/// Resolve a `--inject-incremental-cache` argument to a stable absolute path. +/// +/// Absolute paths pass through unchanged. Relative paths are resolved against +/// the Bazel `output_base`, detected by walking up `cwd` for the first +/// ancestor containing Bazel's `DO_NOT_BUILD_HERE` sentinel file. Every +/// action in the workspace resolves to the same cache location regardless of +/// strategy or cwd. +pub(crate) fn resolve_cache_base(arg: &str) -> PathBuf { + let path = PathBuf::from(arg); + if path.is_absolute() { + return path; + } + let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + if let Some(output_base) = find_output_base(&cwd) { + return output_base.join(arg); + } + cwd.join(arg) +} + +/// Bazel writes `DO_NOT_BUILD_HERE` into every output_base as a sentinel that +/// prevents users from accidentally treating it as a workspace. Its presence +/// is a reliable, Bazel-maintained signal that a directory is an output_base. +fn find_output_base(start: &Path) -> Option { + let mut cur: Option<&Path> = Some(start); + while let Some(d) = cur { + if d.join("DO_NOT_BUILD_HERE").is_file() { + return Some(d.to_path_buf()); + } + cur = d.parent(); + } + None +} + +/// Compute the cache directory for a rustc invocation without creating it. +/// +/// `@argfile` references in `rustc_args` are expanded by reading from disk, +/// so fields like `--crate-name`/`--target`/`--edition` are discovered even +/// when `process_wrapper` has moved them into a param file. A missing argfile +/// is skipped rather than failing the build; the key is computed from whatever +/// args were visible inline. +pub(crate) fn incremental_cache_dir( + rustc_path: &str, + rustc_args: &[&str], + base: &Path, +) -> PathBuf { + let expanded = expand_argfiles(rustc_args); + let refs: Vec<&str> = expanded.iter().map(String::as_str).collect(); + let key = CacheKey::from_rustc_args(rustc_path, &refs); + key.cache_dir(base) +} + +/// Recursively expand `@path` argfile references into their file contents. +/// Missing or unreadable files are dropped; Bazel itself surfaces those errors +/// at action execution time. Visited argfile paths are tracked so self- or +/// mutually-referencing argfiles terminate rather than loop forever. +fn expand_argfiles(args: &[&str]) -> Vec { + let mut out = Vec::with_capacity(args.len()); + let mut visited = HashSet::new(); + for arg in args { + if let Some(path) = arg.strip_prefix('@') { + expand_one(path, &mut out, &mut visited); + } else { + out.push((*arg).to_string()); + } + } + out +} + +fn expand_one(path: &str, out: &mut Vec, visited: &mut HashSet) { + let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| PathBuf::from(path)); + if !visited.insert(canonical) { + return; + } + let Ok(content) = std::fs::read_to_string(path) else { + return; + }; + for line in content.lines() { + if line.is_empty() { + continue; + } + if let Some(nested) = line.strip_prefix('@') { + expand_one(nested, out, visited); + } else { + out.push(line.to_string()); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn identical_args_produce_identical_keys() { + let args = &[ + "--crate-name", + "foo", + "--edition=2021", + "--target", + "x86_64-unknown-linux-gnu", + "-C", + "opt-level=2", + ]; + let k1 = CacheKey::from_rustc_args("/usr/bin/rustc", args); + let k2 = CacheKey::from_rustc_args("/usr/bin/rustc", args); + assert_eq!(k1.args_hash, k2.args_hash); + assert_eq!(k1.crate_name, "foo"); + assert_eq!(k1.edition, "2021"); + assert_eq!(k1.target_triple, "x86_64-unknown-linux-gnu"); + assert_eq!(k1.action_kind, ActionKind::Full); + } + + #[test] + fn output_path_and_error_format_excluded_from_hash() { + let a = &[ + "--crate-name", + "foo", + "-o", + "/out/a", + "--error-format", + "json", + ]; + let b = &[ + "--crate-name", + "foo", + "-o", + "/out/b", + "--error-format", + "human", + ]; + let k1 = CacheKey::from_rustc_args("/usr/bin/rustc", a); + let k2 = CacheKey::from_rustc_args("/usr/bin/rustc", b); + assert_eq!(k1.args_hash, k2.args_hash); + } + + #[test] + fn source_file_excluded_from_hash() { + let a = &["--crate-name", "foo", "src/lib.rs"]; + let b = &["--crate-name", "foo", "other/lib.rs"]; + let k1 = CacheKey::from_rustc_args("/usr/bin/rustc", a); + let k2 = CacheKey::from_rustc_args("/usr/bin/rustc", b); + assert_eq!(k1.args_hash, k2.args_hash); + } + + #[test] + fn no_codegen_detected_as_metadata() { + let args = &["--crate-name", "foo", "-Zno-codegen"]; + let k = CacheKey::from_rustc_args("/usr/bin/rustc", args); + assert_eq!(k.action_kind, ActionKind::Metadata); + } + + #[test] + fn metadata_and_full_get_separate_dirs() { + let full = CacheKey::from_rustc_args("/usr/bin/rustc", &["--crate-name", "foo"]); + let meta = + CacheKey::from_rustc_args("/usr/bin/rustc", &["--crate-name", "foo", "-Zno-codegen"]); + assert_ne!(full.cache_dir("/tmp"), meta.cache_dir("/tmp")); + } + + #[test] + fn resolve_cache_base_passes_absolute_through() { + let abs = if cfg!(windows) { + r"C:\abs\path" + } else { + "/abs/path" + }; + assert_eq!(resolve_cache_base(abs), PathBuf::from(abs)); + } + + #[test] + fn find_output_base_detects_bazel_sentinel() { + let root = std::env::temp_dir().join(format!( + "pw_ob_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let ob = root.join("hash"); + std::fs::create_dir_all(&ob).unwrap(); + std::fs::write(ob.join("DO_NOT_BUILD_HERE"), "").unwrap(); + let cwd = ob.join("execroot").join("_main"); + std::fs::create_dir_all(&cwd).unwrap(); + assert_eq!(find_output_base(&cwd), Some(ob)); + let _ = std::fs::remove_dir_all(&root); + } + + #[test] + fn expand_argfiles_terminates_on_cycle() { + let tmp = std::env::temp_dir().join(format!( + "pw_cycle_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&tmp).unwrap(); + let a = tmp.join("a"); + let b = tmp.join("b"); + std::fs::write(&a, format!("@{}", b.display())).unwrap(); + std::fs::write(&b, format!("@{}", a.display())).unwrap(); + let arg = format!("@{}", a.display()); + let out = expand_argfiles(&[arg.as_str()]); + assert!(out.is_empty(), "cycle should produce no literal args"); + let _ = std::fs::remove_dir_all(&tmp); + } + + #[test] + fn expand_argfiles_reads_nested_file() { + let tmp = std::env::temp_dir().join(format!( + "pw_nest_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&tmp).unwrap(); + let inner = tmp.join("inner"); + let outer = tmp.join("outer"); + std::fs::write(&inner, "--crate-name\nfoo\n").unwrap(); + std::fs::write(&outer, format!("@{}\n--edition=2021\n", inner.display())).unwrap(); + let arg = format!("@{}", outer.display()); + let out = expand_argfiles(&[arg.as_str()]); + assert_eq!(out, vec!["--crate-name", "foo", "--edition=2021"]); + let _ = std::fs::remove_dir_all(&tmp); + } +} diff --git a/util/process_wrapper/main.rs b/util/process_wrapper/main.rs index 8b10955464..0b0331ba03 100644 --- a/util/process_wrapper/main.rs +++ b/util/process_wrapper/main.rs @@ -13,6 +13,7 @@ // limitations under the License. mod flags; +mod incremental_cache; mod options; mod output; mod rustc; diff --git a/util/process_wrapper/options.rs b/util/process_wrapper/options.rs index de0c66dc52..04feb79326 100644 --- a/util/process_wrapper/options.rs +++ b/util/process_wrapper/options.rs @@ -6,6 +6,7 @@ use std::io::{self, Write}; use std::process::exit; use crate::flags::{FlagParseError, Flags, ParseOutcome}; +use crate::incremental_cache; use crate::rustc; use crate::util::*; @@ -64,6 +65,7 @@ pub(crate) fn options() -> Result { let mut rustc_output_format_raw = None; let mut flags = Flags::new(); let mut require_explicit_unstable_features = None; + let mut inject_incremental_cache = None; flags.define_repeated_flag("--subst", "", &mut subst_mapping_raw); flags.define_flag("--stable-status-file", "", &mut stable_status_file_raw); flags.define_flag("--volatile-status-file", "", &mut volatile_status_file_raw); @@ -109,6 +111,13 @@ pub(crate) fn options() -> Result { other -Zallow-features= is present in the rustc flags.", &mut require_explicit_unstable_features, ); + flags.define_flag( + "--inject-incremental-cache", + "If set, computes a cache directory under the given base (relative paths are resolved \ + against the Bazel output_base) and injects -Cincremental= into the child command. \ + Intended for local-only builds; produces non-hermetic outputs.", + &mut inject_incremental_cache, + ); let mut child_args = match flags .parse(env::args().collect()) @@ -237,9 +246,23 @@ pub(crate) fn options() -> Result { ) })?; + let mut child_arguments: Vec = args.to_vec(); + if let Some(base_arg) = inject_incremental_cache { + let base = incremental_cache::resolve_cache_base(&base_arg); + let arg_refs: Vec<&str> = child_arguments.iter().map(String::as_str).collect(); + let dir = incremental_cache::incremental_cache_dir(exec_path, &arg_refs, &base); + std::fs::create_dir_all(&dir).map_err(|e| { + OptionError::Generic(format!( + "failed to create incremental cache directory {}: {e}", + dir.display() + )) + })?; + child_arguments.push(format!("-Cincremental={}", dir.display())); + } + Ok(Options { executable: exec_path.to_owned(), - child_arguments: args.to_vec(), + child_arguments, child_environment: vars, touch_file, copy_output,