diff --git a/crates/cargo-util/src/paths.rs b/crates/cargo-util/src/paths.rs index 5a82ef5d8ad..a4484c17c72 100644 --- a/crates/cargo-util/src/paths.rs +++ b/crates/cargo-util/src/paths.rs @@ -436,18 +436,21 @@ pub fn ancestors<'a>(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncesto pub struct PathAncestors<'a> { current: Option<&'a Path>, - stop_at: Option, + stop_at: Vec, } impl<'a> PathAncestors<'a> { fn new(path: &'a Path, stop_root_at: Option<&Path>) -> PathAncestors<'a> { - let stop_at = env::var("__CARGO_TEST_ROOT") - .ok() - .map(PathBuf::from) - .or_else(|| stop_root_at.map(|p| p.to_path_buf())); + let mut stop_at = Vec::new(); + if let Some(stop_root_at) = stop_root_at { + stop_at.push(stop_root_at.to_path_buf()); + } + if let Ok(test_root) = env::var("__CARGO_TEST_ROOT") { + // HACK: avoid reading `~/.cargo/config` when testing Cargo itself. + stop_at.push(PathBuf::from(test_root)); + } PathAncestors { current: Some(path), - //HACK: avoid reading `~/.cargo/config` when testing Cargo itself. stop_at, } } @@ -460,10 +463,8 @@ impl<'a> Iterator for PathAncestors<'a> { if let Some(path) = self.current { self.current = path.parent(); - if let Some(ref stop_at) = self.stop_at { - if path == stop_at { - self.current = None; - } + if self.stop_at.iter().any(|stop_at| path == stop_at) { + self.current = None; } Some(path) diff --git a/src/cargo/ops/cargo_config.rs b/src/cargo/ops/cargo_config.rs index 8d65da43bf0..ea3ef88be2f 100644 --- a/src/cargo/ops/cargo_config.rs +++ b/src/cargo/ops/cargo_config.rs @@ -283,13 +283,15 @@ fn print_toml_unmerged( // // * CARGO // * CARGO_HOME + // * CARGO_CONFIG_STOP_SEARCH_PATH // * CARGO_NAME // * CARGO_EMAIL // * CARGO_INCREMENTAL // * CARGO_TARGET_DIR // * CARGO_CACHE_RUSTC_INFO // - // All of these except CARGO, CARGO_HOME, and CARGO_CACHE_RUSTC_INFO are + // All of these except CARGO, CARGO_HOME, CARGO_CONFIG_STOP_SEARCH_PATH, + // and CARGO_CACHE_RUSTC_INFO are // actually part of the config, but they are special-cased in the code. // // TODO: It might be a good idea to teach the Config loader to support diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index e6e41ef71e7..a0f6a86d76c 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -138,6 +138,8 @@ pub use schema::*; use super::auth::RegistryConfig; +const CARGO_CONFIG_STOP_SEARCH_PATH_ENV: &str = "CARGO_CONFIG_STOP_SEARCH_PATH"; + /// Helper macro for creating typed access methods. macro_rules! get_value_typed { ($name:ident, $ty:ty, $variant:ident, $expected:expr) => { @@ -654,6 +656,34 @@ impl GlobalContext { self.search_stop_path = Some(path); } + fn search_stop_path_from_env(&self) -> Option { + let path = PathBuf::from( + self.get_env_os(CARGO_CONFIG_STOP_SEARCH_PATH_ENV) + .filter(|path| !path.is_empty())?, + ); + let path = if path.is_absolute() { + path + } else { + self.cwd.join(path) + }; + Some(paths::normalize_path(&path)) + } + + fn effective_search_stop_path( + &self, + pwd: &Path, + env_stop_path: Option<&Path>, + ) -> Option { + let pwd = paths::normalize_path(pwd); + self.search_stop_path + .iter() + .cloned() + .chain(env_stop_path.map(PathBuf::from)) + .map(|path| paths::normalize_path(&path)) + .filter(|path| pwd.starts_with(path)) + .max_by_key(|path| path.components().count()) + } + /// Switches the working directory to [`std::env::current_dir`] /// /// There is not a need to also call [`Self::reload_rooted_at`]. @@ -1680,8 +1710,10 @@ impl GlobalContext { F: FnMut(&Path) -> CargoResult<()>, { let mut seen_dir = HashSet::new(); + let env_stop_path = self.search_stop_path_from_env(); + let search_stop_path = self.effective_search_stop_path(pwd, env_stop_path.as_deref()); - for current in paths::ancestors(pwd, self.search_stop_path.as_deref()) { + for current in paths::ancestors(pwd, search_stop_path.as_deref()) { let config_root = current.join(".cargo"); if let Some(path) = self.get_file_path(&config_root, "config", true)? { walk(&path)?; @@ -1696,7 +1728,10 @@ impl GlobalContext { // Once we're done, also be sure to walk the home directory even if it's not // in our history to be sure we pick up that standard location for // information. - if !seen_dir.contains(&canonical_home) && !seen_dir.contains(home) { + if env_stop_path.is_none() + && !seen_dir.contains(&canonical_home) + && !seen_dir.contains(home) + { if let Some(path) = self.get_file_path(home, "config", true)? { walk(&path)?; } diff --git a/src/doc/man/cargo.md b/src/doc/man/cargo.md index 7cc2041fad6..29d6973385f 100644 --- a/src/doc/man/cargo.md +++ b/src/doc/man/cargo.md @@ -208,7 +208,9 @@ for more information about configuration files. `.cargo/config.toml`\     Cargo automatically searches for a file named `.cargo/config.toml` in the current directory, and all parent directories. These configuration files -will be merged with the global configuration file. +will be merged with the global configuration file. Set `CARGO_CONFIG_STOP_SEARCH_PATH` +to prevent Cargo from searching above a specific directory and from loading +the global configuration file. `$CARGO_HOME/credentials.toml`\     Private authentication information for logging in to a registry. diff --git a/src/doc/src/commands/cargo.md b/src/doc/src/commands/cargo.md index 62b06ab72ac..c0525ed1240 100644 --- a/src/doc/src/commands/cargo.md +++ b/src/doc/src/commands/cargo.md @@ -319,7 +319,9 @@ for more information about configuration files. `.cargo/config.toml`\     Cargo automatically searches for a file named `.cargo/config.toml` in the current directory, and all parent directories. These configuration files -will be merged with the global configuration file. +will be merged with the global configuration file. Set `CARGO_CONFIG_STOP_SEARCH_PATH` +to prevent Cargo from searching above a specific directory and from loading +the global configuration file. `$CARGO_HOME/credentials.toml`\     Private authentication information for logging in to a registry. diff --git a/src/doc/src/reference/config.md b/src/doc/src/reference/config.md index 66d30318b18..ba362fbeab4 100644 --- a/src/doc/src/reference/config.md +++ b/src/doc/src/reference/config.md @@ -25,6 +25,11 @@ With this structure, you can specify configuration per-package, and even possibly check it into version control. You can also specify personal defaults with a configuration file in your home directory. +The upward search can be stopped at a specific directory by setting the +`CARGO_CONFIG_STOP_SEARCH_PATH` environment variable. The directory named by +that variable is still searched, but its ancestors are not. When this is set, +Cargo also does not load `$CARGO_HOME/config.toml`. + If a key is specified in multiple config files, the values will get merged together. Numbers, strings, and booleans will use the value in the deeper config directory taking precedence over ancestor directories, where the diff --git a/src/doc/src/reference/environment-variables.md b/src/doc/src/reference/environment-variables.md index df1b180a097..d8725df9e7f 100644 --- a/src/doc/src/reference/environment-variables.md +++ b/src/doc/src/reference/environment-variables.md @@ -20,6 +20,11 @@ system: location of this directory. Once a crate is cached it is not removed by the clean command. For more details refer to the [guide](../guide/cargo-home.md). +* `CARGO_CONFIG_STOP_SEARCH_PATH` --- Cargo stops searching for + `.cargo/config.toml` files after reaching this directory, inclusive. Ancestor + directories above this path will not be searched. Relative paths are resolved + relative to the current working directory. When this is set, Cargo also does + not load `$CARGO_HOME/config.toml`. * `CARGO_TARGET_DIR` --- Location of where to place all generated artifacts, relative to the current working directory. See [`build.target-dir`] to set via config. diff --git a/tests/testsuite/config.rs b/tests/testsuite/config.rs index fe09aded742..47b0c991a73 100644 --- a/tests/testsuite/config.rs +++ b/tests/testsuite/config.rs @@ -1353,6 +1353,24 @@ Caused by: ); } +#[cargo_test] +fn config_stop_search_path_env() { + write_config_at(".cargo/config.toml", "parent = true"); + write_config_at("foo/.cargo/config.toml", "project = true"); + write_config_at(paths::cargo_home().join("config.toml"), "home = true"); + + for stop_path in [paths::root().join("foo").display().to_string(), "..".into()] { + let gctx = GlobalContextBuilder::new() + .cwd("foo/bar") + .env("CARGO_CONFIG_STOP_SEARCH_PATH", stop_path) + .build(); + + assert_eq!(gctx.get::>("parent").unwrap(), None); + assert_eq!(gctx.get::>("project").unwrap(), Some(true)); + assert_eq!(gctx.get::>("home").unwrap(), None); + } +} + #[cargo_test] fn struct_with_opt_inner_struct() { // Struct with a key that is Option of another struct.