diff --git a/Cargo.lock b/Cargo.lock index 3e101ff0577..9ea1a8291e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,7 +498,7 @@ dependencies = [ [[package]] name = "cargo-util-schemas" -version = "0.10.3" +version = "0.11.0" dependencies = [ "schemars", "semver", diff --git a/Cargo.toml b/Cargo.toml index e80e07f64ed..ca281d1c1d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ cargo-platform = { path = "crates/cargo-platform", version = "0.3.0" } cargo-test-macro = { version = "0.4.8", path = "crates/cargo-test-macro" } cargo-test-support = { version = "0.9.1", path = "crates/cargo-test-support" } cargo-util = { version = "0.2.26", path = "crates/cargo-util" } -cargo-util-schemas = { version = "0.10.3", path = "crates/cargo-util-schemas" } +cargo-util-schemas = { version = "0.11.0", path = "crates/cargo-util-schemas" } cargo_metadata = "0.23.1" clap = "4.5.51" clap_complete = { version = "4.5.60", features = ["unstable-dynamic"] } diff --git a/crates/cargo-test-support/src/publish.rs b/crates/cargo-test-support/src/publish.rs index b538615b7dd..854ad516bf2 100644 --- a/crates/cargo-test-support/src/publish.rs +++ b/crates/cargo-test-support/src/publish.rs @@ -228,6 +228,7 @@ pub(crate) fn create_index_line( yanked: bool, links: Option, rust_version: Option<&str>, + pubtime: Option<&str>, v: Option, ) -> String { // This emulates what crates.io does to retain backwards compatibility. @@ -251,6 +252,9 @@ pub(crate) fn create_index_line( if let Some(rust_version) = rust_version { json["rust_version"] = serde_json::json!(rust_version); } + if let Some(pubtime) = pubtime { + json["pubtime"] = serde_json::json!(pubtime); + } json.to_string() } diff --git a/crates/cargo-test-support/src/registry.rs b/crates/cargo-test-support/src/registry.rs index 138350c5e82..9cf2aaaaad5 100644 --- a/crates/cargo-test-support/src/registry.rs +++ b/crates/cargo-test-support/src/registry.rs @@ -578,6 +578,7 @@ pub struct Package { links: Option, rust_version: Option, cargo_features: Vec, + pubtime: Option, v: Option, } @@ -1243,6 +1244,7 @@ fn save_new_crate( new_crate.links, new_crate.rust_version.as_deref(), None, + None, ); write_to_index(registry_path, &new_crate.name, line, false); @@ -1273,6 +1275,7 @@ impl Package { links: None, rust_version: None, cargo_features: Vec::new(), + pubtime: None, v: None, } } @@ -1460,6 +1463,12 @@ impl Package { self } + /// The publish time for the package in ISO8601 with UTC timezone (e.g. 2025-11-12T19:30:12Z) + pub fn pubtime(&mut self, time: &str) -> &mut Package { + self.pubtime = Some(time.to_owned()); + self + } + /// Sets the index schema version for this package. /// /// See `cargo::sources::registry::IndexPackage` for more information. @@ -1536,6 +1545,7 @@ impl Package { self.yanked, self.links.clone(), self.rust_version.as_deref(), + self.pubtime.as_deref(), self.v, ) }; diff --git a/crates/cargo-util-schemas/Cargo.toml b/crates/cargo-util-schemas/Cargo.toml index 50fc9a471bb..44fbf7a1844 100644 --- a/crates/cargo-util-schemas/Cargo.toml +++ b/crates/cargo-util-schemas/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-util-schemas" -version = "0.10.3" +version = "0.11.0" rust-version = "1.91" # MSRV:1 edition.workspace = true license.workspace = true diff --git a/crates/cargo-util-schemas/index.schema.json b/crates/cargo-util-schemas/index.schema.json index 6435e3ab6e5..5e9eab38a28 100644 --- a/crates/cargo-util-schemas/index.schema.json +++ b/crates/cargo-util-schemas/index.schema.json @@ -68,6 +68,13 @@ "null" ] }, + "pubtime": { + "description": "The publish time for the package. Unstable.\n\nIn ISO8601 with UTC timezone (e.g. 2025-11-12T19:30:12Z)", + "type": [ + "string", + "null" + ] + }, "v": { "description": "The schema version for this entry.\n\nIf this is None, it defaults to version `1`. Entries with unknown\nversions are ignored.\n\nVersion `2` schema adds the `features2` field.\n\nVersion `3` schema adds `artifact`, `bindep_targes`, and `lib` for\nartifact dependencies support.\n\nThis provides a method to safely introduce changes to index entries\nand allow older versions of cargo to ignore newer entries it doesn't\nunderstand. This is honored as of 1.51, so unfortunately older\nversions will ignore it, and potentially misinterpret version 2 and\nnewer entries.\n\nThe intent is that versions older than 1.51 will work with a\npre-existing `Cargo.lock`, but they may not correctly process `cargo\nupdate` or build a lock from scratch. In that case, cargo may\nincorrectly select a new package that uses a new index schema. A\nworkaround is to downgrade any packages that are incompatible with the\n`--precise` flag of `cargo update`.", "type": [ diff --git a/crates/cargo-util-schemas/src/index.rs b/crates/cargo-util-schemas/src/index.rs index 1e5fb0938da..adb9e0c6ca0 100644 --- a/crates/cargo-util-schemas/src/index.rs +++ b/crates/cargo-util-schemas/src/index.rs @@ -46,6 +46,10 @@ pub struct IndexPackage<'a> { /// can be `None` if published before then or if not set in the manifest. #[cfg_attr(feature = "unstable-schema", schemars(with = "Option"))] pub rust_version: Option, + /// The publish time for the package. Unstable. + /// + /// In ISO8601 with UTC timezone (e.g. 2025-11-12T19:30:12Z) + pub pubtime: Option, /// The schema version for this entry. /// /// If this is None, it defaults to version `1`. Entries with unknown diff --git a/src/bin/cargo/commands/generate_lockfile.rs b/src/bin/cargo/commands/generate_lockfile.rs index 878dfe9e964..ef02347b883 100644 --- a/src/bin/cargo/commands/generate_lockfile.rs +++ b/src/bin/cargo/commands/generate_lockfile.rs @@ -1,3 +1,6 @@ +use clap_complete::engine::ArgValueCompleter; +use clap_complete::engine::CompletionCandidate; + use crate::command_prelude::*; use cargo::ops; @@ -9,13 +12,46 @@ pub fn cli() -> Command { .arg_manifest_path() .arg_lockfile_path() .arg_ignore_rust_version_with_help("Ignore `rust-version` specification in packages") + .arg( + clap::Arg::new("publish-time") + .long("publish-time") + .value_name("yyyy-mm-ddThh:mm:ssZ") + .add(ArgValueCompleter::new(datetime_completer)) + .help("Latest publish time allowed for registry packages (unstable)") + .help_heading(heading::MANIFEST_OPTIONS) + ) .after_help(color_print::cstr!( "Run `cargo help generate-lockfile` for more detailed information.\n" )) } +fn datetime_completer(current: &std::ffi::OsStr) -> Vec { + let mut completions = vec![]; + let Some(current) = current.to_str() else { + return completions; + }; + + if current.is_empty() { + // While not likely what people want, it can at least give them a starting point to edit + let timestamp = jiff::Timestamp::now(); + completions.push(CompletionCandidate::new(timestamp.to_string())); + } else if let Ok(date) = current.parse::() { + if let Ok(zoned) = jiff::Zoned::default().with().date(date).build() { + let timestamp = zoned.timestamp(); + completions.push(CompletionCandidate::new(timestamp.to_string())); + } + } + completions +} + pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { - let ws = args.workspace(gctx)?; + let publish_time = args.get_one::("publish-time"); + let mut ws = args.workspace(gctx)?; + if let Some(publish_time) = publish_time { + gctx.cli_unstable() + .fail_if_stable_opt("--publish-time", 5221)?; + ws.set_resolve_publish_time(publish_time.parse().map_err(anyhow::Error::from)?); + } ops::generate_lockfile(&ws)?; Ok(()) } diff --git a/src/cargo/core/resolver/version_prefs.rs b/src/cargo/core/resolver/version_prefs.rs index 61f17404936..a8512a5f5e2 100644 --- a/src/cargo/core/resolver/version_prefs.rs +++ b/src/cargo/core/resolver/version_prefs.rs @@ -22,6 +22,7 @@ pub struct VersionPreferences { prefer_patch_deps: HashMap>, version_ordering: VersionOrdering, rust_versions: Vec, + publish_time: Option, } #[derive(Copy, Clone, Default, PartialEq, Eq, Hash, Debug)] @@ -53,6 +54,10 @@ impl VersionPreferences { self.rust_versions = vers; } + pub fn publish_time(&mut self, publish_time: jiff::Timestamp) { + self.publish_time = Some(publish_time); + } + /// Sort (and filter) the given vector of summaries in-place /// /// Note: all summaries presumed to be for the same package. @@ -63,6 +68,7 @@ impl VersionPreferences { /// 3. `first_version`, falling back to [`VersionPreferences::version_ordering`] when `None` /// /// Filtering: + /// - `publish_time` /// - `first_version` pub fn sort_summaries( &self, @@ -77,6 +83,15 @@ impl VersionPreferences { .map(|deps| deps.iter().any(|d| d.matches_id(*pkg_id))) .unwrap_or(false) }; + if let Some(max_publish_time) = self.publish_time { + summaries.retain(|s| { + if let Some(summary_publish_time) = s.pubtime() { + summary_publish_time <= max_publish_time + } else { + true + } + }); + } summaries.sort_unstable_by(|a, b| { let prefer_a = should_prefer(&a.package_id()); let prefer_b = should_prefer(&b.package_id()); diff --git a/src/cargo/core/summary.rs b/src/cargo/core/summary.rs index 92cc8ce8ad9..7d9dd83fc2e 100644 --- a/src/cargo/core/summary.rs +++ b/src/cargo/core/summary.rs @@ -29,6 +29,7 @@ struct Inner { checksum: Option, links: Option, rust_version: Option, + pubtime: Option, } /// Indicates the dependency inferred from the `dep` syntax that should exist, @@ -90,6 +91,7 @@ impl Summary { checksum: None, links: links.map(|l| l.into()), rust_version, + pubtime: None, }), }) } @@ -124,6 +126,10 @@ impl Summary { self.inner.rust_version.as_ref() } + pub fn pubtime(&self) -> Option { + self.inner.pubtime + } + pub fn override_id(mut self, id: PackageId) -> Summary { Arc::make_mut(&mut self.inner).package_id = id; self @@ -133,6 +139,10 @@ impl Summary { Arc::make_mut(&mut self.inner).checksum = Some(cksum); } + pub fn set_pubtime(&mut self, pubtime: jiff::Timestamp) { + Arc::make_mut(&mut self.inner).pubtime = Some(pubtime); + } + pub fn map_dependencies(self, mut f: F) -> Summary where F: FnMut(Dependency) -> Dependency, diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 2a609163d6e..58124130dad 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -122,6 +122,8 @@ pub struct Workspace<'gctx> { resolve_honors_rust_version: bool, /// The feature unification mode used when building packages. resolve_feature_unification: FeatureUnification, + /// Latest publish time allowed for packages + resolve_publish_time: Option, /// Workspace-level custom metadata custom_metadata: Option, @@ -259,6 +261,7 @@ impl<'gctx> Workspace<'gctx> { resolve_behavior: ResolveBehavior::V1, resolve_honors_rust_version: false, resolve_feature_unification: FeatureUnification::Selected, + resolve_publish_time: None, custom_metadata: None, local_overlays: HashMap::new(), } @@ -717,6 +720,14 @@ impl<'gctx> Workspace<'gctx> { self.resolve_feature_unification } + pub fn set_resolve_publish_time(&mut self, publish_time: jiff::Timestamp) { + self.resolve_publish_time = Some(publish_time); + } + + pub fn resolve_publish_time(&self) -> Option { + self.resolve_publish_time + } + pub fn custom_metadata(&self) -> Option<&toml::Value> { self.custom_metadata.as_ref() } diff --git a/src/cargo/ops/cargo_package/mod.rs b/src/cargo/ops/cargo_package/mod.rs index 92282751f81..d35ae2d6662 100644 --- a/src/cargo/ops/cargo_package/mod.rs +++ b/src/cargo/ops/cargo_package/mod.rs @@ -1229,6 +1229,7 @@ impl<'a> TmpRegistry<'a> { yanked: None, links: new_crate.links.map(|x| x.into()), rust_version: None, + pubtime: None, v: Some(2), })?; diff --git a/src/cargo/ops/cargo_update.rs b/src/cargo/ops/cargo_update.rs index 9b60d3dfd29..ccbbfa01bfd 100644 --- a/src/cargo/ops/cargo_update.rs +++ b/src/cargo/ops/cargo_update.rs @@ -722,6 +722,9 @@ fn status_locking(ws: &Workspace<'_>, num_pkgs: usize) -> CargoResult<()> { write!(&mut cfg, " Rust {rust_version}")?; } write!(&mut cfg, " compatible version{plural}")?; + if let Some(publish_time) = ws.resolve_publish_time() { + write!(&mut cfg, " as of {publish_time}")?; + } } ws.gctx() diff --git a/src/cargo/ops/resolve.rs b/src/cargo/ops/resolve.rs index aa16dacc926..ecd95273f57 100644 --- a/src/cargo/ops/resolve.rs +++ b/src/cargo/ops/resolve.rs @@ -448,6 +448,9 @@ pub fn resolve_with_previous<'gctx>( } version_prefs.rust_versions(rust_versions); } + if let Some(publish_time) = ws.resolve_publish_time() { + version_prefs.publish_time(publish_time); + } let avoid_patch_ids = if register_patches { register_patch_entries(registry, ws, previous, &mut version_prefs, keep_previous)? diff --git a/src/cargo/sources/registry/index/mod.rs b/src/cargo/sources/registry/index/mod.rs index f2cd5ae39a5..656fcfa35d6 100644 --- a/src/cargo/sources/registry/index/mod.rs +++ b/src/cargo/sources/registry/index/mod.rs @@ -217,6 +217,9 @@ fn index_package_to_summary(pkg: &IndexPackage<'_>, source_id: SourceId) -> Carg let links: Option = pkg.links.as_ref().map(|l| l.as_ref().into()); let mut summary = Summary::new(pkgid, deps, &features, links, pkg.rust_version.clone())?; summary.set_checksum(pkg.cksum.clone()); + if let Some(pubtime) = pkg.pubtime.as_ref().and_then(|p| p.parse().ok()) { + summary.set_pubtime(pubtime); + } Ok(summary) } @@ -674,6 +677,7 @@ impl IndexSummary { cksum: Default::default(), yanked: Default::default(), links: Default::default(), + pubtime: Default::default(), }; let summary = index_package_to_summary(&index, source_id)?; (index, summary, false) diff --git a/src/doc/man/cargo-generate-lockfile.md b/src/doc/man/cargo-generate-lockfile.md index 63c17849c81..cd71885edde 100644 --- a/src/doc/man/cargo-generate-lockfile.md +++ b/src/doc/man/cargo-generate-lockfile.md @@ -32,6 +32,15 @@ lockfile and has more options for controlling update behavior. {{> options-ignore-rust-version }} +{{#option "`--publish-time` _yyyy-mm-ddThh:mm:ssZ_" }} +Latest publish time allowed for registry packages (Unstable) + +This is a best-effort filter on allowed packages, including: +- packages from unsupported registries are always accepted +- only the current yank state is respected, not the state as of `--publish-time` +- precision of the publish time +{{/option}} + {{> options-locked }} {{> options-lockfile-path }} diff --git a/src/doc/man/generated_txt/cargo-generate-lockfile.txt b/src/doc/man/generated_txt/cargo-generate-lockfile.txt index 5933cf3b0f0..b9861f1402f 100644 --- a/src/doc/man/generated_txt/cargo-generate-lockfile.txt +++ b/src/doc/man/generated_txt/cargo-generate-lockfile.txt @@ -49,6 +49,18 @@ OPTIONS --ignore-rust-version Ignore rust-version specification in packages. + --publish-time yyyy-mm-ddThh:mm:ssZ + Latest publish time allowed for registry packages (Unstable) + + This is a best-effort filter on allowed packages, including: + + o packages from unsupported registries are always accepted + + o only the current yank state is respected, not the state as of + --publish-time + + o precision of the publish time + --locked Asserts that the exact same dependencies and versions are used as when the existing Cargo.lock file was originally generated. Cargo diff --git a/src/doc/src/commands/cargo-generate-lockfile.md b/src/doc/src/commands/cargo-generate-lockfile.md index bee7ef75963..9cbe400eb1c 100644 --- a/src/doc/src/commands/cargo-generate-lockfile.md +++ b/src/doc/src/commands/cargo-generate-lockfile.md @@ -67,6 +67,17 @@ terminal. +
--publish-time yyyy-mm-ddThh:mm:ssZ
+

Latest publish time allowed for registry packages (Unstable)

+

This is a best-effort filter on allowed packages, including:

+
    +
  • packages from unsupported registries are always accepted
  • +
  • only the current yank state is respected, not the state as of --publish-time
  • +
  • precision of the publish time
  • +
+
+ +
--locked

Asserts that the exact same dependencies and versions are used as when the existing Cargo.lock file was originally generated. Cargo will exit with an diff --git a/src/doc/src/reference/unstable.md b/src/doc/src/reference/unstable.md index 63a811ca1dd..9872e709d04 100644 --- a/src/doc/src/reference/unstable.md +++ b/src/doc/src/reference/unstable.md @@ -78,6 +78,7 @@ Each new feature described below should explain how to use it. * [sbom](#sbom) --- Generates SBOM pre-cursor files for compiled artifacts * [update-breaking](#update-breaking) --- Allows upgrading to breaking versions with `update --breaking` * [feature-unification](#feature-unification) --- Enable new feature unification modes in workspaces + * [lockfile-publish-time] --- Limit resolver to packages older than the specified time * Output behavior * [artifact-dir](#artifact-dir) --- Adds a directory where artifacts are copied to. * [build-dir-new-layout](#build-dir-new-layout) --- Enables the new build-dir filesystem layout @@ -1913,6 +1914,22 @@ Specify which packages participate in [feature unification](../reference/feature * `package`: Dependency features are considered on a package-by-package basis, preferring duplicate builds of dependencies when different sets of features are activated by the packages. +## pubtime + +* Original Issue: [#15491](https://github.com/rust-lang/cargo/issues/15491) +* Tracking Issue: [#16270](https://github.com/rust-lang/cargo/issues/16270) + +Documentation updates: +- Add `pubtime` field to the Index Summary description + +## lockfile-publish-time + +* Original Issue: [#5221](https://github.com/rust-lang/cargo/issues/5221) +* Tracking Issue: [#16271](https://github.com/rust-lang/cargo/issues/16271) + +With `cargo generate-lockfile -Zunstable-options --publish-time