diff --git a/benches/benchsuite/benches/global_cache_tracker.rs b/benches/benchsuite/benches/global_cache_tracker.rs index f879b07eb41..7da1accb96c 100644 --- a/benches/benchsuite/benches/global_cache_tracker.rs +++ b/benches/benchsuite/benches/global_cache_tracker.rs @@ -35,7 +35,7 @@ fn initialize_context() -> GlobalContext { let cwd = homedir.clone(); let mut gctx = GlobalContext::new(shell, cwd, homedir); gctx.nightly_features_allowed = true; - gctx.set_search_stop_path(root()); + gctx.set_config_search_root(root()); gctx.configure( 0, false, diff --git a/crates/cargo-util/src/paths.rs b/crates/cargo-util/src/paths.rs index dd7086180b0..525efa3171c 100644 --- a/crates/cargo-util/src/paths.rs +++ b/crates/cargo-util/src/paths.rs @@ -441,14 +441,14 @@ pub struct PathAncestors<'a> { 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())); + tracing::trace!( + "creating path ancestors iterator from `{}` to `{}`", + path.display(), + stop_root_at.map_or("".to_string(), |p| p.display().to_string()) + ); PathAncestors { current: Some(path), - //HACK: avoid reading `~/.cargo/config` when testing Cargo itself. - stop_at, + stop_at: stop_root_at.map(|p| p.to_path_buf()), } } } @@ -466,6 +466,7 @@ impl<'a> Iterator for PathAncestors<'a> { } } + tracing::trace!("next path ancestor: `{}`", path.display()); Some(path) } else { None diff --git a/src/bin/cargo/cli.rs b/src/bin/cargo/cli.rs index 188fafdc9e5..3d0ed462fa5 100644 --- a/src/bin/cargo/cli.rs +++ b/src/bin/cargo/cli.rs @@ -2,12 +2,14 @@ use anyhow::{anyhow, Context as _}; use cargo::core::{features, CliUnstable}; use cargo::util::context::TermConfig; use cargo::{drop_print, drop_println, CargoResult}; +use cargo_util::paths; use clap::builder::UnknownArgumentValueParser; use itertools::Itertools; use std::collections::HashMap; use std::ffi::OsStr; use std::ffi::OsString; use std::fmt::Write; +use tracing::warn; use super::commands; use super::list_commands; @@ -23,8 +25,88 @@ pub fn main(gctx: &mut GlobalContext) -> CliResult { // In general, try to avoid loading config values unless necessary (like // the [alias] table). + // Register root directories. + // + // A root directory limits how far Cargo can search for files. + // + // Internally there are two notable roots we need to resolve: + // 1. The root when searching for config files (starting directory = cwd). + // 2. The root when searching for manifests (starting directory = manifest-path or cwd directory) + // + // Root directories can be specified via CARGO_ROOTS, --root or the existence of `.cargo/root` files. + // + // If CARGO_ROOTS is not set, the user's home directory is used as a default root. + // - This is a safety measure to avoid reading unsafe config files outside of the user's home + // directory. + // - This is also a measure to avoid triggering home directory automounter issues on some + // systems. + // + // The roots are deduplicated and sorted by their length so we can quickly find the closest root + // to a given starting directory (longest ancestor). + // + // A `SearchRoute` represents a route from a starting directory to the closest root directory. + // + // When there are no roots above a given starting directory, then a `SearchRoute` will use the + // starting directory itself is used as the root. + // - This is a safety measure to avoid reading unsafe config files in unknown locations (such as + // `/tmp`). + // + // There are two cached `SearchRoute`s, one for config files and one for workspace manifests, + // which are used to avoid repeatedly finding the nearest root directory. + + // Should it be an error if a given root doesn't exist? + // A user might configure a root under a `/mnt` directory that is not always mounted? + + let roots_env = gctx.get_env_os("CARGO_ROOTS").map(|s| s.to_owned()); + if let Some(paths_os) = roots_env { + for path in std::env::split_paths(&paths_os) { + gctx.add_root(&path)?; + } + } else { + //HACK: avoid reading `~/.cargo/config` when testing Cargo itself. + let test_root = gctx.get_env_os("__CARGO_TEST_ROOT").map(|s| s.to_owned()); + if let Some(test_root) = test_root { + tracing::debug!( + "no CARGO_ROOTS set, using __CARGO_TEST_ROOT as root: {}", + test_root.display() + ); + // This is a hack to avoid reading `~/.cargo/config` when testing Cargo itself. + gctx.add_root(&test_root)?; + } else if let Some(home) = std::env::home_dir() { + tracing::debug!( + "no CARGO_ROOTS set, using home directory as root: {}", + home.display() + ); + // To be safe by default, and not attempt to read config files outside of the + // user's home directory, we implicitly add the home directory as a root. + // Ref: https://github.com/rust-lang/rfcs/pull/3279 + // + // This is also a measure to avoid triggering home directory automounter issues + gctx.add_root(&home)?; + } + } + let args = cli(gctx).try_get_matches()?; + if let Some(cli_roots) = args.get_many::("root") { + for cli_root in cli_roots { + gctx.add_root(cli_root)?; + } + } + + // Look for any `.cargo/root` markers after we have registered all other roots so + // that other roots can stop us from triggering automounter issues. + let search_route = gctx.find_config_search_route(gctx.cwd()); + + let root_marker = paths::ancestors(&search_route.start, search_route.root.as_deref()) + .find(|current| current.join(".cargo").join("root").exists()); + if let Some(marker) = root_marker { + tracing::debug!("found .cargo/root marker at {}", marker.display()); + gctx.add_root(marker)?; + } else { + tracing::debug!("no .cargo/root marker found"); + } + // Update the process-level notion of cwd if let Some(new_cwd) = args.get_one::("directory") { // This is a temporary hack. @@ -46,7 +128,13 @@ pub fn main(gctx: &mut GlobalContext) -> CliResult { .into()); } std::env::set_current_dir(&new_cwd).context("could not change to requested directory")?; - gctx.reload_cwd()?; + } + + // Reload now that we have established the cwd and root + gctx.reload_cwd()?; + + if gctx.find_nearest_root(gctx.cwd())?.is_none() { + gctx.shell().warn("Cargo is running outside of any root directory, limiting loading of ancestor configs and manifest")?; } let (expanded_args, global_args) = expand_aliases(gctx, args, vec![])?; @@ -640,6 +728,15 @@ See 'cargo help <>' for more information on a sp .value_parser(["auto", "always", "never"]) .ignore_case(true), ) + .arg( + Arg::new("root") + .help("Define a root that limits searching for workspaces and .cargo/ directories") + .long("root") + .value_name("ROOT") + .action(ArgAction::Append) + .value_hint(clap::ValueHint::DirPath) + .value_parser(clap::builder::ValueParser::path_buf()), + ) .arg( Arg::new("directory") .help("Change to DIRECTORY before doing anything (nightly-only)") diff --git a/src/bin/cargo/commands/locate_project.rs b/src/bin/cargo/commands/locate_project.rs index 439f2d5792e..c560d5638ba 100644 --- a/src/bin/cargo/commands/locate_project.rs +++ b/src/bin/cargo/commands/locate_project.rs @@ -30,10 +30,12 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { let workspace; let root = match WhatToFind::parse(args) { WhatToFind::CurrentManifest => { + tracing::trace!("locate-project::exec(): finding root manifest"); root_manifest = args.root_manifest(gctx)?; &root_manifest } WhatToFind::Workspace => { + tracing::trace!("locate-project::exec(): finding workspace root manifest"); workspace = args.workspace(gctx)?; workspace.root_manifest() } diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 57e50ebefee..362256adf60 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -21,7 +21,7 @@ use crate::core::{ use crate::core::{EitherManifest, Package, SourceId, VirtualManifest}; use crate::ops; use crate::sources::{PathSource, SourceConfigMap, CRATES_IO_INDEX, CRATES_IO_REGISTRY}; -use crate::util::context::FeatureUnification; +use crate::util::context::{FeatureUnification, SearchRoute}; use crate::util::edit_distance; use crate::util::errors::{CargoResult, ManifestError}; use crate::util::interning::InternedString; @@ -746,6 +746,7 @@ impl<'gctx> Workspace<'gctx> { /// Returns an error if `manifest_path` isn't actually a valid manifest or /// if some other transient error happens. fn find_root(&mut self, manifest_path: &Path) -> CargoResult> { + debug!("find_root - {}", manifest_path.display()); let current = self.packages.load(manifest_path)?; match current .workspace_config() @@ -2023,12 +2024,14 @@ fn find_workspace_root_with_loader( gctx: &GlobalContext, mut loader: impl FnMut(&Path) -> CargoResult>, ) -> CargoResult> { + let search_route = gctx.find_workspace_manifest_search_route(manifest_path); + // Check if there are any workspace roots that have already been found that would work { let roots = gctx.ws_roots.borrow(); // Iterate through the manifests parent directories until we find a workspace // root. Note we skip the first item since that is just the path itself - for current in manifest_path.ancestors().skip(1) { + for current in paths::ancestors(&search_route.start, search_route.root.as_deref()).skip(1) { if let Some(ws_config) = roots.get(current) { if !ws_config.is_excluded(manifest_path) { // Add `Cargo.toml` since ws_root is the root and not the file @@ -2038,7 +2041,7 @@ fn find_workspace_root_with_loader( } } - for ances_manifest_path in find_root_iter(manifest_path, gctx) { + for ances_manifest_path in find_root_iter(&search_route, gctx) { debug!("find_root - trying {}", ances_manifest_path.display()); if let Some(ws_root_path) = loader(&ances_manifest_path)? { return Ok(Some(ws_root_path)); @@ -2058,10 +2061,10 @@ fn read_root_pointer(member_manifest: &Path, root_link: &str) -> PathBuf { } fn find_root_iter<'a>( - manifest_path: &'a Path, + search_route: &'a SearchRoute, gctx: &'a GlobalContext, ) -> impl Iterator + 'a { - LookBehind::new(paths::ancestors(manifest_path, None).skip(2)) + LookBehind::new(paths::ancestors(&search_route.start, search_route.root.as_deref()).skip(2)) .take_while(|path| !path.curr.ends_with("target/package")) // Don't walk across `CARGO_HOME` when we're looking for the // workspace root. Sometimes a package will be organized with diff --git a/src/cargo/ops/cargo_new.rs b/src/cargo/ops/cargo_new.rs index 88d2d6bf162..d2675883151 100644 --- a/src/cargo/ops/cargo_new.rs +++ b/src/cargo/ops/cargo_new.rs @@ -802,7 +802,7 @@ fn mk(gctx: &GlobalContext, opts: &MkOptions<'_>) -> CargoResult<()> { } let manifest_path = paths::normalize_path(&path.join("Cargo.toml")); - if let Ok(root_manifest_path) = find_root_manifest_for_wd(&manifest_path) { + if let Ok(root_manifest_path) = find_root_manifest_for_wd(gctx, &manifest_path) { let root_manifest = paths::read(&root_manifest_path)?; // Sometimes the root manifest is not a valid manifest, so we only try to parse it if it is. // This should not block the creation of the new project. It is only a best effort to diff --git a/src/cargo/ops/registry/owner.rs b/src/cargo/ops/registry/owner.rs index 7c8246fdfbf..06e4b570925 100644 --- a/src/cargo/ops/registry/owner.rs +++ b/src/cargo/ops/registry/owner.rs @@ -28,7 +28,7 @@ pub fn modify_owners(gctx: &GlobalContext, opts: &OwnersOptions) -> CargoResult< let name = match opts.krate { Some(ref name) => name.clone(), None => { - let manifest_path = find_root_manifest_for_wd(gctx.cwd())?; + let manifest_path = find_root_manifest_for_wd(gctx, gctx.cwd())?; let ws = Workspace::new(&manifest_path, gctx)?; ws.current()?.package_id().name().to_string() } diff --git a/src/cargo/ops/registry/yank.rs b/src/cargo/ops/registry/yank.rs index f46b9332f6b..45f2edca579 100644 --- a/src/cargo/ops/registry/yank.rs +++ b/src/cargo/ops/registry/yank.rs @@ -26,7 +26,7 @@ pub fn yank( let name = match krate { Some(name) => name, None => { - let manifest_path = find_root_manifest_for_wd(gctx.cwd())?; + let manifest_path = find_root_manifest_for_wd(gctx, gctx.cwd())?; let ws = Workspace::new(&manifest_path, gctx)?; ws.current()?.package_id().name().to_string() } diff --git a/src/cargo/util/command_prelude.rs b/src/cargo/util/command_prelude.rs index 8e375f7e1ea..eb389b448ab 100644 --- a/src/cargo/util/command_prelude.rs +++ b/src/cargo/util/command_prelude.rs @@ -1077,7 +1077,7 @@ pub fn root_manifest(manifest_path: Option<&Path>, gctx: &GlobalContext) -> Carg } Ok(path) } else { - find_root_manifest_for_wd(gctx.cwd()) + find_root_manifest_for_wd(gctx, gctx.cwd()) } } @@ -1132,7 +1132,7 @@ fn get_profile_candidates() -> Vec { fn get_workspace_profile_candidates() -> CargoResult> { let gctx = new_gctx_for_completions()?; - let ws = Workspace::new(&find_root_manifest_for_wd(gctx.cwd())?, &gctx)?; + let ws = Workspace::new(&find_root_manifest_for_wd(&gctx, gctx.cwd())?, &gctx)?; let profiles = Profiles::new(&ws, InternedString::new("dev"))?; let mut candidates = Vec::new(); @@ -1216,7 +1216,7 @@ fn get_bin_candidates() -> Vec { fn get_targets_from_metadata() -> CargoResult> { let cwd = std::env::current_dir()?; let gctx = GlobalContext::new(shell::Shell::new(), cwd.clone(), cargo_home_with_cwd(&cwd)?); - let ws = Workspace::new(&find_root_manifest_for_wd(&cwd)?, &gctx)?; + let ws = Workspace::new(&find_root_manifest_for_wd(&gctx, &cwd)?, &gctx)?; let packages = ws.members().collect::>(); @@ -1271,7 +1271,10 @@ fn get_target_triples_from_rustup() -> CargoResult CargoResult> { let cwd = std::env::current_dir()?; let gctx = GlobalContext::new(shell::Shell::new(), cwd.clone(), cargo_home_with_cwd(&cwd)?); - let ws = Workspace::new(&find_root_manifest_for_wd(&PathBuf::from(&cwd))?, &gctx); + let ws = Workspace::new( + &find_root_manifest_for_wd(&gctx, &PathBuf::from(&cwd))?, + &gctx, + ); let rustc = gctx.load_global_rustc(ws.as_ref().ok())?; @@ -1374,7 +1377,7 @@ pub fn get_pkg_id_spec_candidates() -> Vec { fn get_packages() -> CargoResult> { let gctx = new_gctx_for_completions()?; - let ws = Workspace::new(&find_root_manifest_for_wd(gctx.cwd())?, &gctx)?; + let ws = Workspace::new(&find_root_manifest_for_wd(&gctx, gctx.cwd())?, &gctx)?; let requested_kinds = CompileKind::from_requested_targets(ws.gctx(), &[])?; let mut target_data = RustcTargetData::new(&ws, &requested_kinds)?; diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index 93f5e7d6106..0e5f69adc47 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -159,6 +159,30 @@ pub struct CredentialCacheValue { pub operation_independent: bool, } +/// A point-to-point route for searching config files or manifests, resolved +/// based on a starting directory and a set of root directories. +#[derive(Clone, Debug)] +pub struct SearchRoute { + /// The first directory in the route to search (inclusive) + /// The path is canonicalized. + pub start: PathBuf, + /// The last directory in the route to search (inclusive) + /// If not set then the route ends at the root of the filesystem. + /// The path is canonicalized. + pub root: Option, +} + +impl SearchRoute { + pub fn sentinal(path: impl AsRef) -> Self { + let start = path + .as_ref() + .canonicalize() + .expect("failed to canonicalize path"); + let root = Some(start.clone()); + Self { start, root } + } +} + /// Configuration information for cargo. This is not specific to a build, it is information /// relating to cargo itself. #[derive(Debug)] @@ -175,8 +199,16 @@ pub struct GlobalContext { cli_config: Option>, /// The current working directory of cargo cwd: PathBuf, - /// Directory where config file searching should stop (inclusive). - search_stop_path: Option, + /// The full set of root directories that limit config file searching. + /// Sorted by path length, longest first. + sorted_roots: Vec, + /// In case we are running outside of any user-configured root directory, we + /// add the directory of the first manifest as as root directory. + fallback_manifest_root: RefCell>, + /// Directories to search for config files (invalidated if roots or cwd changes). + config_search_route: RefCell>, + /// Directories to search for manifest files (invalidated if roots or starting point changes). + manifest_search_route: RefCell>, /// The location of the cargo executable (path to current process) cargo_exe: LazyCell, /// The location of the rustdoc executable @@ -281,11 +313,18 @@ impl GlobalContext { _ => true, }; + // By default only allow searching the current directory, until roots + // are set. + //let config_search_route = SearchRoute::sentinal(&cwd); + GlobalContext { home_path: Filesystem::new(homedir), shell: RefCell::new(shell), cwd, - search_stop_path: None, + sorted_roots: Vec::new(), + fallback_manifest_root: RefCell::new(None), + config_search_route: RefCell::new(None), + manifest_search_route: RefCell::new(None), values: LazyCell::new(), credential_values: LazyCell::new(), cli_config: None, @@ -567,12 +606,200 @@ impl GlobalContext { } } - /// Sets the path where ancestor config file searching will stop. The - /// given path is included, but its ancestors are not. - pub fn set_search_stop_path>(&mut self, path: P) { - let path = path.into(); - debug_assert!(self.cwd.starts_with(&path)); - self.search_stop_path = Some(path); + pub fn add_root>(&mut self, path: P) -> CargoResult<()> { + let path = path + .as_ref() + .canonicalize() + .context("couldn't canonicalize path")?; + if !self.sorted_roots.iter().any(|root| root == &path) { + self.sorted_roots.push(path); + self.sorted_roots + .sort_by_key(|p| std::cmp::Reverse(p.components().count())); + self.config_search_route = RefCell::new(None); + self.manifest_search_route = RefCell::new(None); + } + Ok(()) + } + + pub fn ensure_fallback_root>(&self, path: P) { + let path = path + .as_ref() + .canonicalize() + .expect("failed to canonicalize path"); + if self.fallback_manifest_root.borrow_mut().is_none() { + tracing::debug!("Setting fallback manifest root to {:?}", path); + *self.fallback_manifest_root.borrow_mut() = Some(path); + *self.config_search_route.borrow_mut() = None; + *self.manifest_search_route.borrow_mut() = None; + } else { + tracing::debug!( + "Fallback manifest root already set to {:?}, not changing", + self.fallback_manifest_root.borrow() + ); + } + } + + pub fn find_nearest_root>( + &self, + start: P, + ) -> CargoResult> { + let start = start + .as_ref() + .canonicalize() + .context("couldn't canonicalize path")?; + + tracing::debug!( + "Searching sorted roots for closest ancestor {:?}", + self.sorted_roots + ); + let root = self + .sorted_roots + .iter() + .find(|root| start.starts_with(root)) + .cloned(); + + if let Some(root) = root { + tracing::debug!("Found candidate root {:?}", root); + Ok(Some((start, root))) + } else if let Some(fallback_root) = self.fallback_manifest_root.borrow().as_ref() { + tracing::debug!("Using manifest path as fallback root"); + Ok(Some((start, fallback_root.clone()))) + } else { + tracing::debug!("No candidate root found for start {:?}", start); + Ok(None) + } + } + + /// Find shortest route between `start` and one of the roots in [Self::sorted_roots]. + /// + /// If no root is found then fallback to root == start so we only search one directory. + /// - This is a safety measure to reduce the risk of reading unsafe state from locations + /// that are not owned by the user (for example building under `/tmp`). + fn find_search_route>(&self, start: P) -> CargoResult { + let start = start.as_ref(); + let route = self.find_nearest_root(&start)?; + + // Cargo no longer allows searching up to the root of the filesystem unless the root is + // explicitly added to `sorted_roots`. + let route = route.unwrap_or_else(|| (start.to_owned(), start.to_owned())); + Ok(SearchRoute { + start: route.0, + root: Some(route.1), + }) + } + + /// Ignore any configured roots and define a config search route between + /// `cwd` and `path`. If `path` is `None`, then the search route will go to + /// the root of the filesystem which could be unsafe. + /// + /// Normally [`update_config_search_route`] should be used instead. + pub fn set_config_search_root>(&mut self, root: Option

) { + let Ok(start) = self.cwd().canonicalize() else { + *self.config_search_route.borrow_mut() = None; + return; + }; + if let Some(path) = root { + let root = path.into(); + debug_assert!(self.cwd.starts_with(&root)); + let Ok(root) = root.canonicalize() else { + *self.config_search_route.borrow_mut() = None; + return; + }; + *self.config_search_route.borrow_mut() = Some(SearchRoute { + start, + root: Some(root), + }); + } else { + *self.config_search_route.borrow_mut() = Some(SearchRoute { start, root: None }); + } + } + + pub fn find_config_search_route>(&self, start: P) -> SearchRoute { + tracing::trace!( + "Finding config search route starting at {:?}", + start.as_ref() + ); + + let start = start + .as_ref() + .canonicalize() + .expect("failed to canonicalize cwd"); + // Return the existing route if the start == path. + if let Some(route) = &*self.config_search_route.borrow() { + if route.start == start { + tracing::trace!("Using cached config search route: {:?}", route); + return route.clone(); + } + } + + let config_search_route = self + .find_search_route(start) + .expect("failed to find config file search route"); + *self.config_search_route.borrow_mut() = Some(config_search_route.clone()); + config_search_route + } + + pub fn find_package_manifest_search_route>(&self, start: P) -> SearchRoute { + tracing::trace!( + "Finding package manifest search route starting at {:?}", + start.as_ref() + ); + + let start = start + .as_ref() + .canonicalize() + .expect("failed to canonicalize path"); + // Return the existing route if the start == path. + if let Some(route) = &*self.manifest_search_route.borrow() { + if route.start == start { + return route.clone(); + } + } + + // As a special case, we allow the traversal of parent directories, when + // outside of all root directories to find the package manifest. + // + // This is a trade off between safety and convenience, so it's e.g. + // possible to unpack a package under `/tmp` and start a build from + // `/tmp/my-package/sub/dir` and find `/tmp/my-package/Cargo.toml`, but + // not allow a potentially unsafe `/tmp/Cargo.toml` workspace to be loaded. + let manifest_search_route = + if let Some((start, root)) = self.find_nearest_root(&start).ok().flatten() { + SearchRoute { + start, + root: Some(root), + } + } else { + SearchRoute { + start, + root: None, // Allow searching up to the root of the filesystem. + } + }; + *self.manifest_search_route.borrow_mut() = Some(manifest_search_route.clone()); + manifest_search_route.clone() + } + + pub fn find_workspace_manifest_search_route>(&self, start: P) -> SearchRoute { + tracing::trace!( + "Finding workspace manifest search route starting at {:?}", + start.as_ref() + ); + let start = start + .as_ref() + .canonicalize() + .expect("failed to canonicalize path"); + // Return the existing route if the start == path. + if let Some(route) = &*self.manifest_search_route.borrow() { + if route.start == start { + return route.clone(); + } + } + + let manifest_search_route = self + .find_search_route(start) + .expect("failed to find manifest search route"); + *self.manifest_search_route.borrow_mut() = Some(manifest_search_route.clone()); + manifest_search_route.clone() } /// Switches the working directory to [`std::env::current_dir`] @@ -589,6 +816,7 @@ impl GlobalContext { })?; self.cwd = cwd; + *self.config_search_route.borrow_mut() = None; self.home_path = Filesystem::new(homedir); self.reload_rooted_at(self.cwd.clone())?; Ok(()) @@ -1257,7 +1485,7 @@ impl GlobalContext { let mut result = Vec::new(); let mut seen = HashSet::new(); let home = self.home_path.clone().into_path_unlocked(); - self.walk_tree(&self.cwd, &home, |path| { + self.walk_config_search_route(&self.cwd, &home, |path| { let mut cv = self._load_file(path, &mut seen, false, WhyLoad::FileDiscovery)?; if self.cli_unstable().config_include { self.load_unmerged_include(&mut cv, &mut seen, &mut result)?; @@ -1298,7 +1526,7 @@ impl GlobalContext { let mut cfg = CV::Table(HashMap::new(), Definition::Path(PathBuf::from("."))); let home = self.home_path.clone().into_path_unlocked(); - self.walk_tree(path, &home, |path| { + self.walk_config_search_route(path, &home, |path| { let value = self.load_file(path)?; cfg.merge(value, false).with_context(|| { format!("failed to merge configuration at `{}`", path.display()) @@ -1676,13 +1904,14 @@ impl GlobalContext { } } - fn walk_tree(&self, pwd: &Path, home: &Path, mut walk: F) -> CargoResult<()> + fn walk_config_search_route(&self, pwd: &Path, home: &Path, mut walk: F) -> CargoResult<()> where F: FnMut(&Path) -> CargoResult<()>, { let mut seen_dir = HashSet::new(); - for current in paths::ancestors(pwd, self.search_stop_path.as_deref()) { + let search_route = self.find_config_search_route(pwd); + for current in paths::ancestors(&search_route.start, search_route.root.as_deref()) { let config_root = current.join(".cargo"); if let Some(path) = self.get_file_path(&config_root, "config", true)? { walk(&path)?; @@ -3152,7 +3381,6 @@ mod tests { #[test] fn disables_multiplexing() { let mut gctx = GlobalContext::new(Shell::new(), "".into(), "".into()); - gctx.set_search_stop_path(std::path::PathBuf::new()); gctx.set_env(Default::default()); let mut http = CargoHttpConfig::default(); diff --git a/src/cargo/util/important_paths.rs b/src/cargo/util/important_paths.rs index 224c4ab8b86..8c3779434de 100644 --- a/src/cargo/util/important_paths.rs +++ b/src/cargo/util/important_paths.rs @@ -2,15 +2,24 @@ use crate::util::errors::CargoResult; use cargo_util::paths; use std::path::{Path, PathBuf}; +use super::GlobalContext; + /// Finds the root `Cargo.toml`. -pub fn find_root_manifest_for_wd(cwd: &Path) -> CargoResult { +pub fn find_root_manifest_for_wd(gctx: &GlobalContext, cwd: &Path) -> CargoResult { let valid_cargo_toml_file_name = "Cargo.toml"; let invalid_cargo_toml_file_name = "cargo.toml"; let mut invalid_cargo_toml_path_exists = false; - for current in paths::ancestors(cwd, None) { + let search_route = gctx.find_package_manifest_search_route(cwd); + for current in paths::ancestors(&search_route.start, search_route.root.as_deref()) { let manifest = current.join(valid_cargo_toml_file_name); if manifest.exists() { + // In case we are running outside of any root directory, the directory for the + // first root manifest we find will become the fallback root. This is part of + // a safety trade-off that allows us to traverse unknown ancestors to find + // a package, but limits the risk of continuing to traverse and load manifests + // that we might not own (such as `/tmp/Cargo.toml`) + gctx.ensure_fallback_root(current); return Ok(manifest); } if current.join(invalid_cargo_toml_file_name).exists() { diff --git a/src/doc/src/reference/config.md b/src/doc/src/reference/config.md index 105de8f502c..77bc1c8cf10 100644 --- a/src/doc/src/reference/config.md +++ b/src/doc/src/reference/config.md @@ -44,6 +44,19 @@ those configuration files if it is invoked from the workspace root > and is the preferred form. If both files exist, Cargo will use the file > without the extension. +The root of the search hierarchy can be constrained in three ways: + +1. By creating a `.cargo/root` file (empty) +2. By setting the `CARGO_ROOT` environment variable +3. Passing `--root`. + +If a root directory is given then Cargo will search parent directories up until +it reaches the root directory, instead of searching all the way up to the root +of the filesystem. Cargo will still check `$CARGO_HOME/config.toml` even if it +is outside of the root directory. If multiple paths are specified then the +effective root is the one that's most-specific (closest to the current working +directory). + ## Configuration format Configuration files are written in the [TOML format][toml] (like the diff --git a/src/doc/src/reference/environment-variables.md b/src/doc/src/reference/environment-variables.md index ae29e6f38e4..76d0225419c 100644 --- a/src/doc/src/reference/environment-variables.md +++ b/src/doc/src/reference/environment-variables.md @@ -20,6 +20,10 @@ 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_ROOT` --- Instead of letting Cargo search every ancestor directory, up + to the root of the filesystem, looking for `.cargo` config directories or + workspace manifests, this limits how far Cargo can search. It doesn't stop + Cargo from reading `$CARGO_HOME/config.toml`, even if it's outside the root. * `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 80be8dabc1c..a82ab611d63 100644 --- a/tests/testsuite/config.rs +++ b/tests/testsuite/config.rs @@ -110,7 +110,7 @@ impl GlobalContextBuilder { let mut gctx = GlobalContext::new(shell, cwd, homedir); gctx.nightly_features_allowed = self.enable_nightly_features || !self.unstable.is_empty(); gctx.set_env(self.env.clone()); - gctx.set_search_stop_path(&root); + gctx.set_config_search_root(&root); gctx.configure( 0, false,