Skip to content

[Pre-RFC] Enable .cargo/root, CARGO_ROOTS + --root to limit config + manifest searches #15620

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benches/benchsuite/benches/global_cache_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 7 additions & 6 deletions crates/cargo-util/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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("<root>".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()),
}
}
}
Expand All @@ -466,6 +466,7 @@ impl<'a> Iterator for PathAncestors<'a> {
}
}

tracing::trace!("next path ancestor: `{}`", path.display());
Some(path)
} else {
None
Expand Down
99 changes: 98 additions & 1 deletion src/bin/cargo/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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::<std::path::PathBuf>("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::<std::path::PathBuf>("directory") {
// This is a temporary hack.
Expand All @@ -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![])?;
Expand Down Expand Up @@ -640,6 +728,15 @@ See '<cyan,bold>cargo help</> <cyan><<command>></>' 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)")
Expand Down
2 changes: 2 additions & 0 deletions src/bin/cargo/commands/locate_project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
13 changes: 8 additions & 5 deletions src/cargo/core/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Option<PathBuf>> {
debug!("find_root - {}", manifest_path.display());
let current = self.packages.load(manifest_path)?;
match current
.workspace_config()
Expand Down Expand Up @@ -2023,12 +2024,14 @@ fn find_workspace_root_with_loader(
gctx: &GlobalContext,
mut loader: impl FnMut(&Path) -> CargoResult<Option<PathBuf>>,
) -> CargoResult<Option<PathBuf>> {
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
Expand All @@ -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));
Expand All @@ -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<Item = PathBuf> + '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
Expand Down
2 changes: 1 addition & 1 deletion src/cargo/ops/cargo_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/cargo/ops/registry/owner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
2 changes: 1 addition & 1 deletion src/cargo/ops/registry/yank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
13 changes: 8 additions & 5 deletions src/cargo/util/command_prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}

Expand Down Expand Up @@ -1132,7 +1132,7 @@ fn get_profile_candidates() -> Vec<clap_complete::CompletionCandidate> {

fn get_workspace_profile_candidates() -> CargoResult<Vec<clap_complete::CompletionCandidate>> {
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();
Expand Down Expand Up @@ -1216,7 +1216,7 @@ fn get_bin_candidates() -> Vec<clap_complete::CompletionCandidate> {
fn get_targets_from_metadata() -> CargoResult<Vec<Target>> {
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::<Vec<_>>();

Expand Down Expand Up @@ -1271,7 +1271,10 @@ fn get_target_triples_from_rustup() -> CargoResult<Vec<clap_complete::Completion
fn get_target_triples_from_rustc() -> CargoResult<Vec<clap_complete::CompletionCandidate>> {
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())?;

Expand Down Expand Up @@ -1374,7 +1377,7 @@ pub fn get_pkg_id_spec_candidates() -> Vec<clap_complete::CompletionCandidate> {
fn get_packages() -> CargoResult<Vec<Package>> {
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)?;
Expand Down
Loading
Loading