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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
4 changes: 4 additions & 0 deletions crates/cargo-test-support/src/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ pub(crate) fn create_index_line(
yanked: bool,
links: Option<String>,
rust_version: Option<&str>,
pubtime: Option<&str>,
v: Option<u32>,
) -> String {
// This emulates what crates.io does to retain backwards compatibility.
Expand All @@ -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()
}
Expand Down
10 changes: 10 additions & 0 deletions crates/cargo-test-support/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,7 @@ pub struct Package {
links: Option<String>,
rust_version: Option<String>,
cargo_features: Vec<String>,
pubtime: Option<String>,
v: Option<u32>,
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1273,6 +1275,7 @@ impl Package {
links: None,
rust_version: None,
cargo_features: Vec::new(),
pubtime: None,
v: None,
}
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1536,6 +1545,7 @@ impl Package {
self.yanked,
self.links.clone(),
self.rust_version.as_deref(),
self.pubtime.as_deref(),
self.v,
)
};
Expand Down
2 changes: 1 addition & 1 deletion crates/cargo-util-schemas/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 7 additions & 0 deletions crates/cargo-util-schemas/index.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
4 changes: 4 additions & 0 deletions crates/cargo-util-schemas/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>"))]
pub rust_version: Option<RustVersion>,
/// The publish time for the package. Unstable.
///
/// In ISO8601 with UTC timezone (e.g. 2025-11-12T19:30:12Z)
pub pubtime: Option<String>,
/// The schema version for this entry.
///
/// If this is None, it defaults to version `1`. Entries with unknown
Expand Down
38 changes: 37 additions & 1 deletion src/bin/cargo/commands/generate_lockfile.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use clap_complete::engine::ArgValueCompleter;
use clap_complete::engine::CompletionCandidate;

use crate::command_prelude::*;

use cargo::ops;
Expand All @@ -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")
Copy link
Member

Choose a reason for hiding this comment

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

mm is ambiguous here 😆.
(no need to change)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had considered doing MM vs mm but

  • People likely pick it up from context
  • I wonder how many people remember which is which
  • I wanted to emphasize the difference between literals (upper case) and substitutions (lower case)

.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 `<bright-cyan,bold>cargo help generate-lockfile</>` for more detailed information.\n"
))
}

fn datetime_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
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::<jiff::civil::Date>() {
if let Ok(zoned) = jiff::Zoned::default().with().date(date).build() {
let timestamp = zoned.timestamp();
completions.push(CompletionCandidate::new(timestamp.to_string()));
}
}
completions
}
Comment on lines +28 to +45
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Didn't feel like writing my own parser for the sake of handling incomplete date, so I threw this together to at least make it easier to get the format correct so people can modify them from here.


pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
let ws = args.workspace(gctx)?;
let publish_time = args.get_one::<String>("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)?;
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to close the original issue and create a new one for tracking?
Benefit of this:

  • Cargo team has full control over the issue. Original author won't delete that accidentally.
  • Sometimes original issue has too many discussion. And we tend to use tracking issue for, well, tracking progress only.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sometimes original issue has too many discussion. And we tend to use tracking issue for, well, tracking progress only.

I know the compiler team is concerned about this but it hasn't really felt like it has been too much of a problem for us.

As an end-user, it can be annoying to have to follow the problem you are tracking go through different issues for different stages of the process.

It also splits up the conversation and history.

The part I worry most about, which is less of a problem in this case, is when we close the original issue as the tracking issue may be for one particular solution which we might later reject and then we need to remember to re-open the original issue (I guess that is a reason to create a tracking issue and then close the original on stabilization?)

I lean towards creating a tracking issue but leaving the original open. Does that make sense?

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good to me.

ws.set_resolve_publish_time(publish_time.parse().map_err(anyhow::Error::from)?);
}
ops::generate_lockfile(&ws)?;
Ok(())
}
15 changes: 15 additions & 0 deletions src/cargo/core/resolver/version_prefs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct VersionPreferences {
prefer_patch_deps: HashMap<InternedString, HashSet<Dependency>>,
version_ordering: VersionOrdering,
rust_versions: Vec<PartialVersion>,
publish_time: Option<jiff::Timestamp>,
}

#[derive(Copy, Clone, Default, PartialEq, Eq, Hash, Debug)]
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

Probably expand this doc comment?

///
/// Note: all summaries presumed to be for the same package.
Expand All @@ -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,
Expand All @@ -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());
Expand Down
10 changes: 10 additions & 0 deletions src/cargo/core/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct Inner {
checksum: Option<String>,
links: Option<InternedString>,
rust_version: Option<RustVersion>,
pubtime: Option<jiff::Timestamp>,
}

/// Indicates the dependency inferred from the `dep` syntax that should exist,
Expand Down Expand Up @@ -90,6 +91,7 @@ impl Summary {
checksum: None,
links: links.map(|l| l.into()),
rust_version,
pubtime: None,
}),
})
}
Expand Down Expand Up @@ -124,6 +126,10 @@ impl Summary {
self.inner.rust_version.as_ref()
}

pub fn pubtime(&self) -> Option<jiff::Timestamp> {
self.inner.pubtime
}

pub fn override_id(mut self, id: PackageId) -> Summary {
Arc::make_mut(&mut self.inner).package_id = id;
self
Expand All @@ -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<F>(self, mut f: F) -> Summary
where
F: FnMut(Dependency) -> Dependency,
Expand Down
11 changes: 11 additions & 0 deletions src/cargo/core/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<jiff::Timestamp>,
/// Workspace-level custom metadata
custom_metadata: Option<toml::Value>,

Expand Down Expand Up @@ -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(),
}
Expand Down Expand Up @@ -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<jiff::Timestamp> {
self.resolve_publish_time
}

pub fn custom_metadata(&self) -> Option<&toml::Value> {
self.custom_metadata.as_ref()
}
Expand Down
1 change: 1 addition & 0 deletions src/cargo/ops/cargo_package/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
})?;

Expand Down
3 changes: 3 additions & 0 deletions src/cargo/ops/cargo_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions src/cargo/ops/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?
Expand Down
4 changes: 4 additions & 0 deletions src/cargo/sources/registry/index/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ fn index_package_to_summary(pkg: &IndexPackage<'_>, source_id: SourceId) -> Carg
let links: Option<InternedString> = 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)
}

Expand Down Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions src/doc/man/cargo-generate-lockfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
12 changes: 12 additions & 0 deletions src/doc/man/generated_txt/cargo-generate-lockfile.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/doc/src/commands/cargo-generate-lockfile.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ terminal.</li>
</dd>


<dt class="option-term" id="option-cargo-generate-lockfile---publish-time"><a class="option-anchor" href="#option-cargo-generate-lockfile---publish-time"><code>--publish-time</code> <em>yyyy-mm-ddThh:mm:ssZ</em></a></dt>
<dd class="option-desc"><p>Latest publish time allowed for registry packages (Unstable)</p>
<p>This is a best-effort filter on allowed packages, including:</p>
<ul>
<li>packages from unsupported registries are always accepted</li>
<li>only the current yank state is respected, not the state as of <code>--publish-time</code></li>
<li>precision of the publish time</li>
</ul>
</dd>


<dt class="option-term" id="option-cargo-generate-lockfile---locked"><a class="option-anchor" href="#option-cargo-generate-lockfile---locked"><code>--locked</code></a></dt>
<dd class="option-desc"><p>Asserts that the exact same dependencies and versions are used as when the
existing <code>Cargo.lock</code> file was originally generated. Cargo will exit with an
Expand Down
Loading