Skip to content

Commit 6ce5357

Browse files
authored
feat(generate-lockfile): Add unstable --publish-time flag (#16265)
### What does this PR try to resolve? Implementation for #5221, #15491 Tracking issues: #16270, #16271 Use cases: - Improved reproduction steps using cargo scripts without including a lockfile - Debugging issues in the past - Manual stop gap for #15973 This seemed like the shortest path to testing the proposed `pubtime` index entries (#15491) so we can unblock other work on it like #15973. While this has some big caveats, the design is straightforward. In contrast, #15973 has a lot more design questions (is this a resolver-wide setting or a per-registry setting, what formats are accepted, etc) that could slow down development and testing `pubtime`. ### How to test and review this PR? Unresolved questions (deferred to the tracking issues): - How do we compensate for the caveats, with one option being to leave this perma-unstable? - How should we deal with Summary `pubtime` parse errors? - How strict should we be on the Summary `pubtime` format? - Should we offer a convenient way of getting a compatible timestamp? Future possibilities: - Ability to lock to a timestamp with `cargo update` updating the timestamp (could be useful for cargo scripts)
2 parents b5354b5 + 57d0592 commit 6ce5357

File tree

22 files changed

+341
-15
lines changed

22 files changed

+341
-15
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ cargo-platform = { path = "crates/cargo-platform", version = "0.3.0" }
3434
cargo-test-macro = { version = "0.4.8", path = "crates/cargo-test-macro" }
3535
cargo-test-support = { version = "0.9.1", path = "crates/cargo-test-support" }
3636
cargo-util = { version = "0.2.26", path = "crates/cargo-util" }
37-
cargo-util-schemas = { version = "0.10.3", path = "crates/cargo-util-schemas" }
37+
cargo-util-schemas = { version = "0.11.0", path = "crates/cargo-util-schemas" }
3838
cargo_metadata = "0.23.1"
3939
clap = "4.5.51"
4040
clap_complete = { version = "4.5.60", features = ["unstable-dynamic"] }

crates/cargo-test-support/src/publish.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ pub(crate) fn create_index_line(
228228
yanked: bool,
229229
links: Option<String>,
230230
rust_version: Option<&str>,
231+
pubtime: Option<&str>,
231232
v: Option<u32>,
232233
) -> String {
233234
// This emulates what crates.io does to retain backwards compatibility.
@@ -251,6 +252,9 @@ pub(crate) fn create_index_line(
251252
if let Some(rust_version) = rust_version {
252253
json["rust_version"] = serde_json::json!(rust_version);
253254
}
255+
if let Some(pubtime) = pubtime {
256+
json["pubtime"] = serde_json::json!(pubtime);
257+
}
254258

255259
json.to_string()
256260
}

crates/cargo-test-support/src/registry.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,7 @@ pub struct Package {
578578
links: Option<String>,
579579
rust_version: Option<String>,
580580
cargo_features: Vec<String>,
581+
pubtime: Option<String>,
581582
v: Option<u32>,
582583
}
583584

@@ -1243,6 +1244,7 @@ fn save_new_crate(
12431244
new_crate.links,
12441245
new_crate.rust_version.as_deref(),
12451246
None,
1247+
None,
12461248
);
12471249

12481250
write_to_index(registry_path, &new_crate.name, line, false);
@@ -1273,6 +1275,7 @@ impl Package {
12731275
links: None,
12741276
rust_version: None,
12751277
cargo_features: Vec::new(),
1278+
pubtime: None,
12761279
v: None,
12771280
}
12781281
}
@@ -1460,6 +1463,12 @@ impl Package {
14601463
self
14611464
}
14621465

1466+
/// The publish time for the package in ISO8601 with UTC timezone (e.g. 2025-11-12T19:30:12Z)
1467+
pub fn pubtime(&mut self, time: &str) -> &mut Package {
1468+
self.pubtime = Some(time.to_owned());
1469+
self
1470+
}
1471+
14631472
/// Sets the index schema version for this package.
14641473
///
14651474
/// See `cargo::sources::registry::IndexPackage` for more information.
@@ -1536,6 +1545,7 @@ impl Package {
15361545
self.yanked,
15371546
self.links.clone(),
15381547
self.rust_version.as_deref(),
1548+
self.pubtime.as_deref(),
15391549
self.v,
15401550
)
15411551
};

crates/cargo-util-schemas/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cargo-util-schemas"
3-
version = "0.10.3"
3+
version = "0.11.0"
44
rust-version = "1.91" # MSRV:1
55
edition.workspace = true
66
license.workspace = true

crates/cargo-util-schemas/index.schema.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@
6868
"null"
6969
]
7070
},
71+
"pubtime": {
72+
"description": "The publish time for the package. Unstable.\n\nIn ISO8601 with UTC timezone (e.g. 2025-11-12T19:30:12Z)",
73+
"type": [
74+
"string",
75+
"null"
76+
]
77+
},
7178
"v": {
7279
"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`.",
7380
"type": [

crates/cargo-util-schemas/src/index.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ pub struct IndexPackage<'a> {
4646
/// can be `None` if published before then or if not set in the manifest.
4747
#[cfg_attr(feature = "unstable-schema", schemars(with = "Option<String>"))]
4848
pub rust_version: Option<RustVersion>,
49+
/// The publish time for the package. Unstable.
50+
///
51+
/// In ISO8601 with UTC timezone (e.g. 2025-11-12T19:30:12Z)
52+
pub pubtime: Option<String>,
4953
/// The schema version for this entry.
5054
///
5155
/// If this is None, it defaults to version `1`. Entries with unknown

src/bin/cargo/commands/generate_lockfile.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use clap_complete::engine::ArgValueCompleter;
2+
use clap_complete::engine::CompletionCandidate;
3+
14
use crate::command_prelude::*;
25

36
use cargo::ops;
@@ -9,13 +12,46 @@ pub fn cli() -> Command {
912
.arg_manifest_path()
1013
.arg_lockfile_path()
1114
.arg_ignore_rust_version_with_help("Ignore `rust-version` specification in packages")
15+
.arg(
16+
clap::Arg::new("publish-time")
17+
.long("publish-time")
18+
.value_name("yyyy-mm-ddThh:mm:ssZ")
19+
.add(ArgValueCompleter::new(datetime_completer))
20+
.help("Latest publish time allowed for registry packages (unstable)")
21+
.help_heading(heading::MANIFEST_OPTIONS)
22+
)
1223
.after_help(color_print::cstr!(
1324
"Run `<bright-cyan,bold>cargo help generate-lockfile</>` for more detailed information.\n"
1425
))
1526
}
1627

28+
fn datetime_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
29+
let mut completions = vec![];
30+
let Some(current) = current.to_str() else {
31+
return completions;
32+
};
33+
34+
if current.is_empty() {
35+
// While not likely what people want, it can at least give them a starting point to edit
36+
let timestamp = jiff::Timestamp::now();
37+
completions.push(CompletionCandidate::new(timestamp.to_string()));
38+
} else if let Ok(date) = current.parse::<jiff::civil::Date>() {
39+
if let Ok(zoned) = jiff::Zoned::default().with().date(date).build() {
40+
let timestamp = zoned.timestamp();
41+
completions.push(CompletionCandidate::new(timestamp.to_string()));
42+
}
43+
}
44+
completions
45+
}
46+
1747
pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
18-
let ws = args.workspace(gctx)?;
48+
let publish_time = args.get_one::<String>("publish-time");
49+
let mut ws = args.workspace(gctx)?;
50+
if let Some(publish_time) = publish_time {
51+
gctx.cli_unstable()
52+
.fail_if_stable_opt("--publish-time", 5221)?;
53+
ws.set_resolve_publish_time(publish_time.parse().map_err(anyhow::Error::from)?);
54+
}
1955
ops::generate_lockfile(&ws)?;
2056
Ok(())
2157
}

src/cargo/core/resolver/version_prefs.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub struct VersionPreferences {
2222
prefer_patch_deps: HashMap<InternedString, HashSet<Dependency>>,
2323
version_ordering: VersionOrdering,
2424
rust_versions: Vec<PartialVersion>,
25+
publish_time: Option<jiff::Timestamp>,
2526
}
2627

2728
#[derive(Copy, Clone, Default, PartialEq, Eq, Hash, Debug)]
@@ -53,6 +54,10 @@ impl VersionPreferences {
5354
self.rust_versions = vers;
5455
}
5556

57+
pub fn publish_time(&mut self, publish_time: jiff::Timestamp) {
58+
self.publish_time = Some(publish_time);
59+
}
60+
5661
/// Sort (and filter) the given vector of summaries in-place
5762
///
5863
/// Note: all summaries presumed to be for the same package.
@@ -63,6 +68,7 @@ impl VersionPreferences {
6368
/// 3. `first_version`, falling back to [`VersionPreferences::version_ordering`] when `None`
6469
///
6570
/// Filtering:
71+
/// - `publish_time`
6672
/// - `first_version`
6773
pub fn sort_summaries(
6874
&self,
@@ -77,6 +83,15 @@ impl VersionPreferences {
7783
.map(|deps| deps.iter().any(|d| d.matches_id(*pkg_id)))
7884
.unwrap_or(false)
7985
};
86+
if let Some(max_publish_time) = self.publish_time {
87+
summaries.retain(|s| {
88+
if let Some(summary_publish_time) = s.pubtime() {
89+
summary_publish_time <= max_publish_time
90+
} else {
91+
true
92+
}
93+
});
94+
}
8095
summaries.sort_unstable_by(|a, b| {
8196
let prefer_a = should_prefer(&a.package_id());
8297
let prefer_b = should_prefer(&b.package_id());

src/cargo/core/summary.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ struct Inner {
2929
checksum: Option<String>,
3030
links: Option<InternedString>,
3131
rust_version: Option<RustVersion>,
32+
pubtime: Option<jiff::Timestamp>,
3233
}
3334

3435
/// Indicates the dependency inferred from the `dep` syntax that should exist,
@@ -90,6 +91,7 @@ impl Summary {
9091
checksum: None,
9192
links: links.map(|l| l.into()),
9293
rust_version,
94+
pubtime: None,
9395
}),
9496
})
9597
}
@@ -124,6 +126,10 @@ impl Summary {
124126
self.inner.rust_version.as_ref()
125127
}
126128

129+
pub fn pubtime(&self) -> Option<jiff::Timestamp> {
130+
self.inner.pubtime
131+
}
132+
127133
pub fn override_id(mut self, id: PackageId) -> Summary {
128134
Arc::make_mut(&mut self.inner).package_id = id;
129135
self
@@ -133,6 +139,10 @@ impl Summary {
133139
Arc::make_mut(&mut self.inner).checksum = Some(cksum);
134140
}
135141

142+
pub fn set_pubtime(&mut self, pubtime: jiff::Timestamp) {
143+
Arc::make_mut(&mut self.inner).pubtime = Some(pubtime);
144+
}
145+
136146
pub fn map_dependencies<F>(self, mut f: F) -> Summary
137147
where
138148
F: FnMut(Dependency) -> Dependency,

0 commit comments

Comments
 (0)