Skip to content
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

Implement CARGO_CONFIG_PATH config file search override #7894

Closed
wants to merge 2 commits into from
Closed
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
228 changes: 143 additions & 85 deletions src/cargo/util/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,131 @@ macro_rules! get_value_typed {
};
}

/// The purpose of this function is to aid in the transition to using
/// .toml extensions on Cargo's config files, which were historically not used.
/// Both 'config.toml' and 'credentials.toml' should be valid with or without extension.
/// When both exist, we want to prefer the one without an extension for
/// backwards compatibility, but warn the user appropriately.
fn get_file_path(
dir: &Path,
filename_without_extension: &str,
warn: bool,
shell: &RefCell<Shell>,
) -> CargoResult<Option<PathBuf>> {
let possible = dir.join(filename_without_extension);
let possible_with_extension = dir.join(format!("{}.toml", filename_without_extension));

if possible.exists() {
if warn && possible_with_extension.exists() {
// We don't want to print a warning if the version
// without the extension is just a symlink to the version
// WITH an extension, which people may want to do to
// support multiple Cargo versions at once and not
// get a warning.
let skip_warning = if let Ok(target_path) = fs::read_link(&possible) {
target_path == possible_with_extension
} else {
false
};

if !skip_warning {
shell.borrow_mut().warn(format!(
"Both `{}` and `{}` exist. Using `{}`",
possible.display(),
possible_with_extension.display(),
possible.display()
))?;
}
}

Ok(Some(possible))
} else if possible_with_extension.exists() {
Ok(Some(possible_with_extension))
} else {
Ok(None)
}
}

/// Select how config files are found
#[derive(Debug)]
pub enum ConfigResolver {
/// Default heirarchical algorithm: walk up from current dir towards root, and
/// also fall back to home directory.
Heirarchical { cwd: PathBuf, home: Option<PathBuf> },
/// Search a list of paths in the specified order
ConfigPath { path: Vec<PathBuf> },
}

impl ConfigResolver {
fn from_env(cwd: &Path, home: Option<&Path>, env: &HashMap<String, String>) -> Self {
if let Some(path) = env.get("CARGO_CONFIG_PATH") {
Self::with_path(&path)
} else {
ConfigResolver::Heirarchical {
cwd: cwd.to_path_buf(),
home: home.map(Path::to_path_buf),
}
}
}

fn with_path(path: &str) -> Self {
ConfigResolver::ConfigPath {
path: path.split(':').map(PathBuf::from).collect(),
}
}

fn resolve<F>(&self, shell: &RefCell<Shell>, mut walk: F) -> CargoResult<()>
where
F: FnMut(&Path) -> CargoResult<()>,
{
let mut stash: HashSet<PathBuf> = HashSet::new();

match self {
ConfigResolver::Heirarchical { cwd, home } => {
for current in paths::ancestors(cwd) {
if let Some(path) =
get_file_path(&current.join(".cargo"), "config", true, shell)?
{
walk(&path)?;
stash.insert(path);
}
}

// Once we're done, also be sure to walk the home directory even if it's not
// in our history to be sure we pick up that standard location for
// information.
if let Some(home) = home {
if let Some(path) = get_file_path(home, "config", true, shell)? {
if !stash.contains(&path) {
walk(&path)?;
}
}
}
}

ConfigResolver::ConfigPath { path } => {
for dir in path {
if let Some(path) = get_file_path(&dir.join(".cargo"), "config", true, shell)? {
if !stash.contains(dir) {
walk(&path)?;
stash.insert(path);
}
}
}
}
}
Ok(())
}
}

/// Configuration information for cargo. This is not specific to a build, it is information
/// relating to cargo itself.
#[derive(Debug)]
pub struct Config {
/// The location of the user's 'home' directory. OS-dependent.
home_path: Filesystem,
/// How to resolve paths of configuration files
resolver: ConfigResolver,
/// Information about how to write messages to the shell
shell: RefCell<Shell>,
/// A collection of configuration options
Expand Down Expand Up @@ -210,6 +329,7 @@ impl Config {
};

Config {
resolver: ConfigResolver::from_env(&cwd, Some(&homedir), &env),
home_path: Filesystem::new(homedir),
shell: RefCell::new(shell),
cwd,
Expand Down Expand Up @@ -418,10 +538,14 @@ impl Config {
}
}

/// Reloads on-disk configuration values, starting at the given path and
/// walking up its ancestors.
pub fn reload_rooted_at<P: AsRef<Path>>(&mut self, path: P) -> CargoResult<()> {
let values = self.load_values_from(path.as_ref())?;
let path = path.as_ref();
// We treat the path as advisory if the user has overridden the config search path
let values = self.load_values_from(&ConfigResolver::from_env(
path,
homedir(path).as_ref().map(PathBuf::as_path),
&self.env,
))?;
self.values.replace(values);
self.merge_cli_args()?;
Ok(())
Expand Down Expand Up @@ -774,22 +898,26 @@ impl Config {

/// Loads configuration from the filesystem.
pub fn load_values(&self) -> CargoResult<HashMap<String, ConfigValue>> {
self.load_values_from(&self.cwd)
self.load_values_from(&self.resolver)
}

fn load_values_from(&self, path: &Path) -> CargoResult<HashMap<String, ConfigValue>> {
fn load_values_from(
&self,
resolver: &ConfigResolver,
) -> CargoResult<HashMap<String, ConfigValue>> {
// This definition path is ignored, this is just a temporary container
// representing the entire file.
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| {
let value = self.load_file(path)?;
cfg.merge(value, false)
.chain_err(|| format!("failed to merge configuration at `{}`", path.display()))?;
Ok(())
})
.chain_err(|| "could not load Cargo configuration")?;
resolver
.resolve(&self.shell, |path| {
let value = self.load_file(path)?;
cfg.merge(value, false).chain_err(|| {
format!("failed to merge configuration at `{}`", path.display())
})?;
Ok(())
})
.chain_err(|| "could not load Cargo configuration")?;

match cfg {
CV::Table(map, _) => Ok(map),
Expand Down Expand Up @@ -935,76 +1063,6 @@ impl Config {
Ok(())
}

/// The purpose of this function is to aid in the transition to using
/// .toml extensions on Cargo's config files, which were historically not used.
/// Both 'config.toml' and 'credentials.toml' should be valid with or without extension.
/// When both exist, we want to prefer the one without an extension for
/// backwards compatibility, but warn the user appropriately.
fn get_file_path(
&self,
dir: &Path,
filename_without_extension: &str,
warn: bool,
) -> CargoResult<Option<PathBuf>> {
let possible = dir.join(filename_without_extension);
let possible_with_extension = dir.join(format!("{}.toml", filename_without_extension));

if possible.exists() {
if warn && possible_with_extension.exists() {
// We don't want to print a warning if the version
// without the extension is just a symlink to the version
// WITH an extension, which people may want to do to
// support multiple Cargo versions at once and not
// get a warning.
let skip_warning = if let Ok(target_path) = fs::read_link(&possible) {
target_path == possible_with_extension
} else {
false
};

if !skip_warning {
self.shell().warn(format!(
"Both `{}` and `{}` exist. Using `{}`",
possible.display(),
possible_with_extension.display(),
possible.display()
))?;
}
}

Ok(Some(possible))
} else if possible_with_extension.exists() {
Ok(Some(possible_with_extension))
} else {
Ok(None)
}
}

fn walk_tree<F>(&self, pwd: &Path, home: &Path, mut walk: F) -> CargoResult<()>
where
F: FnMut(&Path) -> CargoResult<()>,
{
let mut stash: HashSet<PathBuf> = HashSet::new();

for current in paths::ancestors(pwd) {
if let Some(path) = self.get_file_path(&current.join(".cargo"), "config", true)? {
walk(&path)?;
stash.insert(path);
}
}

// Once we're done, also be sure to walk the home directory even if it's not
// in our history to be sure we pick up that standard location for
// information.
if let Some(path) = self.get_file_path(home, "config", true)? {
if !stash.contains(&path) {
walk(&path)?;
}
}

Ok(())
}

/// Gets the index for a registry.
pub fn get_registry_index(&self, registry: &str) -> CargoResult<Url> {
validate_package_name(registry, "registry name", "")?;
Expand Down Expand Up @@ -1044,7 +1102,7 @@ impl Config {
/// Loads credentials config from the credentials file, if present.
pub fn load_credentials(&mut self) -> CargoResult<()> {
let home_path = self.home_path.clone().into_path_unlocked();
let credentials = match self.get_file_path(&home_path, "credentials", true)? {
let credentials = match get_file_path(&home_path, "credentials", true, &self.shell)? {
Some(credentials) => credentials,
None => return Ok(()),
};
Expand Down Expand Up @@ -1560,7 +1618,7 @@ pub fn save_credentials(cfg: &Config, token: String, registry: Option<String>) -
// use the legacy 'credentials'. There's no need to print the warning
// here, because it would already be printed at load time.
let home_path = cfg.home_path.clone().into_path_unlocked();
let filename = match cfg.get_file_path(&home_path, "credentials", false)? {
let filename = match get_file_path(&home_path, "credentials", false, &cfg.shell)? {
Some(path) => match path.file_name() {
Some(filename) => Path::new(filename).to_owned(),
None => Path::new("credentials").to_owned(),
Expand Down
24 changes: 20 additions & 4 deletions src/doc/src/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ This document explains how Cargo’s configuration system works, as well as
available keys or configuration. For configuration of a package through its
manifest, see the [manifest format](manifest.md).

### Hierarchical structure
### How configuration files are found

Cargo allows local configuration for a particular package as well as global
configuration. It looks for configuration files in the current directory and
configuration. It has two mechanisms for this.

By default, it looks for configuration files in the current directory and
all parent directories. If, for example, Cargo were invoked in
`/projects/foo/bar/baz`, then the following configuration files would be
probed for and unified in this order:
Expand All @@ -25,10 +27,24 @@ With this structure, you can specify configuration per-package, and even
possibly check it into version control. You can also specify personal defaults
with a configuration file in your home directory.

Alternatively, if the `CARGO_CONFIG_PATH` environment variable is set, it is
used as a `:`-delimited path of directories. Each of these directories is used
in the order they appear in `CARGO_CONFIG_PATH`. For example the path
`CARGO_CONFIG_PATH=.:..:/projects/common:/home/me` will search in:

* `./.cargo/config`
* `../.cargo/config`
* `/projects/common/.cargo/config`
* `/home/me/.cargo/config`

No other directories are searched, including `CARGO_HOME`.

If a key is specified in multiple config files, the values will get merged
together. Numbers, strings, and booleans will use the value in the deeper
together. Numbers, strings, and booleans will use the value in the first
resolved config file - when using heirarchical search, the deeper
config directory taking precedence over ancestor directories, where the
home directory is the lowest priority. Arrays will be joined together.
home directory is the lowest priority, and the earlier directory in `CARGO_CONFIG_PATH`
when it is used. Arrays will be joined together.

> **Note:** Cargo also reads config files without the `.toml` extension, such as
> `.cargo/config`. Support for the `.toml` extension was added in version 1.39
Expand Down
50 changes: 50 additions & 0 deletions tests/testsuite/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1258,3 +1258,53 @@ fn string_list_advanced_env() {
"error in environment variable `CARGO_KEY3`: expected string, found integer",
);
}

#[cargo_test]
fn config_path() {
let somedir = paths::root().join("somedir");
fs::create_dir_all(somedir.join(".cargo")).unwrap();
fs::write(
somedir.join(".cargo").join("config"),
"
b = 'replacedfile'
c = 'newfile'
",
)
.unwrap();

let otherdir = paths::root().join("otherdir");
fs::create_dir_all(otherdir.join(".cargo")).unwrap();
fs::write(
otherdir.join(".cargo").join("config"),
"
a = 'file1'
b = 'file2'
",
)
.unwrap();

let cwd = paths::root().join("cwd");
fs::create_dir_all(cwd.join(".cargo")).unwrap();
fs::write(
cwd.join(".cargo").join("config"),
"
a = 'BAD1'
b = 'BAD2'
c = 'BAD3'
",
)
.unwrap();

let mut config = ConfigBuilder::new()
.cwd(&cwd)
.env(
"CARGO_CONFIG_PATH",
format!("{}:{}", somedir.display(), otherdir.display()),
)
.build();
config.reload_rooted_at(paths::root()).unwrap();

assert_eq!(config.get::<String>("a").unwrap(), "file1");
assert_eq!(config.get::<String>("b").unwrap(), "replacedfile");
assert_eq!(config.get::<String>("c").unwrap(), "newfile");
}