Skip to content

Commit 1cc83bd

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: #7723 ### What is stabilized The `include` configuration key allows loading additional config files. **Supported syntax:** - Array: `include = ["a.toml", "b.toml"]` - Inline tables (preferred): `include = [{ path = "optional.toml", optional = true }]` - Array of tables (not preferred): `[[include]]` with `path` and `optional` fields **Key behaviors:** - Paths are relative to the including config file and must end with `.toml` - Glob syntax and templated paths (with `{}` braces} are disallowed in paths. - 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"` — #9306 * Conditional include: conditions like gitconfig's `includeIf` — #7723 (comment) * Variable substitution and template: placeholders like `{CONFIG_DIR}` or `{CARGO_HOME}` — #15769 * Implicit-include: like `.cargo/config.user.toml` or `.cargo/config.d` for config fragments — [#t-cargo > Built-in `.cargo/config.local.toml`for non-committed config](https://rust-lang.zulipchat.com/#narrow/channel/246057-t-cargo/topic/Built-in.20.60.2Ecargo.2Fconfig.2Elocal.2Etoml.60for.20non-committed.20config/with/558705263) * Environment variable include support: like `CARGO_INCLUDE=path/to/config.toml` — #6728 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** (#12298, 2023-06-21) (#16285, 2025-11-21) The syntax has a couple restrictions: * Path must end with `.toml` extension * Path must not contain glob syntax or template braces The team considered 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** (#16174, 2025-10-30) (#16298 2025-11-25) The feature started with simple string/array syntax. The team debated and decided to add table syntax, and remove single string shorthand before stabilization to allow future extensions and reduce complexity. **4. Optional includes by default vs. explicit** (#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 `path` and `optional` fields in table syntax **This does NOT prevent**: - Adding glob/wildcard support - Adding conditional includes - Adding variable substitution and template **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 precedence if people already include those paths. The only possible way to support it is accepting `.cargo/config.toml/` as a directory and treat it as a config fragments directory. `.cargo/config.toml` (and the legacy `cargo/config/`) is the only path Cargo reserves. ## 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. A board's config (used by entering its directory) is defined by pulling from role-based config slices. - **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. - **User and project configuration**: 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 an optional `.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, glob/template syntax restrictions ### Known limitations Issue #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 (#6699) - 2019-12-19: initial implementation (#7649) - 2023-06-21: file extension restriction added (#12298) - 2025-10-30: table syntax support added (#16174) - 2025-10-31: optional field support added (#16180) - 2025-11-21: glob and template syntax restriction added (#16285) - 2025-11-25: single string shorthand syntax removed (#16298) ## 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 #7723
1 parent 33be686 commit 1cc83bd

File tree

7 files changed

+150
-367
lines changed

7 files changed

+150
-367
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"),
@@ -979,6 +978,8 @@ const STABILIZED_PACKAGE_WORKSPACE: &str =
979978

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

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

13671369
// Unstable features
13681370
// Sorted alphabetically:
@@ -1377,7 +1379,6 @@ impl CliUnstable {
13771379
"build-std-features" => self.build_std_features = Some(parse_list(v)),
13781380
"cargo-lints" => self.cargo_lints = parse_empty(k, v)?,
13791381
"codegen-backend" => self.codegen_backend = parse_empty(k, v)?,
1380-
"config-include" => self.config_include = parse_empty(k, v)?,
13811382
"direct-minimal-versions" => self.direct_minimal_versions = parse_empty(k, v)?,
13821383
"dual-proc-macros" => self.dual_proc_macros = parse_empty(k, v)?,
13831384
"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
@@ -187,10 +187,8 @@ pub const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[
187187
enum WhyLoad {
188188
/// Loaded due to a request from the global cli arg `--config`
189189
///
190-
/// Indirect configs loaded via [`config-include`] are also seen as from cli args,
190+
/// Indirect configs loaded via [`ConfigInclude`] are also seen as from cli args,
191191
/// if the initial config is being loaded from cli.
192-
///
193-
/// [`config-include`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#config-include
194192
Cli,
195193
/// Loaded due to config file discovery.
196194
FileDiscovery,
@@ -1139,19 +1137,7 @@ impl GlobalContext {
11391137
self.merge_cli_args()?;
11401138
}
11411139

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

11561142
// Ignore errors in the configuration files. We don't want basic
11571143
// commands like `cargo version` to error out due to config file
@@ -1278,9 +1264,7 @@ impl GlobalContext {
12781264
let home = self.home_path.clone().into_path_unlocked();
12791265
self.walk_tree(&self.cwd, &home, |path| {
12801266
let mut cv = self._load_file(path, &mut seen, false, WhyLoad::FileDiscovery)?;
1281-
if self.cli_unstable().config_include {
1282-
self.load_unmerged_include(&mut cv, &mut seen, &mut result)?;
1283-
}
1267+
self.load_unmerged_include(&mut cv, &mut seen, &mut result)?;
12841268
result.push(cv);
12851269
Ok(())
12861270
})
@@ -1351,11 +1335,9 @@ impl GlobalContext {
13511335
///
13521336
/// This is actual implementation of loading a config value from a path.
13531337
///
1354-
/// * `includes` determines whether to load configs from [`config-include`].
1338+
/// * `includes` determines whether to load configs from [`ConfigInclude`].
13551339
/// * `seen` is used to check for cyclic includes.
13561340
/// * `why_load` tells why a config is being loaded.
1357-
///
1358-
/// [`config-include`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#config-include
13591341
fn _load_file(
13601342
&self,
13611343
path: &Path,
@@ -1407,10 +1389,7 @@ impl GlobalContext {
14071389
) -> CargoResult<CV> {
14081390
// Get the list of files to load.
14091391
let includes = self.include_paths(&mut value, true)?;
1410-
// Check unstable.
1411-
if !self.cli_unstable().config_include {
1412-
return Ok(value);
1413-
}
1392+
14141393
// Accumulate all values here.
14151394
let mut root = CV::Table(HashMap::new(), value.definition().clone());
14161395
for include in includes {

src/doc/src/reference/config.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,58 @@ cargo --config "target.'cfg(all(target_arch = \"arm\", target_os = \"none\"))'.r
269269
cargo --config profile.dev.package.image.opt-level=3 …
270270
```
271271

272+
## Including extra configuration files
273+
274+
Configuration can include other configuration files using the top-level `include` key.
275+
This allows sharing configuration across multiple projects
276+
or splitting complex configurations into multiple files.
277+
278+
### `include`
279+
280+
* Type: array of strings or tables
281+
* Default: none
282+
* Environment: not supported
283+
284+
Loads additional configuration files.
285+
Paths are relative to the configuration file that includes them.
286+
Only paths ending with `.toml` are accepted.
287+
288+
Supports the following formats:
289+
290+
```toml
291+
# array of paths
292+
include = [
293+
"frodo.toml",
294+
"samwise.toml",
295+
]
296+
297+
# inline tables for more control
298+
include = [
299+
{ path = "required.toml" },
300+
{ path = "optional.toml", optional = true },
301+
]
302+
```
303+
304+
> **Note:** For better readability and to avoid confusion, it is recommended to:
305+
> - Place `include` at the top of the configuration file
306+
> - Put one include per line for clearer version control diffs
307+
> - Use inline table syntax when optional includes are needed
308+
309+
When using table syntax, the following fields are supported:
310+
311+
* `path` (string, required): Path to the config file to include.
312+
* `optional` (boolean, default: false): If `true`, missing files are silently
313+
skipped instead of causing an error.
314+
315+
The merge behavior of `include` is different from other config values:
316+
317+
1. Config values are first loaded from the `include` paths.
318+
* Included files are loaded left to right,
319+
with values from later files taking precedence over earlier ones.
320+
* This step recurses if included config files also contain `include` keys.
321+
2. Then, the config file's own values are merged on top of the included config,
322+
taking highest precedence.
323+
272324
## Config-relative paths
273325

274326
Paths in config files may be absolute, relative, or a bare name without any path separators.

src/doc/src/reference/unstable.md

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

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

640-
## config-include
641-
* Tracking Issue: [#7723](https://github.com/rust-lang/cargo/issues/7723)
642-
643-
This feature requires the `-Zconfig-include` command-line option.
644-
645-
The `include` key in a config file can be used to load another config file.
646-
For example:
647-
648-
```toml
649-
# .cargo/config.toml
650-
include = ["other-config.toml"]
651-
652-
[build]
653-
jobs = 4
654-
```
655-
656-
```toml
657-
# .cargo/other-config.toml
658-
[build]
659-
rustflags = ["-W", "unsafe-code"]
660-
```
661-
662-
### Documentation updates
663-
664-
> put this after `## Command-line overrides` before `## Config-relative paths`
665-
> to emphasize its special nature than other config keys.
666-
667-
#### Including extra configuration files
668-
669-
Configuration can include other configuration files using the top-level `include` key.
670-
This allows sharing configuration across multiple projects
671-
or splitting complex configurations into multiple files.
672-
673-
##### `include`
674-
675-
* Type: array of strings or tables
676-
* Default: none
677-
* Environment: not supported
678-
679-
Loads additional configuration files.
680-
Paths are relative to the configuration file that includes them.
681-
Only paths ending with `.toml` are accepted.
682-
683-
Supports the following formats:
684-
685-
```toml
686-
# array of paths
687-
include = [
688-
"frodo.toml",
689-
"samwise.toml",
690-
]
691-
692-
# inline tables for more control
693-
include = [
694-
{ path = "required.toml" },
695-
{ path = "optional.toml", optional = true },
696-
]
697-
```
698-
699-
> **Note:** For better readability and to avoid confusion, it is recommended to:
700-
> - Place `include` at the top of the configuration file
701-
> - Put one include per line for clearer version control diffs
702-
> - Use inline table syntax when optional includes are needed
703-
704-
When using table syntax, the following fields are supported:
705-
706-
* `path` (string, required): Path to the config file to include.
707-
* `optional` (boolean, default: false): If `true`, missing files are silently
708-
skipped instead of causing an error.
709-
710-
The merge behavior of `include` is different from other config values:
711-
712-
1. Config values are first loaded from the `include` paths.
713-
* Included files are loaded left to right,
714-
with values from later files taking precedence over earlier ones.
715-
* This step recurses if included config files also contain `include` keys.
716-
2. Then, the config file's own values are merged on top of the included config,
717-
taking highest precedence.
718-
719639
## target-applies-to-host
720640
* Original Pull Request: [#9322](https://github.com/rust-lang/cargo/pull/9322)
721641
* Tracking Issue: [#9453](https://github.com/rust-lang/cargo/issues/9453)
@@ -2347,3 +2267,9 @@ See the [config documentation](config.md#buildbuild-dir) for information about c
23472267

23482268
The `--build-plan` argument for the `build` command has been removed in 1.93.0-nightly.
23492269
See <https://github.com/rust-lang/cargo/issues/7614> for the reason for its removal.
2270+
2271+
## config-include
2272+
2273+
Support for including extra configuration files via the `include` config key
2274+
has been stabilized in 1.93.0.
2275+
See the [`include` config documentation](config.md#include) for more.

0 commit comments

Comments
 (0)