Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 46 additions & 7 deletions src/cargo/util/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 `{}`",
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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 {}",
Expand Down Expand Up @@ -2495,17 +2516,20 @@ 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 {
fn new(p: impl Into<PathBuf>, def: Definition) -> Self {
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.
Expand All @@ -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<PathBuf> {
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)
}
}
}

Expand Down
71 changes: 57 additions & 14 deletions src/doc/src/reference/unstable.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
63 changes: 63 additions & 0 deletions tests/testsuite/config_include.rs
Original file line number Diff line number Diff line change
Expand Up @@ -621,3 +621,66 @@ 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();
assert_eq!(gctx.get::<i32>("key1").unwrap(), 1);
assert_eq!(gctx.get::<i32>("key2").unwrap(), 2);
}

#[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]
"#]],
);
}