From 36768d5eee10e3358a083fa591f07f14272b0d28 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 18:57:16 +0000 Subject: [PATCH 1/4] Start #1832: one cargo config chain Assisted-by: Claude Code:claude-opus-5-5 From 099c132f9ceb1548ce81d8abc814c10611b2ec66 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:04:45 +0000 Subject: [PATCH 2/4] Read cargo config files via config_layer_paths The build-cache cleaner walked the .cargo/config chain itself and preferred config.toml when both config and config.toml existed. Cargo reads the extensionless file, so the cleaner removed fingerprints from a target dir cargo never used. ConfigDirs::read now takes its file list from source_replacement::config_layer_paths, which the source replacement reader already uses, and keeps only the [build] lookup. It also stops reading $CARGO_HOME twice when an ancestor's .cargo is it. Fixes #1832 Assisted-by: Claude Code:claude-opus-5-5 --- .../src/utils/cargo_build_cache.rs | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) 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..7d19af0da 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. @@ -136,10 +138,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 +150,9 @@ 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, Path::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 +415,32 @@ 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()] + ); + } + #[test] fn build_dirs_honours_env_config_and_workspace_root() { let t = tempfile::tempdir().unwrap(); From 3b882eaa7ffddb17627926ba16c1af2af879f960 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:33:43 +0000 Subject: [PATCH 3/4] Resolve a relative cwd before the cargo walks build_dirs walked the ancestors of the cwd as given, and --cwd defaults to ".", whose ancestors stop at ".". Run from a workspace member, the cleaner found neither the workspace root nor a parent .cargo/config, so it invalidated no build dir at all. It now walks the absolute path. The config file probe also consults the open group commit, as the read that follows it does, so the two agree on captured files. Assisted-by: Claude Code:claude-opus-5-5 --- .../src/utils/cargo_build_cache.rs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) 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 7d19af0da..74c5ed85b 100644 --- a/crates/socket-patch-core/src/utils/cargo_build_cache.rs +++ b/crates/socket-patch-core/src/utils/cargo_build_cache.rs @@ -58,6 +58,9 @@ 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. + let cwd = &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); @@ -151,7 +154,9 @@ struct ConfigDirs { impl ConfigDirs { fn read(cwd: &Path, cargo_home: Option<&Path>) -> Self { let mut out = ConfigDirs::default(); - for (file, base) in config_layer_paths(cwd, cargo_home, Path::is_file) { + 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; @@ -441,6 +446,33 @@ mod tests { ); } + /// The default `--cwd .` still finds the workspace root and the + /// ancestor config above it. + #[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(); + let prev = std::env::current_dir().unwrap(); + std::env::set_current_dir(&member).unwrap(); + let d = build_dirs_with(Path::new("."), &|_| None, None); + std::env::set_current_dir(prev).unwrap(); + assert_eq!( + d.dirs, + vec![std::fs::canonicalize(root.join("out")).unwrap()] + ); + } + #[test] fn build_dirs_honours_env_config_and_workspace_root() { let t = tempfile::tempdir().unwrap(); From 2c715eb185b6868047164544be5c81988aec1934 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 19:59:08 +0000 Subject: [PATCH 4/4] Fold .. out of the cwd before the cargo walks std::path::absolute keeps .., so --cwd ../app walked the ancestors of the directory being left and could take its build-dir. Fold the absolute cwd lexically, as cargo_crawler::resolve_for does. Assisted-by: Claude Code:claude-opus-5-5 --- .../src/utils/cargo_build_cache.rs | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) 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 74c5ed85b..e4b3668c4 100644 --- a/crates/socket-patch-core/src/utils/cargo_build_cache.rs +++ b/crates/socket-patch-core/src/utils/cargo_build_cache.rs @@ -59,8 +59,11 @@ pub fn build_dirs_with( 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. - let cwd = &std::path::absolute(cwd).unwrap_or_else(|_| cwd.to_path_buf()); + // 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); @@ -446,8 +449,8 @@ mod tests { ); } - /// The default `--cwd .` still finds the workspace root and the - /// ancestor config above it. + /// 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() { @@ -463,14 +466,24 @@ mod tests { "[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 d = build_dirs_with(Path::new("."), &|_| None, None); + 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(); - assert_eq!( - d.dirs, - vec![std::fs::canonicalize(root.join("out")).unwrap()] - ); + let out = vec![std::fs::canonicalize(root.join("out")).unwrap()]; + assert_eq!(from_member.dirs, out); + assert_eq!(from_sibling.dirs, out); } #[test]