Problems of `.cargo/config` files and possible solutions

Cargo configuration files (named .cargo/config or .cargo/config.toml) provide extensive configuration options for Cargo invocations. See the official documentation for a detailed explanation of Cargo's configuration system.

Unfortunately, there are a few general problems of configuration files in my opinion that often lead to errors and confusion in practice. This document tries to categorize and summarize these issues (as suggested by the Cargo team), with the goal of eventually resolving them. I categorized the issues into three classes: builds with --manifest-path, hierarchical merging, and overriding config values.

Builds with --manifest-path

Cargo supports building a project in a different directory using the --manifest-path command-line argument. Since this does not change the current working directory, it still reads the .cargo/config files that apply to the directory that cargo was invoked from, instead of reading the config files relative to the crate that is being built. This can lead to build errors or other unexpected results.

Example

Consider the following file system layout:

foo/
    .cargo/config.toml
bar/
    .cargo/config.toml
    Cargo.toml
    src/

Imagine we are currently in directory foo and try to build the crate in the bar directory using cargo's --manifest-path flag:

cargo build --manifest-path ../bar/Cargo.toml

Since the current working directory is foo, cargo will read the foo/.cargo/config.toml file for the build, not the bar/.cargo/config.toml file, which would be used if we started the build from within the bar folder.

There are basically two problems with this: First, the config values of the foo directory are applied, which might be incompatible with the bar crate. Second, the bar crate might require the config values specified in its configuration for a successful build, but this file is completely ignored.

The working directory does change for the build

While not directly related, it is worth mentioning that cargo does change the current working directory to the root of the crate that is being compiled, but it only does so after parsing the configuration. For example, a relative path in a build.rs build script will always resolve relative to the project root, even if the crate is built from a different directory using the --manifest-path argument.

Proposed Solutions

The current behavior frequently causes problems in practice, so multiple proposals have been created to change this behavior or at least allow to customize it:

  • Cargo does not always search for .cargo/config file in project root
    • Proposes to always choose config file at project root instead of depending on current working directory
    • Main motivation: --manifest-path and --package flags should use config of selected package
    • Problems:
      • Breaking change
      • When using different config files on --package: Which config should be chosen if multiple packages are built (e.g. on cargo build --workspace)?
  • env var for original invocation directory
    • Motivation: Builds with --manifest-path parse config of current working directory -> leads to build errors
    • Proposes a CARGO_INVOCATION_PWD make cargo change its working directory before starting the build
    • Problems:
      • Environment variables cumbersome to use (maybe add a command-line flag too?)
      • More general than it needs to be (maybe only allow changing the working directory to the crate root instead of allowing arbitrary directories?)
  • Add a --ignore-local-config flag for ignoring local .cargo/config.toml files
    • Proposal: Provide a way to completely ignore config files
      • This allows builds with --manifest-path to at least opt-out of applying the configuration of the current working directory.
      • The target crate config can then be included by using the config-include/config-cli features on the command line.
      • Exception: Still consider the .cargo/config file at the user's home directory, similar to the behavior of cargo install (for system-wide settings such as term.color)

Hierarchical Merging

Config files are automatically merged together with all config files in root directories. This can lead to problems because a config file in a parent directory can break the build or cause other undesired behavior.

As one example, consider a auto-generated crate created in a temporary /tmp/foo directory. Building this crate will automatically apply a /tmp/.cargo/config.toml file if present, which could easily have been created by another program.

Proposed Solutions

There are multiple proposals to allow disabling hierarchical merging if desired:

  • Add flag to ignore all parent directory configs
    • Proposes a --no-parent CLI flag that disables the hierarchical merging of config files
    • Motivation: Cargo behaves unexpectedly because of config values set in parent directories
  • Fine-grain control of Config.toml discovery
    • Motivation: No way to opt-out of hierarchical merging of config files
    • Proposal: Add a CARGO_CONFIG_PATH environment variable that explicitly specifies all the locations where Cargo is allowed to read config values from
      • Similar to the system's PATH environment variable
      • Completely overrides the default hierarchical config discovery
    • Problems:
      • Might require changing the environment variable for every project (if different config files should be considered)
      • Some config files might rely on hierarchical merging
  • The --ignore-local-config proposal from above might be also relevant here.

Overriding Config Values

Cargo config files are very useful for setting reasonable default values for a project. For example, an embedded crate that is only buildable on a single target system might have a config file that sets build.target so that the developer does not need to pass a --target argument on every build.

However, there are sometimes special cases where the configured default values do not work. For example, a sub-crate might want to disable incremental compilation for some reasons.

Basic Override Support

There were the following proposals for adding basic support for overriding .cargo/config.toml settings:

  • Proposal: Command-line config
    • This was accepted and implemented in #7649
    • Adds an unstable --config command line argument for overriding specific config values (documented here, tracked in #7722)
    • Adds support for including other configuration files, both from within a config file and from the command line (documented here, tracked in #7723)
  • Want env var specifying overriding config file
    • Proposes a CARGO_CONFIG_INCLUDE environment variable that allows listing additional config files that should be considered by Cargo
    • Motivation: Overriding specific config settings for some Cargo invocations
    • Mostly resolved by the implementation of the "command-line config" proposal

Reset a Config Key

While the command-line config support allows overriding all config keys, it is still not possible to reset all config keys back to their "not-set" state. For example, there is currently no way to disable a default target set in a build.target key, so that cargo build uses the host target again.

I personally stumbled across this many times, so I opened multiple proposals to fix some problems that this causes in practice:

Feedback

Have you encountered any of the above issues in practice? Which solutions would you prefer? Do you see any other issues of the cargo configuration system that don't fall into the categories above?

6 Likes

I agree the config is problematic. I have another problem with it: it's in a hidden directory, and the config file is separate from Cargo.toml, so it gets overlooked and forgotten.

How about moving all project-specific configuration out of .cargo/config to Cargo.toml? The package-specific merging of the config files won't matter, if packages won't need to depend on it.

For example, IMHO target-cpu=native belongs to Cargo.toml's [profile.release], and shouldn't be in [build] rustflags in .cargo/config.

7 Likes

Personally, I would like to transition to a model where only two .cargo/config files are ever read: the one in the workspace (or next to the Cargo.toml iff not in a workspace), and the one in $HOME. No traversing upwards to parent directories (e.g. /tmp/.cargo/config which anyone could create, which is arguably a security problem).

14 Likes

It's been a while since I've looked at .cargo/config so I've kind of paged the issues out that kept me from using it. The project structure I had was of the form

foo/
  Cargo.toml
libbar/
 .cargo/config
  Cargo.toml
libbaz/
 .cargo/config
  Cargo.toml

Where foo depends on libbaz, and libbaz depends on libbar, via path dependencies, in particular we want the config to apply respectively to libbaz and libbar but not actually be merged.

I don't really see this as exactly the problems described in the post, basically instead of traversing up towards the root from the current build directory, I'm needing somewhat the opposite a mechanism that loads configs as it descends down the build tree.

I like the idea! Do I understand you correctly that you not only want to remove the hierarchical structure, but also change the behavior with --manifest-path (use config file at workspace root instead of depending on current working dir)? This would solve most problems, but how do we avoid breaking existing users of config files?

1 Like

We could avoid breakage in three ways:

  1. When building a crate for a new edition (2021 or newer) we could automatically use the new behavior.
  2. We could read ~/.cargo/config first, and look for a directive opting into the new behavior.
  3. We could announce the intended transition, and print a warning whenever a config file is found by traversing parent directories, with an option to opt explicitly for the old or new behavior if desired, then eventually make the new behavior the default.

This doesn't affect Cargo.toml files in crates, only .cargo/config files, which means that changing behavior here shouldn't cause any compatibility issue with crates.

3 Likes

opting in to the new behavior via the config file we're trying to do away with seems a poor plan.

either of the other two seem like good plans though.

2 Likes

The proposal was not to eliminate ~/.cargo/config; it was to eliminate the "search upwards through parent directories" behavior, and look only at ~/.cargo/config and the workspace/project config.

2 Likes

Oh, well, sure I guess.

.cargo/config should go away as much as possible, and move things into Cargo.toml as much as possible, but that could be done separately from altering the search system.

3 Likes

Is there any discussion thread about moving (some) .cargo/config values into Cargo.toml yet?

I'm less worried about removing the upwards traversal than I'm about the behavior change with --manifest-path, i.e. reading the config file at the workspace root of the referenced crate instead of the file in the current working directory.

Good point!

Hmm, folks, does it makes sense to do away with .cargo/config.toml completely?

What functions does it serve that cannot be configured by some per-target sections in Cargo.toml and/or build.rs for example?

In my experience .cargo/config.toml is primarily about configuring for the system, which you can't do in the Cargo.toml because that is used on many peoples systems (which is also why it doesn't make sense to commit it into the repo in the majority of cases). As an example I have target."wasm32-wasi".runner = "/nix/store/wzlwr5qbwskdl47z3g16zr2422f3f8hs-wasmtime-15.0.1/bin/wasmtime" which is only valid on NixOS, and only if that version of the package has been installed, there's no way you could add that to the Cargo.toml. (Though I've primarily migrated to using CARGO_ env-vars instead as they're easier to compose).

2 Likes

Tracking Issue: Support project-specific config in manifest · Issue #12738 · rust-lang/cargo · GitHub is our issue for tracking the transitioning project settings from .cargo/config.toml to Cargo.toml but not everything is project settings. For example, we are looking to leverage more terminal capabilities. A recent release did this by starting to add link support. Fully conformant terminals should gracefully degrade but not all are fully conformant. We have heuristics to detect if its a compliant terminal but have a config field to override it in case of a mistake until the person is able to report back to us and we get it into a new release. I plan to do similar for improving our ASCII art and hope to one day do the same for richer progress output from cargo.

1 Like

My biggest problem with Cargo config files is that whatever configuration in $HOME may not compose well with the configuration in your project. For example, I may want to set rustflags in $HOME (to use the mold linker in all projects for example) and this may clash with the project settings. So I would love to have some notion of composable Cargo configs.

Configs in $HOME also messes up any reproducibility, unless you somehow include its contents in the compilation input. (However, to be fair, many things also messes up reproducibility)