Skip to content

Commit ce9f95b

Browse files
committed
Avoid double-caching when ccache is installed in PATH
On Linux, ccache is typically installed in $PATH as /usr/lib64/ccache/g++ or similar. If we keep it there, all compilations will be cached by both ccache and sccache. While the user could easily disable ccache with CCACHE_DISABLE, it's reasonable to assume many will forget and will have their disk space doubly consumed by both caches. Better to recognize this and disable ccache under sccache. This patch does this by removing the ccache binary paths from $PATH. The check is fuzzy - any directory that has a `ccache` component is removed. It's assumed that people won't have directories with such names in $PATH that are not ccache masquarade directories. Fixes #2519
1 parent 2b92721 commit ce9f95b

File tree

3 files changed

+142
-9
lines changed

3 files changed

+142
-9
lines changed

src/compiler/compiler.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ use crate::dist::pkg;
3535
use crate::lru_disk_cache;
3636
use crate::mock_command::{CommandChild, CommandCreatorSync, RunCommand, exit_status};
3737
use crate::server;
38-
use crate::util::{fmt_duration_as_secs, run_input_output};
38+
use crate::util::{filter_ccache_from_path, fmt_duration_as_secs, run_input_output};
3939
use crate::{counted_array, dist};
4040
use async_trait::async_trait;
4141
use filetime::FileTime;
@@ -194,10 +194,13 @@ impl CompileCommandImpl for SingleCompileCommand {
194194
env_vars,
195195
cwd,
196196
} = self;
197+
// Filter out ccache directories from PATH to avoid double-caching
198+
// when ccache is also installed on the system.
199+
let env_vars = filter_ccache_from_path(env_vars.to_vec());
197200
let mut cmd = creator.clone().new_command_sync(executable);
198201
cmd.args(arguments)
199202
.env_clear()
200-
.envs(env_vars.to_vec())
203+
.envs(env_vars)
201204
.current_dir(cwd);
202205
run_input_output(cmd, None).await
203206
}
@@ -1662,6 +1665,10 @@ compiler_version=__VERSION__
16621665
.to_vec();
16631666
let (tempdir, src) = write_temp_file(&pool, "testfile.c".as_ref(), test).await?;
16641667

1668+
// Filter out ccache directories from PATH to avoid double-caching
1669+
// when ccache is also installed on the system.
1670+
let env = filter_ccache_from_path(env);
1671+
16651672
let executable = executable.as_ref();
16661673
let mut cmd = creator.clone().new_command_sync(executable);
16671674
cmd.stdout(Stdio::piped())

src/compiler/nvcc.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::compiler::{
2525
use crate::mock_command::{
2626
CommandChild, CommandCreator, CommandCreatorSync, ExitStatusValue, RunCommand, exit_status,
2727
};
28-
use crate::util::{OsStrExt, run_input_output};
28+
use crate::util::{OsStrExt, filter_ccache_from_path, run_input_output};
2929
use crate::{counted_array, dist, protocol, server};
3030
use async_trait::async_trait;
3131
use fs::File;
@@ -181,6 +181,10 @@ impl CCompilerImpl for Nvcc {
181181
_ => Err(anyhow!("PCH not supported by nvcc")),
182182
}?;
183183

184+
// Filter out ccache directories from PATH to avoid double-caching
185+
// when ccache is also installed on the system.
186+
let env_vars = filter_ccache_from_path(env_vars.clone());
187+
184188
let initialize_cmd_and_args = || {
185189
let mut command = creator.clone().new_command_sync(executable);
186190
command
@@ -913,7 +917,7 @@ async fn select_nvcc_subcommands<T, F>(
913917
creator: &T,
914918
executable: &Path,
915919
cwd: &Path,
916-
env_vars: &mut Vec<(OsString, OsString)>,
920+
env_vars: &mut [(OsString, OsString)],
917921
remap_filenames: bool,
918922
arguments: &[OsString],
919923
select_subcommand: F,
@@ -938,13 +942,17 @@ where
938942
);
939943
}
940944

945+
// Filter out ccache directories from PATH to avoid double-caching
946+
// when ccache is also installed on the system.
947+
let mut env_vars = filter_ccache_from_path(env_vars.to_vec());
948+
941949
let mut nvcc_dryrun_cmd = creator.clone().new_command_sync(executable);
942950

943951
nvcc_dryrun_cmd
944952
.args(&[arguments, &["--dryrun".into(), "--keep".into()][..]].concat())
945953
.env_clear()
946954
.current_dir(cwd)
947-
.envs(env_vars.to_vec());
955+
.envs(env_vars.clone());
948956

949957
let nvcc_dryrun_output = run_input_output(nvcc_dryrun_cmd, None).await?;
950958

@@ -1257,14 +1265,18 @@ where
12571265
);
12581266
}
12591267

1268+
// Filter out ccache directories from PATH to avoid double-caching
1269+
// when ccache is also installed on the system.
1270+
let env_vars = filter_ccache_from_path(env_vars.to_vec());
1271+
12601272
let out = match cacheable {
12611273
Cacheable::No => {
12621274
let mut cmd = creator.clone().new_command_sync(exe);
12631275

12641276
cmd.args(args)
12651277
.current_dir(cwd)
12661278
.env_clear()
1267-
.envs(env_vars.to_vec());
1279+
.envs(env_vars.clone());
12681280

12691281
run_input_output(cmd, None)
12701282
.await
@@ -1275,11 +1287,11 @@ where
12751287
let args = dist::strings_to_osstrings(args);
12761288

12771289
match srvc
1278-
.compiler_info(exe.clone(), cwd.to_owned(), &args, env_vars)
1290+
.compiler_info(exe.clone(), cwd.to_owned(), &args, &env_vars)
12791291
.await
12801292
{
12811293
Err(err) => error_to_output(err),
1282-
Ok(compiler) => match compiler.parse_arguments(&args, cwd, env_vars) {
1294+
Ok(compiler) => match compiler.parse_arguments(&args, cwd, &env_vars) {
12831295
CompilerArguments::NotCompilation => Err(anyhow!("Not compilation")),
12841296
CompilerArguments::CannotCache(why, extra_info) => Err(extra_info
12851297
.map_or_else(

src/util.rs

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,11 +1015,125 @@ pub fn num_cpus() -> usize {
10151015
std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get)
10161016
}
10171017

1018+
/// Filter out ccache directories from PATH environment variable.
1019+
///
1020+
/// This prevents double-caching when ccache is installed and has
1021+
/// /usr/lib/ccache or /usr/lib64/ccache (or similar) in PATH, since those
1022+
/// directories contain wrapper scripts that would call ccache.
1023+
pub fn filter_ccache_from_path(env_vars: Vec<(OsString, OsString)>) -> Vec<(OsString, OsString)> {
1024+
use std::env;
1025+
1026+
const CCACHE_DIR_COMPONENT: &str = "ccache";
1027+
1028+
let path_contains_ccache = |value: &OsString| -> bool {
1029+
std::env::split_paths(value).any(|p| {
1030+
p.components()
1031+
.any(|c| c.as_os_str() == CCACHE_DIR_COMPONENT)
1032+
})
1033+
};
1034+
1035+
// First pass: check if any PATH contains ccache
1036+
let has_ccache = env_vars
1037+
.iter()
1038+
.any(|(key, value)| key == "PATH" && path_contains_ccache(value));
1039+
1040+
if !has_ccache {
1041+
return env_vars;
1042+
}
1043+
1044+
// Second pass: filter out ccache directories from PATH
1045+
env_vars
1046+
.into_iter()
1047+
.map(|(key, value)| {
1048+
if key == "PATH" {
1049+
let filtered_path = env::split_paths(&value)
1050+
.filter(|p| {
1051+
!p.components()
1052+
.any(|c| c.as_os_str() == CCACHE_DIR_COMPONENT)
1053+
})
1054+
.collect::<Vec<_>>();
1055+
let new_value = env::join_paths(filtered_path).unwrap_or(value.clone());
1056+
(key, new_value)
1057+
} else {
1058+
(key, value)
1059+
}
1060+
})
1061+
.collect()
1062+
}
1063+
10181064
#[cfg(test)]
10191065
mod tests {
1020-
use super::{OsStrExt, TimeMacroFinder};
1066+
use super::{OsStrExt, TimeMacroFinder, filter_ccache_from_path};
10211067
use std::ffi::{OsStr, OsString};
10221068

1069+
#[test]
1070+
fn test_filter_ccache_from_path() {
1071+
use std::env;
1072+
1073+
// Create a PATH with ccache directories
1074+
let path_with_ccache = env::join_paths([
1075+
"/usr/bin",
1076+
"/usr/lib64/ccache",
1077+
"/usr/local/bin",
1078+
"/usr/lib/ccache",
1079+
"/home/user/bin",
1080+
])
1081+
.unwrap();
1082+
1083+
let env_vars = vec![
1084+
(OsString::from("HOME"), OsString::from("/home/user")),
1085+
(OsString::from("PATH"), path_with_ccache),
1086+
(OsString::from("LANG"), OsString::from("en_US.UTF-8")),
1087+
];
1088+
1089+
let filtered = filter_ccache_from_path(env_vars);
1090+
1091+
// Other env vars should be unchanged
1092+
assert_eq!(filtered.len(), 3);
1093+
assert_eq!(
1094+
filtered.iter().find(|(k, _)| k == "HOME").unwrap().1,
1095+
OsString::from("/home/user")
1096+
);
1097+
assert_eq!(
1098+
filtered.iter().find(|(k, _)| k == "LANG").unwrap().1,
1099+
OsString::from("en_US.UTF-8")
1100+
);
1101+
1102+
// PATH should have ccache directories removed
1103+
let new_path = &filtered.iter().find(|(k, _)| k == "PATH").unwrap().1;
1104+
let expected_path =
1105+
env::join_paths(["/usr/bin", "/usr/local/bin", "/home/user/bin"]).unwrap();
1106+
assert_eq!(new_path, &expected_path);
1107+
}
1108+
1109+
#[test]
1110+
fn test_filter_ccache_from_path_no_ccache() {
1111+
use std::env;
1112+
1113+
// Create a PATH without ccache directories
1114+
let path_without_ccache = env::join_paths(["/usr/bin", "/usr/local/bin"]).unwrap();
1115+
1116+
let env_vars = vec![(OsString::from("PATH"), path_without_ccache.clone())];
1117+
1118+
let filtered = filter_ccache_from_path(env_vars);
1119+
1120+
assert_eq!(filtered.len(), 1);
1121+
assert_eq!(filtered[0].1, path_without_ccache);
1122+
}
1123+
1124+
#[test]
1125+
fn test_filter_ccache_from_path_no_path() {
1126+
// No PATH variable at all
1127+
let env_vars = vec![
1128+
(OsString::from("HOME"), OsString::from("/home/user")),
1129+
(OsString::from("LANG"), OsString::from("en_US.UTF-8")),
1130+
];
1131+
1132+
let filtered = filter_ccache_from_path(env_vars.clone());
1133+
1134+
assert_eq!(filtered, env_vars);
1135+
}
1136+
10231137
#[test]
10241138
fn simple_starts_with() {
10251139
let a: &OsStr = "foo".as_ref();

0 commit comments

Comments
 (0)