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
60 changes: 60 additions & 0 deletions doc/book/src/reference/config.md

@weihanglo weihanglo Aug 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before diving into other aspects of the stabilization, have we got any feedback for this using in CI / production?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I know of.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I did not see this answered, I can share my testing here.

I tested the min-publish-age feature as a system-wide default on Amazon Linux 2023 (x86_64 + arm64) VMs with rustc/cargo 1.100.0-nightly (2026-08-27).

Setup

  • rustc 1.100.0-nightly (e457a7b0d 2026-08-27) / cargo 1.100.0-nightly (e8cb624 2026-08-22)
  • System-wide /etc/cargo/config.toml with global-min-publish-age = "1 day"
  • Validated across 16 test scenarios including a real-world project build (Firecracker)

Findings

  1. cargo build --locked produces identical Cargo.lock with and without the cooldown. I diffed Cargo.lock byte-for-byte from Firecracker builds with vs without cooldown.
  2. Large dependency graphs can be processed. An 192-crate workspace (tokio + reqwest + axum) resolves and builds with no issues when the 1-day delay is active. The cooldown actively held back packages: Cargo reported Locking 160 packages to highest Rust 1.100.0-nightly compatible versions as of 24 hours ago. We still obtained a working dependency set.
  3. Project-local override work. .cargo/config.toml with global-min-publish-age = "0 days" overrides the system default.
  4. Upgrade/update path works. cargo update -p <crate> upgrades to the newest cooldown-compatible version. CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow was accepted on this nightly for bypassing the policy.
  5. I find the diagnostic message is clear enough: Locking 7 packages to highest Rust 1.100.0-nightly compatible versions as of 24 hours ago -- having a UTC timestamp would be better if it's in logs, but that's fine.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly the kind of real world testing I was looking for. Thanks a lot for taking time to test and document it!

Comment thread
epage marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,13 @@ rpath = false # Sets the rpath linking option.
[resolver]
lockfile-path = "…" # Overrides the path used for
incompatible-rust-versions = "allow" # Specifies how resolver reacts to these
incompatible-publish-age = "deny" # Specifies how resolver treats recently published versions

[registries.<name>] # registries other than crates.io
index = "…" # URL of the registry index
token = "…" # authentication token for the registry
credential-provider = "cargo:token" # The credential provider for this registry.
min-publish-age = "7 days" # Override `registry.global-min-publish-age` for this registry

[registries.crates-io]
protocol = "sparse" # The protocol to use to access crates.io.
Expand All @@ -167,6 +169,7 @@ default = "…" # name of the default registry
token = "…" # authentication token for crates.io
credential-provider = "cargo:token" # The credential provider for crates.io.
global-credential-providers = ["cargo:token"] # The credential providers to use by default.
global-min-publish-age = "7 days" # The time span allowed for registry packages to use by default.

[source.<name>] # source definition and replacement
replace-with = "…" # replace this source with the given named source
Expand Down Expand Up @@ -1161,6 +1164,23 @@ See the [resolver](resolver.md#rust-version) chapter for more details.
> - `allow` is supported on any version
> - `fallback` is respected as of 1.84

#### `resolver.incompatible-publish-age`
* Type: string
* Default: `"deny"`
* Environment: `CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE`

When resolving the version of a dependency,
specify the behavior for versions with a `pubtime` (if present)
that is incompatible with the configured `min-publish-age`.
Values include:

- `allow`: treat pubtime-incompatible versions like any other version
- `deny`: ignore pubtime-incompatible versions unless they already exist in the lock file

See the [resolver](resolver.md#publish-age) chapter for more details.

> **MSRV:** Respected as of 1.100+

### `[registries]`

The `[registries]` table is used for specifying additional [registries]. It
Expand Down Expand Up @@ -1200,6 +1220,26 @@ If the value exists in the [`[credential-alias]`](#credential-alias) table, the

See [Registry Authentication](registry-authentication.md) for more information.

#### `registries.<name>.min-publish-age`
* Type: string
* Default: [`registry.global-min-publish-age`](#registryglobal-min-publish-age)
* Environment: `CARGO_REGISTRIES_<name>_MIN_PUBLISH_AGE`

Specifies the minimum timespan since a version's `pubtime` that may be
considered for [`resolver.incompatible-publish-age`] for packages from this
registry. If not set, [`registry.global-min-publish-age`](#registryglobal-min-publish-age) will be used.

Will be ignored if the registry does not support this.

It supports the following values:

- An integer followed by "seconds", "minutes", "hours", "days", "weeks", or "months"
- `"0"` to allow all packages

Generally, `"0"`, `"N days"`, and `"N weeks"` will be used.

> **MSRV:** Respected as of 1.100+

#### `registries.crates-io.protocol`
* Type: string
* Default: `"sparse"`
Expand Down Expand Up @@ -1275,6 +1315,25 @@ referenced here by its alias.

See [Registry Authentication](registry-authentication.md) for more information.

#### `registry.global-min-publish-age`
* Type: string
* Default: `"0"`
* Environment: `CARGO_REGISTRY_GLOBAL_MIN_PUBLISH_AGE`

Specifies the global minimum timespan since a version's `pubtime` that it may
be considered for [`resolver.incompatible-publish-age`] for packages.
If `min-publish-age` is not set for a specific registry using
`registries.<name>.min-publish-age`, Cargo will use this minimum publish age.

It supports the following values:

- An integer followed by "seconds", "minutes", "hours", "days", "weeks", or "months"
- `"0"` to allow all packages

Generally, `"0"`, `"N days"`, and `"N weeks"` will be used.
Comment on lines +1330 to +1333

@tats-u tats-u Aug 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Is a "day" always 24 hours even when crossing a Daylight Saving Time boundary?
  • How about the additional ISO 8601 duration format support (e.g. "P7D")?
  • "1 day" (and other similar singular forms) is rejected in favor of "1 days". Singular quantifiers should be also allowed (SQL allows both FETCH FIRST 1 ROW ONLY and FETCH FIRST 1 ROWS ONLY).

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The definition of this friendly format is here. They are all fixed seconds. And both singular and plural forms are supported.

let factor = match right {
"second" | "seconds" => 1,
"minute" | "minutes" => 60,
"hour" | "hours" => 60 * 60,
"day" | "days" => 24 * 60 * 60,
"week" | "weeks" => 7 * 24 * 60 * 60,
"month" | "months" => 2_629_746, // average is 30.436875 days
_ => return None,

How about the additional ISO 8601 duration format support (e.g. "P7D")?

We can consider it, though extending this would also need to extend pre-existing cache.auto-clean-frequency format. We probably don't want to block the stabilization on that.

That said, I saw uv has done a great job documenting this. We probably could polish ours as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about the additional ISO 8601 duration format support (e.g. "P7D")?

Would appreciate if you dont mind opening a new issue for it!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let factor = match right {
"second" | "seconds" => 1,
"minute" | "minutes" => 60,
"hour" | "hours" => 60 * 60,
"day" | "days" => 24 * 60 * 60,
"week" | "weeks" => 7 * 24 * 60 * 60,
"month" | "months" => 2_629_746, // average is 30.436875 days
_ => return None,

Glad to see it. No problem.

this would also need to extend pre-existing cache.auto-clean-frequency format.

The above logic is shared by both options, isn't it?

We probably don't want to block the stabilization on that.

I see. You can go ahead with the current logic.

That said, I saw uv has done a great job documenting this. We probably could polish ours as well.

I hope we can adopt it in both options in the future.

Sorry for having bothered you.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good points. No worries and thank you!


> **MSRV:** Respected as of 1.100+

### `[source]`

The `[source]` table defines the registry sources available. See [Source
Expand Down Expand Up @@ -1562,3 +1621,4 @@ Report progress to the terminal emulator for display in places like the task bar
[crates.io]: https://crates.io/
[target triple]: ../appendix/glossary.md#target '"target" (glossary)'
[`<triple>`]: ../appendix/glossary.md#target '"target" (glossary)'
[`resolver.incompatible-publish-age`]: config.md#resolverincompatible-publish-age
7 changes: 7 additions & 0 deletions doc/book/src/reference/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,15 @@ In summary, the supported environment variables are:
* `CARGO_PROFILE_<name>_STRIP` --- Controls stripping of symbols and/or debuginfos, see [`profile.<name>.strip`].
* `CARGO_REGISTRIES_<name>_CREDENTIAL_PROVIDER` --- Credential provider for a registry, see [`registries.<name>.credential-provider`].
* `CARGO_REGISTRIES_<name>_INDEX` --- URL of a registry index, see [`registries.<name>.index`].
* `CARGO_REGISTRIES_<name>_MIN_PUBLISH_AGE` --- Minimum publish age for packages from a registry, see [`registries.<name>.min-publish-age`].
* `CARGO_REGISTRIES_<name>_TOKEN` --- Authentication token of a registry, see [`registries.<name>.token`].
* `CARGO_REGISTRIES_CRATES_IO_PROTOCOL` --- The protocol used to access [crates.io], see [`registries.crates-io.protocol`].
* `CARGO_REGISTRY_CREDENTIAL_PROVIDER` --- Credential provider for [crates.io], see [`registry.credential-provider`].
* `CARGO_REGISTRY_DEFAULT` --- Default registry for the `--registry` flag, see [`registry.default`].
* `CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS` --- Credential providers for registries that do not have a specific provider defined. See [`registry.global-credential-providers`].
* `CARGO_REGISTRY_GLOBAL_MIN_PUBLISH_AGE` --- Default minimum publish age for packages from registries, see [`registry.global-min-publish-age`].
* `CARGO_REGISTRY_TOKEN` --- Authentication token for [crates.io], see [`registry.token`].
* `CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE` --- How recently published versions are treated during dependency resolution, see [`resolver.incompatible-publish-age`].
* `CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS` --- How incompatible Rust versions are treated during dependency resolution, see [`resolver.incompatible-rust-versions`].
* `CARGO_RESOLVER_LOCKFILE_PATH` --- The path to the lockfile, see [`resolver.lockfile-path`].
* `CARGO_TARGET_<triple>_LINKER` --- The linker to use, see [`target.<triple>.linker`]. The triple must be [converted to uppercase and underscores](config.md#environment-variables).
Expand Down Expand Up @@ -211,13 +214,17 @@ In summary, the supported environment variables are:
[`profile.<name>.strip`]: config.md#profilenamestrip
[`resolver.lockfile-path`]: config.md#resolverlockfile-path
[`resolver.incompatible-rust-versions`]: config.md#resolverincompatible-rust-versions
[`resolver.incompatible-publish-age`]: config.md#resolverincompatible-publish-age
[`registries.<name>.credential-provider`]: config.md#registriesnamecredential-provider
[`registries.<name>.index`]: config.md#registriesnameindex
[`registries.<name>.min-publish-age`]: config.md#registriesnamemin-publish-age
[`registries.<name>.token`]: config.md#registriesnametoken
[`registries.crates-io.protocol`]: config.md#registriescrates-ioprotocol
[`registry.credential-provider`]: config.md#registrycredential-provider
[`registry.default`]: config.md#registrydefault
[`registry.global-credential-providers`]: config.md#registryglobal-credential-providers
[`registry.global-min-publish-age`]: config.md#registryglobal-min-publish-age
[`registry.min-publish-age`]: config.md#registrymin-publish-age
[`registry.token`]: config.md#registrytoken
[`target.<triple>.linker`]: config.md#targettriplelinker
[`target.<triple>.runner`]: config.md#targettriplerunner
Expand Down
14 changes: 14 additions & 0 deletions doc/book/src/reference/resolver.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,20 @@ the resolver will need to pick one version that works in both cases and that wou
[Rust version]: rust-version.md
[`resolver.incompatible-rust-versions`]: config.md#resolverincompatible-rust-versions

### Publish age

Versions with a publish time newer than the configured [`min-publish-age`][`registry.global-min-publish-age`]
are considered pubtime-incompatible.
When [`resolver.incompatible-publish-age`] is set to `deny`,
the resolver will ignore these versions
unless they already exist in the `Cargo.lock` file.
Setting the config to `allow` would disable the check,
which if combined with `cargo update --precise`,
cargo would pull in a specific version and its transitive dependencies.

[`registry.global-min-publish-age`]: config.md#registryglobal-min-publish-age
[`resolver.incompatible-publish-age`]: config.md#resolverincompatible-publish-age

### Features

For the purpose of generating `Cargo.lock`, the resolver builds the dependency
Expand Down
104 changes: 6 additions & 98 deletions doc/book/src/reference/unstable.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ Each new feature described below should explain how to use it.
* [sbom](#sbom) --- Generates SBOM pre-cursor files for compiled artifacts
* [feature-unification](#feature-unification) --- Enable new feature unification modes in workspaces
* [lockfile-publish-time](#lockfile-publish-time) --- Limit resolver to packages older than the specified time
* [min-publish-age](#min-publish-age) --- Filters out dependency versions published more recently than a configured minimum age.
* Output behavior
* [artifact-dir](#artifact-dir) --- Adds a directory where artifacts are copied to.
* [Different binary name](#different-binary-name) --- Assign a name to the built binary that is separate from the crate name.
Expand Down Expand Up @@ -2060,103 +2059,6 @@ option:
hint-msrv = true
```

## min-publish-age

* Tracking Issue: [#17009](https://github.com/rust-lang/cargo/issues/17009)
* RFC: [#3923](https://github.com/rust-lang/rfcs/pull/3923)

The `-Zmin-publish-age` feature allows users to specify a minimum age for
dependency versions. When specified, Cargo won't use a version of a registry
crate that is newer than the minimum age, with a way to override for exceptions
like urgent security fixes.

For example, in your `<repo>/.cargo/config.toml`:

```toml
[registry]
global-min-publish-age = "14 days"
```

### Added to Configuration

The following will be added to Cargo's configuration format:

```toml
[resolver]
incompatible-publish-age = "deny" # Specifies how resolver reacts to these

[registries.<name>]
min-publish-age = "..." # Override `registry.global-min-publish-age` for this registry

[registry]
global-min-publish-age = "0" # Minimum time span allowed for registry packages by default
```

#### `resolver.incompatible-publish-age`

* Type: String
* Default: `"deny"`
* Environment: `CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE`

When resolving the version of a dependency,
specify the behavior for versions with a `pubtime` (if present)
that is incompatible with the configured `min-publish-age`.
Values include:

- `allow`: treat pubtime-incompatible versions like any other version
- `deny`: ignore pubtime-incompatible versions unless they already exist in the lock file

#### `registries.<name>.min-publish-age`

* Type: String
* Default: none
* Environment: `CARGO_REGISTRIES_<name>_MIN_PUBLISH_AGE`

Specifies the minimum timespan since a version's `pubtime` that it may be
considered for `resolver.incompatible-publish-age` for packages from this
registry. If not set, `registry.global-min-publish-age` will be used.

Will be ignored if the registry does not support this.

It supports the following values:

- An integer followed by "seconds", "minutes", "hours", "days", "weeks", or "months"
- `"0"` to allow all packages

#### `registry.global-min-publish-age`

* Type: String
* Default: `"0"`
* Environment: `CARGO_REGISTRY_GLOBAL_MIN_PUBLISH_AGE`

Specifies the global minimum timespan since a version's `pubtime` that it may
be considered for `resolver.incompatible-publish-age` for packages.
If `min-publish-age` is not set for a specific registry using
`registries.<name>.min-publish-age`, Cargo will use this minimum publish age.

It supports the following values:

- An integer followed by "seconds", "minutes", "hours", "days", "weeks", or "months"
- `"0"` to allow all packages

### Added to Resolver

The following will be added to the [resolver chapter] as a sibling section to
"Yanked versions":

> "Pubtime-incompatible versions"
>
> Versions with a publish time newer than the configured `min-publish-age`
> are considered pubtime-incompatible.
> When `resolver.incompatible-publish-age` is set to `deny`,
> the resolver will ignore these versions
> unless they already exist in the `Cargo.lock` file.
> Setting the config to `allow` would disable the check,
> which if combined with `cargo update --precise`,
> cargo would pull in a specific version and its transitive dependencies.

[resolver chapter]: ../reference/resolver.md

# Stabilized and removed features

## Compile progress
Expand Down Expand Up @@ -2466,3 +2368,9 @@ The new build-dir filesystem layout was stabilized in the 1.100.0 release.
Cargo's linting system and the `[lints.cargo]` table have been stabilized in Rust 1.100.
See the [lints chapter](lints.md) and [the lints section](manifest.md#the-lints-section)
for information about configuring Cargo lints.

## min-publish-age

Minimum publish-age configuration for dependency resolution was stabilized in Rust 1.100.
See the [minimum publish-age configuration](config.md#registryglobal-min-publish-age)
and [resolver behavior](resolver.md#publish-age) for more information.
5 changes: 0 additions & 5 deletions src/resolver/version_prefs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ impl PublishAgePolicy {
///
/// Returns `None` when either meets
///
/// * the `-Zmin-publish-age` gate is off
/// * the resolver is configured to allow pubtime-incompatible versions
/// * no threshold is configured at all
pub fn new(now: Option<jiff::Timestamp>, gctx: &GlobalContext) -> CargoResult<Option<Self>> {
Expand All @@ -193,10 +192,6 @@ impl PublishAgePolicy {
now: Option<jiff::Timestamp>,
gctx: &GlobalContext,
) -> CargoResult<Option<Self>> {
if !gctx.cli_unstable().min_publish_age {
return Ok(None);
}

let parse = |key: &str, config: Option<String>| -> CargoResult<MinPublishAge> {
let Some(config) = config else {
return Ok(MinPublishAge::Unset);
Expand Down
6 changes: 4 additions & 2 deletions src/workspace/features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -902,7 +902,6 @@ unstable_cli_options!(
hint_msrv: bool = ("Enable passing `package.rust-version` to rustc for lints"),
host_config: bool = ("Enable the `[host]` section in the .cargo/config.toml file"),
json_target_spec: bool = ("Enable `.json` target spec files"),
min_publish_age: bool = ("Enable the `min-publish-age` configuration for dependency version age filtering"),
minimal_versions: bool = ("Resolve minimal dependency versions instead of maximum"),
msrv_policy: bool = ("Enable rust-version aware policy within cargo"),
mtime_on_use: bool = ("Configure Cargo to update the mtime of used files"),
Expand Down Expand Up @@ -1022,6 +1021,9 @@ const STABILIZED_WARNINGS: &str = "The `build.warnings` config key is now always

const STABILIZED_BUILD_DIR_NEW_LAYOUT: &str = "build.build-dir-new-layout is now always enabled.";

const STABILIZED_MIN_PUBLISH_AGE: &str =
"The `min-publish-age` configuration is now always available.";

fn deserialize_comma_separated_list<'de, D>(
deserializer: D,
) -> Result<Option<Vec<String>>, D::Error>
Expand Down Expand Up @@ -1424,6 +1426,7 @@ impl CliUnstable {
"warnings" => stabilized_warn(k, "1.97", STABILIZED_WARNINGS),
"build-dir-new-layout" => stabilized_warn(k, "1.100", STABILIZED_BUILD_DIR_NEW_LAYOUT),
"cargo-lints" => stabilized_warn(k, "1.100", STABILIZED_CARGO_LINTS),
"min-publish-age" => stabilized_warn(k, "1.100", STABILIZED_MIN_PUBLISH_AGE),

// Unstable features
// Sorted alphabetically:
Expand Down Expand Up @@ -1460,7 +1463,6 @@ impl CliUnstable {
}
"host-config" => self.host_config = parse_empty(k, v)?,
"json-target-spec" => self.json_target_spec = parse_empty(k, v)?,
"min-publish-age" => self.min_publish_age = parse_empty(k, v)?,
"hint-msrv" => self.hint_msrv = parse_empty(k, v)?,
"next-lockfile-bump" => self.next_lockfile_bump = parse_empty(k, v)?,
"minimal-versions" => self.minimal_versions = parse_empty(k, v)?,
Expand Down
33 changes: 0 additions & 33 deletions src/workspace/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,15 +358,6 @@ impl<'gctx> Workspace<'gctx> {
.warn("ignoring `resolver.feature-unification` without `-Zfeature-unification`")?;
};

if !self.gctx().cli_unstable().min_publish_age {
if config.incompatible_publish_age.is_some() {
self.gctx().shell().warn(
"ignoring `resolver.incompatible-publish-age` without `-Zmin-publish-age`",
)?;
}
warn_unused_min_publish_age(self.gctx())?;
}

if let Some(lockfile_path) = config.lockfile_path {
// Reserve the ability to add templates in the future.
let replacements: [(&str, &str); 0] = [];
Expand Down Expand Up @@ -2099,30 +2090,6 @@ impl WorkspaceRootConfig {
}
}

fn warn_unused_min_publish_age(gctx: &GlobalContext) -> CargoResult<()> {
if gctx
.get::<Option<String>>("registry.global-min-publish-age")?
.is_some()
{
gctx.shell()
.warn("ignoring `registry.global-min-publish-age` without `-Zmin-publish-age`")?;
}

if let Some(context::ConfigValue::Table(registries, _)) = gctx.values()?.get("registries") {
for (name, val) in registries {
if let context::ConfigValue::Table(val, _) = val {
if val.contains_key("min-publish-age") {
gctx.shell().warn(format!(
"ignoring `registries.{name}.min-publish-age` without `-Zmin-publish-age`"
))?;
}
}
}
}

Ok(())
}

pub fn resolve_relative_path(
label: &str,
old_root: &Path,
Expand Down
Loading