[Pre-RFC] Proposal: bound Cargo's implicit upward discovery for config.toml files

Proposal: bound Cargo's implicit upward discovery

Cargo currently walks from cwd toward filesystem root when discovering .cargo/config.toml. That is useful interactively, but tools do not have a supported way to say "only discover config inside this project/build directory".

This affects tools like Pixi, generated build directories, /tmp builds, and other wrappers that can set CARGO_HOME but cannot prevent parent .cargo/config.toml from being loaded.

I propose adding one discovery-boundary primitive, exposed first as an env var because wrappers need it to propagate to nested Cargo invocations:

CARGO_DISCOVERY_CEILINGS=<path-list>

For any upward discovery starting at cwd, Cargo picks the nearest configured ancestor and searches only up to that directory, inclusive.

When used for config discovery:

  • load .cargo/config.toml from cwd through the active ceiling
  • do not inspect ancestors above the ceiling
  • do not implicitly load $CARGO_HOME/config.toml
  • still allow explicit --config <path> and include

The $CARGO_HOME behavior matters: otherwise a tool still cannot describe the implicit config graph it is willing to load. Users who want home config can add it explicitly with --config "$CARGO_HOME/config.toml" or an include.

This is intended to be the config-discovery half of the same problem as workspace/package discovery ceilings. I think config discovery can be phased first, but the primitive should be designed so #7621 and #7871 do not grow separate incompatible knobs.

Related threads:

2 Likes

I like the idea, and I'd love to see "only look in cwd" become the default on an edition.

2 Likes

Perhaps a chain = <bool> parameter in the configuration file about whether to support chaining. Then a workspace config can say "no", but the constituent crates say "yes".

1 Like

Or inverting it like .editorconfig: root = true

1 Like

I appreciate the comments!

Unfortunately, a config key would not solve the main use case for us. We build third-party repositories under a tool-managed environment, and we do not necessarily control or want to modify their .cargo/config.toml.

It also makes the boundary part of config discovery itself: Cargo would need to discover and load config files in order to know where config discovery should stop. That can maybe be designed as special side-band state, but for wrappers/tools we need the authoritative boundary to come from outside the discovered config graph.

I think a repository-owned marker like root = true could still be useful for projects that want to declare their own boundary. It just seems complementary to, rather than a replacement for, an invocation/tool-controlled boundary.

Partial workaround: If you can have a directory path that is clean from unwanted configs, you can cd to it and use --manifest-path=/elsewhere/Cargo.toml to separate Cargo's config search (relative to cwd) from the location of the crate being built.

Crates invoking cargo from build.rs will unfortunately revert to their own crate's cwd.

The problem is that most sources will be somewhere in $HOME: That is typically the one place users are allowed to place persistent files. So $HOME/.cargo/config.toml will be picked up by walking upward from the project's Cargo.toml. This is the case even when setting $CARGO_HOME to point somewhere else and of the current working directory.

This is typically great way for a user to override settings she wants applied to all her projects. But it is a real problem when you want a reproducible way to build the exact same configuration across different machines.

We can not place files outside the source directory we are trying to build: That is not "our" directory anymore, and no tool should litter in unexpected places. Having some environment variable would be the most convenient for our use case.

I might have misunderstood what you meant though:

I tired

cd /tmp && cargo install --path="$HOME/some/where" --root="..."

... and get a build failure due to config options I set in my ~/.cargo/config.toml.

Cargo install does not accept --manifest-path, so maybe I am doing something wrong?

I actually tried a bit more: cd /tmp && cargo build --manifest-path=$HOME/some/where/Cargo.toml works, while cargo build --manifest-path="$HOME/some/where/CArgo.toml" fails to build with my broken configuration in $HOME/.cargo/config.toml.

So the work-around you suggested works indeed works for cargo build. Is there a similar work-around for cargo install?

I tired to cargo build first and then do a cargo install as a separate step. Cargo is really thorough: It does not recompile any of the files, but it does notice that the linker has changed and tries to relinking the binary, triggering my broken linker configuration in $HOME/.cargo/config.toml.

I shouldn't skip talking about use cases first, though when I was stabilizing config-include I actually came up with a maybe-simpler implementation idea. Let me just dump it here.

We can add a top-level config-discovery=true|false key in Cargo configuration. As a native Cargo config field, it automatically supports TOML config, env CARGO_CONFIG_DISCOVERY, and CLI --config config-discovery. You can just run env CARGO_CONFIG_DISCOVERY=false cargo build to stop loading any config, as env wins over file discovery.

Also during the stabilization, we reserves the room for templating paths in include, so potentially we can implement things like

config-discovery = false
[[include]]
path = "{workspace-root}/.cargo/config.toml"
[[include]]
path = "{cargo-config-home}/config.toml"

to read only config from CARGO_HOME and your workspace root.

While search ceiling looks nice and convenience, I personally think whoever depended on a fragile auto-discovery was just because config-include wasn't stabilized. They should migrate to include if not yet.

EDIT: "{workspace-root}/.cargo/config.toml" may not work, at least config needs to be loaded all before manifest loading. Interleaving them is a non-trivial architecture change and might be messy to do in Cargo

I think user config should normally still be included even if discovery stops, or I guess I'll have to go back to configuring everything through system-wide CARGO_ vars instead. Things like overriding the build.target-dir shouldn't be ignored by some projects.

1 Like

~/.cargo/config.toml is actually looked at even when building outside of your home dir: Configuration - The Cargo Book

1 Like

It looks in $CARGO_HOME/.cargo/config.toml, not $HOME/.cargo/config.toml. It is easy to work around that way :slight_smile:

I agree: That should be the default behavior, but a user should be able to opt out for some or all builds.