Skip to content

Commit 5d75611

Browse files
committed
.cargo/root, CARGO_ROOT, --root limit config + workspace searches
If a root directory is specified then cargo will only search ancestors looking for `.cargo` config files and workspaces up until the root directory is reached (instead of walking to the root of the filesystem). A root can be specified in three ways: 1. The existence of a `.cargo/root` file (discovered by checking parents up towards the root of the filesystem) 2. By setting the `CARGO_ROOT` environment variable 3. By passing `--root` on the command line If more than one is specified then the effective root is the one that's most-specific / closest to the current working directory. ### What does this PR try to resolve? Fixes: #5418 (for my use case then #6805 isn't a practical workaround) This goes some way to allow nesting of workspaces, by adding a way to limit the directories that Cargo searches when looking for config files and workspace manifests. This does not enable nesting in the form of workspace inheritance - but does provide the means to technically have a filesystem with nested workspaces (that aren't aware of each other) and be able to hide any outer (unrelated) workspace while building a nested workspace. This gives more flexibility for tools that want to use cargo as an implementation detail. In particular this allows you to sandbox the build of nested third-party workspaces that may be (unknowingly) dynamically unpacked within an outer workspace, in situations where neither the workspace being built and the outer workspace are owned by the tool that is managing the build. For example a tool based on rustdoc-json should be able to fetch and build documentation for third-party crates under any user-specified build/target directory without having to worry about spooky action at a distance due to config files and workspaces in ancestor directories. In my case, I have a runtime for coding with LLMs that is given a repo to work on and is expected to keep its artifacts contained to a `.ai/` directory. This runtime supports building markdown documentation for Rust crates, which involves using cargo to generate rustdoc-json data. That tool is expected to keep its artifacts contained within `.ai/docs/rust/build/`. It's possible that the project itself is Rust based and could define a workspace or `.cargo/config.toml` but from the pov of this toolchain those have nothing to do with the crate whose documentation is being generated (which are packages downloaded from crates.io). ### How to test and review this PR? TODO: write tests
1 parent 21a67e2 commit 5d75611

File tree

5 files changed

+100
-2
lines changed

5 files changed

+100
-2
lines changed

src/bin/cargo/cli.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anyhow::{anyhow, Context as _};
22
use cargo::core::{features, CliUnstable};
33
use cargo::util::context::TermConfig;
44
use cargo::{drop_print, drop_println, CargoResult};
5+
use cargo_util::paths;
56
use clap::builder::UnknownArgumentValueParser;
67
use itertools::Itertools;
78
use std::collections::HashMap;
@@ -17,6 +18,34 @@ use crate::util::is_rustup;
1718
use cargo::core::shell::ColorChoice;
1819
use cargo::util::style;
1920

21+
fn closest_valid_root<'a>(
22+
cwd: &std::path::Path,
23+
config_root: Option<&'a std::path::Path>,
24+
env_root: Option<&'a std::path::Path>,
25+
cli_root: Option<&'a std::path::Path>,
26+
) -> anyhow::Result<Option<&'a std::path::Path>> {
27+
for (name, root) in [
28+
(".cargo/root", config_root),
29+
("CARGO_ROOT", env_root),
30+
("--root", cli_root),
31+
] {
32+
if let Some(root) = root {
33+
if !cwd.starts_with(root) {
34+
return Err(anyhow::format_err!(
35+
"the {} `{}` is not a parent of the current working directory `{}`",
36+
name,
37+
root.display(),
38+
cwd.display()
39+
));
40+
}
41+
}
42+
}
43+
Ok([config_root, env_root, cli_root]
44+
.into_iter()
45+
.flatten()
46+
.max_by_key(|root| root.components().count()))
47+
}
48+
2049
#[tracing::instrument(skip_all)]
2150
pub fn main(gctx: &mut GlobalContext) -> CliResult {
2251
// CAUTION: Be careful with using `config` until it is configured below.
@@ -25,6 +54,7 @@ pub fn main(gctx: &mut GlobalContext) -> CliResult {
2554

2655
let args = cli(gctx).try_get_matches()?;
2756

57+
let mut need_reload = false;
2858
// Update the process-level notion of cwd
2959
if let Some(new_cwd) = args.get_one::<std::path::PathBuf>("directory") {
3060
// This is a temporary hack.
@@ -46,6 +76,44 @@ pub fn main(gctx: &mut GlobalContext) -> CliResult {
4676
.into());
4777
}
4878
std::env::set_current_dir(&new_cwd).context("could not change to requested directory")?;
79+
need_reload = true;
80+
}
81+
82+
// A root directory can be specified via CARGO_ROOT, --root or the existence of a `.cargo/root` file.
83+
// If more than one is specified, the effective root is the one closest to the current working directory.
84+
85+
let cwd = std::env::current_dir().context("could not get current working directory")?;
86+
// Windows UNC paths are OK here
87+
let cwd = cwd
88+
.canonicalize()
89+
.context("could not canonicalize current working directory")?;
90+
let config_root = paths::ancestors(&cwd, gctx.search_stop_path())
91+
.find(|current| current.join(".cargo").join("root").exists());
92+
let env_root = std::env::var_os("CARGO_ROOT")
93+
.map(std::path::PathBuf::from)
94+
.map(|p| {
95+
p.canonicalize()
96+
.context("could not canonicalize CARGO_ROOT")
97+
})
98+
.transpose()?;
99+
let env_root = env_root.as_deref();
100+
101+
let cli_root = args
102+
.get_one::<std::path::PathBuf>("root")
103+
.map(|p| {
104+
p.canonicalize()
105+
.context("could not canonicalize requested root directory")
106+
})
107+
.transpose()?;
108+
let cli_root = cli_root.as_deref();
109+
110+
if let Some(root) = closest_valid_root(&cwd, config_root, env_root, cli_root)? {
111+
tracing::debug!("root directory: {}", root.display());
112+
gctx.set_search_stop_path(root);
113+
need_reload = true;
114+
}
115+
116+
if need_reload {
49117
gctx.reload_cwd()?;
50118
}
51119

@@ -640,6 +708,14 @@ See '<cyan,bold>cargo help</> <cyan><<command>></>' for more information on a sp
640708
.value_parser(["auto", "always", "never"])
641709
.ignore_case(true),
642710
)
711+
.arg(
712+
Arg::new("root")
713+
.help("Define a root that limits searching for workspaces and .cargo/ directories")
714+
.long("root")
715+
.value_name("ROOT")
716+
.value_hint(clap::ValueHint::DirPath)
717+
.value_parser(clap::builder::ValueParser::path_buf()),
718+
)
643719
.arg(
644720
Arg::new("directory")
645721
.help("Change to DIRECTORY before doing anything (nightly-only)")

src/cargo/core/workspace.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2028,7 +2028,7 @@ fn find_workspace_root_with_loader(
20282028
let roots = gctx.ws_roots.borrow();
20292029
// Iterate through the manifests parent directories until we find a workspace
20302030
// root. Note we skip the first item since that is just the path itself
2031-
for current in manifest_path.ancestors().skip(1) {
2031+
for current in paths::ancestors(manifest_path, gctx.search_stop_path()).skip(1) {
20322032
if let Some(ws_config) = roots.get(current) {
20332033
if !ws_config.is_excluded(manifest_path) {
20342034
// Add `Cargo.toml` since ws_root is the root and not the file
@@ -2061,7 +2061,7 @@ fn find_root_iter<'a>(
20612061
manifest_path: &'a Path,
20622062
gctx: &'a GlobalContext,
20632063
) -> impl Iterator<Item = PathBuf> + 'a {
2064-
LookBehind::new(paths::ancestors(manifest_path, None).skip(2))
2064+
LookBehind::new(paths::ancestors(manifest_path, gctx.search_stop_path()).skip(2))
20652065
.take_while(|path| !path.curr.ends_with("target/package"))
20662066
// Don't walk across `CARGO_HOME` when we're looking for the
20672067
// workspace root. Sometimes a package will be organized with

src/cargo/util/context/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,11 @@ impl GlobalContext {
567567
}
568568
}
569569

570+
/// Gets the path where ancestor config file and workspace searching will stop.
571+
pub fn search_stop_path(&self) -> Option<&Path> {
572+
self.search_stop_path.as_deref()
573+
}
574+
570575
/// Sets the path where ancestor config file searching will stop. The
571576
/// given path is included, but its ancestors are not.
572577
pub fn set_search_stop_path<P: Into<PathBuf>>(&mut self, path: P) {

src/doc/src/reference/config.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ those configuration files if it is invoked from the workspace root
4444
> and is the preferred form. If both files exist, Cargo will use the file
4545
> without the extension.
4646
47+
The root of the search hierarchy can be constrained in three ways:
48+
49+
1. By creating a `.cargo/root` file (empty)
50+
2. By setting the `CARGO_ROOT` environment variable
51+
3. Passing `--root`.
52+
53+
If a root directory is given then Cargo will search parent directories up until
54+
it reaches the root directory, instead of searching all the way up to the root
55+
of the filesystem. Cargo will still check `$CARGO_HOME/config.toml` even if it
56+
is outside of the root directory. If multiple paths are specified then the
57+
effective root is the one that's most-specific (closest to the current working
58+
directory).
59+
4760
## Configuration format
4861

4962
Configuration files are written in the [TOML format][toml] (like the

src/doc/src/reference/environment-variables.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ system:
2020
location of this directory. Once a crate is cached it is not removed by the
2121
clean command.
2222
For more details refer to the [guide](../guide/cargo-home.md).
23+
* `CARGO_ROOT` --- Instead of letting Cargo search every ancestor directory, up
24+
to the root of the filesystem, looking for `.cargo` config directories or
25+
workspace manifests, this limits how far Cargo can search. It doesn't stop
26+
Cargo from reading `$CARGO_HOME/config.toml`, even if it's outside the root.
2327
* `CARGO_TARGET_DIR` --- Location of where to place all generated artifacts,
2428
relative to the current working directory. See [`build.target-dir`] to set
2529
via config.

0 commit comments

Comments
 (0)