pub package_features: bool,
pub advanced_env: bool,
pub config_profile: bool,
+ pub config_include: bool,
pub dual_proc_macros: bool,
pub mtime_on_use: bool,
pub named_profiles: bool,
"package-features" => self.package_features = parse_empty(k, v)?,
"advanced-env" => self.advanced_env = parse_empty(k, v)?,
"config-profile" => self.config_profile = parse_empty(k, v)?,
+ "config-include" => self.config_include = parse_empty(k, v)?,
"dual-proc-macros" => self.dual_proc_macros = parse_empty(k, v)?,
// can also be set in .cargo/config or with and ENV
"mtime-on-use" => self.mtime_on_use = parse_empty(k, v)?,
let home = self.home_path.clone().into_path_unlocked();
self.walk_tree(path, &home, |path| {
- let mut contents = String::new();
- let mut file = File::open(&path)?;
- file.read_to_string(&mut contents)
- .chain_err(|| format!("failed to read configuration file `{}`", path.display()))?;
- let toml = cargo_toml::parse(&contents, path, self).chain_err(|| {
- format!("could not parse TOML configuration in `{}`", path.display())
- })?;
- let value =
- CV::from_toml(Definition::Path(path.to_path_buf()), toml).chain_err(|| {
- format!(
- "failed to load TOML configuration from `{}`",
- path.display()
- )
- })?;
+ let value = self.load_file(path)?;
cfg.merge(value, false)
.chain_err(|| format!("failed to merge configuration at `{}`", path.display()))?;
Ok(())
}
}
+ fn load_file(&self, path: &Path) -> CargoResult<ConfigValue> {
+ let mut seen = HashSet::new();
+ self._load_file(path, &mut seen)
+ }
+
+ fn _load_file(&self, path: &Path, seen: &mut HashSet<PathBuf>) -> CargoResult<ConfigValue> {
+ if !seen.insert(path.to_path_buf()) {
+ bail!(
+ "config `include` cycle detected with path `{}`",
+ path.display()
+ );
+ }
+ let contents = fs::read_to_string(path)
+ .chain_err(|| format!("failed to read configuration file `{}`", path.display()))?;
+ let toml = cargo_toml::parse(&contents, path, self)
+ .chain_err(|| format!("could not parse TOML configuration in `{}`", path.display()))?;
+ let value = CV::from_toml(Definition::Path(path.to_path_buf()), toml).chain_err(|| {
+ format!(
+ "failed to load TOML configuration from `{}`",
+ path.display()
+ )
+ })?;
+ let value = self.load_includes(value, seen)?;
+ Ok(value)
+ }
+
+ /// Load any `include` files listed in the given `value`.
+ ///
+ /// Returns `value` with the given include files merged into it.
+ ///
+ /// `seen` is used to check for cyclic includes.
+ fn load_includes(&self, mut value: CV, seen: &mut HashSet<PathBuf>) -> CargoResult<CV> {
+ // Get the list of files to load.
+ let (includes, def) = match &mut value {
+ CV::Table(table, _def) => match table.remove("include") {
+ Some(CV::String(s, def)) => (vec![(s, def.clone())], def),
+ Some(CV::List(list, def)) => (list, def),
+ Some(other) => bail!(
+ "`include` expected a string or list, but found {} in `{}`",
+ other.desc(),
+ other.definition()
+ ),
+ None => {
+ return Ok(value);
+ }
+ },
+ _ => unreachable!(),
+ };
+ // Check unstable.
+ if !self.cli_unstable().config_include {
+ self.shell().warn(format!("config `include` in `{}` ignored, the -Zconfig-include command-line flag is required",
+ def))?;
+ return Ok(value);
+ }
+ // Accumulate all values here.
+ let mut root = CV::Table(HashMap::new(), value.definition().clone());
+ for (path, def) in includes {
+ let abs_path = match &def {
+ Definition::Path(p) => p.parent().unwrap().join(&path),
+ Definition::Environment(_) | Definition::Cli => self.cwd().join(&path),
+ };
+ self._load_file(&abs_path, seen)
+ .and_then(|include| root.merge(include, true))
+ .chain_err(|| format!("failed to load config include `{}` from `{}`", path, def))?;
+ }
+ root.merge(value, true)?;
+ Ok(root)
+ }
+
/// Add config arguments passed on the command line.
fn merge_cli_args(&mut self) -> CargoResult<()> {
- // The clone here is a bit unfortunate, but needed due to mutable
- // borrow, and desire to show the entire arg in the error message.
- let cli_args = match self.cli_config.clone() {
+ let cli_args = match &self.cli_config {
Some(cli_args) => cli_args,
None => return Ok(()),
};
- // Force values to be loaded.
- let _ = self.values()?;
- let values = self.values_mut()?;
+ let mut loaded_args = CV::Table(HashMap::new(), Definition::Cli);
for arg in cli_args {
// TODO: This should probably use a more narrow parser, reject
// comments, blank lines, [headers], etc.
let toml_v: toml::Value = toml::de::from_str(&arg)
.chain_err(|| format!("failed to parse --config argument `{}`", arg))?;
- let table = match toml_v {
- toml::Value::Table(table) => table,
- _ => unreachable!(),
- };
- if table.len() != 1 {
+ let toml_table = toml_v.as_table().unwrap();
+ if toml_table.len() != 1 {
bail!(
- "--config argument `{}` expected exactly one key=value pair, got: {:?}",
+ "--config argument `{}` expected exactly one key=value pair, got {} keys",
arg,
- table
+ toml_table.len()
);
}
- let (key, value) = table.into_iter().next().unwrap();
- let value = CV::from_toml(Definition::Cli, value)
+ let tmp_table = CV::from_toml(Definition::Cli, toml_v)
.chain_err(|| format!("failed to convert --config argument `{}`", arg))?;
+ let mut seen = HashSet::new();
+ let tmp_table = self
+ .load_includes(tmp_table, &mut seen)
+ .chain_err(|| format!("failed to load --config include"))?;
+ loaded_args
+ .merge(tmp_table, true)
+ .chain_err(|| format!("failed to merge --config argument `{}`", arg))?;
+ }
+ // Force values to be loaded.
+ let _ = self.values()?;
+ let values = self.values_mut()?;
+ let loaded_map = match loaded_args {
+ CV::Table(table, _def) => table,
+ _ => unreachable!(),
+ };
+ for (key, value) in loaded_map.into_iter() {
match values.entry(key) {
Vacant(entry) => {
entry.insert(value);
}
- Occupied(mut entry) => entry
- .get_mut()
- .merge(value, true)
- .chain_err(|| format!("failed to merge --config argument `{}`", arg))?,
+ Occupied(mut entry) => entry.get_mut().merge(value, true).chain_err(|| {
+ format!(
+ "failed to merge --config key `{}` into `{}`",
+ entry.key(),
+ entry.get().definition(),
+ )
+ })?,
};
}
Ok(())
None => return Ok(()),
};
- let mut contents = String::new();
- let mut file = File::open(&credentials)?;
- file.read_to_string(&mut contents).chain_err(|| {
- format!(
- "failed to read configuration file `{}`",
- credentials.display()
- )
- })?;
-
- let toml = cargo_toml::parse(&contents, &credentials, self).chain_err(|| {
- format!(
- "could not parse TOML configuration in `{}`",
- credentials.display()
- )
- })?;
-
- let mut value =
- CV::from_toml(Definition::Path(credentials.clone()), toml).chain_err(|| {
- format!(
- "failed to load TOML configuration from `{}`",
- credentials.display()
- )
- })?;
-
+ let mut value = self.load_file(&credentials)?;
// Backwards compatibility for old `.cargo/credentials` layout.
{
let (value_map, def) = match value {
| (expected, found @ CV::List(_, _))
| (expected, found @ CV::Table(_, _)) => {
return Err(internal(format!(
- "expected {}, but found {}",
+ "failed to merge config value from `{}` into `{}`: expected {}, but found {}",
+ found.definition(),
+ expected.definition(),
expected.desc(),
found.desc()
)));
# Example of overriding a profile setting.
cargo --config profile.dev.package.image.opt-level=3 …
```
+
+### config-include
+* Original Issue: [#6699](https://github.com/rust-lang/cargo/issues/6699)
+
+The `include` key in a config file can be used to load another config file. It
+takes a string for a path to another file relative to the config file, or a
+list of strings. It requires the `-Zconfig-include` command-line option.
+
+```toml
+# .cargo/config
+include = '../../some-common-config.toml'
+```
+
+The config values are first loaded from the include path, and then the config
+file's own values are merged on top of it.
+
+This can be paired with [config-cli](#config-cli) to specify a file to load
+from the command-line:
+
+```console
+cargo +nightly -Zunstable-options -Zconfig-include --config 'include="somefile.toml"' build
+```
+
+CLI paths are relative to the current working directory.
use std::path::{Path, PathBuf};
use cargo::core::{enable_nightly_features, Shell};
-use cargo::util::config::{self, Config, SslVersionConfig};
+use cargo::util::config::{self, Config, SslVersionConfig, StringList};
use cargo::util::toml::{self, VecStringOrBool as VSOB};
use cargo::CargoResult;
use cargo_test_support::{normalized_lines_match, paths, project, t};
}
/// Sets the current working directory where config files will be loaded.
- pub fn cwd(&mut self, path: impl Into<PathBuf>) -> &mut Self {
- self.cwd = Some(path.into());
+ pub fn cwd(&mut self, path: impl AsRef<Path>) -> &mut Self {
+ self.cwd = Some(paths::root().join(path.as_ref()));
self
}
ConfigBuilder::new().build()
}
+/// Read the output from Config.
+pub fn read_output(config: Config) -> String {
+ drop(config); // Paranoid about flushing the file.
+ let path = paths::root().join("shell.out");
+ fs::read_to_string(path).unwrap()
+}
+
#[cargo_test]
fn read_env_vars_for_config() {
let p = project()
}
pub fn write_config(config: &str) {
- let path = paths::root().join(".cargo/config");
+ write_config_at(paths::root().join(".cargo/config"), config);
+}
+
+pub fn write_config_at(path: impl AsRef<Path>, contents: &str) {
+ let path = paths::root().join(path.as_ref());
fs::create_dir_all(path.parent().unwrap()).unwrap();
- fs::write(path, config).unwrap();
+ fs::write(path, contents).unwrap();
}
fn write_config_toml(config: &str) {
- let path = paths::root().join(".cargo/config.toml");
- fs::create_dir_all(path.parent().unwrap()).unwrap();
- fs::write(path, config).unwrap();
+ write_config_at(paths::root().join(".cargo/config.toml"), config);
}
// Several test fail on windows if the user does not have permission to
t!(symlink_file(&toml_path, &symlink_path));
}
-fn assert_error<E: Borrow<failure::Error>>(error: E, msgs: &str) {
+pub fn assert_error<E: Borrow<failure::Error>>(error: E, msgs: &str) {
let causes = error
.borrow()
.iter_chain()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n");
- if !normalized_lines_match(msgs, &causes, None) {
+ assert_match(msgs, &causes);
+}
+
+pub fn assert_match(expected: &str, actual: &str) {
+ if !normalized_lines_match(expected, actual, None) {
panic!(
- "Did not find expected:\n{}\nActual error:\n{}\n",
- msgs, causes
+ "Did not find expected:\n{}\nActual:\n{}\n",
+ expected, actual
);
}
}
assert_eq!(config.get::<Option<i32>>("foo.f1").unwrap(), Some(1));
// It should NOT have warned for the symlink.
- drop(config); // Paranoid about flushing the file.
- let path = paths::root().join("shell.out");
- let output = fs::read_to_string(path).unwrap();
+ let output = read_output(config);
let unexpected = "\
warning: Both `[..]/.cargo/config` and `[..]/.cargo/config.toml` exist. Using `[..]/.cargo/config`
";
assert_eq!(config.get::<Option<i32>>("foo.f1").unwrap(), Some(1));
// But it also should have warned.
- drop(config); // Paranoid about flushing the file.
- let path = paths::root().join("shell.out");
- let output = fs::read_to_string(path).unwrap();
+ let output = read_output(config);
let expected = "\
warning: Both `[..]/.cargo/config` and `[..]/.cargo/config.toml` exist. Using `[..]/.cargo/config`
";
- if !normalized_lines_match(expected, &output, None) {
- panic!(
- "Did not find expected:\n{}\nActual error:\n{}\n",
- expected, output
- );
- }
+ assert_match(expected, &output);
}
#[cargo_test]
assert_eq!(s, S { f1: None });
// Verify the warnings.
- drop(config); // Paranoid about flushing the file.
- let path = paths::root().join("shell.out");
- let output = fs::read_to_string(path).unwrap();
+ let output = read_output(config);
let expected = "\
warning: unused config key `S.unused` in `[..]/.cargo/config`
";
- if !normalized_lines_match(expected, &output, None) {
- panic!(
- "Did not find expected:\n{}\nActual error:\n{}\n",
- expected, output
- );
- }
+ assert_match(expected, &output);
}
#[cargo_test]
let config = ConfigBuilder::new().config_arg("a = ['a']").build_err();
assert_error(
config.unwrap_err(),
- "failed to merge --config argument `a = ['a']`\n\
- expected boolean, but found array",
+ "\
+failed to merge --config key `a` into `[..]/.cargo/config`
+failed to merge config value from `--config cli option` into `[..]/.cargo/config`: \
+expected boolean, but found array",
);
// config-cli and advanced-env
.unwrap()
.is_none());
}
+
+#[cargo_test]
+fn table_merge_failure() {
+ // Config::merge fails to merge entries in two tables.
+ write_config_at(
+ "foo/.cargo/config",
+ "
+ [table]
+ key = ['foo']
+ ",
+ );
+ write_config_at(
+ ".cargo/config",
+ "
+ [table]
+ key = 'bar'
+ ",
+ );
+
+ #[derive(Debug, Deserialize)]
+ struct Table {
+ key: StringList,
+ }
+ let config = ConfigBuilder::new().cwd("foo").build();
+ assert_error(
+ config.get::<Table>("table").unwrap_err(),
+ "\
+could not load Cargo configuration
+
+Caused by:
+ failed to merge configuration at `[..]/.cargo/config`
+
+Caused by:
+ failed to merge key `table` between [..]/foo/.cargo/config and [..]/.cargo/config
+
+Caused by:
+ failed to merge key `key` between [..]/foo/.cargo/config and [..]/.cargo/config
+
+Caused by:
+ failed to merge config value from `[..]/.cargo/config` into `[..]/foo/.cargo/config`: \
+ expected array, but found string",
+ );
+}
+
+#[cargo_test]
+fn non_string_in_array() {
+ // Currently only strings are supported.
+ write_config("foo = [1, 2, 3]");
+ let config = new_config();
+ assert_error(
+ config.get::<Vec<i32>>("foo").unwrap_err(),
+ "\
+could not load Cargo configuration
+
+Caused by:
+ failed to load TOML configuration from `[..]/.cargo/config`
+
+Caused by:
+ failed to parse key `foo`
+
+Caused by:
+ expected string but found integer in list",
+ );
+}
//! Tests for the --config CLI option.
-use super::config::{write_config, ConfigBuilder};
+use super::config::{assert_error, assert_match, read_output, write_config, ConfigBuilder};
use cargo::util::config::Definition;
-use cargo_test_support::{normalized_lines_match, paths, project};
+use cargo_test_support::{paths, project};
use std::fs;
#[cargo_test]
.build();
config.build_config().unwrap();
- drop(config); // Paranoid about flushing the file.
- let path = paths::root().join("shell.out");
- let output = fs::read_to_string(path).unwrap();
+ let output = read_output(config);
let expected = "\
warning: unused config key `build.unused` in `--config cli option`
";
- if !normalized_lines_match(expected, &output, None) {
- panic!(
- "Did not find expected:\n{}\nActual error:\n{}\n",
- expected, output
- );
- }
+ assert_match(expected, &output);
}
#[cargo_test]
assert_eq!(config.get::<String>("b").unwrap(), "cli1");
assert_eq!(config.get::<String>("c").unwrap(), "cli2");
}
+
+#[cargo_test]
+fn bad_parse() {
+ // Fail to TOML parse.
+ let config = ConfigBuilder::new().config_arg("abc").build_err();
+ assert_error(
+ config.unwrap_err(),
+ "\
+failed to parse --config argument `abc`
+expected an equals, found eof at line 1 column 4",
+ );
+}
+
+#[cargo_test]
+fn too_many_values() {
+ // Currently restricted to only 1 value.
+ let config = ConfigBuilder::new().config_arg("a=1\nb=2").build_err();
+ assert_error(
+ config.unwrap_err(),
+ "\
+--config argument `a=1
+b=2` expected exactly one key=value pair, got 2 keys",
+ );
+
+ let config = ConfigBuilder::new().config_arg("").build_err();
+ assert_error(
+ config.unwrap_err(),
+ "\
+ --config argument `` expected exactly one key=value pair, got 0 keys",
+ );
+}
+
+#[cargo_test]
+fn bad_cv_convert() {
+ // ConfigValue does not support all TOML types.
+ let config = ConfigBuilder::new().config_arg("a=2019-12-01").build_err();
+ assert_error(
+ config.unwrap_err(),
+ "\
+failed to convert --config argument `a=2019-12-01`
+failed to parse key `a`
+found TOML configuration value of unknown type `datetime`",
+ );
+}
+
+#[cargo_test]
+fn fail_to_merge_multiple_args() {
+ // Error message when multiple args fail to merge.
+ let config = ConfigBuilder::new()
+ .config_arg("foo='a'")
+ .config_arg("foo=['a']")
+ .build_err();
+ // This is a little repetitive, but hopefully the user can figure it out.
+ assert_error(
+ config.unwrap_err(),
+ "\
+failed to merge --config argument `foo=['a']`
+failed to merge key `foo` between --config cli option and --config cli option
+failed to merge config value from `--config cli option` into `--config cli option`: \
+expected string, but found array",
+ );
+}
--- /dev/null
+//! Tests for `include` config field.
+
+use super::config::{
+ assert_error, assert_match, read_output, write_config, write_config_at, ConfigBuilder,
+};
+
+#[cargo_test]
+fn gated() {
+ // Requires -Z flag.
+ write_config("include='other'");
+ let config = ConfigBuilder::new().build();
+ let output = read_output(config);
+ let expected = "\
+warning: config `include` in `[..]/.cargo/config` ignored, \
+the -Zconfig-include command-line flag is required
+";
+ assert_match(expected, &output);
+}
+
+#[cargo_test]
+fn simple() {
+ // Simple test.
+ write_config_at(
+ ".cargo/config",
+ "
+ include = 'other'
+ key1 = 1
+ key2 = 2
+ ",
+ );
+ write_config_at(
+ ".cargo/other",
+ "
+ key2 = 3
+ key3 = 4
+ ",
+ );
+ let config = ConfigBuilder::new().unstable_flag("config-include").build();
+ assert_eq!(config.get::<i32>("key1").unwrap(), 1);
+ assert_eq!(config.get::<i32>("key2").unwrap(), 2);
+ assert_eq!(config.get::<i32>("key3").unwrap(), 4);
+}
+
+#[cargo_test]
+fn left_to_right() {
+ // How it merges multiple includes.
+ write_config_at(
+ ".cargo/config",
+ "
+ include = ['one', 'two']
+ primary = 1
+ ",
+ );
+ write_config_at(
+ ".cargo/one",
+ "
+ one = 1
+ primary = 2
+ ",
+ );
+ write_config_at(
+ ".cargo/two",
+ "
+ two = 2
+ primary = 3
+ ",
+ );
+ let config = ConfigBuilder::new().unstable_flag("config-include").build();
+ assert_eq!(config.get::<i32>("primary").unwrap(), 1);
+ assert_eq!(config.get::<i32>("one").unwrap(), 1);
+ assert_eq!(config.get::<i32>("two").unwrap(), 2);
+}
+
+#[cargo_test]
+fn missing_file() {
+ // Error when there's a missing file.
+ write_config("include='missing'");
+ let config = ConfigBuilder::new().unstable_flag("config-include").build();
+ assert_error(
+ config.get::<i32>("whatever").unwrap_err(),
+ "\
+could not load Cargo configuration
+
+Caused by:
+ failed to load config include `missing` from `[..]/.cargo/config`
+
+Caused by:
+ failed to read configuration file `[..]/.cargo/missing`
+
+Caused by:
+ No such file or directory (os error 2)",
+ );
+}
+
+#[cargo_test]
+fn cycle() {
+ // Detects a cycle.
+ write_config_at(".cargo/config", "include='one'");
+ write_config_at(".cargo/one", "include='two'");
+ write_config_at(".cargo/two", "include='config'");
+ let config = ConfigBuilder::new().unstable_flag("config-include").build();
+ assert_error(
+ config.get::<i32>("whatever").unwrap_err(),
+ "\
+could not load Cargo configuration
+
+Caused by:
+ failed to load config include `one` from `[..]/.cargo/config`
+
+Caused by:
+ failed to load config include `two` from `[..]/.cargo/one`
+
+Caused by:
+ failed to load config include `config` from `[..]/.cargo/two`
+
+Caused by:
+ config `include` cycle detected with path `[..]/.cargo/config`",
+ );
+}
+
+#[cargo_test]
+fn cli_include() {
+ // Using --config with include.
+ // CLI takes priority over files.
+ write_config_at(
+ ".cargo/config",
+ "
+ foo = 1
+ bar = 2
+ ",
+ );
+ write_config_at(".cargo/config-foo", "foo = 2");
+ let config = ConfigBuilder::new()
+ .unstable_flag("config-include")
+ .config_arg("include='.cargo/config-foo'")
+ .build();
+ assert_eq!(config.get::<i32>("foo").unwrap(), 2);
+ assert_eq!(config.get::<i32>("bar").unwrap(), 2);
+}
+
+#[cargo_test]
+fn bad_format() {
+ // Not a valid format.
+ write_config("include = 1");
+ let config = ConfigBuilder::new().unstable_flag("config-include").build();
+ assert_error(
+ config.get::<i32>("whatever").unwrap_err(),
+ "\
+could not load Cargo configuration
+
+Caused by:
+ `include` expected a string or list, but found integer in `[..]/.cargo/config`",
+ );
+}
+
+#[cargo_test]
+fn cli_include_failed() {
+ // Error message when CLI include fails to load.
+ let config = ConfigBuilder::new()
+ .unstable_flag("config-include")
+ .config_arg("include='foobar'")
+ .build_err();
+ assert_error(
+ config.unwrap_err(),
+ "\
+failed to load --config include
+failed to load config include `foobar` from `--config cli option`
+failed to read configuration file `[..]/foobar`
+No such file or directory (os error 2)",
+ );
+}
+
+#[cargo_test]
+fn cli_merge_failed() {
+ // Error message when CLI include merge fails.
+ write_config("foo = ['a']");
+ write_config_at(
+ ".cargo/other",
+ "
+ foo = 'b'
+ ",
+ );
+ let config = ConfigBuilder::new()
+ .unstable_flag("config-include")
+ .config_arg("include='.cargo/other'")
+ .build_err();
+ // Maybe this error message should mention it was from an include file?
+ assert_error(
+ config.unwrap_err(),
+ "\
+failed to merge --config key `foo` into `[..]/.cargo/config`
+failed to merge config value from `[..]/.cargo/other` into `[..]/.cargo/config`: \
+expected array, but found string",
+ );
+}
mod concurrent;
mod config;
mod config_cli;
+mod config_include;
mod corrupt_git;
mod cross_compile;
mod cross_publish;