From e794520225220eeb2c3d41f1ca0d8a5287467a9e Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 30 Oct 2025 18:52:36 -0400 Subject: [PATCH 1/3] test(config-include): optional field Capture current behavior where optional field is not yet supported. --- tests/testsuite/config_include.rs | 76 +++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/testsuite/config_include.rs b/tests/testsuite/config_include.rs index 21b3b54fd87..f332117ad34 100644 --- a/tests/testsuite/config_include.rs +++ b/tests/testsuite/config_include.rs @@ -621,3 +621,79 @@ Caused by: "#]], ); } + +#[cargo_test] +fn optional_include_missing_and_existing() { + write_config_at( + ".cargo/config.toml", + " + key1 = 1 + + [[include]] + path = 'missing.toml' + optional = true + + [[include]] + path = 'other.toml' + optional = true + ", + ); + write_config_at( + ".cargo/other.toml", + " + key2 = 2 + ", + ); + + let gctx = GlobalContextBuilder::new() + .unstable_flag("config-include") + .build_err(); + assert_error( + gctx.unwrap_err(), + str![[r#" +could not load Cargo configuration + +Caused by: + failed to load config include `missing.toml` from `[ROOT]/.cargo/config.toml` + +Caused by: + failed to read configuration file `[ROOT]/.cargo/missing.toml` + +Caused by: + [NOT_FOUND] +"#]], + ); +} + +#[cargo_test] +fn optional_false_missing_file() { + write_config_at( + ".cargo/config.toml", + " + key1 = 1 + + [[include]] + path = 'missing.toml' + optional = false + ", + ); + + let gctx = GlobalContextBuilder::new() + .unstable_flag("config-include") + .build_err(); + assert_error( + gctx.unwrap_err(), + str![[r#" +could not load Cargo configuration + +Caused by: + failed to load config include `missing.toml` from `[ROOT]/.cargo/config.toml` + +Caused by: + failed to read configuration file `[ROOT]/.cargo/missing.toml` + +Caused by: + [NOT_FOUND] +"#]], + ); +} From aeaf4b9a21a504236ca7e5f250355d7b16701576 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 30 Oct 2025 19:00:44 -0400 Subject: [PATCH 2/3] feat(config-include): add optional field support When `optional=true` Cargo silently skip missing config-include files. Example config: ```toml [[include]] path = 'optional-config.toml' optional = true ``` --- src/cargo/util/context/mod.rs | 53 +++++++++++++++++++++++++++---- tests/testsuite/config_include.rs | 19 ++--------- 2 files changed, 49 insertions(+), 23 deletions(-) diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index 8c989c2b83a..e0c758076fb 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -1258,8 +1258,12 @@ impl GlobalContext { ) -> CargoResult<()> { let includes = self.include_paths(cv, false)?; for include in includes { + let Some(abs_path) = include.resolve_path(self) else { + continue; + }; + let mut cv = self - ._load_file(&include.abs_path(self), seen, false, WhyLoad::FileDiscovery) + ._load_file(&abs_path, seen, false, WhyLoad::FileDiscovery) .with_context(|| { format!( "failed to load config include `{}` from `{}`", @@ -1369,7 +1373,11 @@ impl GlobalContext { // Accumulate all values here. let mut root = CV::Table(HashMap::new(), value.definition().clone()); for include in includes { - self._load_file(&include.abs_path(self), seen, true, why_load) + let Some(abs_path) = include.resolve_path(self) else { + continue; + }; + + self._load_file(&abs_path, seen, true, why_load) .and_then(|include| root.merge(include, true)) .with_context(|| { format!( @@ -1410,7 +1418,20 @@ impl GlobalContext { ), None => bail!("missing field `path` at `include[{idx}]` in `{def}`"), }; - Ok(ConfigInclude::new(s, def)) + + // Extract optional `include.optional` field + let optional = match table.remove("optional") { + Some(CV::Boolean(b, _)) => b, + Some(other) => bail!( + "expected a boolean, but found {} at `include[{idx}].optional` in `{def}`", + other.desc() + ), + None => false, + }; + + let mut include = ConfigInclude::new(s, def); + include.optional = optional; + Ok(include) } other => bail!( "expected a string or table, but found {} at `include[{idx}]` in {}", @@ -2495,6 +2516,8 @@ struct ConfigInclude { /// Could be either relative or absolute. path: PathBuf, def: Definition, + /// Whether this include is optional (missing files are silently ignored) + optional: bool, } impl ConfigInclude { @@ -2502,10 +2525,11 @@ impl ConfigInclude { Self { path: p.into(), def, + optional: false, } } - /// Gets the absolute path of the config-include config file. + /// Resolves the absolute path for this include. /// /// For file based include, /// it is relative to parent directory of the config file includes it. @@ -2514,12 +2538,27 @@ impl ConfigInclude { /// /// For CLI based include (e.g., `--config 'include = "foo.toml"'`), /// it is relative to the current working directory. - fn abs_path(&self, gctx: &GlobalContext) -> PathBuf { - match &self.def { + /// + /// Returns `None` if this is an optional include and the file doesn't exist. + /// Otherwise returns `Some(PathBuf)` with the absolute path. + fn resolve_path(&self, gctx: &GlobalContext) -> Option { + let abs_path = match &self.def { Definition::Path(p) | Definition::Cli(Some(p)) => p.parent().unwrap(), Definition::Environment(_) | Definition::Cli(None) | Definition::BuiltIn => gctx.cwd(), } - .join(&self.path) + .join(&self.path); + + if self.optional && !abs_path.exists() { + tracing::info!( + "skipping optional include `{}` in `{}`: file not found at `{}`", + self.path.display(), + self.def, + abs_path.display(), + ); + None + } else { + Some(abs_path) + } } } diff --git a/tests/testsuite/config_include.rs b/tests/testsuite/config_include.rs index f332117ad34..444f7e86ba4 100644 --- a/tests/testsuite/config_include.rs +++ b/tests/testsuite/config_include.rs @@ -647,22 +647,9 @@ fn optional_include_missing_and_existing() { let gctx = GlobalContextBuilder::new() .unstable_flag("config-include") - .build_err(); - assert_error( - gctx.unwrap_err(), - str![[r#" -could not load Cargo configuration - -Caused by: - failed to load config include `missing.toml` from `[ROOT]/.cargo/config.toml` - -Caused by: - failed to read configuration file `[ROOT]/.cargo/missing.toml` - -Caused by: - [NOT_FOUND] -"#]], - ); + .build(); + assert_eq!(gctx.get::("key1").unwrap(), 1); + assert_eq!(gctx.get::("key2").unwrap(), 2); } #[cargo_test] From b9b13d07741d6795e3f2b4db1585efa85b412599 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 30 Oct 2025 20:57:39 -0400 Subject: [PATCH 3/3] docs(config-include): polish and mention new fields Restructure config-include documentation: - Brief intro with examples - Added the "Documentation updates" subsection I am a bit not sure whether we should have standalone sections like `include.path` and `include.optional`, as we cannot really set `include.path = "config.toml"` directly. Or maybe `include[].path` is good for heading? --- src/doc/src/reference/unstable.md | 71 +++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 2767e247ea1..f3863736d5b 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -658,28 +658,71 @@ like to stabilize it somehow! This feature requires the `-Zconfig-include` command-line option. -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 an -array of config file paths. Only path ending with `.toml` is accepted. +The `include` key in a config file can be used to load another config file. +For example: + +```toml +# .cargo/config.toml +include = "other-config.toml" + +[build] +jobs = 4 +``` + +```toml +# .cargo/other-config.toml +[build] +rustflags = ["-W", "unsafe-code"] +``` + +### Documentation updates + +#### `include` + +* Type: string, array of strings, or array of tables +* Default: none + +Loads additional config files. Paths are relative to the config file that +includes them. Only paths ending with `.toml` are accepted. + +Supports the following formats: ```toml -# a path ending with `.toml` +# single path include = "path/to/mordor.toml" -# or an array of paths +# array of paths include = ["frodo.toml", "samwise.toml"] + +# inline tables +include = [ + "simple.toml", + { path = "optional.toml", optional = true } +] + +# array of tables +[[include]] +path = "required.toml" + +[[include]] +path = "optional.toml" +optional = true ``` -Unlike other config values, the merge behavior of the `include` key is -different. When a config file contains an `include` key: +When using table syntax (inline tables or array of tables), the following +fields are supported: + +* `path` (string, required): Path to the config file to include. +* `optional` (boolean, default: false): If `true`, missing files are silently + skipped instead of causing an error. + +The merge behavior of `include` is different from other config values: -1. The config values are first loaded from the `include` path. - * If the value of the `include` key is an array of paths, the config values - are loaded and merged from left to right for each path. - * Recurse this step if the config values from the `include` path also - contain an `include` key. -2. Then, the config file's own values are merged on top of the config - from the `include` path. +1. Config values are first loaded from the `include` path. + * If `include` is an array, config values are loaded and merged from left + to right for each path. + * This step recurses if included config files also contain `include` keys. +2. Then, the config file's own values are merged on top of the included config. ## target-applies-to-host * Original Pull Request: [#9322](https://github.com/rust-lang/cargo/pull/9322)