diff --git a/src/cache/cache_io.rs b/src/cache/cache_io.rs index c4c2fa134..9a16c3986 100644 --- a/src/cache/cache_io.rs +++ b/src/cache/cache_io.rs @@ -15,11 +15,25 @@ use crate::errors::*; use fs_err as fs; use std::fmt; use std::io::{Cursor, Read, Seek, Write}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use tempfile::NamedTempFile; use zip::write::FileOptions; use zip::{CompressionMethod, ZipArchive, ZipWriter}; +/// Atomically persist a NamedTempFile: fsync, close the writable fd, rename. +/// +/// Caller MUST hold FORK_LOCK.read() for the entire NamedTempFile lifetime +/// (from creation through this call). See FORK_LOCK in lib.rs. +pub(crate) fn persist_temp_file(tmp: NamedTempFile, dest: &Path) -> Result<()> { + tmp.as_file() + .sync_all() + .with_context(|| format!("failed to fsync temp file before persisting to {:?}", dest))?; + let tmp_path = tmp.into_temp_path(); + tmp_path + .persist(dest) + .map_err(|e| anyhow::anyhow!("Failed to persist {:?} to {:?}: {}", e.path, dest, e.error)) +} + /// Cache object sourced by a file. #[derive(Clone)] pub struct FileObjectSource { @@ -147,13 +161,14 @@ impl CacheRead { Some(d) => d, None => bail!("Output file without a parent directory!"), }; - // Write the cache entry to a tempfile and then atomically - // move it to its final location so that other rustc invocations - // happening in parallel don't see a partially-written file. + // Hold FORK_LOCK while we have a writable fd to prevent + // fork() from inheriting it (causes ETXTBSY, see lib.rs). + let _fork_guard = crate::FORK_LOCK.read().unwrap(); let mut tmp = NamedTempFile::new_in(dir)?; match (self.get_object(&key, &mut tmp), optional) { (Ok(mode), _) => { - tmp.persist(&path)?; + persist_temp_file(tmp, &path)?; + drop(_fork_guard); if let Some(mode) = mode { set_file_mode(&path, mode)?; } diff --git a/src/cache/disk.rs b/src/cache/disk.rs index ebd65d1a3..52d9384cf 100644 --- a/src/cache/disk.rs +++ b/src/cache/disk.rs @@ -152,6 +152,8 @@ impl Storage for DiskCache { self.pool .spawn_blocking(move || { let start = Instant::now(); + // Hold FORK_LOCK while writable fd exists (see lib.rs). + let _fork_guard = crate::FORK_LOCK.read().unwrap(); let mut f = lru .lock() .unwrap() @@ -159,6 +161,7 @@ impl Storage for DiskCache { .prepare_add(key, data.len() as u64)?; f.as_file_mut().write_all(&data)?; lru.lock().unwrap().get().unwrap().commit(f)?; + drop(_fork_guard); Ok(start.elapsed()) }) .await? @@ -208,6 +211,8 @@ impl Storage for DiskCache { } let key = normalize_key(key); + // Hold FORK_LOCK while writable fd exists (see lib.rs). + let _fork_guard = crate::FORK_LOCK.read().unwrap(); let mut f = self .preprocessor_cache .lock() @@ -215,13 +220,15 @@ impl Storage for DiskCache { .get_or_init()? .prepare_add(key, 0)?; preprocessor_cache_entry.serialize_to(BufWriter::new(f.as_file_mut()))?; - Ok(self + let result = self .preprocessor_cache .lock() .unwrap() .get() .unwrap() - .commit(f)?) + .commit(f); + drop(_fork_guard); + Ok(result?) } } diff --git a/src/compiler/compiler.rs b/src/compiler/compiler.rs index 1d9ccff75..d9b0b7121 100644 --- a/src/compiler/compiler.rs +++ b/src/compiler/compiler.rs @@ -820,6 +820,110 @@ where fn language(&self) -> Language; } +/// Scan a command's arguments for `--out-dir` and extract the output directory path. +/// Returns `Some((index_of_value, path))` if found, `None` otherwise. +/// Handles both `--out-dir ` (separated) and `--out-dir=` (joined). +fn find_out_dir(args: &[OsString]) -> Option<(usize, PathBuf)> { + let mut i = 0; + while i < args.len() { + let arg = args[i].to_string_lossy(); + if arg == "--out-dir" { + if i + 1 < args.len() { + return Some((i + 1, PathBuf::from(&args[i + 1]))); + } + } else if let Some(path) = arg.strip_prefix("--out-dir=") { + return Some((i, PathBuf::from(path))); + } + i += 1; + } + None +} + +/// Execute a compile command, redirecting `--out-dir` to a temporary directory +/// and atomically renaming outputs to the real output directory afterward. +/// +/// This prevents orphaned compiler processes (from killed cargo invocations) +/// from corrupting output files — they write to a temp dir that becomes +/// irrelevant when the next build creates its own temp dir. +async fn execute_with_atomic_out_dir( + compile_cmd: &dyn CompileCommand, + service: &server::SccacheService, + creator: &T, + out_pretty: &str, +) -> Result +where + T: CommandCreatorSync, +{ + let args = compile_cmd.get_arguments(); + + // Only redirect if --out-dir is present (rustc-specific) + let Some((out_dir_idx, real_out_dir)) = find_out_dir(&args) else { + return compile_cmd.execute(service, creator).await; + }; + + // Create temp dir inside the real out-dir (same filesystem for atomic rename) + let temp_dir = tempfile::Builder::new() + .prefix(".sccache-tmp-") + .tempdir_in(&real_out_dir) + .with_context(|| { + format!( + "failed to create temp dir in {:?} for atomic output", + real_out_dir + ) + })?; + let temp_path = temp_dir.path().to_path_buf(); + + trace!( + "[{}]: Redirecting --out-dir from {:?} to {:?}", + out_pretty, real_out_dir, temp_path + ); + + // Build new argument list with --out-dir replaced + let mut new_args: Vec = args.clone(); + let orig_arg = new_args[out_dir_idx].to_string_lossy(); + if orig_arg.starts_with("--out-dir=") { + new_args[out_dir_idx] = OsString::from(format!("--out-dir={}", temp_path.display())); + } else { + new_args[out_dir_idx] = OsString::from(&temp_path); + } + + // Execute with a new command using the rewritten arguments + let mut cmd = creator + .clone() + .new_command_sync(compile_cmd.get_executable()); + cmd.args(&new_args) + .env_clear() + .envs(compile_cmd.get_env_vars()) + .current_dir(compile_cmd.get_cwd()); + let output = run_input_output(cmd, None).await?; + + // After compilation, atomically rename each output file to the real dir + if output.status.success() { + match std::fs::read_dir(&temp_path) { + Ok(entries) => { + for entry in entries { + let entry = entry?; + let file_name = entry.file_name(); + let src = entry.path(); + let dst = real_out_dir.join(&file_name); + std::fs::rename(&src, &dst) + .with_context(|| format!("failed to rename {:?} to {:?}", src, dst))?; + trace!("[{}]: Renamed {:?} -> {:?}", out_pretty, file_name, dst); + } + } + Err(e) => { + warn!( + "[{}]: Failed to read temp dir {:?}: {}", + out_pretty, temp_path, e + ); + } + } + } + // temp_dir dropped here — removes the now-empty directory + + Ok(output) +} + #[cfg(not(feature = "dist-client"))] async fn dist_or_local_compile( service: &server::SccacheService, @@ -839,8 +943,7 @@ where .context("Failed to generate compile commands")?; debug!("[{}]: Compiling locally", out_pretty); - compile_cmd - .execute(&service, &creator) + execute_with_atomic_out_dir(compile_cmd.as_ref(), &service, &creator, &out_pretty) .await .map(move |o| (cacheable, DistType::NoDist, o)) } @@ -873,10 +976,14 @@ where Some(dc) => dc, None => { debug!("[{}]: Compiling locally", out_pretty); - return compile_cmd - .execute(service, &creator) - .await - .map(move |o| (cacheable, DistType::NoDist, o)); + return execute_with_atomic_out_dir( + compile_cmd.as_ref(), + service, + &creator, + &out_pretty, + ) + .await + .map(move |o| (cacheable, DistType::NoDist, o)); } }; diff --git a/src/compiler/rust.rs b/src/compiler/rust.rs index 7a0fdabc3..2fdbcedb0 100644 --- a/src/compiler/rust.rs +++ b/src/compiler/rust.rs @@ -228,6 +228,7 @@ pub struct RustCompilation { pub struct CrateTypes { rlib: bool, staticlib: bool, + others: Vec, } /// Emit types that we will cache. @@ -1075,6 +1076,7 @@ fn parse_arguments(arguments: &[OsString], cwd: &Path) -> CompilerArguments CompilerArguments { // We can't cache non-rlib/staticlib crates, because rustc invokes the // system linker to link them, and we don't know about all the linker inputs. + // However, if SCCACHE_RUST_CRATE_TYPE_ALLOW_HASH is set, we allow caching + // all crate types. The env var value is hashed into the cache key so that + // machines with different linker setups get separate cache entries. if !others.is_empty() { - let others: Vec<&str> = others.iter().map(String::as_str).collect(); - let others_string = others.join(","); - cannot_cache!("crate-type", others_string) + if std::env::var("SCCACHE_RUST_CRATE_TYPE_ALLOW_HASH").is_err() { + let others: Vec<&str> = others.iter().map(String::as_str).collect(); + let others_string = others.join(","); + cannot_cache!("crate-type", others_string) + } + crate_types.others.extend(others.iter().cloned()); } crate_types.rlib |= rlib; crate_types.staticlib |= staticlib; @@ -1235,10 +1243,12 @@ fn parse_arguments(arguments: &[OsString], cwd: &Path) -> CompilerArguments