diff --git a/docs/Rust.md b/docs/Rust.md index 5d6f98c3cf..f36b1a2704 100644 --- a/docs/Rust.md +++ b/docs/Rust.md @@ -9,5 +9,9 @@ sccache includes support for caching Rust compilation. This includes many caveat * Procedural macros that read files from the filesystem may not be cached properly. * `rustc`'s incremental compilation needs to be disabled. See [The Cargo Book](https://doc.rust-lang.org/cargo/reference/profiles.html#incremental) * Crates that invoke the system linker cannot be cached. Examples are `bin`, `dylib`, `cdylib`, and `proc-macro` crates. +* `SCCACHE_BASEDIRS` normalizes paths in Rust cache keys when rustc's `--remap-path-prefix` covers the working directory with the default or `all` remap scope. Cached dep-info is rebuilt with output targets and dependency records from the current invocation. +* Path normalization remains location-sensitive when explicit external crates or dynamic libraries on crate search paths may invoke procedural macros, because they can observe physical paths without reporting a dependency to rustc. +* Rust path normalization is limited to ASCII paths on Windows. +* Distributed Rust compilation falls back to local compilation when `--remap-path-prefix` is used with a non-identity path transformer. If you are using Rust 1.18 or later, you can ask cargo to wrap all compilation with sccache by setting `RUSTC_WRAPPER=sccache` in your build environment. diff --git a/src/compiler/compiler.rs b/src/compiler/compiler.rs index 50c4a4ecdd..f446b5c465 100644 --- a/src/compiler/compiler.rs +++ b/src/compiler/compiler.rs @@ -638,7 +638,11 @@ where }; let hit = CompileResult::CacheHit(duration); - match entry.extract_objects(filtered_outputs, &pool).await { + let extraction = entry + .extract_objects(filtered_outputs, &pool) + .await + .and_then(|()| compilation.postprocess_cache_hit(&cwd)); + match extraction { Ok(()) => Ok(CacheLookupResult::Success(hit, output)), Err(e) => { if e.downcast_ref::().is_some() { @@ -1112,6 +1116,11 @@ where true } + /// Adjust extracted outputs for the current invocation after a cache hit. + fn postprocess_cache_hit(&self, _cwd: &Path) -> Result<()> { + Ok(()) + } + /// Returns an iterator over the results of this compilation. /// /// Each item is a descriptive (and unique) name of the output paired with diff --git a/src/compiler/rust.rs b/src/compiler/rust.rs index 779e9dd79e..7502e9ab0a 100644 --- a/src/compiler/rust.rs +++ b/src/compiler/rust.rs @@ -50,7 +50,7 @@ use std::future::Future; use std::hash::Hash; #[cfg(feature = "dist-client")] use std::io; -use std::io::{BufReader, Read}; +use std::io::{BufReader, Read, Write}; use std::iter; use std::path::{Path, PathBuf}; use std::pin::Pin; @@ -62,6 +62,239 @@ use std::time; use crate::errors::*; +#[cfg(not(windows))] +fn strip_basedir_prefix<'a>(value: &'a [u8], basedirs: &[Vec]) -> Cow<'a, [u8]> { + let prefix_len = basedirs + .iter() + .filter_map(|basedir| { + value + .starts_with(basedir) + .then_some(basedir.len()) + .or_else(|| (basedir.strip_suffix(b"/") == Some(value)).then_some(value.len())) + }) + .max() + .unwrap_or(0); + Cow::Borrowed(&value[prefix_len..]) +} + +#[cfg(windows)] +fn strip_basedir_prefix<'a>(value: &'a [u8], basedirs: &[Vec]) -> Cow<'a, [u8]> { + if !value.is_ascii() { + return Cow::Borrowed(value); + } + let normalized = crate::util::normalize_win_path(value); + let prefix_len = basedirs + .iter() + .filter_map(|basedir| { + normalized + .starts_with(basedir) + .then_some(basedir.len()) + .or_else(|| { + (basedir.strip_suffix(b"/") == Some(normalized.as_slice())) + .then_some(normalized.len()) + }) + }) + .max() + .unwrap_or(0); + if prefix_len == 0 { + Cow::Borrowed(value) + } else { + Cow::Borrowed(&value[prefix_len..]) + } +} + +fn normalize_arg_value<'a>( + flag: &OsString, + value: &'a OsString, + basedirs: &[Vec], +) -> Cow<'a, [u8]> { + let value = value.as_encoded_bytes(); + if flag == "--remap-path-prefix" { + let Some(separator) = value.iter().rposition(|byte| *byte == b'=') else { + return Cow::Borrowed(value); + }; + let stripped = strip_basedir_prefix(&value[..separator], basedirs); + if stripped.len() == separator { + return Cow::Borrowed(value); + } + let mut result = stripped.into_owned(); + result.extend_from_slice(&value[separator..]); + return Cow::Owned(result); + } + Cow::Borrowed(value) +} + +fn remap_scope_is_all(arguments: &[(OsString, Option)]) -> bool { + arguments + .iter() + .filter_map(|(flag, value)| { + if flag == "--remap-path-scope" { + return value.as_ref()?.to_str(); + } + if flag == "-Z" { + return value.as_ref()?.to_str()?.strip_prefix("remap-path-scope="); + } + flag.to_str()?.strip_prefix("--remap-path-scope=") + }) + .next_back() + .is_none_or(|scope| scope.split(',').any(|scope| scope == "all")) +} + +fn remap_path(path: &Path, arguments: &[(OsString, Option)]) -> Option { + if !remap_scope_is_all(arguments) { + return None; + } + for (flag, value) in arguments.iter().rev() { + if flag != "--remap-path-prefix" { + continue; + } + let (prefix, replacement) = value.as_ref()?.to_str()?.rsplit_once('=')?; + if let Ok(suffix) = path.strip_prefix(prefix) { + let remapped = if suffix.as_os_str().is_empty() { + PathBuf::from(replacement) + } else { + Path::new(replacement).join(suffix) + }; + return Some(remapped.into_os_string()); + } + } + None +} + +fn is_path_cargo_env(var: &OsString) -> bool { + matches!( + var.to_str(), + Some( + "CARGO_HOME" + | "CARGO_INSTALL_ROOT" + | "CARGO_MANIFEST_DIR" + | "CARGO_MANIFEST_PATH" + | "CARGO_TARGET_DIR" + | "CARGO_TARGET_TMPDIR" + ) + ) || var.as_encoded_bytes().starts_with(b"CARGO_BIN_EXE_") +} + +fn omits_dep_info_target(flag: &OsString, value: Option<&OsString>) -> bool { + flag == "-Z" + && value.and_then(|value| value.to_str()).is_some_and(|value| { + value.split_once('=').map_or(value, |(name, _)| name) == "dep-info-omit-d-target" + }) +} + +#[derive(Clone, Debug)] +struct DepInfoTemplate { + target_dependencies: String, + tail: String, +} + +fn rewrite_dep_info_targets( + dep_info: &Path, + outputs: &HashMap, + cwd: &Path, + template: &DepInfoTemplate, +) -> Result<()> { + let dep_info = cwd.join(dep_info); + let deps = fs::read_to_string(&dep_info).context("Failed to read cached Rust dep-info")?; + let mut output_targets = Vec::new(); + + for line in deps.split_inclusive('\n') { + if line.trim().is_empty() { + continue; + } + + let mut matched = None; + for (name, output) in outputs { + let marker = format!("{name}: "); + for (marker_start, _) in line.match_indices(&marker) { + let is_boundary = if marker_start == 0 { + true + } else { + let previous = line.as_bytes()[marker_start - 1]; + #[cfg(windows)] + let is_separator = matches!(previous, b'/' | b'\\'); + #[cfg(not(windows))] + let is_separator = previous == b'/'; + #[cfg(windows)] + let is_drive_relative = marker_start == 2 + && previous == b':' + && line.as_bytes()[0].is_ascii_alphabetic(); + #[cfg(not(windows))] + let is_drive_relative = false; + is_separator || is_drive_relative + }; + if !is_boundary { + continue; + } + + let separator = marker_start + name.len(); + if let Some((best_separator, best_name_len, _)) = matched { + if separator != best_separator { + bail!( + "Ambiguous output target in cached Rust dep-info {}", + dep_info.display() + ); + } + if name.len() > best_name_len { + matched = Some((separator, name.len(), output)); + } + } else { + matched = Some((separator, name.len(), output)); + } + } + } + let Some((_, _, output)) = matched else { + if output_targets.is_empty() { + bail!( + "No output targets matched cached Rust dep-info {}", + dep_info.display() + ); + } + break; + }; + + output_targets.push(output); + } + + let mut rewritten = String::with_capacity(deps.len()); + for (index, output) in output_targets.iter().enumerate() { + rewritten.push_str(&output.path.to_string_lossy()); + rewritten.push_str(&template.target_dependencies); + if index + 1 < output_targets.len() { + rewritten.push('\n'); + } + } + rewritten.push_str(&template.tail); + + if rewritten != deps { + let parent = dep_info + .parent() + .context("Cached Rust dep-info has no parent directory")?; + let permissions = fs::metadata(&dep_info)?.permissions(); + let mut temp = tempfile::NamedTempFile::new_in(parent) + .context("Failed to create temporary Rust dep-info")?; + temp.write_all(rewritten.as_bytes())?; + #[cfg(not(windows))] + temp.as_file().set_permissions(permissions.clone())?; + #[cfg(windows)] + if permissions.readonly() { + let mut writable = permissions.clone(); + writable.set_readonly(false); + fs::set_permissions(&dep_info, writable)?; + } + if let Err(error) = temp.persist(&dep_info) { + #[cfg(windows)] + if permissions.readonly() { + let _ = fs::set_permissions(&dep_info, permissions); + } + return Err(error.error).context("Failed to replace cached Rust dep-info"); + } + #[cfg(windows)] + fs::set_permissions(&dep_info, permissions)?; + } + Ok(()) +} + #[cfg(feature = "dist-client")] const RLIB_PREFIX: &str = "lib"; #[cfg(feature = "dist-client")] @@ -217,6 +450,8 @@ pub struct RustCompilation { crate_types: CrateTypes, /// If dependency info is being emitted, the name of the dep info file. dep_info: Option, + /// Dependency records generated locally for the current invocation. + dep_info_template: Option, /// The current working directory cwd: PathBuf, /// The environment variables @@ -235,7 +470,7 @@ static ALLOWED_EMIT: LazyLock> = LazyLock::new(|| ["link", "metadata", "dep-info"].iter().copied().collect()); /// Version number for cache key. -const CACHE_VERSION: &[u8] = b"6"; +const CACHE_VERSION: &[u8] = b"7"; /// Get absolute paths for all source files and env-deps listed in rustc's dep-info output. async fn get_source_files_and_env_deps( @@ -246,7 +481,7 @@ async fn get_source_files_and_env_deps( cwd: &Path, env_vars: &[(OsString, OsString)], pool: &tokio::runtime::Handle, -) -> Result<(Vec, Vec<(OsString, OsString)>)> +) -> Result<(Vec, Vec<(OsString, OsString)>, DepInfoTemplate)> where T: CommandCreatorSync, { @@ -278,7 +513,7 @@ where }) .await?; - parsed.map(move |(files, env_deps)| { + parsed.map(move |(files, env_deps, template)| { trace!( "[{}]: got {} source files and {} env-deps from dep-info in {}", crate_name, @@ -288,13 +523,16 @@ where ); // Just to make sure we capture temp_dir. drop(temp_dir); - (files, env_deps) + (files, env_deps, template) }) } /// Parse dependency info from `file` and return a Vec of files mentioned. /// Treat paths as relative to `cwd`. -fn parse_dep_file(file: T, cwd: U) -> Result<(Vec, Vec<(OsString, OsString)>)> +fn parse_dep_file( + file: T, + cwd: U, +) -> Result<(Vec, Vec<(OsString, OsString)>, DepInfoTemplate)> where T: AsRef, U: AsRef, @@ -302,7 +540,25 @@ where let mut f = fs::File::open(file.as_ref())?; let mut deps = String::new(); f.read_to_string(&mut deps)?; - Ok((parse_dep_info(&deps, cwd), parse_env_dep_info(&deps))) + let (target_line, tail) = deps + .split_once('\n') + .context("Rust dep-info has no target line")?; + let target = file.as_ref().to_string_lossy(); + let mut target_dependencies = target_line + .strip_prefix(&*target) + .filter(|suffix| suffix.starts_with(": ")) + .context("Rust dep-info target does not match requested path")? + .to_owned(); + target_dependencies.push('\n'); + let source_files = parse_dep_info(&format!("target{target_dependencies}"), cwd); + Ok(( + source_files, + parse_env_dep_info(&deps), + DepInfoTemplate { + target_dependencies, + tail: tail.to_owned(), + }, + )) } fn parse_dep_info(dep_info: &str, cwd: T) -> Vec @@ -1045,6 +1301,7 @@ counted_array!(static ARGS: [ArgInfo; _] = [ take_arg!("--pretty", OsString, CanBeSeparated(b'='), NotCompilation), take_arg!("--print", OsString, CanBeSeparated(b'='), NotCompilation), take_arg!("--remap-path-prefix", OsString, CanBeSeparated(b'='), PassThrough), + take_arg!("--remap-path-scope", OsString, CanBeSeparated(b'='), PassThrough), take_arg!("--sysroot", PathBuf, CanBeSeparated(b'='), TooHardPath), take_arg!("--target", ArgTarget, CanBeSeparated(b'='), Target), take_arg!("--unpretty", OsString, CanBeSeparated(b'='), NotCompilation), @@ -1388,10 +1645,31 @@ where _may_dist: bool, pool: &tokio::runtime::Handle, _rewrite_includes_only: bool, - _storage: Arc, + storage: Arc, _cache_control: CacheControl, ) -> Result> { trace!("[{}]: generate_hash_key", self.parsed_args.crate_name); + let basedirs = storage.basedirs(); + // Procedural macros can observe the physical working directory and inherited environment + // variables without reporting them in rustc dep-info. Conservatively retain physical paths + // whenever an explicit extern or dynamic crate search entry may load one. + let can_normalize_paths = !basedirs.is_empty() + && self.parsed_args.externs.is_empty() + && !self.parsed_args.crate_link_paths.iter().any(|path| { + fs::read_dir(path) + .map(|mut entries| { + entries.any(|entry| { + entry.map_or(true, |entry| { + entry + .path() + .extension() + .is_some_and(|ext| ext == DLL_EXTENSION) + }) + }) + }) + .unwrap_or(true) + }); + let normalized_basedirs: &[Vec] = if can_normalize_paths { basedirs } else { &[] }; // TODO: this doesn't produce correct arguments if they should be concatenated - should use iter_os_strings let os_string_arguments: Vec<(OsString, Option)> = self .parsed_args @@ -1404,13 +1682,21 @@ where ) }) .collect(); + let remap_input_path = |path: &Path| { + if can_normalize_paths { + remap_path(path, &os_string_arguments) + } else { + None + } + }; // `filtered_arguments` omits --emit and --out-dir arguments. // It's used for invoking rustc with `--emit=dep-info` to get the list of // source files for this crate. let filtered_arguments = os_string_arguments .iter() .filter_map(|(arg, val)| { - if arg == "--emit" || arg == "--out-dir" { + if arg == "--emit" || arg == "--out-dir" || omits_dep_info_target(arg, val.as_ref()) + { None } else { Some((arg, val)) @@ -1422,7 +1708,7 @@ where // Find all the source files and hash them let source_hashes_pool = pool.clone(); let source_files_and_hashes_and_env_deps = async { - let (source_files, env_deps) = get_source_files_and_env_deps( + let (source_files, env_deps, dep_info_template) = get_source_files_and_env_deps( creator, &self.parsed_args.crate_name, &self.executable, @@ -1433,7 +1719,7 @@ where ) .await?; let source_hashes = hash_all(&source_files, &source_hashes_pool).await?; - Ok((source_files, source_hashes, env_deps)) + Ok((source_files, source_hashes, env_deps, dep_info_template)) }; // Hash the contents of the externs listed on the commandline. @@ -1474,12 +1760,11 @@ where let abs_target_json = cwd.join(path); target_json_files.push(abs_target_json); } - let target_json_hash = hash_all(&target_json_files, pool); // Perform all hashing operations on the files. let ( - (source_files, source_hashes, mut env_deps), + (source_files, source_hashes, mut env_deps, dep_info_template), extern_hashes, staticlib_hashes, target_json_hash, @@ -1501,12 +1786,10 @@ where } let weak_toolchain_key = m.clone().finish(); // 3. The full commandline (self.arguments) - // TODO: there will be full paths here, it would be nice to - // normalize them so we can get cross-machine cache hits. // A few argument types are not passed in a deterministic order // by cargo: --extern, -L, --cfg. We'll filter those out, sort them, // and append them to the rest of the arguments. - let args = { + { let (mut sortables, rest): (Vec<_>, Vec<_>) = os_string_arguments .iter() // We exclude a few arguments from the hash: @@ -1530,22 +1813,72 @@ where // out, sort them, and append them to the rest of the arguments. .partition(|&(arg, _)| arg == "--cfg"); sortables.sort(); - rest.into_iter() - .chain(sortables) - .flat_map(|(arg, val)| iter::once(arg).chain(val.as_ref())) - .fold(OsString::new(), |mut a, b| { - a.push(b); - a - }) - }; - args.hash(&mut HashToDigest { digest: &mut m }); - // 4. The digest of all source files (this includes src file from cmdline). + let mut hash_arg = |normalized: bool, value: &[u8]| { + normalized.hash(&mut HashToDigest { digest: &mut m }); + value.hash(&mut HashToDigest { digest: &mut m }); + }; + for (arg, value) in rest.into_iter().chain(sortables) { + let arg_bytes = arg.as_encoded_bytes(); + if value.is_none() { + if let Some(remapped) = remap_input_path(Path::new(arg)) { + hash_arg(true, remapped.as_encoded_bytes()); + } else { + hash_arg(false, arg_bytes); + } + } else { + hash_arg(false, arg_bytes); + } + if let Some(value) = value { + let value_bytes = value.as_encoded_bytes(); + let normalized = normalize_arg_value(arg, value, normalized_basedirs); + let normalized_bytes: &[u8] = &normalized; + hash_arg(normalized_bytes != value_bytes, normalized_bytes); + } + } + } + // 4. The effective path and digest of all source files (this includes src file from cmdline). // 5. The digest of all files listed on the commandline (self.externs). // 6. The digest of all static libraries listed on the commandline (self.staticlibs). // 7. The digest of the content of the target json file specified via `--target` (if any). - for h in source_hashes + let mut source_inputs = source_files + .iter() + .zip(source_hashes) + .map(|(path, hash)| { + if let Some(remapped) = remap_input_path(path) { + (true, remapped, path.as_os_str().to_owned(), hash) + } else { + ( + false, + path.as_os_str().to_owned(), + path.as_os_str().to_owned(), + hash, + ) + } + }) + .collect::>(); + source_inputs.sort(); + for index in 0..source_inputs.len() { + let (normalized, path, original_path, hash) = &source_inputs[index]; + let previous_is_duplicate = index > 0 + && normalized == &source_inputs[index - 1].0 + && path == &source_inputs[index - 1].1; + let next_is_duplicate = + source_inputs + .get(index + 1) + .is_some_and(|(other_normalized, other_path, _, _)| { + normalized == other_normalized && path == other_path + }); + let has_duplicate_path = previous_is_duplicate || next_is_duplicate; + normalized.hash(&mut HashToDigest { digest: &mut m }); + path.hash(&mut HashToDigest { digest: &mut m }); + has_duplicate_path.hash(&mut HashToDigest { digest: &mut m }); + if has_duplicate_path { + original_path.hash(&mut HashToDigest { digest: &mut m }); + } + m.update(hash.as_bytes()); + } + for h in extern_hashes .into_iter() - .chain(extern_hashes) .chain(staticlib_hashes) .chain(target_json_hash) { @@ -1558,6 +1891,8 @@ where for (var, val) in env_deps.iter() { var.hash(&mut HashToDigest { digest: &mut m }); m.update(b"="); + // rustc reports variables read by env! and option_env! here. Their values can be + // embedded verbatim in the artifact, so they must remain location-sensitive. val.hash(&mut HashToDigest { digest: &mut m }); } let mut env_vars: Vec<_> = env_vars @@ -1568,6 +1903,9 @@ where .cloned() .collect(); env_vars.sort(); + let normalize_cargo_paths = can_normalize_paths + && remap_input_path(&cwd).is_some() + && env_vars.iter().any(|(var, _)| is_path_cargo_env(var)); for (var, val) in env_vars.iter() { if !var.starts_with("CARGO_") { continue; @@ -1589,10 +1927,27 @@ where var.hash(&mut HashToDigest { digest: &mut m }); m.update(b"="); - val.hash(&mut HashToDigest { digest: &mut m }); + let val_bytes = val.as_encoded_bytes(); + let normalized = if normalize_cargo_paths && is_path_cargo_env(var) { + strip_basedir_prefix(val_bytes, basedirs) + } else { + Cow::Borrowed(val_bytes) + }; + let normalized_bytes: &[u8] = &normalized; + (normalized_bytes != val_bytes).hash(&mut HashToDigest { digest: &mut m }); + normalized.hash(&mut HashToDigest { digest: &mut m }); } // 9. The cwd of the compile. This will wind up in the rlib. - cwd.hash(&mut HashToDigest { digest: &mut m }); + let cwd_bytes = cwd.as_os_str().as_encoded_bytes(); + if let Some(remapped) = remap_input_path(&cwd) { + true.hash(&mut HashToDigest { digest: &mut m }); + remapped + .as_encoded_bytes() + .hash(&mut HashToDigest { digest: &mut m }); + } else { + false.hash(&mut HashToDigest { digest: &mut m }); + cwd_bytes.hash(&mut HashToDigest { digest: &mut m }); + } // 10. The version of the compiler. self.version.hash(&mut HashToDigest { digest: &mut m }); @@ -1719,6 +2074,7 @@ where .chain(abs_externs) .chain(abs_staticlibs) .collect(); + let dep_info_template = dep_info.as_ref().map(|_| dep_info_template); Ok(HashResult { key: m.finish(), @@ -1733,6 +2089,7 @@ where crate_name: self.parsed_args.crate_name.clone(), crate_types: self.parsed_args.crate_types.clone(), dep_info, + dep_info_template, cwd, env_vars, #[cfg(feature = "dist-client")] @@ -1816,6 +2173,17 @@ impl Compilation for RustCompilation { }; } + if !path_transformer.is_identity() + && arguments + .iter() + .any(|argument| argument.flag_str() == Some("--remap-path-prefix")) + { + debug!( + "Distributed Rust compilation does not support path remaps with a non-identity path transformer" + ); + return None; + } + let mut dist_arguments = vec![]; let mut saw_target = false; @@ -1908,6 +2276,17 @@ impl Compilation for RustCompilation { Ok((CCompileCommand::new(command), dist_command, Cacheable::Yes)) } + fn postprocess_cache_hit(&self, cwd: &Path) -> Result<()> { + if let Some(dep_info) = &self.dep_info { + let template = self + .dep_info_template + .as_ref() + .context("Missing local Rust dep-info template")?; + rewrite_dep_info_targets(dep_info, &self.outputs, cwd, template)?; + } + Ok(()) + } + #[cfg(feature = "dist-client")] fn into_dist_packagers( self: Box, @@ -2760,6 +3139,70 @@ release: 1.66.1 LLVM version: 15.0.2 "#; + #[cfg(feature = "dist-client")] + fn remapped_compilation(root: &Path) -> RustCompilation { + RustCompilation { + executable: root.join("rustc"), + host: "x86_64-unknown-linux-gnu".to_owned(), + sysroot: root.join("sysroot"), + rlib_dep_reader: None, + arguments: vec![ + Argument::Raw(root.join("src/lib.rs").into_os_string()), + Argument::WithValue( + "--remap-path-prefix", + ArgData::PassThrough(format!("{}=/workspace", root.display()).into()), + ArgDisposition::Separated, + ), + ], + inputs: vec![root.join("src/lib.rs")], + outputs: HashMap::new(), + crate_link_paths: vec![], + crate_name: "test".to_owned(), + crate_types: CrateTypes { + rlib: true, + staticlib: false, + }, + dep_info: None, + dep_info_template: None, + cwd: root.to_owned(), + env_vars: vec![], + } + } + + #[cfg(all(feature = "dist-client", unix))] + #[test] + fn test_distribute_remap_with_identity_path_transformer() { + let compilation = remapped_compilation(Path::new("/work")); + let mut path_transformer = dist::PathTransformer::new(); + let (_, dist_command, _) = >, + >>::generate_compile_commands( + &compilation, &mut path_transformer, false + ) + .unwrap(); + let dist_command = dist_command.unwrap(); + assert!( + dist_command + .arguments + .windows(2) + .any(|args| args == ["--remap-path-prefix", "/work=/workspace"]) + ); + } + + #[cfg(all(feature = "dist-client", windows))] + #[test] + fn test_remap_falls_back_with_non_identity_path_transformer() { + let compilation = remapped_compilation(Path::new("C:\\work")); + let mut path_transformer = dist::PathTransformer::new(); + let (_, dist_command, _) = >, + >>::generate_compile_commands( + &compilation, &mut path_transformer, false + ) + .unwrap(); + assert!(dist_command.is_none()); + } + #[test] #[allow(clippy::cognitive_complexity)] fn test_parse_arguments_simple() { @@ -3235,6 +3678,36 @@ abc def.rs: assert_eq!(pathvec!["abc def.rs", "baz.rs"], parse_dep_info(deps, "")); } + #[cfg(not(windows))] + #[test] + fn test_parse_dep_file_with_colon_space_target() { + let tempdir = tempfile::tempdir().unwrap(); + let dep_dir = tempdir.path().join("a: b"); + fs::create_dir(&dep_dir).unwrap(); + let dep_file = dep_dir.join("deps.d"); + fs::write( + &dep_file, + format!("{}: source.rs\n\nsource.rs:\n", dep_file.display()), + ) + .unwrap(); + + let (files, _, template) = parse_dep_file(&dep_file, tempdir.path()).unwrap(); + + assert_eq!(vec![tempdir.path().join("source.rs")], files); + assert_eq!(": source.rs\n", template.target_dependencies); + assert_eq!("\nsource.rs:\n", template.tail); + } + + #[test] + fn test_omits_dep_info_target() { + let z_flag = OsString::from("-Z"); + let omit_target = OsString::from("dep-info-omit-d-target"); + let omit_target_value = OsString::from("dep-info-omit-d-target=yes"); + assert!(omits_dep_info_target(&z_flag, Some(&omit_target))); + assert!(omits_dep_info_target(&z_flag, Some(&omit_target_value))); + assert!(!omits_dep_info_target(&z_flag, None)); + } + #[cfg(not(windows))] #[test] fn test_parse_dep_info_cwd() { @@ -3436,7 +3909,11 @@ proc_macro false ); } - fn mock_dep_info(creator: &Arc>, dep_srcs: &[&str]) { + fn mock_dep_info( + creator: &Arc>, + dep_srcs: &[&str], + env_deps: &[(&str, &str)], + ) { // Mock the `rustc --emit=dep-info` process by writing // a dep-info file. let mut sorted_deps = dep_srcs @@ -3444,6 +3921,10 @@ proc_macro false .map(|s| (*s).to_string()) .collect::>(); sorted_deps.sort(); + let env_deps = env_deps + .iter() + .map(|(var, val)| ((*var).to_string(), (*val).to_string())) + .collect::>(); next_command_calls(creator, move |args| { let mut dep_info_path = None; let mut it = args.iter(); @@ -3455,10 +3936,18 @@ proc_macro false } let dep_info_path = dep_info_path.unwrap(); let mut f = File::create(dep_info_path)?; - writeln!(f, "blah: {}", sorted_deps.iter().join(" "))?; + writeln!( + f, + "{}: {}", + Path::new(dep_info_path).display(), + sorted_deps.iter().join(" ") + )?; for d in sorted_deps.iter() { writeln!(f, "{}:", d)?; } + for (var, val) in &env_deps { + writeln!(f, "# env-dep:{var}={val}")?; + } Ok(MockChild::new(exit_status(0), "", "")) }); } @@ -3545,7 +4034,7 @@ proc_macro false }, }); let creator = new_creator(); - mock_dep_info(&creator, &["foo.rs", "bar.rs"]); + mock_dep_info(&creator, &["foo.rs", "bar.rs"], &[]); mock_file_names(&creator, &["foo.rlib", "foo.a"]); let runtime = single_threaded_runtime(); let pool = runtime.handle().clone(); @@ -3584,11 +4073,21 @@ proc_macro false // sysroot shlibs digests. m.update(FAKE_DIGEST.as_bytes()); // Arguments, with cfgs sorted at the end. - OsStr::new("ab--cfgabc--cfgxyz").hash(&mut HashToDigest { digest: &mut m }); - // bar.rs (source file, from dep-info) - m.update(empty_digest.as_bytes()); - // foo.rs (source file, from dep-info) - m.update(empty_digest.as_bytes()); + for arg in ["a", "b", "--cfg", "abc", "--cfg", "xyz"] { + false.hash(&mut HashToDigest { digest: &mut m }); + arg.as_bytes().hash(&mut HashToDigest { digest: &mut m }); + } + // Source files, sorted by effective path. + for source in ["bar.rs", "foo.rs"] { + false.hash(&mut HashToDigest { digest: &mut m }); + f.tempdir + .path() + .join(source) + .into_os_string() + .hash(&mut HashToDigest { digest: &mut m }); + false.hash(&mut HashToDigest { digest: &mut m }); + m.update(empty_digest.as_bytes()); + } // bar.rlib (extern crate, from externs) m.update(empty_digest.as_bytes()); // libbaz.a (static library, from staticlibs), containing a single @@ -3597,11 +4096,19 @@ proc_macro false // Env vars OsStr::new("CARGO_BLAH").hash(&mut HashToDigest { digest: &mut m }); m.update(b"="); - OsStr::new("abc").hash(&mut HashToDigest { digest: &mut m }); + false.hash(&mut HashToDigest { digest: &mut m }); + b"abc".as_slice().hash(&mut HashToDigest { digest: &mut m }); OsStr::new("CARGO_PKG_NAME").hash(&mut HashToDigest { digest: &mut m }); m.update(b"="); - OsStr::new("foo").hash(&mut HashToDigest { digest: &mut m }); - f.tempdir.path().hash(&mut HashToDigest { digest: &mut m }); + false.hash(&mut HashToDigest { digest: &mut m }); + b"foo".as_slice().hash(&mut HashToDigest { digest: &mut m }); + // cwd + false.hash(&mut HashToDigest { digest: &mut m }); + f.tempdir + .path() + .as_os_str() + .as_encoded_bytes() + .hash(&mut HashToDigest { digest: &mut m }); TEST_RUSTC_VERSION.hash(&mut HashToDigest { digest: &mut m }); let digest = m.finish(); assert_eq!(res.key, digest); @@ -3612,11 +4119,55 @@ proc_macro false fn hash_key( f: &TestFixture, - args: &[&'static str], + args: &[&str], env_vars: &[(OsString, OsString)], pre_func: F, preprocessor_cache_mode: bool, ) -> String + where + F: Fn(&Path) -> Result<()>, + { + hash_key_with_env_deps( + f, + args, + env_vars, + pre_func, + preprocessor_cache_mode, + vec![], + (&[], &["foo.rs"]), + ) + } + + fn hash_key_with_basedirs( + f: &TestFixture, + args: &[&str], + env_vars: &[(OsString, OsString)], + pre_func: F, + basedirs: Vec>, + ) -> String + where + F: Fn(&Path) -> Result<()>, + { + hash_key_with_env_deps( + f, + args, + env_vars, + pre_func, + false, + basedirs, + (&[], &["foo.rs"]), + ) + } + + fn hash_key_with_env_deps( + f: &TestFixture, + args: &[&str], + env_vars: &[(OsString, OsString)], + pre_func: F, + preprocessor_cache_mode: bool, + basedirs: Vec>, + dep_info: (&[(&str, &str)], &[&str]), + ) -> String where F: Fn(&Path) -> Result<()>, { @@ -3652,7 +4203,7 @@ proc_macro false let runtime = single_threaded_runtime(); let pool = runtime.handle().clone(); - mock_dep_info(&creator, &["foo.rs"]); + mock_dep_info(&creator, dep_info.1, dep_info.0); mock_file_names(&creator, &["foo.rlib"]); hasher .generate_hash_key( @@ -3662,7 +4213,7 @@ proc_macro false false, &pool, false, - Arc::new(MockStorage::new(None, preprocessor_cache_mode)), + Arc::new(MockStorage::new(None, preprocessor_cache_mode).with_basedirs(basedirs)), CacheControl::Default, ) .wait() @@ -3935,6 +4486,457 @@ proc_macro false ); } + const BASEDIR_ARGS: &[&str] = &[ + "--emit", + "link", + "foo.rs", + "--out-dir", + "out", + "--crate-name", + "foo", + "--crate-type", + "lib", + ]; + + #[test] + fn test_rewrite_cached_dep_info_targets() { + let tempdir = tempfile::tempdir().unwrap(); + let dep_info = PathBuf::from("new/out/foo.d"); + fs::create_dir_all(tempdir.path().join("new/out")).unwrap(); + #[cfg(windows)] + let cached_targets = "C:foo.d: /old/source/lib.rs\n\nC:libfoo.rlib: /old/source/lib.rs\n\n/old/source/lib.rs:\n\n"; + #[cfg(not(windows))] + let cached_targets = "/old/a: b/out/foo.d: /old/source/lib.rs\n\n/old/a: b/out/libfoo.rlib: /old/source/lib.rs\n\n/old/source/lib.rs:\n\n"; + fs::write( + tempdir.path().join(&dep_info), + format!("{cached_targets}# env-dep:OUT_DIR=/tmp/foo.d: value\n"), + ) + .unwrap(); + let mut permissions = fs::metadata(tempdir.path().join(&dep_info)) + .unwrap() + .permissions(); + permissions.set_readonly(true); + fs::set_permissions(tempdir.path().join(&dep_info), permissions).unwrap(); + let outputs = HashMap::from([ + ( + "foo.d".to_owned(), + ArtifactDescriptor { + path: "new/a: b/out/foo.d".into(), + optional: false, + }, + ), + ( + "libfoo.rlib".to_owned(), + ArtifactDescriptor { + path: "new/a: b/out/libfoo.rlib".into(), + optional: false, + }, + ), + ]); + let template = DepInfoTemplate { + target_dependencies: ": /new/source/lib.rs\n".to_owned(), + tail: "\n/new/source/lib.rs:\n\n# env-dep:OUT_DIR=/new/out\n# checksum:123 file_len:1 /new/source/lib.rs\n" + .to_owned(), + }; + + rewrite_dep_info_targets(&dep_info, &outputs, tempdir.path(), &template).unwrap(); + + assert_eq!( + fs::read_to_string(tempdir.path().join(&dep_info)).unwrap(), + "new/a: b/out/foo.d: /new/source/lib.rs\n\nnew/a: b/out/libfoo.rlib: /new/source/lib.rs\n\n/new/source/lib.rs:\n\n# env-dep:OUT_DIR=/new/out\n# checksum:123 file_len:1 /new/source/lib.rs\n" + ); + assert!( + fs::metadata(tempdir.path().join(dep_info)) + .unwrap() + .permissions() + .readonly() + ); + } + + #[test] + fn test_rewrite_cached_dep_info_rejects_ambiguous_target() { + let tempdir = tempfile::tempdir().unwrap(); + let dep_info = PathBuf::from("out/foo.d"); + fs::create_dir_all(tempdir.path().join("out")).unwrap(); + fs::write( + tempdir.path().join(&dep_info), + "/old/foo.d: dir/out/foo.d: src/lib.rs\n", + ) + .unwrap(); + let outputs = HashMap::from([( + "foo.d".to_owned(), + ArtifactDescriptor { + path: dep_info.clone(), + optional: false, + }, + )]); + let template = DepInfoTemplate { + target_dependencies: ": src/lib.rs\n".to_owned(), + tail: "\nsrc/lib.rs:\n".to_owned(), + }; + + assert!(rewrite_dep_info_targets(&dep_info, &outputs, tempdir.path(), &template).is_err()); + } + + fn basedir_for(path: &Path) -> Vec { + let bytes = path.to_string_lossy().into_owned().into_bytes(); + #[cfg(windows)] + let bytes = crate::util::normalize_win_path(&bytes); + let mut bytes = bytes; + bytes.push(b'/'); + bytes + } + + #[test] + fn test_basedirs_stable_across_absolute_paths() { + let f1 = TestFixture::new(); + let f2 = TestFixture::new(); + let unremapped_key = |f: &TestFixture| { + hash_key_with_basedirs( + f, + BASEDIR_ARGS, + &[], + nothing, + vec![basedir_for(f.tempdir.path())], + ) + }; + assert_ne!(unremapped_key(&f1), unremapped_key(&f2)); + + let key = |f: &TestFixture| { + let manifest = f.tempdir.path().join("package"); + let remap = format!("{}=/workspace", f.tempdir.path().display()); + let mut args = BASEDIR_ARGS.to_vec(); + args.extend(["--remap-path-prefix", &remap]); + hash_key_with_basedirs( + f, + &args, + &[("CARGO_MANIFEST_DIR".into(), manifest.into_os_string())], + nothing, + vec![basedir_for(f.tempdir.path())], + ) + }; + assert_eq!(key(&f1), key(&f2)); + } + + #[test] + fn test_basedirs_stable_across_absolute_source_arguments() { + let f1 = TestFixture::new(); + let f2 = TestFixture::new(); + let key = |f: &TestFixture| { + let source = f.tempdir.path().join("foo.rs"); + let source = source.to_str().unwrap(); + let remap = format!("{}=/workspace", f.tempdir.path().display()); + let args = [ + "--emit", + "link", + source, + "--out-dir", + "out", + "--crate-name", + "foo", + "--crate-type", + "lib", + "--remap-path-prefix", + &remap, + ]; + hash_key_with_basedirs(f, &args, &[], nothing, vec![basedir_for(f.tempdir.path())]) + }; + + assert_eq!(key(&f1), key(&f2)); + } + + #[test] + fn test_basedirs_keep_unremapped_source_paths() { + let f1 = TestFixture::new(); + let f2 = TestFixture::new(); + f1.touch("external.rs").unwrap(); + let external = f1.tempdir.path().join("external.rs"); + let external = external.to_str().unwrap(); + + let key = |f: &TestFixture| { + let remap = format!("{}=/workspace", f.tempdir.path().display()); + let mut args = BASEDIR_ARGS.to_vec(); + args.extend(["--remap-path-prefix", &remap]); + hash_key_with_env_deps( + f, + &args, + &[], + nothing, + false, + vec![basedir_for(f.tempdir.path())], + (&[], &["foo.rs", external]), + ) + }; + + assert_ne!(key(&f1), key(&f2)); + } + + #[test] + fn test_basedirs_preserve_duplicate_remap_source_identity() { + let f = TestFixture::new(); + let first_dir = f.tempdir.path().join("first"); + let second_dir = f.tempdir.path().join("second"); + fs::create_dir_all(&first_dir).unwrap(); + fs::create_dir_all(&second_dir).unwrap(); + let first_source = first_dir.join("source.rs"); + let second_source = second_dir.join("source.rs"); + let first_source_str = first_source.to_str().unwrap(); + let second_source_str = second_source.to_str().unwrap(); + let first_remap = format!("{}=/workspace", first_dir.display()); + let second_remap = format!("{}=/workspace", second_dir.display()); + let mut args = BASEDIR_ARGS.to_vec(); + args.extend([ + "--remap-path-prefix", + &first_remap, + "--remap-path-prefix", + &second_remap, + ]); + + let key = |first_contents: &str, second_contents: &str| { + hash_key_with_env_deps( + &f, + &args, + &[], + |_| { + fs::write(&first_source, first_contents)?; + fs::write(&second_source, second_contents)?; + Ok(()) + }, + false, + vec![basedir_for(f.tempdir.path())], + (&[], &["foo.rs", first_source_str, second_source_str]), + ) + }; + + assert_ne!(key("first", "second"), key("second", "first")); + } + + #[test] + fn test_basedirs_preserve_paths_with_external_crates() { + let f1 = TestFixture::new(); + let f2 = TestFixture::new(); + let dependency_name = "dependency.rlib"; + f1.touch(dependency_name).unwrap(); + f2.touch(dependency_name).unwrap(); + + let key = |f: &TestFixture| { + let remap = format!("{}=/workspace", f.tempdir.path().display()); + let dependency = format!( + "dependency={}", + f.tempdir.path().join(dependency_name).display() + ); + let mut args = BASEDIR_ARGS + .iter() + .map(|arg| (*arg).to_owned()) + .collect::>(); + args.extend([ + "--remap-path-prefix".to_owned(), + remap, + "--extern".to_owned(), + dependency, + ]); + let args = args.iter().map(String::as_str).collect::>(); + hash_key_with_env_deps( + f, + &args, + &[], + nothing, + false, + vec![basedir_for(f.tempdir.path())], + (&[], &["foo.rs"]), + ) + }; + + assert_ne!(key(&f1), key(&f2)); + } + + #[test] + fn test_basedirs_preserve_paths_with_dynamic_crate_search() { + let f1 = TestFixture::new(); + let f2 = TestFixture::new(); + for f in [&f1, &f2] { + let deps = f.tempdir.path().join("deps"); + fs::create_dir(&deps).unwrap(); + File::create(deps.join(format!("proc_macro.{DLL_EXTENSION}"))).unwrap(); + } + + let key = |f: &TestFixture| { + let remap = format!("{}=/workspace", f.tempdir.path().display()); + let dependency_path = format!("dependency={}", f.tempdir.path().join("deps").display()); + let mut args = BASEDIR_ARGS + .iter() + .map(|arg| (*arg).to_owned()) + .collect::>(); + args.extend([ + "--remap-path-prefix".to_owned(), + remap, + "-L".to_owned(), + dependency_path, + ]); + let args = args.iter().map(String::as_str).collect::>(); + hash_key_with_env_deps( + f, + &args, + &[], + nothing, + false, + vec![basedir_for(f.tempdir.path())], + (&[], &["foo.rs"]), + ) + }; + + assert_ne!(key(&f1), key(&f2)); + } + + #[test] + fn test_basedirs_preserve_path_sensitive_env_dependencies() { + let f1 = TestFixture::new(); + let f2 = TestFixture::new(); + let manifest1 = f1.tempdir.path().to_string_lossy().into_owned(); + let manifest2 = f2.tempdir.path().to_string_lossy().into_owned(); + let remap1 = format!("{}=/workspace", f1.tempdir.path().display()); + let remap2 = format!("{}=/workspace", f2.tempdir.path().display()); + let mut args1 = BASEDIR_ARGS.to_vec(); + let mut args2 = BASEDIR_ARGS.to_vec(); + args1.extend(["--remap-path-prefix", &remap1]); + args2.extend(["--remap-path-prefix", &remap2]); + let k1 = hash_key_with_env_deps( + &f1, + &args1, + &[("CARGO_MANIFEST_DIR".into(), manifest1.clone().into())], + nothing, + false, + vec![basedir_for(f1.tempdir.path())], + (&[("CARGO_MANIFEST_DIR", &manifest1)], &["foo.rs"]), + ); + let k2 = hash_key_with_env_deps( + &f2, + &args2, + &[("CARGO_MANIFEST_DIR".into(), manifest2.clone().into())], + nothing, + false, + vec![basedir_for(f2.tempdir.path())], + (&[("CARGO_MANIFEST_DIR", &manifest2)], &["foo.rs"]), + ); + + assert_ne!(k1, k2); + } + + #[test] + fn test_normalize_path_arguments() { + let basedirs = [b"/home/user/".to_vec()]; + for (flag, value, expected) in [ + ("--remap-path-prefix", "/home/user", "/home/user"), + ("--remap-path-prefix", "/other=/new", "/other=/new"), + ("--remap-path-prefix", "/home/user=/new", "=/new"), + ("--remap-path-prefix", "/home/user/src=/new", "src=/new"), + ( + "-C", + "profile-use=/home/user/profile", + "profile-use=/home/user/profile", + ), + ("-C", "metadata=/home/user/id", "metadata=/home/user/id"), + ( + "-C", + "link-arg=-Wl,-rpath,/home/user/lib", + "link-arg=-Wl,-rpath,/home/user/lib", + ), + ] { + let flag = flag.into(); + let value = value.into(); + assert_eq!( + &*super::normalize_arg_value(&flag, &value, &basedirs), + expected.as_bytes() + ); + } + + assert_eq!( + &*super::strip_basedir_prefix( + b"/home/user/src/lib.rs", + &[b"/home/".to_vec(), b"/home/user/".to_vec()] + ), + b"src/lib.rs" + ); + assert_eq!( + &*super::strip_basedir_prefix(b"/other/path", &basedirs), + b"/other/path" + ); + assert!(super::is_path_cargo_env(&"CARGO_MANIFEST_DIR".into())); + assert!(super::is_path_cargo_env(&"CARGO_INSTALL_ROOT".into())); + assert!(!super::is_path_cargo_env(&"CARGO_PKG_DESCRIPTION".into())); + + let remap = |prefix: &str| { + vec![( + "--remap-path-prefix".into(), + Some(format!("{prefix}=/workspace").into()), + )] + }; + let joined = + |prefix: &str, suffix: &str| Some(Path::new(prefix).join(suffix).into_os_string()); + assert!(super::remap_path(Path::new("/home/user/project"), &remap("/home/user")).is_some()); + assert!(super::remap_path(Path::new("/home/user"), &remap("/home/user=part")).is_none()); + assert_eq!( + super::remap_path(Path::new("/home/user"), &remap("/")), + joined("/workspace", "home/user") + ); + assert_eq!( + super::remap_path(Path::new("/home/user/project"), &remap("/home/user/")), + joined("/workspace", "project") + ); + assert!( + super::remap_path(Path::new("/home/username/project"), &remap("/home/user")).is_none() + ); + assert_eq!( + super::remap_path(Path::new("/home/a=b/project"), &remap("/home/a=b")), + joined("/workspace", "project") + ); + + let overlapping = vec![ + ("--remap-path-prefix".into(), Some("/home=/first".into())), + ( + "--remap-path-prefix".into(), + Some("/home/user=/last".into()), + ), + ]; + assert_eq!( + super::remap_path(Path::new("/home/user/project"), &overlapping), + joined("/last", "project") + ); + + let mut diagnostics = remap("/home/user"); + diagnostics.push(("--remap-path-scope=diagnostics".into(), None)); + assert!(super::remap_path(Path::new("/home/user/project"), &diagnostics).is_none()); + let mut diagnostics = remap("/home/user"); + diagnostics.push(("--remap-path-scope".into(), Some("diagnostics".into()))); + assert!(super::remap_path(Path::new("/home/user/project"), &diagnostics).is_none()); + let mut all = remap("/home/user"); + all.push(("--remap-path-scope=all".into(), None)); + assert!(super::remap_path(Path::new("/home/user/project"), &all).is_some()); + all.push(("-Z".into(), Some("remap-path-scope=diagnostics".into()))); + assert!(super::remap_path(Path::new("/home/user/project"), &all).is_none()); + all.push(("--remap-path-scope=all".into(), None)); + assert!(super::remap_path(Path::new("/home/user/project"), &all).is_some()); + } + + #[cfg(windows)] + #[test] + fn test_windows_basedir_matching_preserves_suffix() { + let basedirs = [b"c:/work/repo/".to_vec()]; + assert_eq!( + &*super::strip_basedir_prefix(b"C:\\Work\\Repo\\MixedCase", &basedirs), + b"MixedCase" + ); + assert_eq!( + &*super::strip_basedir_prefix(b"C:\\Work\\Repo\\\xc4\xb0", &basedirs), + b"C:\\Work\\Repo\\\xc4\xb0" + ); + let remap = vec![( + "--remap-path-prefix".into(), + Some("C:\\WORK=/workspace".into()), + )]; + assert!(super::remap_path(Path::new("C:\\work\\project"), &remap).is_none()); + } + #[test] fn test_parse_unstable_profile_flag() { let h = parses!( @@ -3996,6 +4998,41 @@ proc_macro false ))); } + #[test] + fn test_parse_remap_path_scope() { + for h in [ + parses!( + "--crate-name", + "foo", + "--crate-type", + "lib", + "./src/lib.rs", + "--emit=dep-info,link", + "--out-dir", + "/out", + "--remap-path-scope", + "all" + ), + parses!( + "--crate-name", + "foo", + "--crate-type", + "lib", + "./src/lib.rs", + "--emit=dep-info,link", + "--out-dir", + "/out", + "--remap-path-scope=all" + ), + ] { + assert!(h.arguments.contains(&Argument::WithValue( + "--remap-path-scope", + ArgData::PassThrough(OsString::from("all")), + ArgDisposition::Separated + ))); + } + } + #[test] fn test_parse_target() { // Parse a --target argument that is a string (not a path to a .json file). diff --git a/src/dist/mod.rs b/src/dist/mod.rs index 6bc1024aa8..3eb2d6ccb8 100644 --- a/src/dist/mod.rs +++ b/src/dist/mod.rs @@ -103,6 +103,9 @@ mod path_transform { dist_to_local_path: HashMap::new(), } } + pub fn is_identity(&self) -> bool { + false + } pub fn as_dist_abs(&mut self, p: &Path) -> Option { if !p.is_absolute() { return None; @@ -190,6 +193,7 @@ mod path_transform { #[test] fn test_basic() { let mut pt = PathTransformer::new(); + assert!(!pt.is_identity()); assert_eq!(pt.as_dist(Path::new("C:/a")).unwrap(), "/prefix/disk-C/a"); assert_eq!( pt.as_dist(Path::new(r#"C:\a\b.c"#)).unwrap(), @@ -276,6 +280,9 @@ mod path_transform { pub fn new() -> Self { PathTransformer } + pub fn is_identity(&self) -> bool { + true + } pub fn as_dist_abs(&mut self, p: &Path) -> Option { if !p.is_absolute() { return None; @@ -292,6 +299,11 @@ mod path_transform { Some(PathBuf::from(p)) } } + + #[test] + fn test_identity() { + assert!(PathTransformer::new().is_identity()); + } } pub fn osstrings_to_strings(osstrings: &[OsString]) -> Option> { diff --git a/src/test/mock_storage.rs b/src/test/mock_storage.rs index d4260e79e2..ab939e0262 100644 --- a/src/test/mock_storage.rs +++ b/src/test/mock_storage.rs @@ -28,6 +28,7 @@ pub struct MockStorage { tx: mpsc::UnboundedSender>, delay: Option, preprocessor_cache_mode: bool, + basedirs: Vec>, } impl MockStorage { @@ -39,9 +40,15 @@ impl MockStorage { rx: Arc::new(Mutex::new(rx)), delay, preprocessor_cache_mode, + basedirs: Vec::new(), } } + pub(crate) fn with_basedirs(mut self, basedirs: Vec>) -> Self { + self.basedirs = basedirs; + self + } + /// Queue up `res` to be returned as the next result from `Storage::get`. pub(crate) fn next_get(&self, res: Result) { self.tx.unbounded_send(res).unwrap(); @@ -75,6 +82,9 @@ impl Storage for MockStorage { async fn max_size(&self) -> Result> { Ok(None) } + fn basedirs(&self) -> &[Vec] { + &self.basedirs + } fn preprocessor_cache_mode_config(&self) -> PreprocessorCacheModeConfig { PreprocessorCacheModeConfig { use_preprocessor_cache_mode: self.preprocessor_cache_mode, diff --git a/tests/sccache_cargo.rs b/tests/sccache_cargo.rs index 6734a33f5b..55ae0db0f1 100644 --- a/tests/sccache_cargo.rs +++ b/tests/sccache_cargo.rs @@ -37,6 +37,77 @@ fn test_rust_cargo_build() -> Result<()> { test_rust_cargo_cmd("build", SccacheTest::new(None)?) } +#[test] +#[serial] +fn test_rust_cargo_basedirs_cross_dir_cache_hit() -> Result<()> { + let work = tempfile::Builder::new() + .prefix("sccache_basedirs_xdir") + .tempdir() + .context("tempdir")?; + // On macOS `/var/...` is a symlink to `/private/var/...` and cargo reports + // the resolved target for CARGO_MANIFEST_DIR. Basedirs are compared by + // byte prefix, so the user-supplied path must be in the same canonical + // form. Windows `fs::canonicalize` returns `\\?\`-prefixed UNC paths that + // cargo does not emit, so only canonicalize on Unix. + #[cfg(unix)] + let work_root = fs::canonicalize(work.path())?; + #[cfg(not(unix))] + let work_root = work.path().to_path_buf(); + let crate_a = work_root.join("a"); + let crate_b = work_root.join("b"); + for crate_dir in [&crate_a, &crate_b] { + fs::create_dir_all(crate_dir.join("src"))?; + let lib_path = crate_dir + .join("src/lib.rs") + .to_string_lossy() + .replace('\\', "/"); + fs::write( + crate_dir.join("Cargo.toml"), + format!( + "[package]\nname = \"basedirs-test\"\nversion = \"0.0.0\"\nedition = \"2021\"\n\n[lib]\npath = {lib_path:?}\n" + ), + )?; + fs::write(crate_dir.join("src/lib.rs"), "pub fn value() -> u8 { 1 }\n")?; + } + + let sep = if cfg!(windows) { ';' } else { ':' }; + let basedirs = format!("{}{sep}{}", crate_a.display(), crate_b.display()); + let test = SccacheTest::new(None)?; + restart_sccache(&test, Some(vec![("SCCACHE_BASEDIRS".into(), basedirs)]))?; + + for crate_dir in [&crate_a, &crate_b] { + Command::new(CARGO.as_os_str()) + .args(["build", "--lib"]) + .envs(test.env.iter().cloned()) + .env("CARGO_INSTALL_ROOT", crate_dir.join("install")) + .env("CARGO_TARGET_DIR", crate_dir.join("target")) + .env( + "CARGO_ENCODED_RUSTFLAGS", + format!("--remap-path-prefix={}=/workspace", crate_dir.display()), + ) + .current_dir(crate_dir) + .assert() + .try_success()?; + } + + let dep_info = fs::read_dir(crate_b.join("target/debug/deps"))? + .find_map(|entry| { + let path = entry.ok()?.path(); + path.extension() + .is_some_and(|extension| extension == "d") + .then_some(path) + }) + .context("missing dep-info for second checkout")?; + let dep_info = fs::read_to_string(dep_info)?; + assert!(!dep_info.contains(crate_a.to_string_lossy().as_ref())); + assert!(dep_info.contains(crate_b.to_string_lossy().as_ref())); + + test.show_stats()? + .try_stdout(predicates::str::contains(r#""cache_hits":{"counts":{"Rust":1}"#).from_utf8())? + .try_success()?; + Ok(()) +} + #[test] #[serial] fn test_rust_cargo_build_readonly() -> Result<()> { diff --git a/tests/sccache_rustc.rs b/tests/sccache_rustc.rs index 8fd4e22649..49d35b4cd3 100644 --- a/tests/sccache_rustc.rs +++ b/tests/sccache_rustc.rs @@ -112,7 +112,7 @@ while [ "$#" -gt 0 ]; do --emit) shift if [ "$1" = dep-info ]; then - echo "deps.d: RUST_FILE.rs" > "$3" + echo "$3: RUST_FILE.rs" > "$3" exec echo "RUST_FILE.rs:" "$3" fi ;;