From 5e92221cebe3e206737f21a4769dfc96b67e351b Mon Sep 17 00:00:00 2001 From: Mark Dittmer Date: Thu, 21 May 2026 18:13:20 +0000 Subject: [PATCH] SQUASH ME: More minor fix-ups and a parallel strategy for invoking charon gherrit-pr-id: G2gh3jnakn7ptbfzxsxqzjfyavl6xa2oj --- anneal/v2/Cargo.lock | 52 +++++++++++++ anneal/v2/Cargo.toml | 1 + anneal/v2/src/charon.rs | 138 +++++++++++++++++++-------------- anneal/v2/src/main.rs | 24 +++--- anneal/v2/src/setup.rs | 75 +++++++----------- anneal/v2/src/util.rs | 67 +++++++++++----- anneal/v2/tests/integration.rs | 1 + 7 files changed, 225 insertions(+), 133 deletions(-) diff --git a/anneal/v2/Cargo.lock b/anneal/v2/Cargo.lock index f33756edff..1ee6c2127b 100644 --- a/anneal/v2/Cargo.lock +++ b/anneal/v2/Cargo.lock @@ -171,6 +171,7 @@ dependencies = [ "indicatif", "log", "miette", + "rayon", "serde", "serde_json", "sha2 0.10.9", @@ -326,6 +327,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.7" @@ -387,6 +413,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -896,6 +928,26 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_users" version = "0.5.2" diff --git a/anneal/v2/Cargo.toml b/anneal/v2/Cargo.toml index fb3275eff5..722d2aca9c 100644 --- a/anneal/v2/Cargo.toml +++ b/anneal/v2/Cargo.toml @@ -60,6 +60,7 @@ fs2 = "0.4" walkdir = "2.5.0" sha2 = "0.10" tempfile = "3.27.0" +rayon = "1.11.0" # FIXME: Drop patch if forked change gets merged. [patch.crates-io] diff --git a/anneal/v2/src/charon.rs b/anneal/v2/src/charon.rs index bc0b44583a..ddcb7e3c0a 100644 --- a/anneal/v2/src/charon.rs +++ b/anneal/v2/src/charon.rs @@ -10,11 +10,13 @@ //! - Validating the extraction result. use anyhow::Context as _; +use rayon::prelude::IntoParallelRefIterator as _; +use rayon::prelude::ParallelIterator as _; /// Runs Charon on the specified packages to generate LLBC artifacts. /// -/// This function requires `LockedRoots` to ensure that it has exclusive access -/// to the `llbc` output directory. It iterates over each `AnnealArtifact`, +/// This function requires [`crate::resolve::LockedRoots`] to ensure that it has exclusive access +/// to the `llbc` output directory. It iterates over each [`crate::scanner::AnnealArtifact`], /// constructs the appropriate `charon` command, and executes it. /// /// It handles: @@ -23,45 +25,43 @@ use anyhow::Context as _; /// - **Entry Points**: passing the computed `start_from` roots to Charon to /// minimize extraction scope. /// - **Output Handling**: capturing stdout/stderr, parsing JSON compiler -/// messages, and rendering them using `DiagnosticMapper`. +/// messages, and rendering them using [`crate::diagnostics::DiagnosticMapper`]. pub fn run_charon( args: &crate::resolve::Args, + toolchain: &crate::setup::Toolchain, roots: &crate::resolve::LockedRoots, packages: &[crate::scanner::AnnealArtifact], - toolchain: &crate::setup::Toolchain, ) -> anyhow::Result<()> { let llbc_root = roots.llbc_root(); std::fs::create_dir_all(&llbc_root).context("Failed to create LLBC output directory")?; - let rust_bin = toolchain.rust_bin(); - let rust_lib = toolchain.rust_lib(); - - // We prepend the managed Rust toolchain's `bin` directory to `PATH`. This is - // necessary because Charon is a compiler frontend that invokes `cargo` and - // `rustc` under the hood. To ensure hermeticity and correctness, we must force - // Charon to use our pinned nightly compiler version rather than falling back - // to whatever version happens to be installed in the system `PATH`. - let new_path = crate::util::prepend_to_env_var("PATH", rust_bin); + // Prepend to `PATH` to ensure that when `charon` delegates to tools such as + // `cargo` and `rustc` it invokes the correct rust toolchain. + let new_path = crate::util::prepend_to_env_var("PATH", &toolchain.rust_bin()); let lib_env_var = if cfg!(target_os = "macos") { "DYLD_LIBRARY_PATH" } else { "LD_LIBRARY_PATH" }; - // We set `LD_LIBRARY_PATH` (or `DYLD_LIBRARY_PATH` on macOS) to point to the - // managed Rust toolchain's `lib` directory. This is strictly required because - // `charon-driver` is a dynamic executable that links against `rustc` compiler - // dynamic libraries (like `librustc_driver-*.so`). Without this, the dynamic - // linker will fail to load the libraries when `charon-driver` is executed. - let new_lib_path = crate::util::prepend_to_env_var(lib_env_var, rust_lib); - - for artifact in packages { + // Set `LD_LIBRARY_PATH` (or `DYLD_LIBRARY_PATH` on macOS) to point to the + // managed Rust toolchain's `lib` directory so that dynamic executables (like + // `charon-driver` which links against `rustc` dynamic libraries) can find them. + let new_lib_path = crate::util::prepend_to_env_var(lib_env_var, &toolchain.rust_lib()); + + // Global print mutex to prevent interleaved printing of consolidated artifact buffers. + let print_mutex = std::sync::Arc::new(std::sync::Mutex::new(())); + + // Determine if we should show progress bar. + let show_progress = std::env::var("ANNEAL_NO_PROGRESS").is_err() && packages.len() == 1; + + packages.par_iter().try_for_each(|artifact| { if artifact.start_from.is_empty() { - continue; + return Ok(()); } log::info!("Invoking Charon on package '{}'...", artifact.name.package_name); let mut cmd = toolchain.command(crate::setup::Tool::Charon); - // We set `CHARON_TOOLCHAIN_IS_IN_PATH=1` to instruct Charon to bypass its + // Set `CHARON_TOOLCHAIN_IS_IN_PATH=1` to instruct Charon to bypass its // standard toolchain resolution logic (which expects the compiler to be // managed via `rustup`). Instead, it will directly use the `rustc` and // `cargo` binaries we prepended to the `PATH` environment variable. @@ -72,15 +72,10 @@ pub fn run_charon( cmd.arg("cargo"); cmd.arg("--preset=aeneas"); - // Output artifacts to target/anneal//llbc. let llbc_path = artifact.llbc_path(roots); - log::debug!("Writing .llbc file to {:?}", llbc_path); cmd.arg("--dest-file").arg(llbc_path); - // We use `--abort-on-error` to fail fast. If Charon (or `rustc`) - // encounters a compilation error or translation failure (e.g., an - // unsupported Rust feature), it will terminate the process immediately - // rather than attempting to proceed and translate other parts of the crate. + // Fail fast on errors. cmd.arg("--abort-on-error"); for item in &artifact.items { @@ -141,11 +136,12 @@ pub fn run_charon( cmd.arg("--manifest-path").arg(&artifact.manifest_path); + use crate::resolve::AnnealTargetKind::*; match artifact.target_kind { - crate::resolve::AnnealTargetKind::Lib | crate::resolve::AnnealTargetKind::RLib | crate::resolve::AnnealTargetKind::ProcMacro | crate::resolve::AnnealTargetKind::CDyLib | crate::resolve::AnnealTargetKind::DyLib | crate::resolve::AnnealTargetKind::StaticLib => cmd.arg("--lib"), - crate::resolve::AnnealTargetKind::Bin => cmd.args(["--bin", &artifact.name.target_name]), - crate::resolve::AnnealTargetKind::Example => cmd.args(["--example", &artifact.name.target_name]), - crate::resolve::AnnealTargetKind::Test => cmd.args(["--test", &artifact.name.target_name]), + Lib | RLib | ProcMacro | CDyLib | DyLib | StaticLib => cmd.arg("--lib"), + Bin => cmd.args(["--bin", &artifact.name.target_name]), + Example => cmd.args(["--example", &artifact.name.target_name]), + Test => cmd.args(["--test", &artifact.name.target_name]), }; // Forward all feature-related flags. @@ -159,10 +155,9 @@ pub fn run_charon( cmd.arg("--features").arg(feature); } - // We share `CARGO_TARGET_DIR` (`target/anneal/cargo_target`) across all - // Charon invocations to enable Cargo's incremental build cache. This - // prevents Cargo from recompiling identical workspace dependencies from - // scratch for each individual target, saving significant compilation time. + // Reuse the main target directory for dependencies to save time: share + // `CARGO_TARGET_DIR` (`target/anneal/cargo_target`) across all Charon + // invocations to enable Cargo's incremental build cache. cmd.env("CARGO_TARGET_DIR", roots.cargo_target_dir()); log::debug!("Executing charon command: {:?}", cmd); @@ -179,18 +174,28 @@ pub fn run_charon( let safety_buffer = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); let safety_buffer_clone = std::sync::Arc::clone(&safety_buffer); + // Local buffer to collect all output (diagnostics) for this artifact. + let artifact_diagnostics = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); + let artifact_diagnostics_clone = std::sync::Arc::clone(&artifact_diagnostics); + let mut mapper = crate::diagnostics::DiagnosticMapper::new(roots.workspace().clone()); - let res = crate::util::run_command_with_progress(cmd, "Compiling...", move |line, pb| { + let progress_msg = if show_progress { Some("Compiling...") } else { None }; + + let res = crate::util::run_command_with_progress(cmd, progress_msg, move |line, pb| { if let Ok(msg) = serde_json::from_str::(line) { match msg { cargo_metadata::Message::CompilerArtifact(a) => { - pb.set_message(format!("Compiling {}", a.target.name)); + if let Some(p) = pb { + p.set_message(format!("Compiling {}", a.target.name)); + } } cargo_metadata::Message::CompilerMessage(msg) => { - pb.suspend(|| { - mapper.render_miette(&msg.message, |s| eprintln!("{}", s)); - }); + let mut rendered = String::new(); + mapper.render_miette(&msg.message, |s| rendered.push_str(&s)); + if !rendered.is_empty() { + artifact_diagnostics_clone.lock().unwrap().push(rendered); + } if matches!( msg.message.level, cargo_metadata::diagnostic::DiagnosticLevel::Error | cargo_metadata::diagnostic::DiagnosticLevel::Ice @@ -211,15 +216,23 @@ pub fn run_charon( log::trace!("Charon for '{}' took {:.2?}", artifact.name.package_name, start.elapsed()); - // FIXME: There's a subtle edge case here – if we get error output AND - // Rustc ICE's, there's a good chance that the JSON error messages we - // print won't include all relevant information – some will be printed - // via stderr. In this case, `output_error = true` and so we bail and - // discard stderr, which will swallow information from the user. + // Lock the print mutex to print this artifact's consolidated output atomically. + let _lock = print_mutex.lock().unwrap(); + + // Print all collected diagnostics for this artifact. + let diags = artifact_diagnostics.lock().unwrap(); + if !diags.is_empty() { + eprintln!("=== Diagnostics for '{}' ===", artifact.name.package_name); + for diag in diags.iter() { + eprintln!("{}", diag); + } + } + if output_error.load(std::sync::atomic::Ordering::Relaxed) { anyhow::bail!("Diagnostic error in charon"); } else if !res.status.success() { - // "Silent Death" dump. + // Print safety buffer on failure (also atomically since we hold print_mutex). + eprintln!("=== Failure output for '{}' ===", artifact.name.package_name); for line in safety_buffer.lock().unwrap().iter() { eprintln!("{}", line); } @@ -229,7 +242,9 @@ pub fn run_charon( } anyhow::bail!("Charon failed with status: {}", res.status); } - } + + Ok(()) + })?; Ok(()) } @@ -244,12 +259,15 @@ mod tests { #[test] fn test_run_charon_simple() { - // 1. Create a temporary directory + // Disable progress bar for test. + unsafe { std::env::set_var("ANNEAL_NO_PROGRESS", "1"); } + + // 1. Create a temporary directory. let temp_dir = tempfile::tempdir().unwrap(); let proj_dir = temp_dir.path().join("test_proj"); fs::create_dir_all(&proj_dir).unwrap(); - // 2. Create a simple Cargo.toml + // 2. Create a simple Cargo.toml. let cargo_toml = r#" [package] name = "test_proj" @@ -261,7 +279,7 @@ mod tests { "#; fs::write(proj_dir.join("Cargo.toml"), cargo_toml).unwrap(); - // 3. Create a simple src/lib.rs + // 3. Create a simple src/lib.rs. fs::create_dir_all(proj_dir.join("src")).unwrap(); let lib_rs = r#" pub fn add(left: usize, right: usize) -> usize { @@ -270,36 +288,38 @@ mod tests { "#; fs::write(proj_dir.join("src").join("lib.rs"), lib_rs).unwrap(); - // 4. Construct Args pointing to this temp project + // 4. Construct Args pointing to this temp project. let args = Args::try_parse_from(&[ "cargo-anneal", "--manifest-path", proj_dir.join("Cargo.toml").to_str().unwrap(), ]).unwrap(); - // 5. Resolve roots + // 5. Resolve roots. let roots = resolve_roots(&args).unwrap(); - // 6. Scan workspace (our simplified scanner will find `add` function) + // 6. Scan workspace (our simplified scanner will find `add` function). let packages = scan_workspace(&roots).unwrap(); assert_eq!(packages.len(), 1); assert!(packages[0].start_from.contains("crate::add")); - // 7. Lock run root + // 7. Lock run root. let locked_roots = roots.lock_run_root().unwrap(); - // 8. Resolve test-only stripped toolchain and run charon + // 8. Resolve test-only stripped toolchain and run charon. let toolchain_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("toolchains") .join("charon-only"); let toolchain = crate::setup::Toolchain::new_test(toolchain_root); - let res = run_charon(&args, &locked_roots, &packages, &toolchain); + let res = run_charon(&args, &toolchain, &locked_roots, &packages); assert!(res.is_ok(), "charon failed: {:?}", res.err()); - // 9. Verify .llbc file exists + // 9. Verify .llbc file exists. let llbc_path = packages[0].llbc_path(&locked_roots); assert!(llbc_path.exists(), "llbc file not found at {:?}", llbc_path); + + unsafe { std::env::remove_var("ANNEAL_NO_PROGRESS"); } } } diff --git a/anneal/v2/src/main.rs b/anneal/v2/src/main.rs index c36af66a0a..b4ab5a6ca6 100644 --- a/anneal/v2/src/main.rs +++ b/anneal/v2/src/main.rs @@ -17,7 +17,7 @@ mod scanner; mod charon; mod setup; -/// Anneal: A Literate Verification Toolchain. +/// Anneal: A Literate Verification Toolchain #[derive(clap::Parser, Debug)] #[command(name = "cargo-anneal", version, about, long_about = None)] struct Cli { @@ -27,18 +27,21 @@ struct Cli { #[derive(clap::Subcommand, Debug)] enum Commands { - /// Setup Anneal dependencies. + /// Setup Anneal dependencies Setup(SetupArgs), - /// Expand a crate (runs Charon). + /// Expand a crate (runs Charon) Expand(ExpandArgs), - /// Setup test-only stripped toolchain (dev only). + /// Setup test-only stripped toolchain (dev only) + /// + /// FIXME: Add GitHub actions that will block changes that would update + /// tests/toolchains/ files if TestSetup were invoked without committing them. #[cfg(feature = "exocrate_tests")] TestSetup, } #[derive(clap::Parser, Debug)] pub struct SetupArgs { - /// Path to a local dependency archive to use instead of downloading. + /// Path to a local dependency archive to use instead of downloading #[arg(long, value_name = "path-to-local-archive")] pub local_archive: Option, } @@ -48,7 +51,7 @@ pub struct ExpandArgs { #[command(flatten)] pub resolve_args: crate::resolve::Args, - /// Controls where LLBC output is placed on the filesystem. + /// Controls where LLBC output is placed on the filesystem #[arg(long, value_name = "output-dir")] pub output_dir: Option, } @@ -72,7 +75,7 @@ fn expand(args: ExpandArgs) -> anyhow::Result<()> { locked_roots.llbc_override = Some(output_dir); } let toolchain = crate::setup::Toolchain::resolve()?; - crate::charon::run_charon(&args.resolve_args, &locked_roots, &packages, &toolchain)?; + crate::charon::run_charon(&args.resolve_args, &toolchain, &locked_roots, &packages)?; Ok(()) } @@ -107,14 +110,17 @@ fn main() { } } -#[cfg(all(test, feature = "exocrate_tests"))] +#[cfg(test)] mod tests { + #[cfg(feature = "exocrate_tests")] #[test] fn test_setup() { + unsafe { std::env::set_var("__ANNEAL_LOCAL_DEV", "1"); } super::setup(super::SetupArgs { // ASSUMPTION: Dependency builder installs archive at // `target/anneal-exocrate.tar.zst`. local_archive: Some("target/anneal-exocrate.tar.zst".into()), - }) + }); + unsafe { std::env::remove_var("__ANNEAL_LOCAL_DEV"); } } } diff --git a/anneal/v2/src/setup.rs b/anneal/v2/src/setup.rs index dca295d4f1..d3800ae4f0 100644 --- a/anneal/v2/src/setup.rs +++ b/anneal/v2/src/setup.rs @@ -38,12 +38,13 @@ impl Tool { } } +const AENEAS_DIR: &str = "aeneas"; +const RUST_SYSROOT: &str = "rust"; +const BIN_DIR: &str = "bin"; +const LIB_DIR: &str = "lib"; + pub struct Toolchain { - pub root: std::path::PathBuf, - aeneas_bin_dir: std::path::PathBuf, - rust_sysroot: std::path::PathBuf, - rust_bin: std::path::PathBuf, - rust_lib: std::path::PathBuf, + root: std::path::PathBuf, } impl Toolchain { @@ -56,35 +57,27 @@ impl Toolchain { let root = CONFIG .resolve_installation_dir(location) .context("Toolchain not installed. Please run 'cargo anneal setup' first.")?; + Ok(Self { root }) + } - let aeneas_bin_dir = root.join("aeneas").join("bin"); - let rust_sysroot = root.join("rust"); - let rust_bin = rust_sysroot.join("bin"); - let rust_lib = rust_sysroot.join("lib"); - - Ok(Self { - root, - aeneas_bin_dir, - rust_sysroot, - rust_bin, - rust_lib, - }) + pub fn root(&self) -> &std::path::Path { + &self.root } - pub fn aeneas_bin_dir(&self) -> &std::path::Path { - &self.aeneas_bin_dir + pub fn aeneas_bin_dir(&self) -> std::path::PathBuf { + self.root.join(AENEAS_DIR).join(BIN_DIR) } - pub fn rust_sysroot(&self) -> &std::path::Path { - &self.rust_sysroot + pub fn rust_sysroot(&self) -> std::path::PathBuf { + self.root.join(RUST_SYSROOT) } - pub fn rust_bin(&self) -> &std::path::Path { - &self.rust_bin + pub fn rust_bin(&self) -> std::path::PathBuf { + self.rust_sysroot().join(BIN_DIR) } - pub fn rust_lib(&self) -> &std::path::Path { - &self.rust_lib + pub fn rust_lib(&self) -> std::path::PathBuf { + self.rust_sysroot().join(LIB_DIR) } pub fn command(&self, tool: Tool) -> std::process::Command { @@ -93,17 +86,7 @@ impl Toolchain { #[cfg(test)] pub fn new_test(root: std::path::PathBuf) -> Self { - let aeneas_bin_dir = root.join("aeneas").join("bin"); - let rust_sysroot = root.join("rust"); - let rust_bin = rust_sysroot.join("bin"); - let rust_lib = rust_sysroot.join("lib"); - Self { - root, - aeneas_bin_dir, - rust_sysroot, - rust_bin, - rust_lib, - } + Self { root } } } @@ -127,10 +110,7 @@ pub fn run_setup(args: SetupArgs) -> anyhow::Result<()> { #[cfg(feature = "exocrate_tests")] pub fn run_test_setup() -> anyhow::Result<()> { - // FIXME: Add GitHub actions that will block changes that would update - // tests/toolchains/ files if TestSetup were invoked without committing them. - - println!("Running standard setup..."); + log::debug!("Running standard setup..."); run_setup(SetupArgs { local_archive: Some("target/anneal-exocrate.tar.zst".into()), })?; @@ -142,7 +122,7 @@ pub fn run_test_setup() -> anyhow::Result<()> { .join("charon-only"); if dest_dir.exists() { - println!("Cleaning existing test toolchain at {:?}", dest_dir); + log::debug!("Cleaning existing test toolchain at {:?}", dest_dir); std::fs::remove_dir_all(&dest_dir).context("Failed to clean test toolchain dir")?; } std::fs::create_dir_all(&dest_dir)?; @@ -157,7 +137,7 @@ pub fn run_test_setup() -> anyhow::Result<()> { Ok(()) }; - println!("Copying Charon binaries..."); + log::debug!("Copying Charon binaries..."); copy_file( &toolchain.aeneas_bin_dir().join("charon"), &dest_dir.join("aeneas").join("bin").join("charon"), @@ -167,13 +147,13 @@ pub fn run_test_setup() -> anyhow::Result<()> { &dest_dir.join("aeneas").join("bin").join("charon-driver"), )?; - println!("Copying rustc binary..."); + log::debug!("Copying rustc binary..."); copy_file( &toolchain.rust_bin().join("rustc"), &dest_dir.join("rust").join("bin").join("rustc"), )?; - println!("Copying rust libraries (this may take a while)..."); + log::debug!("Copying rust libraries (this may take a while)..."); let src_lib = toolchain.rust_lib(); let dest_lib = dest_dir.join("rust").join("lib"); @@ -188,14 +168,15 @@ pub fn run_test_setup() -> anyhow::Result<()> { } } - println!("Test toolchain successfully set up at {:?}", dest_dir); + log::debug!("Test toolchain successfully set up at {:?}", dest_dir); Ok(()) } -#[cfg(all(test, feature = "exocrate_tests"))] +#[cfg(test)] mod tests { use super::*; + #[cfg(feature = "exocrate_tests")] #[test] fn test_toolchain_paths() { // Ensure toolchain is installed locally for test. @@ -203,7 +184,7 @@ mod tests { let toolchain = Toolchain::resolve().expect("Failed to resolve toolchain"); - assert!(toolchain.root.is_dir(), "root is not a directory: {:?}", toolchain.root); + assert!(toolchain.root().is_dir(), "root is not a directory: {:?}", toolchain.root()); assert!(toolchain.aeneas_bin_dir().is_dir(), "aeneas_bin_dir is not a directory: {:?}", toolchain.aeneas_bin_dir()); assert!(toolchain.rust_sysroot().is_dir(), "rust_sysroot is not a directory: {:?}", toolchain.rust_sysroot()); assert!(toolchain.rust_bin().is_dir(), "rust_bin is not a directory: {:?}", toolchain.rust_bin()); diff --git a/anneal/v2/src/util.rs b/anneal/v2/src/util.rs index af365b048e..ceda2961dd 100644 --- a/anneal/v2/src/util.rs +++ b/anneal/v2/src/util.rs @@ -6,9 +6,9 @@ use std::io::BufRead as _; /// /// This struct guarantees that the process holds an OS-level file lock /// guarding the specified directory. -pub struct DirLock { +pub(crate) struct DirLock { /// The path to the directory being guarded. - pub path: std::path::PathBuf, + pub(crate) path: std::path::PathBuf, // Kept alive to hold the flock. _file: std::fs::File, } @@ -21,7 +21,7 @@ impl DirLock { /// the directory itself to avoid platform-specific issues with /// directory locking and to ensure the lock file persists even if /// the directory is cleaned. - pub fn lock_exclusive(path: std::path::PathBuf) -> anyhow::Result { + pub(crate) fn lock_exclusive(path: std::path::PathBuf) -> anyhow::Result { let file = Self::open_lock_file(&path)?; file.lock_exclusive() .with_context(|| format!("Failed to acquire exclusive lock on {:?}", path))?; @@ -32,7 +32,7 @@ impl DirLock { /// /// Multiple processes can hold shared locks simultaneously, but an /// exclusive lock will block until all shared locks are released. - pub fn lock_shared(path: std::path::PathBuf) -> anyhow::Result { + pub(crate) fn lock_shared(path: std::path::PathBuf) -> anyhow::Result { let file = Self::open_lock_file(&path)?; file.lock_shared() .with_context(|| format!("Failed to acquire shared lock on {:?}", path))?; @@ -70,7 +70,7 @@ impl DirLock { /// Walks a directory recursively and replaces string patterns inside `.trace` /// files. This is used to patch non-portable paths generated by Lake. -pub fn patch_trace_files(dir: &std::path::Path, replacements: &[(&str, &str)]) -> anyhow::Result<()> { +pub(crate) fn patch_trace_files(dir: &std::path::Path, replacements: &[(&str, &str)]) -> anyhow::Result<()> { if dir.exists() { let walker = walkdir::WalkDir::new(dir).into_iter(); for entry in walker { @@ -119,11 +119,11 @@ pub(crate) struct ProcessOutput { /// its stdout line-by-line in the main thread while showing a progress spinner. pub(crate) fn run_command_with_progress( mut cmd: std::process::Command, - progress_msg: &str, + progress_msg: Option<&str>, mut process_stdout_line: F, ) -> anyhow::Result where - F: FnMut(&str, &indicatif::ProgressBar) -> anyhow::Result<()>, + F: FnMut(&str, Option<&indicatif::ProgressBar>) -> anyhow::Result<()>, { cmd.stdout(std::process::Stdio::piped()); cmd.stderr(std::process::Stdio::piped()); @@ -143,24 +143,31 @@ where })); } - let pb = indicatif::ProgressBar::new_spinner(); - pb.set_style( - indicatif::ProgressStyle::default_spinner() - .template("{spinner:.green} {msg}") - .unwrap(), - ); - pb.enable_steady_tick(std::time::Duration::from_millis(100)); - pb.set_message(progress_msg.to_string()); + let pb = progress_msg.map(|msg| { + let pb = indicatif::ProgressBar::new_spinner(); + pb.set_style( + indicatif::ProgressStyle::default_spinner() + .template("{spinner:.green} {msg}") + .unwrap(), + ); + pb.enable_steady_tick(std::time::Duration::from_millis(100)); + pb.set_message(msg.to_string()); + pb + }); if let Some(stdout) = child.stdout.take() { let reader = std::io::BufReader::new(stdout); for line in reader.lines().map_while(Result::ok) { - process_stdout_line(&line, &pb)?; - pb.tick(); + process_stdout_line(&line, pb.as_ref())?; + if let Some(ref p) = pb { + p.tick(); + } } } - pb.finish_and_clear(); + if let Some(ref p) = pb { + p.finish_and_clear(); + } let status = child.wait().context("Failed to wait for child process")?; @@ -175,3 +182,27 @@ where Ok(ProcessOutput { status, stderr_lines }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_prepend_to_env_var() { + let var_name = "ANNEAL_TEST_VAR"; + unsafe { std::env::remove_var(var_name); } + + // Test when var is empty. + let path1 = std::path::Path::new("/path/one"); + let res1 = prepend_to_env_var(var_name, path1); + assert_eq!(res1, "/path/one"); + + // Test when var is not empty. + unsafe { std::env::set_var(var_name, &res1); } + let path2 = std::path::Path::new("/path/two"); + let res2 = prepend_to_env_var(var_name, path2); + assert_eq!(res2, "/path/two:/path/one"); + + unsafe { std::env::remove_var(var_name); } + } +} diff --git a/anneal/v2/tests/integration.rs b/anneal/v2/tests/integration.rs index 9047a7b6dc..46eafc9aa7 100644 --- a/anneal/v2/tests/integration.rs +++ b/anneal/v2/tests/integration.rs @@ -18,6 +18,7 @@ fn test_expand_subcommand_simple() { .arg(&output_dir); cmd.env("__ANNEAL_LOCAL_DEV", "1"); + cmd.env("ANNEAL_NO_PROGRESS", "1"); let output = cmd.output().expect("failed to execute cargo-anneal");