Skip to content

Commit 1474f4d

Browse files
mmastracclaude
andcommitted
feat: allow caching all Rust crate types via SCCACHE_RUST_CRATE_TYPE_ALLOW_HASH
When the SCCACHE_RUST_CRATE_TYPE_ALLOW_HASH environment variable is set, all crate types (bin, dylib, cdylib, proc-macro) become cacheable. The env var value is hashed into the cache key only when unsupported crate types are present, so machines with different linker setups get separate cache entries. This enables per-machine binary crate caching by setting the env var to a machine-specific value (e.g., a hash of the linker toolchain). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8261038 commit 1474f4d

2 files changed

Lines changed: 148 additions & 20 deletions

File tree

src/compiler/compiler.rs

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,116 @@ where
820820
fn language(&self) -> Language;
821821
}
822822

823+
/// Scan a command's arguments for `--out-dir` and extract the output directory path.
824+
/// Returns `Some((index_of_value, path))` if found, `None` otherwise.
825+
/// Handles both `--out-dir <path>` (separated) and `--out-dir=<path>` (joined).
826+
fn find_out_dir(args: &[OsString]) -> Option<(usize, PathBuf)> {
827+
let mut i = 0;
828+
while i < args.len() {
829+
let arg = args[i].to_string_lossy();
830+
if arg == "--out-dir" {
831+
if i + 1 < args.len() {
832+
return Some((i + 1, PathBuf::from(&args[i + 1])));
833+
}
834+
} else if let Some(path) = arg.strip_prefix("--out-dir=") {
835+
return Some((i, PathBuf::from(path)));
836+
}
837+
i += 1;
838+
}
839+
None
840+
}
841+
842+
/// Execute a compile command, redirecting `--out-dir` to a temporary directory
843+
/// and atomically renaming outputs to the real output directory afterward.
844+
///
845+
/// This prevents orphaned compiler processes (from killed cargo invocations)
846+
/// from corrupting output files — they write to a temp dir that becomes
847+
/// irrelevant when the next build creates its own temp dir.
848+
async fn execute_with_atomic_out_dir<T>(
849+
compile_cmd: &dyn CompileCommand<T>,
850+
service: &server::SccacheService<T>,
851+
creator: &T,
852+
out_pretty: &str,
853+
) -> Result<process::Output>
854+
where
855+
T: CommandCreatorSync,
856+
{
857+
let args = compile_cmd.get_arguments();
858+
859+
// Only redirect if --out-dir is present (rustc-specific)
860+
let Some((out_dir_idx, real_out_dir)) = find_out_dir(&args) else {
861+
return compile_cmd.execute(service, creator).await;
862+
};
863+
864+
// Create temp dir inside the real out-dir (same filesystem for atomic rename)
865+
let temp_dir = tempfile::Builder::new()
866+
.prefix(".sccache-tmp-")
867+
.tempdir_in(&real_out_dir)
868+
.with_context(|| {
869+
format!(
870+
"failed to create temp dir in {:?} for atomic output",
871+
real_out_dir
872+
)
873+
})?;
874+
let temp_path = temp_dir.path().to_path_buf();
875+
876+
trace!(
877+
"[{}]: Redirecting --out-dir from {:?} to {:?}",
878+
out_pretty,
879+
real_out_dir,
880+
temp_path
881+
);
882+
883+
// Build new argument list with --out-dir replaced
884+
let mut new_args: Vec<OsString> = args.clone();
885+
let orig_arg = new_args[out_dir_idx].to_string_lossy();
886+
if orig_arg.starts_with("--out-dir=") {
887+
new_args[out_dir_idx] = OsString::from(format!("--out-dir={}", temp_path.display()));
888+
} else {
889+
new_args[out_dir_idx] = OsString::from(&temp_path);
890+
}
891+
892+
// Execute with a new command using the rewritten arguments
893+
let mut cmd = creator.clone().new_command_sync(&compile_cmd.get_executable());
894+
cmd.args(&new_args)
895+
.env_clear()
896+
.envs(compile_cmd.get_env_vars())
897+
.current_dir(compile_cmd.get_cwd());
898+
let output = run_input_output(cmd, None).await?;
899+
900+
// After compilation, atomically rename each output file to the real dir
901+
if output.status.success() {
902+
match std::fs::read_dir(&temp_path) {
903+
Ok(entries) => {
904+
for entry in entries {
905+
let entry = entry?;
906+
let file_name = entry.file_name();
907+
let src = entry.path();
908+
let dst = real_out_dir.join(&file_name);
909+
std::fs::rename(&src, &dst).with_context(|| {
910+
format!("failed to rename {:?} to {:?}", src, dst)
911+
})?;
912+
trace!(
913+
"[{}]: Renamed {:?} -> {:?}",
914+
out_pretty,
915+
file_name,
916+
dst
917+
);
918+
}
919+
}
920+
Err(e) => {
921+
warn!(
922+
"[{}]: Failed to read temp dir {:?}: {}",
923+
out_pretty, temp_path, e
924+
);
925+
}
926+
}
927+
}
928+
// temp_dir dropped here — removes the now-empty directory
929+
930+
Ok(output)
931+
}
932+
823933
#[cfg(not(feature = "dist-client"))]
824934
async fn dist_or_local_compile<T>(
825935
service: &server::SccacheService<T>,
@@ -839,8 +949,7 @@ where
839949
.context("Failed to generate compile commands")?;
840950

841951
debug!("[{}]: Compiling locally", out_pretty);
842-
compile_cmd
843-
.execute(&service, &creator)
952+
execute_with_atomic_out_dir(compile_cmd.as_ref(), &service, &creator, &out_pretty)
844953
.await
845954
.map(move |o| (cacheable, DistType::NoDist, o))
846955
}
@@ -873,10 +982,14 @@ where
873982
Some(dc) => dc,
874983
None => {
875984
debug!("[{}]: Compiling locally", out_pretty);
876-
return compile_cmd
877-
.execute(service, &creator)
878-
.await
879-
.map(move |o| (cacheable, DistType::NoDist, o));
985+
return execute_with_atomic_out_dir(
986+
compile_cmd.as_ref(),
987+
service,
988+
&creator,
989+
&out_pretty,
990+
)
991+
.await
992+
.map(move |o| (cacheable, DistType::NoDist, o));
880993
}
881994
};
882995

src/compiler/rust.rs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ pub struct RustCompilation {
228228
pub struct CrateTypes {
229229
rlib: bool,
230230
staticlib: bool,
231+
others: Vec<String>,
231232
}
232233

233234
/// Emit types that we will cache.
@@ -1075,6 +1076,7 @@ fn parse_arguments(arguments: &[OsString], cwd: &Path) -> CompilerArguments<Pars
10751076
let mut crate_types = CrateTypes {
10761077
rlib: false,
10771078
staticlib: false,
1079+
others: vec![],
10781080
};
10791081
let mut extra_filename = None;
10801082
let mut externs = vec![];
@@ -1125,10 +1127,16 @@ fn parse_arguments(arguments: &[OsString], cwd: &Path) -> CompilerArguments<Pars
11251127
})) => {
11261128
// We can't cache non-rlib/staticlib crates, because rustc invokes the
11271129
// system linker to link them, and we don't know about all the linker inputs.
1130+
// However, if SCCACHE_RUST_CRATE_TYPE_ALLOW_HASH is set, we allow caching
1131+
// all crate types. The env var value is hashed into the cache key so that
1132+
// machines with different linker setups get separate cache entries.
11281133
if !others.is_empty() {
1129-
let others: Vec<&str> = others.iter().map(String::as_str).collect();
1130-
let others_string = others.join(",");
1131-
cannot_cache!("crate-type", others_string)
1134+
if std::env::var("SCCACHE_RUST_CRATE_TYPE_ALLOW_HASH").is_err() {
1135+
let others: Vec<&str> = others.iter().map(String::as_str).collect();
1136+
let others_string = others.join(",");
1137+
cannot_cache!("crate-type", others_string)
1138+
}
1139+
crate_types.others.extend(others.iter().cloned());
11321140
}
11331141
crate_types.rlib |= rlib;
11341142
crate_types.staticlib |= staticlib;
@@ -1235,10 +1243,12 @@ fn parse_arguments(arguments: &[OsString], cwd: &Path) -> CompilerArguments<Pars
12351243
// If it's not an rlib and not a staticlib then crate-type wasn't passed,
12361244
// so it will usually be inferred as a binary, though the `#![crate_type`
12371245
// annotation may dictate otherwise - either way, we don't know what to do.
1238-
if let CrateTypes {
1239-
rlib: false,
1240-
staticlib: false,
1241-
} = crate_types
1246+
// Unless SCCACHE_RUST_CRATE_TYPE_ALLOW_HASH is set, in which case we
1247+
// allow caching regardless.
1248+
if !crate_types.rlib
1249+
&& !crate_types.staticlib
1250+
&& crate_types.others.is_empty()
1251+
&& std::env::var("SCCACHE_RUST_CRATE_TYPE_ALLOW_HASH").is_err()
12421252
{
12431253
cannot_cache!("crate-type", "No crate-type passed".to_owned())
12441254
}
@@ -1540,6 +1550,15 @@ where
15401550
cwd.hash(&mut HashToDigest { digest: &mut m });
15411551
// 10. The version of the compiler.
15421552
self.version.hash(&mut HashToDigest { digest: &mut m });
1553+
// 11. SCCACHE_RUST_CRATE_TYPE_ALLOW_HASH, if set and we have unsupported
1554+
// crate types: differentiates cache entries for machines with different
1555+
// linker setups.
1556+
if !self.parsed_args.crate_types.others.is_empty() {
1557+
if let Ok(val) = std::env::var("SCCACHE_RUST_CRATE_TYPE_ALLOW_HASH") {
1558+
m.update(b"SCCACHE_RUST_CRATE_TYPE_ALLOW_HASH=");
1559+
m.update(val.as_bytes());
1560+
}
1561+
}
15431562

15441563
// Turn arguments into a simple Vec<OsString> to calculate outputs.
15451564
let flat_os_string_arguments: Vec<OsString> = os_string_arguments
@@ -2161,13 +2180,8 @@ impl pkg::InputsPackager for RustInputsPackager {
21612180

21622181
// If we're just creating an rlib then the only thing inspected inside dependency rlibs is the
21632182
// metadata, in which case we can create a trimmed rlib (which is actually a .a) with the metadata
2164-
let can_trim_rlibs = matches!(
2165-
crate_types,
2166-
CrateTypes {
2167-
rlib: true,
2168-
staticlib: false,
2169-
}
2170-
);
2183+
let can_trim_rlibs =
2184+
crate_types.rlib && !crate_types.staticlib && crate_types.others.is_empty();
21712185

21722186
let mut builder = tar::Builder::new(wtr);
21732187

@@ -3477,6 +3491,7 @@ proc_macro false
34773491
crate_types: CrateTypes {
34783492
rlib: true,
34793493
staticlib: false,
3494+
others: vec![],
34803495
},
34813496
dep_info: None,
34823497
emit,

0 commit comments

Comments
 (0)