diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs index 603f2581f95..2df3e13d0b7 100644 --- a/src/cargo/util/config/mod.rs +++ b/src/cargo/util/config/mod.rs @@ -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, +) -> CargoResult> { + 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 }, + /// Search a list of paths in the specified order + ConfigPath { path: Vec }, +} + +impl ConfigResolver { + fn from_env(cwd: &Path, home: Option<&Path>, env: &HashMap) -> 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(&self, shell: &RefCell, mut walk: F) -> CargoResult<()> + where + F: FnMut(&Path) -> CargoResult<()>, + { + let mut stash: HashSet = HashSet::new(); + + match self { + ConfigResolver::Heirarchical { cwd, home } => { + for current in paths::ancestors(cwd) { + if let Some(path) = + get_file_path(¤t.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, /// A collection of configuration options @@ -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, @@ -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>(&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(()) @@ -774,22 +898,26 @@ impl Config { /// Loads configuration from the filesystem. pub fn load_values(&self) -> CargoResult> { - self.load_values_from(&self.cwd) + self.load_values_from(&self.resolver) } - fn load_values_from(&self, path: &Path) -> CargoResult> { + fn load_values_from( + &self, + resolver: &ConfigResolver, + ) -> CargoResult> { // 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), @@ -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> { - 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(&self, pwd: &Path, home: &Path, mut walk: F) -> CargoResult<()> - where - F: FnMut(&Path) -> CargoResult<()>, - { - let mut stash: HashSet = HashSet::new(); - - for current in paths::ancestors(pwd) { - if let Some(path) = self.get_file_path(¤t.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 { validate_package_name(registry, "registry name", "")?; @@ -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(()), }; @@ -1560,7 +1618,7 @@ pub fn save_credentials(cfg: &Config, token: String, registry: Option) - // 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(), diff --git a/src/doc/src/reference/config.md b/src/doc/src/reference/config.md index 6fafaecc962..b3f236bbd7d 100644 --- a/src/doc/src/reference/config.md +++ b/src/doc/src/reference/config.md @@ -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: @@ -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 diff --git a/tests/testsuite/config.rs b/tests/testsuite/config.rs index 4cbea32f54a..d2bb2fe5f72 100644 --- a/tests/testsuite/config.rs +++ b/tests/testsuite/config.rs @@ -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::("a").unwrap(), "file1"); + assert_eq!(config.get::("b").unwrap(), "replacedfile"); + assert_eq!(config.get::("c").unwrap(), "newfile"); +}