Skip to content

Commit 598878b

Browse files
authored
feat(config-include): add optional field support (#16180)
### What does this PR try to resolve? When `optional=true` Cargo silently skip missing config-include files. Example config: ```toml [[include]] path = 'optional-config.toml' optional = true ``` Part of <#7723 (comment)>. ### How to test and review this PR? CI passes. In the updated doc, 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?Capture current behavior where optional field is not yet supported.
2 parents e1a37a3 + b9b13d0 commit 598878b

File tree

3 files changed

+166
-21
lines changed

3 files changed

+166
-21
lines changed

src/cargo/util/context/mod.rs

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,8 +1258,12 @@ impl GlobalContext {
12581258
) -> CargoResult<()> {
12591259
let includes = self.include_paths(cv, false)?;
12601260
for include in includes {
1261+
let Some(abs_path) = include.resolve_path(self) else {
1262+
continue;
1263+
};
1264+
12611265
let mut cv = self
1262-
._load_file(&include.abs_path(self), seen, false, WhyLoad::FileDiscovery)
1266+
._load_file(&abs_path, seen, false, WhyLoad::FileDiscovery)
12631267
.with_context(|| {
12641268
format!(
12651269
"failed to load config include `{}` from `{}`",
@@ -1369,7 +1373,11 @@ impl GlobalContext {
13691373
// Accumulate all values here.
13701374
let mut root = CV::Table(HashMap::new(), value.definition().clone());
13711375
for include in includes {
1372-
self._load_file(&include.abs_path(self), seen, true, why_load)
1376+
let Some(abs_path) = include.resolve_path(self) else {
1377+
continue;
1378+
};
1379+
1380+
self._load_file(&abs_path, seen, true, why_load)
13731381
.and_then(|include| root.merge(include, true))
13741382
.with_context(|| {
13751383
format!(
@@ -1410,7 +1418,20 @@ impl GlobalContext {
14101418
),
14111419
None => bail!("missing field `path` at `include[{idx}]` in `{def}`"),
14121420
};
1413-
Ok(ConfigInclude::new(s, def))
1421+
1422+
// Extract optional `include.optional` field
1423+
let optional = match table.remove("optional") {
1424+
Some(CV::Boolean(b, _)) => b,
1425+
Some(other) => bail!(
1426+
"expected a boolean, but found {} at `include[{idx}].optional` in `{def}`",
1427+
other.desc()
1428+
),
1429+
None => false,
1430+
};
1431+
1432+
let mut include = ConfigInclude::new(s, def);
1433+
include.optional = optional;
1434+
Ok(include)
14141435
}
14151436
other => bail!(
14161437
"expected a string or table, but found {} at `include[{idx}]` in {}",
@@ -2495,17 +2516,20 @@ struct ConfigInclude {
24952516
/// Could be either relative or absolute.
24962517
path: PathBuf,
24972518
def: Definition,
2519+
/// Whether this include is optional (missing files are silently ignored)
2520+
optional: bool,
24982521
}
24992522

25002523
impl ConfigInclude {
25012524
fn new(p: impl Into<PathBuf>, def: Definition) -> Self {
25022525
Self {
25032526
path: p.into(),
25042527
def,
2528+
optional: false,
25052529
}
25062530
}
25072531

2508-
/// Gets the absolute path of the config-include config file.
2532+
/// Resolves the absolute path for this include.
25092533
///
25102534
/// For file based include,
25112535
/// it is relative to parent directory of the config file includes it.
@@ -2514,12 +2538,27 @@ impl ConfigInclude {
25142538
///
25152539
/// For CLI based include (e.g., `--config 'include = "foo.toml"'`),
25162540
/// it is relative to the current working directory.
2517-
fn abs_path(&self, gctx: &GlobalContext) -> PathBuf {
2518-
match &self.def {
2541+
///
2542+
/// Returns `None` if this is an optional include and the file doesn't exist.
2543+
/// Otherwise returns `Some(PathBuf)` with the absolute path.
2544+
fn resolve_path(&self, gctx: &GlobalContext) -> Option<PathBuf> {
2545+
let abs_path = match &self.def {
25192546
Definition::Path(p) | Definition::Cli(Some(p)) => p.parent().unwrap(),
25202547
Definition::Environment(_) | Definition::Cli(None) | Definition::BuiltIn => gctx.cwd(),
25212548
}
2522-
.join(&self.path)
2549+
.join(&self.path);
2550+
2551+
if self.optional && !abs_path.exists() {
2552+
tracing::info!(
2553+
"skipping optional include `{}` in `{}`: file not found at `{}`",
2554+
self.path.display(),
2555+
self.def,
2556+
abs_path.display(),
2557+
);
2558+
None
2559+
} else {
2560+
Some(abs_path)
2561+
}
25232562
}
25242563
}
25252564

src/doc/src/reference/unstable.md

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -658,28 +658,71 @@ like to stabilize it somehow!
658658

659659
This feature requires the `-Zconfig-include` command-line option.
660660

661-
The `include` key in a config file can be used to load another config file. It
662-
takes a string for a path to another file relative to the config file, or an
663-
array of config file paths. Only path ending with `.toml` is accepted.
661+
The `include` key in a config file can be used to load another config file.
662+
For example:
663+
664+
```toml
665+
# .cargo/config.toml
666+
include = "other-config.toml"
667+
668+
[build]
669+
jobs = 4
670+
```
671+
672+
```toml
673+
# .cargo/other-config.toml
674+
[build]
675+
rustflags = ["-W", "unsafe-code"]
676+
```
677+
678+
### Documentation updates
679+
680+
#### `include`
681+
682+
* Type: string, array of strings, or array of tables
683+
* Default: none
684+
685+
Loads additional config files. Paths are relative to the config file that
686+
includes them. Only paths ending with `.toml` are accepted.
687+
688+
Supports the following formats:
664689

665690
```toml
666-
# a path ending with `.toml`
691+
# single path
667692
include = "path/to/mordor.toml"
668693

669-
# or an array of paths
694+
# array of paths
670695
include = ["frodo.toml", "samwise.toml"]
696+
697+
# inline tables
698+
include = [
699+
"simple.toml",
700+
{ path = "optional.toml", optional = true }
701+
]
702+
703+
# array of tables
704+
[[include]]
705+
path = "required.toml"
706+
707+
[[include]]
708+
path = "optional.toml"
709+
optional = true
671710
```
672711

673-
Unlike other config values, the merge behavior of the `include` key is
674-
different. When a config file contains an `include` key:
712+
When using table syntax (inline tables or array of tables), the following
713+
fields are supported:
714+
715+
* `path` (string, required): Path to the config file to include.
716+
* `optional` (boolean, default: false): If `true`, missing files are silently
717+
skipped instead of causing an error.
718+
719+
The merge behavior of `include` is different from other config values:
675720

676-
1. The config values are first loaded from the `include` path.
677-
* If the value of the `include` key is an array of paths, the config values
678-
are loaded and merged from left to right for each path.
679-
* Recurse this step if the config values from the `include` path also
680-
contain an `include` key.
681-
2. Then, the config file's own values are merged on top of the config
682-
from the `include` path.
721+
1. Config values are first loaded from the `include` path.
722+
* If `include` is an array, config values are loaded and merged from left
723+
to right for each path.
724+
* This step recurses if included config files also contain `include` keys.
725+
2. Then, the config file's own values are merged on top of the included config.
683726

684727
## target-applies-to-host
685728
* Original Pull Request: [#9322](https://github.com/rust-lang/cargo/pull/9322)

tests/testsuite/config_include.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,3 +621,66 @@ Caused by:
621621
"#]],
622622
);
623623
}
624+
625+
#[cargo_test]
626+
fn optional_include_missing_and_existing() {
627+
write_config_at(
628+
".cargo/config.toml",
629+
"
630+
key1 = 1
631+
632+
[[include]]
633+
path = 'missing.toml'
634+
optional = true
635+
636+
[[include]]
637+
path = 'other.toml'
638+
optional = true
639+
",
640+
);
641+
write_config_at(
642+
".cargo/other.toml",
643+
"
644+
key2 = 2
645+
",
646+
);
647+
648+
let gctx = GlobalContextBuilder::new()
649+
.unstable_flag("config-include")
650+
.build();
651+
assert_eq!(gctx.get::<i32>("key1").unwrap(), 1);
652+
assert_eq!(gctx.get::<i32>("key2").unwrap(), 2);
653+
}
654+
655+
#[cargo_test]
656+
fn optional_false_missing_file() {
657+
write_config_at(
658+
".cargo/config.toml",
659+
"
660+
key1 = 1
661+
662+
[[include]]
663+
path = 'missing.toml'
664+
optional = false
665+
",
666+
);
667+
668+
let gctx = GlobalContextBuilder::new()
669+
.unstable_flag("config-include")
670+
.build_err();
671+
assert_error(
672+
gctx.unwrap_err(),
673+
str![[r#"
674+
could not load Cargo configuration
675+
676+
Caused by:
677+
failed to load config include `missing.toml` from `[ROOT]/.cargo/config.toml`
678+
679+
Caused by:
680+
failed to read configuration file `[ROOT]/.cargo/missing.toml`
681+
682+
Caused by:
683+
[NOT_FOUND]
684+
"#]],
685+
);
686+
}

0 commit comments

Comments
 (0)