diff --git a/crates/socket-patch-core/src/utils/cargo_build_cache.rs b/crates/socket-patch-core/src/utils/cargo_build_cache.rs index 8c12b1c08..e4b3668c4 100644 --- a/crates/socket-patch-core/src/utils/cargo_build_cache.rs +++ b/crates/socket-patch-core/src/utils/cargo_build_cache.rs @@ -21,6 +21,8 @@ use std::path::{Path, PathBuf}; +use crate::formats::cargo::source_replacement::config_layer_paths; + /// The build directories of the project at `cwd`, and the config values /// naming one this module cannot resolve (a `{workspace-path-hash}` /// template) — the caller tells the user to clean those by hand. @@ -56,6 +58,12 @@ pub fn build_dirs_with( env: &dyn Fn(&str) -> Option, cargo_home: Option<&Path>, ) -> BuildDirs { + // `--cwd` defaults to `.`, whose `ancestors()` stop at `.`: walk the + // absolute path so a parent's `Cargo.lock` and `.cargo/config` count, + // with `..` folded so `--cwd ../app` doesn't walk the directory left. + let cwd = &crate::utils::relpath::normalize_lexically_keeping_escapes( + &std::path::absolute(cwd).unwrap_or_else(|_| cwd.to_path_buf()), + ); let env = |k: &str| env(k).filter(|v| !v.trim().is_empty()); let ws_root = workspace_root(cwd); let config = ConfigDirs::read(cwd, cargo_home); @@ -136,10 +144,9 @@ fn workspace_root(cwd: &Path) -> PathBuf { } /// `build.target-dir` / `build.build-dir` from the config files cargo -/// reads for `cwd`: `.cargo/config.toml` (or legacy `.cargo/config`) in -/// `cwd` and every ancestor, then `$CARGO_HOME/config.toml`; the deepest -/// file that sets a key wins. A relative value resolves against the -/// directory holding the `.cargo` directory. +/// reads for `cwd` ([`config_layer_paths`]); the deepest file that sets a +/// key wins. A relative value resolves against the directory holding the +/// `.cargo` directory. #[derive(Default)] struct ConfigDirs { target_dir: Option, @@ -149,19 +156,11 @@ struct ConfigDirs { impl ConfigDirs { fn read(cwd: &Path, cargo_home: Option<&Path>) -> Self { - let mut files: Vec<(PathBuf, PathBuf)> = cwd - .ancestors() - .map(|d| (d.join(".cargo"), d.to_path_buf())) - .collect(); - if let Some(home) = cargo_home { - let base = home.parent().map(Path::to_path_buf).unwrap_or_default(); - files.push((home.to_path_buf(), base)); - } let mut out = ConfigDirs::default(); - for (dot_cargo, base) in files { - let text = ["config.toml", "config"].iter().find_map(|f| { - crate::utils::fs::read_regular_to_string_sync(&dot_cargo.join(f)).ok() - }); + for (file, base) in config_layer_paths(cwd, cargo_home, |p| { + crate::utils::group_commit::exists(p).unwrap_or_else(|| p.is_file()) + }) { + let text = crate::utils::fs::read_regular_to_string_sync(&file).ok(); let Some(doc) = text.and_then(|t| t.parse::().ok()) else { continue; }; @@ -424,6 +423,69 @@ mod tests { } } + /// With both `.cargo/config` and `.cargo/config.toml`, cargo reads the + /// extensionless one; so must the cleaner. + #[test] + fn build_dirs_read_the_legacy_config_when_both_exist() { + let t = tempfile::tempdir().unwrap(); + let root = t.path().join("ws"); + std::fs::create_dir_all(root.join(".cargo")).unwrap(); + std::fs::create_dir_all(root.join("legacy")).unwrap(); + std::fs::create_dir_all(root.join("modern")).unwrap(); + std::fs::write( + root.join(".cargo/config"), + "[build]\ntarget-dir = \"legacy\"\n", + ) + .unwrap(); + std::fs::write( + root.join(".cargo/config.toml"), + "[build]\ntarget-dir = \"modern\"\n", + ) + .unwrap(); + let d = build_dirs_with(&root, &|_| None, None); + assert_eq!( + d.dirs, + vec![std::fs::canonicalize(root.join("legacy")).unwrap()] + ); + } + + /// A relative cwd (the default `--cwd .`, or `../ws/member`) finds the + /// workspace root and the ancestor config above it, and nothing else. + #[test] + #[serial_test::serial] + fn build_dirs_walk_above_a_relative_cwd() { + let t = tempfile::tempdir().unwrap(); + let root = t.path().join("ws"); + let member = root.join("member"); + std::fs::create_dir_all(root.join(".cargo")).unwrap(); + std::fs::create_dir_all(root.join("out")).unwrap(); + std::fs::create_dir_all(&member).unwrap(); + std::fs::write(root.join("Cargo.lock"), "").unwrap(); + std::fs::write( + root.join(".cargo/config.toml"), + "[build]\ntarget-dir = \"out\"\n", + ) + .unwrap(); + // `--cwd ../ws/member` from a sibling whose config must not count. + let left = t.path().join("left"); + std::fs::create_dir_all(left.join(".cargo")).unwrap(); + std::fs::create_dir_all(left.join("wrong")).unwrap(); + std::fs::write( + left.join(".cargo/config.toml"), + "[build]\nbuild-dir = \"wrong\"\n", + ) + .unwrap(); + let prev = std::env::current_dir().unwrap(); + std::env::set_current_dir(&member).unwrap(); + let from_member = build_dirs_with(Path::new("."), &|_| None, None); + std::env::set_current_dir(&left).unwrap(); + let from_sibling = build_dirs_with(Path::new("../ws/member"), &|_| None, None); + std::env::set_current_dir(prev).unwrap(); + let out = vec![std::fs::canonicalize(root.join("out")).unwrap()]; + assert_eq!(from_member.dirs, out); + assert_eq!(from_sibling.dirs, out); + } + #[test] fn build_dirs_honours_env_config_and_workspace_root() { let t = tempfile::tempdir().unwrap();