Skip to content

Commit df5a6c3

Browse files
committed
feat: stabilize -Zconfig-include
# Stabilization report ## Summary The `include` key in Cargo configuration files allows loading additional config files, enabling better organization, sharing, and management of Cargo configurations across projects and environments. This feature has been available under the `-Zconfig-include` flag since 2019 (Cargo 1.42) and has seen real-world usage. The stabilization includes support for multiple syntax forms and the `optional` field, which were added in October 2025 based on user feedback. Tracking issue: rust-lang#7723 ### What is stabilized The `include` configuration key allows loading additional config files. **Supported syntax:** - String: `include = "path.toml"` - Array: `include = ["a.toml", "b.toml"]` - Inline tables: `include = [{ path = "optional.toml", optional = true }]` - Array of tables: `[[include]]` with `path` and `optional` fields **Key behaviors:** - Paths are relative to the including config file and must end with `.toml` - Merge follows precedence order: included files (left-to-right) → parent config - `optional = true` silently skips missing files (default: `false`) - Cyclic includes are detected and reported as errors See the config documentation for complete details and examples. ### Future extensions Several potential extensions are not implemented at this time: * Glob patterns: like `include = "config.d/*.toml"` * Conditional include: conditions like gitconfig's `includeIf` * Variable substitution and template: placeholders like `{CONFIG_DIR}` or `{CARGO_HOME}` * Implicit-include: like `.cargo/config.user.toml` or `.cargo/config.d` for config fragments See "Doors closed" for more. ## Design ### Key evolution All significant changes occurred during the unstable period (2019-2024) and were approved by the Cargo team. **1. File naming restrictions** (rust-lang#12298, 2023-06-21) The team decided the restriction was reasonable. The restriction applies to config file discovery but not to `--config` CLI arguments which has already been stabilized. **2. Loading precedence for arrays** Config values in array elements are loaded left to right, with later values taking precedence. The parent config file's values always take precedence over included configs. This provides intuitive layering behavior. **3. Syntax complexity** (rust-lang#16174, 2025-10-30) The feature started with simple string/array syntax. The team debated and decided to add table syntax before stabilization to allow future extensions. **4. Optional includes by default vs. explicit** (rust-lang#16180, 2025-10-31) Some users wanted missing files to be silently ignored by default for local customization workflows. Others wanted errors to catch typos. The team chose to error by default but added an explicit `optional = true` field, requiring users to be intentional about optional behavior. ### Nightly extensions No nightly-only extensions remain. The feature is fully stabilized as implemented. ### Doors closed **This stabilization commits to**: 1. Supporting the `include` key in Cargo configuration 2. Relative path resolution from the including config file 3. Left-to-right merge order for arrays 4. Parent config taking precedence over includes 5. The `.toml` file extension requirement 6. The `path` and `optional` fields in table syntax **This does NOT prevent**: - Adding glob/wildcard support - Adding conditional includes - Adding variable substitution and template The `[[include]]` table syntax could optionally have a field to enable the future extensions above. For example, ```toml [[include]] path = "path/config/*.toml" glob = true [[include]] path = "path/*/config.toml" if = "<some-condition>" [[include]] path = "path/*/config.toml" templatized = true ``` **This MAY prevent**: * Adding new implicit-include for user local config or config fragments directory As we are going to allow all file paths. Adding any implicit includes after stabilization might break the merge precendence if people already include those paths. ## Feedback ### Call for testing No formal "call for testing" was issued, but the feature has been available under `-Zconfig-include` since Cargo 1.42 (2019) and has seen real-world adoption. ### Use cases Users reported use cases: - **Sharing flags and environment conditionally**: [Tock OS](https://github.com/tock/tock), [esp-hal](https://github.com/esp-rs/esp-hal), rtos, and some FFI libraries use it for preset management across multiple board configurations for different hardware platforms, architectures, and downstream crates. - **Beyond hierarchical discovery**: Some use cases require explicit includes because configs need to be loaded from locations outside the hierarchical path, or need to be conditionally included based on per-package or per-machine requirements that can't rely on the directory structure alone. This usually happens in a meta build system that generates configs, especially when setting `CARGO_HOME` to a different location off the hierarchical path. - **Per-project profile overrides**: Projects with checked-in configs (e.g., `[profile.test] debug = false` for CI) can allow developers to override settings locally without modifying the checked-in file. Developers can include `.cargo/local-config.toml` without using git workarounds like `update-index --assume-unchanged`. ### Coverage Test coverage is comprehensive in `tests/testsuite/config_include.rs`: - Merge behavior: left-to-right order, hierarchy interaction - Path handling: relative paths, different directory structures - Cycle detection: Direct and indirect cycles - Error cases: missing files, invalid paths, wrong extensions, missing required fields - Syntax variations: string, array, inline table, array of tables - Optional includes: missing optional files, mixed optional/required - CLI integration: includes from `--config` arguments - Forward compatibility: unknown table fields ### Known limitations Issue rust-lang#15769 tracks inconsistent relative path behavior between `include` paths (relative to config file) and other config paths like `build.target-dir` (relative to cargo root). This is considered a known limitation and confusion that can be addressed separately and doesn't block stabilization. No other known limitations blocking stabilization. ## History - 2019-02-25: Original proposal (rust-lang#6699) - 2019-12-19: initial implementation (rust-lang#7649) - 2023-06-21: file extension restriction added (rust-lang#12298) - 2025-10-30: table syntax support added (rust-lang#16174) - 2025-10-31: optional field support added (rust-lang#16180) ## Acknowledgments Contributors to this feature: - `@ehuss` for initial implementation and design - `@weihanglo` for extra syntax support and enhancement - `@rust-lang/cargo` team for the support, review and feedback - All users providing feedback in rust-lang#7723 (not going to tag each of them)
1 parent 14c6f00 commit df5a6c3

File tree

7 files changed

+139
-344
lines changed

7 files changed

+139
-344
lines changed

src/cargo/core/features.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -856,7 +856,6 @@ unstable_cli_options!(
856856
cargo_lints: bool = ("Enable the `[lints.cargo]` table"),
857857
checksum_freshness: bool = ("Use a checksum to determine if output is fresh rather than filesystem mtime"),
858858
codegen_backend: bool = ("Enable the `codegen-backend` option in profiles in .cargo/config.toml file"),
859-
config_include: bool = ("Enable the `include` key in config files"),
860859
direct_minimal_versions: bool = ("Resolve minimal dependency versions instead of maximum (direct dependencies only)"),
861860
dual_proc_macros: bool = ("Build proc-macros for both the host and the target"),
862861
feature_unification: bool = ("Enable new feature unification modes in workspaces"),
@@ -978,6 +977,8 @@ const STABILIZED_PACKAGE_WORKSPACE: &str =
978977

979978
const STABILIZED_BUILD_DIR: &str = "build.build-dir is now always enabled.";
980979

980+
const STABILIZED_CONFIG_INCLUDE: &str = "The `include` config key is now always available";
981+
981982
fn deserialize_comma_separated_list<'de, D>(
982983
deserializer: D,
983984
) -> Result<Option<Vec<String>>, D::Error>
@@ -1362,6 +1363,7 @@ impl CliUnstable {
13621363
"doctest-xcompile" => stabilized_warn(k, "1.89", STABILIZED_DOCTEST_XCOMPILE),
13631364
"package-workspace" => stabilized_warn(k, "1.89", STABILIZED_PACKAGE_WORKSPACE),
13641365
"build-dir" => stabilized_warn(k, "1.91", STABILIZED_BUILD_DIR),
1366+
"config-include" => stabilized_warn(k, "1.93", STABILIZED_CONFIG_INCLUDE),
13651367

13661368
// Unstable features
13671369
// Sorted alphabetically:
@@ -1376,7 +1378,6 @@ impl CliUnstable {
13761378
"build-std-features" => self.build_std_features = Some(parse_list(v)),
13771379
"cargo-lints" => self.cargo_lints = parse_empty(k, v)?,
13781380
"codegen-backend" => self.codegen_backend = parse_empty(k, v)?,
1379-
"config-include" => self.config_include = parse_empty(k, v)?,
13801381
"direct-minimal-versions" => self.direct_minimal_versions = parse_empty(k, v)?,
13811382
"dual-proc-macros" => self.dual_proc_macros = parse_empty(k, v)?,
13821383
"feature-unification" => self.feature_unification = parse_empty(k, v)?,

src/cargo/util/context/mod.rs

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,8 @@ pub const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[
186186
enum WhyLoad {
187187
/// Loaded due to a request from the global cli arg `--config`
188188
///
189-
/// Indirect configs loaded via [`config-include`] are also seen as from cli args,
189+
/// Indirect configs loaded via [`ConfigInclude`] are also seen as from cli args,
190190
/// if the initial config is being loaded from cli.
191-
///
192-
/// [`config-include`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#config-include
193191
Cli,
194192
/// Loaded due to config file discovery.
195193
FileDiscovery,
@@ -1138,19 +1136,7 @@ impl GlobalContext {
11381136
self.merge_cli_args()?;
11391137
}
11401138

1141-
// Load the unstable flags from config file here first, as the config
1142-
// file itself may enable inclusion of other configs. In that case, we
1143-
// want to re-load configs with includes enabled:
11441139
self.load_unstable_flags_from_config()?;
1145-
if self.unstable_flags.config_include {
1146-
// If the config was already loaded (like when fetching the
1147-
// `[alias]` table), it was loaded with includes disabled because
1148-
// the `unstable_flags` hadn't been set up, yet. Any values
1149-
// fetched before this step will not process includes, but that
1150-
// should be fine (`[alias]` is one of the only things loaded
1151-
// before configure). This can be removed when stabilized.
1152-
self.reload_rooted_at(self.cwd.clone())?;
1153-
}
11541140

11551141
// Ignore errors in the configuration files. We don't want basic
11561142
// commands like `cargo version` to error out due to config file
@@ -1277,9 +1263,7 @@ impl GlobalContext {
12771263
let home = self.home_path.clone().into_path_unlocked();
12781264
self.walk_tree(&self.cwd, &home, |path| {
12791265
let mut cv = self._load_file(path, &mut seen, false, WhyLoad::FileDiscovery)?;
1280-
if self.cli_unstable().config_include {
1281-
self.load_unmerged_include(&mut cv, &mut seen, &mut result)?;
1282-
}
1266+
self.load_unmerged_include(&mut cv, &mut seen, &mut result)?;
12831267
result.push(cv);
12841268
Ok(())
12851269
})
@@ -1350,11 +1334,9 @@ impl GlobalContext {
13501334
///
13511335
/// This is actual implementation of loading a config value from a path.
13521336
///
1353-
/// * `includes` determines whether to load configs from [`config-include`].
1337+
/// * `includes` determines whether to load configs from [`ConfigInclude`].
13541338
/// * `seen` is used to check for cyclic includes.
13551339
/// * `why_load` tells why a config is being loaded.
1356-
///
1357-
/// [`config-include`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#config-include
13581340
fn _load_file(
13591341
&self,
13601342
path: &Path,
@@ -1406,10 +1388,7 @@ impl GlobalContext {
14061388
) -> CargoResult<CV> {
14071389
// Get the list of files to load.
14081390
let includes = self.include_paths(&mut value, true)?;
1409-
// Check unstable.
1410-
if !self.cli_unstable().config_include {
1411-
return Ok(value);
1412-
}
1391+
14131392
// Accumulate all values here.
14141393
let mut root = CV::Table(HashMap::new(), value.definition().clone());
14151394
for include in includes {

src/doc/src/reference/config.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,57 @@ without pipelining.
828828
Specifies a custom user-agent header to use. The default if not specified is a
829829
string that includes Cargo's version.
830830

831+
### `include`
832+
833+
* Type: string, array of strings, or array of tables
834+
* Default: none
835+
* Environment: not supported
836+
837+
Loads additional config files. Paths are relative to the config file that
838+
includes them. Only paths ending with `.toml` are accepted.
839+
840+
Supports the following formats:
841+
842+
```toml
843+
# single path
844+
include = "path/to/mordor.toml"
845+
846+
# array of paths
847+
include = ["frodo.toml", "samwise.toml"]
848+
849+
# inline tables
850+
include = [
851+
"simple.toml",
852+
{ path = "optional.toml", optional = true }
853+
]
854+
855+
# array of tables
856+
[[include]]
857+
path = "required.toml"
858+
859+
[[include]]
860+
path = "optional.toml"
861+
optional = true
862+
```
863+
864+
When using table syntax (inline tables or array of tables), the following
865+
fields are supported:
866+
867+
* `path` (string, required): Path to the config file to include.
868+
* `optional` (boolean, default: false): If `true`, missing files are silently
869+
skipped instead of causing an error.
870+
871+
The merge behavior of `include` follows a precedence order
872+
where later values take precedence over earlier ones:
873+
874+
1. Config values are first loaded from the `include` path.
875+
* If `include` is an array,
876+
config values are loaded and merged from left to right for each path
877+
with later values taking precedence.
878+
* This step recurses if included config files also contain `include` keys.
879+
2. Then, the config file's own values are merged on top of the included config,
880+
taking highest precedence.
881+
831882
### `[install]`
832883

833884
The `[install]` table defines defaults for the [`cargo install`] command.

src/doc/src/reference/unstable.md

Lines changed: 6 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ Each new feature described below should explain how to use it.
119119
* [Build analysis](#build-analysis) --- Record and persist detailed build metrics across runs, with new commands to query past builds.
120120
* [`rustc-unicode`](#rustc-unicode) --- Enables `rustc`'s unicode error format in Cargo's error messages
121121
* Configuration
122-
* [config-include](#config-include) --- Adds the ability for config files to include other files.
123122
* [`cargo config`](#cargo-config) --- Adds a new subcommand for viewing config files.
124123
* Registries
125124
* [publish-timeout](#publish-timeout) --- Controls the timeout between uploading the crate and being available in the index
@@ -636,77 +635,9 @@ like to stabilize it somehow!
636635

637636
[rust-lang/rust#64158]: https://github.com/rust-lang/rust/pull/64158
638637

639-
## config-include
640-
* Tracking Issue: [#7723](https://github.com/rust-lang/cargo/issues/7723)
641-
642-
This feature requires the `-Zconfig-include` command-line option.
643-
644-
The `include` key in a config file can be used to load another config file.
645-
For example:
646-
647-
```toml
648-
# .cargo/config.toml
649-
include = "other-config.toml"
650-
651-
[build]
652-
jobs = 4
653-
```
654-
655-
```toml
656-
# .cargo/other-config.toml
657-
[build]
658-
rustflags = ["-W", "unsafe-code"]
659-
```
660638

661639
### Documentation updates
662640

663-
#### `include`
664-
665-
* Type: string, array of strings, or array of tables
666-
* Default: none
667-
668-
Loads additional config files. Paths are relative to the config file that
669-
includes them. Only paths ending with `.toml` are accepted.
670-
671-
Supports the following formats:
672-
673-
```toml
674-
# single path
675-
include = "path/to/mordor.toml"
676-
677-
# array of paths
678-
include = ["frodo.toml", "samwise.toml"]
679-
680-
# inline tables
681-
include = [
682-
"simple.toml",
683-
{ path = "optional.toml", optional = true }
684-
]
685-
686-
# array of tables
687-
[[include]]
688-
path = "required.toml"
689-
690-
[[include]]
691-
path = "optional.toml"
692-
optional = true
693-
```
694-
695-
When using table syntax (inline tables or array of tables), the following
696-
fields are supported:
697-
698-
* `path` (string, required): Path to the config file to include.
699-
* `optional` (boolean, default: false): If `true`, missing files are silently
700-
skipped instead of causing an error.
701-
702-
The merge behavior of `include` is different from other config values:
703-
704-
1. Config values are first loaded from the `include` path.
705-
* If `include` is an array, config values are loaded and merged from left
706-
to right for each path.
707-
* This step recurses if included config files also contain `include` keys.
708-
2. Then, the config file's own values are merged on top of the included config.
709-
710641
## target-applies-to-host
711642
* Original Pull Request: [#9322](https://github.com/rust-lang/cargo/pull/9322)
712643
* Tracking Issue: [#9453](https://github.com/rust-lang/cargo/issues/9453)
@@ -2327,3 +2258,9 @@ See the [config documentation](config.md#buildbuild-dir) for information about c
23272258

23282259
The `--build-plan` argument for the `build` command has been removed in 1.93.0-nightly.
23292260
See <https://github.com/rust-lang/cargo/issues/7614> for the reason for its removal.
2261+
2262+
## config-include
2263+
2264+
Support for including extra configuration files via the `include` config key
2265+
has been stabilized in 1.93.0.
2266+
See the [`include` config documentation](config.md#include) for more.

0 commit comments

Comments
 (0)