From 32ebb6887161f968886418ecb7dbc31b80cc1dc0 Mon Sep 17 00:00:00 2001 From: rejuvenile <2027618+rejuvenile@users.noreply.github.com> Date: Tue, 24 Mar 2026 15:48:15 -0700 Subject: [PATCH] Add incremental Rust compilation support via Bazel-managed tree artifacts Rustc's incremental compilation cache is stored as Bazel-managed tree artifacts so the path is identical on local and remote workers. The process wrapper seeds the cache from previous builds using hardlinks (with EXDEV fallback), with a configurable size threshold to skip small crates where loading overhead exceeds benefit. Key changes: - rustc.bzl: Declare tree artifact outputs for incremental state, gate on workspace crates, separate main/metadata artifacts - rust.bzl/settings.bzl: Add incremental and seed_threshold_mb settings - process_wrapper: Add --copy-seed, --seed-prev-dir, --seed-min-mb, --write-unused-inputs, --exit-early flags for cache management - Parallel hardlinking for trees with >256 files --- rust/private/rust.bzl | 6 ++ rust/private/rustc.bzl | 118 +++++++++++++++++++-- rust/settings/BUILD.bazel | 6 ++ rust/settings/settings.bzl | 38 +++++++ util/process_wrapper/main.rs | 179 +++++++++++++++++++++++++++++++- util/process_wrapper/options.rs | 128 +++++++++++++++++++++-- 6 files changed, 458 insertions(+), 17 deletions(-) diff --git a/rust/private/rust.bzl b/rust/private/rust.bzl index d50abbf180..d5d6d1574a 100644 --- a/rust/private/rust.bzl +++ b/rust/private/rust.bzl @@ -621,6 +621,12 @@ RUSTC_ATTRS = { "_is_proc_macro_dep": attr.label( default = Label("//rust/private:is_proc_macro_dep"), ), + "_incremental": attr.label( + default = Label("//rust/settings:incremental"), + ), + "_incremental_seed_threshold_mb": attr.label( + default = Label("//rust/settings:incremental_seed_threshold_mb"), + ), "_is_proc_macro_dep_enabled": attr.label( default = Label("//rust/private:is_proc_macro_dep_enabled"), ), diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index 34e19bd4d1..5b5aebd4b5 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -882,6 +882,27 @@ def _will_emit_object_file(emit): def _remove_codegen_units(flag): return None if flag.startswith("-Ccodegen-units") else flag +def _should_enable_incremental(ctx): + """Check if incremental should be enabled (workspace targets only, not opt). + + Args: + ctx (ctx): The rule's context object. + + Returns: + bool: True if incremental compilation should be enabled. + """ + if is_exec_configuration(ctx): # proc-macros, build scripts + return False + if not hasattr(ctx.attr, "_incremental"): + return False + if not ctx.attr._incremental[BuildSettingInfo].value: + return False + if ctx.var["COMPILATION_MODE"] == "opt": + return False + if ctx.label.workspace_root: + return False # External deps only; workspace targets have empty root + return True + def construct_arguments( *, ctx, @@ -911,7 +932,8 @@ def construct_arguments( force_depend_on_objects = False, skip_expanding_rustc_env = False, require_explicit_unstable_features = False, - error_format = None): + error_format = None, + incremental = None): """Builds an Args object containing common rustc flags Args: @@ -945,6 +967,7 @@ def construct_arguments( skip_expanding_rustc_env (bool): Whether to skip expanding CrateInfo.rustc_env_attr require_explicit_unstable_features (bool): Whether to require all unstable features to be explicitly opted in to using `-Zallow-features=...`. error_format (str, optional): Error format to pass to the `--error-format` command line argument. If set to None, uses the "_error_format" entry in `attr`. + incremental (File, optional): Tree artifact for incremental compilation cache. If set, adds -Cincremental= to rustc flags. Returns: tuple: A tuple of the following items @@ -1072,6 +1095,13 @@ def construct_arguments( rustc_flags.add(compilation_mode.debug_info, format = "--codegen=debuginfo=%s") rustc_flags.add(compilation_mode.strip_level, format = "--codegen=strip=%s") + # Incremental compilation via tree artifact. Both main and metadata + # actions MUST use -Cincremental (SVH encodes incremental-specific + # options beyond codegen-units). Bazel creates tree artifact dirs + # automatically before the action runs. + if incremental: + rustc_flags.add_all([incremental], format_each = "-Cincremental=%s", expand_directories = False) + # For determinism to help with build distribution and such if remap_path_prefix != None: rustc_flags.add("--remap-path-prefix=${{pwd}}={}".format(remap_path_prefix)) @@ -1233,6 +1263,7 @@ def construct_arguments( rustc_path = rustc_path, rustc_flags = rustc_flags, all = [process_wrapper_flags, rustc_path, rustc_flags], + incremental_enabled = incremental != None, ) return args, env @@ -1400,12 +1431,52 @@ def rustc_compile_action( elif ctx.attr.require_explicit_unstable_features == -1: require_explicit_unstable_features = toolchain.require_explicit_unstable_features + # Declare tree artifact outputs for incremental compilation cache. + # Making the incremental state a Bazel-managed tree artifact means: + # 1. The path is identical on local and remote workers + # 2. Bazel distributes the cache via CAS for consistent state + # 3. Sandboxing and remote execution work normally + # Both main and metadata actions need -Cincremental for matching SVH, but use + # SEPARATE tree artifacts so --rustc-quit-on-rmeta doesn't corrupt the main cache. + incr_enabled = _should_enable_incremental(ctx) + incr_tree = None + incr_metadata_tree = None + incr_seed = None + incr_unused_inputs = None + seed_threshold_mb = 20 + if incr_enabled: + safe_name = ctx.label.name.replace("::", "__").replace("/", "_") + incr_tree = ctx.actions.declare_directory(safe_name + "-incr", sibling = crate_info.output) + if build_metadata: + incr_metadata_tree = ctx.actions.declare_directory(safe_name + "-incr-metadata", sibling = crate_info.output) + + # Seed action: copy previous build's incr state into a tree artifact + # using process_wrapper (with parallel hardlinks and size threshold). + incr_seed = ctx.actions.declare_directory(safe_name + "-incr-seed", sibling = crate_info.output) + incr_unused_inputs = ctx.actions.declare_file(safe_name + "-incr-unused-inputs.txt", sibling = crate_info.output) + prev_incr_dir = "{}/{}/{}-incr".format(ctx.bin_dir.path, ctx.label.package, safe_name) + seed_threshold_mb = ctx.attr._incremental_seed_threshold_mb[BuildSettingInfo].value + seed_pw_args = ctx.actions.args() + seed_pw_args.add("--seed-prev-dir", prev_incr_dir) + seed_pw_args.add(incr_seed.path) + seed_pw_args.add("--seed-min-mb", str(seed_threshold_mb)) + seed_pw_args.add("--exit-early", "true") + ctx.actions.run( + executable = ctx.executable._process_wrapper, + inputs = crate_info.srcs, + outputs = [incr_seed], + arguments = [seed_pw_args], + execution_requirements = {"no-cache": "1", "no-remote": "1", "no-sandbox": "1"}, + mnemonic = "RustcIncrSeed", + progress_message = "Capturing incremental seed for {}".format(ctx.label.name), + ) + args, env_from_args = construct_arguments( ctx = ctx, attr = attr, file = ctx.file, toolchain = toolchain, - tool_path = toolchain.rustc.path, + tool_path = toolchain.system_rustc_path if toolchain.system_rustc_path else toolchain.rustc.path, cc_toolchain = cc_toolchain, emit = emit, feature_configuration = feature_configuration, @@ -1423,6 +1494,7 @@ def rustc_compile_action( use_json_output = bool(build_metadata) or bool(rustc_output) or bool(rustc_rmeta_output), skip_expanding_rustc_env = skip_expanding_rustc_env, require_explicit_unstable_features = require_explicit_unstable_features, + incremental = incr_tree, ) args_metadata = None @@ -1432,7 +1504,7 @@ def rustc_compile_action( attr = attr, file = ctx.file, toolchain = toolchain, - tool_path = toolchain.rustc.path, + tool_path = toolchain.system_rustc_path if toolchain.system_rustc_path else toolchain.rustc.path, cc_toolchain = cc_toolchain, emit = emit, feature_configuration = feature_configuration, @@ -1450,6 +1522,7 @@ def rustc_compile_action( use_json_output = True, build_metadata = True, require_explicit_unstable_features = require_explicit_unstable_features, + incremental = incr_metadata_tree, ) env = dict(ctx.configuration.default_shell_env) @@ -1490,6 +1563,10 @@ def rustc_compile_action( action_outputs = list(outputs) if rustc_output: action_outputs.append(rustc_output) + if incr_tree: + action_outputs.append(incr_tree) + if incr_unused_inputs: + action_outputs.append(incr_unused_inputs) # Get the compilation mode for the current target. compilation_mode = get_compilation_mode_opts(ctx, toolchain) @@ -1506,16 +1583,33 @@ def rustc_compile_action( dsym_folder = ctx.actions.declare_directory(crate_info.output.basename + ".dSYM", sibling = crate_info.output) action_outputs.append(dsym_folder) + # Use RustcLink mnemonic for crate types that involve linking (bin, cdylib, dylib, proc-macro). + # This allows different execution strategies for compilation vs linking. + is_link_action = crate_info.type in ("bin", "cdylib", "dylib", "proc-macro") + action_mnemonic = "RustcLink" if is_link_action else "Rustc" + if ctx.executable._process_wrapper: - # Run as normal + main_inputs = compile_inputs + main_arguments = args.all + main_unused_inputs_list = None + if incr_seed: + main_inputs = depset([incr_seed], transitive = [compile_inputs]) + seed_args = ctx.actions.args() + seed_args.add_all("--copy-seed", [incr_seed, incr_tree], expand_directories = False) + seed_args.add("--seed-min-mb", str(seed_threshold_mb)) + seed_args.add_all("--write-unused-inputs", [incr_unused_inputs, incr_seed], expand_directories = False) + main_arguments = [seed_args] + args.all + main_unused_inputs_list = incr_unused_inputs + ctx.actions.run( executable = ctx.executable._process_wrapper, - inputs = compile_inputs, + inputs = main_inputs, outputs = action_outputs, env = env, - arguments = args.all, - mnemonic = "Rustc", - progress_message = "Compiling Rust {} {}{} ({} file{})".format( + arguments = main_arguments, + mnemonic = action_mnemonic, + progress_message = "{} Rust {} {}{} ({} file{})".format( + "Linking" if is_link_action else "Compiling", crate_info.type, ctx.label.name, formatted_version, @@ -1524,12 +1618,18 @@ def rustc_compile_action( ), toolchain = "@rules_rust//rust:toolchain_type", resource_set = get_rustc_resource_set(toolchain), + unused_inputs_list = main_unused_inputs_list, ) if args_metadata: + metadata_outputs = [build_metadata] + [x for x in [rustc_rmeta_output] if x] + + if incr_metadata_tree: + metadata_outputs.append(incr_metadata_tree) + ctx.actions.run( executable = ctx.executable._process_wrapper, inputs = compile_inputs, - outputs = [build_metadata] + [x for x in [rustc_rmeta_output] if x], + outputs = metadata_outputs, env = env, arguments = args_metadata.all, mnemonic = "RustcMetadata", diff --git a/rust/settings/BUILD.bazel b/rust/settings/BUILD.bazel index daa02a4c0c..60e99968ab 100644 --- a/rust/settings/BUILD.bazel +++ b/rust/settings/BUILD.bazel @@ -25,6 +25,8 @@ load( "extra_rustc_env", "extra_rustc_flag", "extra_rustc_flags", + "incremental", + "incremental_seed_threshold_mb", "incompatible_change_clippy_error_format", "incompatible_do_not_include_data_in_compile_data", "lto", @@ -107,6 +109,10 @@ extra_rustc_flag() extra_rustc_flags() +incremental() + +incremental_seed_threshold_mb() + incompatible_change_clippy_error_format() incompatible_do_not_include_data_in_compile_data() diff --git a/rust/settings/settings.bzl b/rust/settings/settings.bzl index b6954f41ce..3c2e1e6f24 100644 --- a/rust/settings/settings.bzl +++ b/rust/settings/settings.bzl @@ -369,6 +369,44 @@ def clippy_error_format(): build_setting_default = "human", ) +# buildifier: disable=unnamed-macro +def incremental(): + """Enable incremental Rust compilation via Bazel-managed tree artifacts. + + When enabled, workspace crates (not external dependencies) get + ``-Cincremental=`` passed to rustc. The incremental + state is stored as a Bazel-managed tree artifact, enabling cache + sharing across builds. The process wrapper handles seeding the + incremental cache from a previous build's output. + + Usage: + ``--@rules_rust//rust/settings:incremental=true`` + """ + bool_flag( + name = "incremental", + build_setting_default = False, + ) + +# buildifier: disable=unnamed-macro +def incremental_seed_threshold_mb(): + """Minimum incremental cache size (in MiB) to seed from the previous build. + + When the previous build's incremental cache is smaller than this threshold, + the seed step is skipped because rustc's overhead loading the dep-graph and + query-cache exceeds any incremental benefit for small crates. The + ``-Cincremental`` flag is still passed so rustc writes state for future builds; + only the seed read is skipped. + + Set to 0 to always seed (even tiny caches). + + Usage: + ``--@rules_rust//rust/settings:incremental_seed_threshold_mb=20`` + """ + int_flag( + name = "incremental_seed_threshold_mb", + build_setting_default = 20, + ) + # buildifier: disable=unnamed-macro def incompatible_change_clippy_error_format(): """A flag to enable the `clippy_error_format` setting. diff --git a/util/process_wrapper/main.rs b/util/process_wrapper/main.rs index 39a6d6db16..a6e5127605 100644 --- a/util/process_wrapper/main.rs +++ b/util/process_wrapper/main.rs @@ -20,8 +20,9 @@ mod util; use std::collections::HashMap; use std::fmt; -use std::fs::{copy, OpenOptions}; +use std::fs::{self, copy, OpenOptions}; use std::io; +use std::path::Path; use std::process::{exit, Command, ExitStatus, Stdio}; use tinyjson::JsonValue; @@ -117,9 +118,176 @@ fn process_line( } } +/// Cross-device link error (POSIX `EXDEV`, errno 18). +const EXDEV_RAW: i32 = 18; + +/// Result of walking a directory tree. +struct TreeWalk { + /// Destination directories to create, in parent-before-child order. + dirs: Vec, + /// (source, destination) file pairs to hardlink or copy. + files: Vec<(std::path::PathBuf, std::path::PathBuf)>, + /// Total size of all source files in bytes. + total_bytes: u64, +} + +/// Walk `src` recursively, collecting directory and file entries mapped to `dst`. +/// Symlinks are skipped (defense-in-depth). Also computes the total file size +/// in a single pass. +fn collect_tree_entries(src: &Path, dst: &Path) -> io::Result { + let mut dirs = Vec::new(); + let mut files = Vec::new(); + let mut total_bytes = 0u64; + let mut stack = vec![(src.to_path_buf(), dst.to_path_buf())]; + while let Some((s, d)) = stack.pop() { + let entries = match fs::read_dir(&s) { + Ok(e) => e, + Err(e) if e.kind() == io::ErrorKind::NotFound => continue, + Err(e) => return Err(e), + }; + for entry in entries { + let entry = entry?; + let ft = entry.file_type()?; + if ft.is_symlink() { + continue; + } + let src_path = entry.path(); + let dst_path = d.join(entry.file_name()); + if ft.is_dir() { + dirs.push(dst_path.clone()); + stack.push((src_path, dst_path)); + } else { + if let Ok(meta) = entry.metadata() { + total_bytes += meta.len(); + } + files.push((src_path, dst_path)); + } + } + } + Ok(TreeWalk { + dirs, + files, + total_bytes, + }) +} + +/// Hardlink (or copy on `EXDEV`) a single file from `src` to `dst`. +fn hardlink_one(src: &Path, dst: &Path) -> io::Result<()> { + match fs::hard_link(src, dst) { + Ok(()) => Ok(()), + Err(e) if e.raw_os_error() == Some(EXDEV_RAW) => { + fs::copy(src, dst)?; + Ok(()) + } + Err(e) => Err(e), + } +} + +/// Create directories and hardlink files from a pre-collected tree walk. +/// Falls back to `fs::copy` on cross-device errors. +/// File hardlinks are parallelized across threads for large trees. +fn hardlink_entries(walk: &TreeWalk) -> io::Result { + for dir in &walk.dirs { + fs::create_dir(dir)?; + } + + let count = walk.files.len(); + if count == 0 { + return Ok(0); + } + + const PARALLEL_THRESHOLD: usize = 256; + if count < PARALLEL_THRESHOLD { + for (s, d) in &walk.files { + hardlink_one(s, d)?; + } + return Ok(count); + } + + let n_threads = std::thread::available_parallelism() + .map(std::num::NonZero::get) + .unwrap_or(4) + .min(8); + let chunk_size = (count + n_threads - 1) / n_threads; + + std::thread::scope(|s| { + let handles: Vec<_> = walk + .files + .chunks(chunk_size) + .map(|chunk| { + s.spawn(move || -> io::Result<()> { + for (src, dst) in chunk { + hardlink_one(src, dst)?; + } + Ok(()) + }) + }) + .collect(); + for handle in handles { + handle.join().expect("hardlink thread panicked")?; + } + Ok(count) + }) +} + +/// Seed the incremental compilation cache by hardlinking from a source +/// directory. Skips seeding when the source is below `min_bytes`. +/// Errors are logged but never fatal (cold start fallback). +fn seed_incremental_cache(src: &Path, dst: &Path, min_bytes: u64, label: &str) { + let walk = match collect_tree_entries(src, dst) { + Ok(w) => w, + Err(e) => { + eprintln!("process_wrapper: {label} seed walk failed: {e}, starting cold"); + return; + } + }; + if walk.total_bytes < min_bytes { + debug_log!( + "process_wrapper: {label} seed {} below threshold ({} < {min_bytes}), skipping", + src.display(), + walk.total_bytes + ); + return; + } + match hardlink_entries(&walk) { + Ok(0) => debug_log!("process_wrapper: empty {label} seed at {}", src.display()), + Ok(n) => debug_log!( + "process_wrapper: {label} seeded {n} files ({} bytes) {} -> {}", + walk.total_bytes, + src.display(), + dst.display() + ), + Err(e) => eprintln!("process_wrapper: {label} seed hardlink failed: {e}, starting cold"), + } +} + fn main() -> Result<(), ProcessWrapperError> { let opts = options().map_err(|e| ProcessWrapperError(e.to_string()))?; + // Seed the incremental compilation cache from the previous build's output. + let seed_min_bytes = opts.seed_min_mb * 1024 * 1024; + if let Some((ref seed_dir, ref dest_dir)) = opts.copy_seed { + seed_incremental_cache( + Path::new(seed_dir), + Path::new(dest_dir), + seed_min_bytes, + "copy", + ); + } + + if let Some((ref prev_dir, ref dest_dir)) = opts.seed_prev_dir { + let prev_path = Path::new(prev_dir); + if prev_path.is_dir() { + seed_incremental_cache(prev_path, Path::new(dest_dir), seed_min_bytes, "prev"); + } else { + debug_log!("process_wrapper: prev seed {prev_dir} not found, cold start"); + } + } + + if opts.exit_early { + return Ok(()); + } + let mut command = Command::new(opts.executable); command .args(opts.child_arguments) @@ -226,6 +394,15 @@ fn main() -> Result<(), ProcessWrapperError> { )) })?; } + // Write the unused inputs list so Bazel excludes the seed directory + // from cache key computation. This ensures that changes to the + // incremental seed (which changes every build) don't cause cache + // misses for the rustc action -- only source file changes do. + if let Some((ref unused_file, ref input_path)) = opts.write_unused_inputs { + std::fs::write(unused_file, format!("{input_path}\n")).unwrap_or_else(|e| { + eprintln!("process_wrapper: failed to write unused inputs to {unused_file}: {e}"); + }); + } } exit(code) diff --git a/util/process_wrapper/options.rs b/util/process_wrapper/options.rs index 2f252cadc7..dc00931012 100644 --- a/util/process_wrapper/options.rs +++ b/util/process_wrapper/options.rs @@ -49,6 +49,21 @@ pub(crate) struct Options { pub(crate) rustc_quit_on_rmeta: bool, // This controls the output format of rustc messages. pub(crate) rustc_output_format: Option, + // If set to (seed_dir, dest_dir), copies the seed directory contents into + // dest_dir before spawning the child process. Used to seed the incremental + // compilation tree artifact from the previous build's state. + pub(crate) copy_seed: Option<(String, String)>, + // If set to (file_path, input_path), writes input_path to file_path after + // the child process completes successfully. Used to populate the + // unused_inputs_list file so Bazel excludes the seed from cache keys. + pub(crate) write_unused_inputs: Option<(String, String)>, + // If set to (prev_dir, dest_dir), reads directly from prev_dir (which is + // outside the sandbox) and hardlinks/copies into dest_dir. + pub(crate) seed_prev_dir: Option<(String, String)>, + // Minimum incremental cache size (in MiB) to seed from the previous build. + pub(crate) seed_min_mb: u64, + // If true, exit with 0 after pre-exec operations without spawning a child. + pub(crate) exit_early: bool, } pub(crate) fn options() -> Result { @@ -66,6 +81,11 @@ pub(crate) fn options() -> Result { let mut output_file = None; let mut rustc_quit_on_rmeta_raw = None; let mut rustc_output_format_raw = None; + let mut copy_seed_raw = None; + let mut write_unused_inputs_raw = None; + let mut seed_prev_dir_raw = None; + let mut seed_min_mb_raw = None; + let mut exit_early_raw = None; let mut flags = Flags::new(); let mut require_explicit_unstable_features = None; flags.define_repeated_flag("--subst", "", &mut subst_mapping_raw); @@ -121,6 +141,39 @@ pub(crate) fn options() -> Result { other -Zallow-features= is present in the rustc flags.", &mut require_explicit_unstable_features, ); + flags.define_repeated_flag( + "--copy-seed", + "Copy a seed directory into the output directory before spawning \ + the child process. Takes two values: . Used \ + to seed the incremental compilation tree artifact from the previous build.", + &mut copy_seed_raw, + ); + flags.define_repeated_flag( + "--write-unused-inputs", + "Write an input path to a file after the child process succeeds. \ + Takes two values: . Used to populate the \ + unused_inputs_list so Bazel excludes the seed from cache keys.", + &mut write_unused_inputs_raw, + ); + flags.define_repeated_flag( + "--seed-prev-dir", + "Read incremental cache directly from a previous build's output \ + directory and hardlink/copy into the output directory. \ + Takes two values: .", + &mut seed_prev_dir_raw, + ); + flags.define_flag( + "--seed-min-mb", + "Minimum incremental cache size (in MiB) to seed from. \ + Caches below this threshold are skipped. Default: 20.", + &mut seed_min_mb_raw, + ); + flags.define_flag( + "--exit-early", + "Exit with 0 after pre-exec operations (seed, etc.) \ + without spawning a child process.", + &mut exit_early_raw, + ); let mut child_args = match flags .parse(env::args().collect()) @@ -202,6 +255,16 @@ pub(crate) fn options() -> Result { let require_explicit_unstable_features = require_explicit_unstable_features.is_some_and(|s| s == "true"); + let seed_min_mb = seed_min_mb_raw + .map(|s| { + s.parse::().map_err(|e| { + OptionError::Generic(format!("invalid --seed-min-mb value '{s}': {e}")) + }) + }) + .transpose()? + .unwrap_or(20); + let exit_early = exit_early_raw.is_some_and(|s| s == "true"); + // Append all the arguments fetched from files to those provided via command line. child_args.append(&mut file_arguments); let child_args = prepare_args( @@ -211,16 +274,62 @@ pub(crate) fn options() -> Result { None, None, )?; + // Split the executable path from the rest of the arguments. - let (exec_path, args) = child_args.split_first().ok_or_else(|| { - OptionError::Generic( - "at least one argument after -- is required (the child process path)".to_owned(), - ) - })?; + // When --exit-early is set, no child process is needed. + let (exec_path, args) = if exit_early && child_args.is_empty() { + (String::new(), Vec::new()) + } else { + let (e, a) = child_args.split_first().ok_or_else(|| { + OptionError::Generic( + "at least one argument after -- is required (the child process path)".to_owned(), + ) + })?; + (e.to_owned(), a.to_vec()) + }; + + // Process --copy-seed + let copy_seed = copy_seed_raw + .map(|cs| { + if cs.len() != 2 { + return Err(OptionError::Generic(format!( + "\"--copy-seed\" needs exactly 2 parameters, {} provided", + cs.len() + ))); + } + Ok((cs[0].to_owned(), cs[1].to_owned())) + }) + .transpose()?; + + // Process --write-unused-inputs + let write_unused_inputs = write_unused_inputs_raw + .map(|wu| { + if wu.len() != 2 { + return Err(OptionError::Generic(format!( + "\"--write-unused-inputs\" needs exactly 2 parameters, {} provided", + wu.len() + ))); + } + Ok((wu[0].to_owned(), wu[1].to_owned())) + }) + .transpose()?; + + // Process --seed-prev-dir + let seed_prev_dir = seed_prev_dir_raw + .map(|sp| { + if sp.len() != 2 { + return Err(OptionError::Generic(format!( + "\"--seed-prev-dir\" needs exactly 2 parameters, {} provided", + sp.len() + ))); + } + Ok((sp[0].to_owned(), sp[1].to_owned())) + }) + .transpose()?; Ok(Options { - executable: exec_path.to_owned(), - child_arguments: args.to_vec(), + executable: exec_path, + child_arguments: args, child_environment: vars, touch_file, copy_output, @@ -229,6 +338,11 @@ pub(crate) fn options() -> Result { output_file, rustc_quit_on_rmeta, rustc_output_format, + copy_seed, + write_unused_inputs, + seed_prev_dir, + seed_min_mb, + exit_early, }) }