From bf051daf5784e19cf9cf7cdaef271c543d318bc0 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 7 May 2026 14:36:29 -0400 Subject: [PATCH 01/11] refactor(util): extract `parse_time_span` So it can be reused by the upcoming `-Zmin-publish-age`. --- src/bin/cargo/commands/clean.rs | 5 ++++- src/cargo/core/gc.rs | 37 ++------------------------------ src/cargo/util/mod.rs | 1 + src/cargo/util/time_span.rs | 38 +++++++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 src/cargo/util/time_span.rs diff --git a/src/bin/cargo/commands/clean.rs b/src/bin/cargo/commands/clean.rs index e9ee35fdf20..0bde5a343a7 100644 --- a/src/bin/cargo/commands/clean.rs +++ b/src/bin/cargo/commands/clean.rs @@ -1,7 +1,10 @@ use crate::command_prelude::*; use crate::util::cache_lock::CacheLockMode; +use crate::util::time_span::parse_time_span; + use cargo::core::gc::Gc; -use cargo::core::gc::{GcOpts, parse_human_size, parse_time_span}; +use cargo::core::gc::GcOpts; +use cargo::core::gc::parse_human_size; use cargo::core::global_cache_tracker::GlobalCacheTracker; use cargo::ops::CleanContext; use cargo::ops::{self, CleanOptions}; diff --git a/src/cargo/core/gc.rs b/src/cargo/core/gc.rs index 324cede649b..0a7a30c239c 100644 --- a/src/cargo/core/gc.rs +++ b/src/cargo/core/gc.rs @@ -22,6 +22,7 @@ use crate::core::global_cache_tracker::{self, GlobalCacheTracker}; use crate::ops::CleanContext; use crate::util::cache_lock::{CacheLock, CacheLockMode}; +use crate::util::time_span::maybe_parse_time_span; use crate::{CargoResult, GlobalContext}; use anyhow::{Context as _, format_err}; use serde::Deserialize; @@ -372,41 +373,6 @@ fn parse_time_span_for_config(config_name: &str, span: &str) -> CargoResult Option { - let Some(right_i) = span.find(|c: char| !c.is_ascii_digit()) else { - return None; - }; - let (left, mut right) = span.split_at(right_i); - if right.starts_with(' ') { - right = &right[1..]; - } - let count: u64 = left.parse().ok()?; - 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, - }; - Some(Duration::from_secs(factor * count)) -} - -/// Parses a time span string. -pub fn parse_time_span(span: &str) -> CargoResult { - maybe_parse_time_span(span).ok_or_else(|| { - format_err!( - "expected a value of the form \ - \"N seconds/minutes/days/weeks/months\", got: {span:?}" - ) - }) -} - /// Parses a file size using metric or IEC units. pub fn parse_human_size(input: &str) -> CargoResult { let re = regex::Regex::new(r"(?i)^([0-9]+(\.[0-9])?) ?(b|kb|mb|gb|kib|mib|gib)?$").unwrap(); @@ -441,6 +407,7 @@ pub fn parse_human_size(input: &str) -> CargoResult { #[cfg(test)] mod tests { + use crate::util::time_span::maybe_parse_time_span; use super::*; #[test] fn time_spans() { diff --git a/src/cargo/util/mod.rs b/src/cargo/util/mod.rs index a49ed84b6dd..3254deb1f7c 100644 --- a/src/cargo/util/mod.rs +++ b/src/cargo/util/mod.rs @@ -69,6 +69,7 @@ pub mod rustc; mod semver_eval_ext; mod semver_ext; pub mod sqlite; +pub mod time_span; pub mod toml; pub mod toml_mut; mod unhashed; diff --git a/src/cargo/util/time_span.rs b/src/cargo/util/time_span.rs new file mode 100644 index 00000000000..5b635683811 --- /dev/null +++ b/src/cargo/util/time_span.rs @@ -0,0 +1,38 @@ +use std::time::Duration; + +use crate::CargoResult; + +/// Parses a time span string. +pub fn parse_time_span(span: &str) -> CargoResult { + maybe_parse_time_span(span).ok_or_else(|| { + anyhow::format_err!( + "expected a value of the form \ + \"N seconds/minutes/hours/days/weeks/months\", got: {span:?}" + ) + }) +} + +/// Parses a time span string. +/// +/// Returns None if the value is not valid. See [`parse_time_span`] if you +/// need a variant that generates an error message. +pub fn maybe_parse_time_span(span: &str) -> Option { + let Some(right_i) = span.find(|c: char| !c.is_ascii_digit()) else { + return None; + }; + let (left, mut right) = span.split_at(right_i); + if right.starts_with(' ') { + right = &right[1..]; + } + let count: u64 = left.parse().ok()?; + 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, + }; + Some(Duration::from_secs(factor * count)) +} From b4644e9709cc11b9b4927b48b1d97df06cc8556f Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Wed, 13 May 2026 19:57:11 -0400 Subject: [PATCH 02/11] test(util): extract parse_time_span tests --- src/cargo/core/gc.rs | 24 ------------------------ src/cargo/util/time_span.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/cargo/core/gc.rs b/src/cargo/core/gc.rs index 0a7a30c239c..29a3fe7a687 100644 --- a/src/cargo/core/gc.rs +++ b/src/cargo/core/gc.rs @@ -407,20 +407,10 @@ pub fn parse_human_size(input: &str) -> CargoResult { #[cfg(test)] mod tests { - use crate::util::time_span::maybe_parse_time_span; use super::*; #[test] fn time_spans() { let d = |x| Some(Duration::from_secs(x)); - assert_eq!(maybe_parse_time_span("0 seconds"), d(0)); - assert_eq!(maybe_parse_time_span("1second"), d(1)); - assert_eq!(maybe_parse_time_span("23 seconds"), d(23)); - assert_eq!(maybe_parse_time_span("5 minutes"), d(60 * 5)); - assert_eq!(maybe_parse_time_span("2 hours"), d(60 * 60 * 2)); - assert_eq!(maybe_parse_time_span("1 day"), d(60 * 60 * 24)); - assert_eq!(maybe_parse_time_span("2 weeks"), d(60 * 60 * 24 * 14)); - assert_eq!(maybe_parse_time_span("6 months"), d(2_629_746 * 6)); - assert_eq!(parse_frequency("5 seconds").unwrap(), d(5)); assert_eq!(parse_frequency("always").unwrap(), d(0)); assert_eq!(parse_frequency("never").unwrap(), None); @@ -428,20 +418,6 @@ mod tests { #[test] fn time_span_errors() { - assert_eq!(maybe_parse_time_span(""), None); - assert_eq!(maybe_parse_time_span("1"), None); - assert_eq!(maybe_parse_time_span("second"), None); - assert_eq!(maybe_parse_time_span("+2 seconds"), None); - assert_eq!(maybe_parse_time_span("day"), None); - assert_eq!(maybe_parse_time_span("-1 days"), None); - assert_eq!(maybe_parse_time_span("1.5 days"), None); - assert_eq!(maybe_parse_time_span("1 dayz"), None); - assert_eq!(maybe_parse_time_span("always"), None); - assert_eq!(maybe_parse_time_span("never"), None); - assert_eq!(maybe_parse_time_span("1 day "), None); - assert_eq!(maybe_parse_time_span(" 1 day"), None); - assert_eq!(maybe_parse_time_span("1 second"), None); - let e = parse_time_span_for_config("cache.global-clean.max-src-age", "-1 days").unwrap_err(); assert_eq!( diff --git a/src/cargo/util/time_span.rs b/src/cargo/util/time_span.rs index 5b635683811..8ef61ed1aa9 100644 --- a/src/cargo/util/time_span.rs +++ b/src/cargo/util/time_span.rs @@ -36,3 +36,38 @@ pub fn maybe_parse_time_span(span: &str) -> Option { }; Some(Duration::from_secs(factor * count)) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn time_spans() { + let d = |x| Some(Duration::from_secs(x)); + assert_eq!(maybe_parse_time_span("0 seconds"), d(0)); + assert_eq!(maybe_parse_time_span("1second"), d(1)); + assert_eq!(maybe_parse_time_span("23 seconds"), d(23)); + assert_eq!(maybe_parse_time_span("5 minutes"), d(60 * 5)); + assert_eq!(maybe_parse_time_span("2 hours"), d(60 * 60 * 2)); + assert_eq!(maybe_parse_time_span("1 day"), d(60 * 60 * 24)); + assert_eq!(maybe_parse_time_span("2 weeks"), d(60 * 60 * 24 * 14)); + assert_eq!(maybe_parse_time_span("6 months"), d(2_629_746 * 6)); + } + + #[test] + fn time_span_errors() { + assert_eq!(maybe_parse_time_span(""), None); + assert_eq!(maybe_parse_time_span("1"), None); + assert_eq!(maybe_parse_time_span("second"), None); + assert_eq!(maybe_parse_time_span("+2 seconds"), None); + assert_eq!(maybe_parse_time_span("day"), None); + assert_eq!(maybe_parse_time_span("-1 days"), None); + assert_eq!(maybe_parse_time_span("1.5 days"), None); + assert_eq!(maybe_parse_time_span("1 dayz"), None); + assert_eq!(maybe_parse_time_span("always"), None); + assert_eq!(maybe_parse_time_span("never"), None); + assert_eq!(maybe_parse_time_span("1 day "), None); + assert_eq!(maybe_parse_time_span(" 1 day"), None); + assert_eq!(maybe_parse_time_span("1 second"), None); + } +} From 8e732046b28e44d71cb2fddcdc5b365ef25ca388 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Mon, 18 May 2026 08:44:39 +0200 Subject: [PATCH 03/11] refactor: drop `yanked_whitelist` from source loading Most call sites constructed with an empty allowlist, and `PackageRegistry::load` was the only place that passed a real list so we do a post construction after PackageRegistry construction in `load`. The parameter was only load-bearing for `PackageRegistry`. --- src/cargo/core/compiler/future_incompat.rs | 2 +- src/cargo/core/registry.rs | 8 ++++- src/cargo/core/source_id.rs | 23 ++++--------- src/cargo/ops/cargo_install.rs | 6 ++-- src/cargo/ops/registry/mod.rs | 10 +++--- src/cargo/ops/registry/publish.rs | 3 +- src/cargo/ops/vendor.rs | 6 ++-- src/cargo/sources/config.rs | 39 ++++++++-------------- src/cargo/sources/registry/mod.rs | 19 ++--------- 9 files changed, 43 insertions(+), 73 deletions(-) diff --git a/src/cargo/core/compiler/future_incompat.rs b/src/cargo/core/compiler/future_incompat.rs index 0df342b4b6c..d8c70ae61d2 100644 --- a/src/cargo/core/compiler/future_incompat.rs +++ b/src/cargo/core/compiler/future_incompat.rs @@ -319,7 +319,7 @@ fn get_updates(ws: &Workspace<'_>, package_ids: &BTreeSet) -> Option< let sources: HashMap<_, _> = source_ids .into_iter() .filter_map(|sid| { - let source = map.load(sid, &HashSet::new()).ok()?; + let source = map.load(sid).ok()?; Some((sid, source)) }) .collect(); diff --git a/src/cargo/core/registry.rs b/src/cargo/core/registry.rs index 055b3b63ff3..60fa0badfd2 100644 --- a/src/cargo/core/registry.rs +++ b/src/cargo/core/registry.rs @@ -533,10 +533,16 @@ impl<'gctx> PackageRegistry<'gctx> { debug!("loading source {}", source_id); let source = self .source_config - .load(source_id, &self.yanked_whitelist.borrow()) + .load(source_id) .with_context(|| format!("unable to update {}", source_id))?; assert_eq!(source.source_id(), source_id); + let yanked_whitelist = self.yanked_whitelist.borrow(); + if !yanked_whitelist.is_empty() { + let pkgs: Vec<_> = yanked_whitelist.iter().copied().collect(); + source.add_to_yanked_whitelist(&pkgs); + } + if kind == Kind::Override { self.overrides.borrow_mut().push(source_id); } diff --git a/src/cargo/core/source_id.rs b/src/cargo/core/source_id.rs index dade72b5a4b..0d27e3b5441 100644 --- a/src/cargo/core/source_id.rs +++ b/src/cargo/core/source_id.rs @@ -1,5 +1,4 @@ use crate::core::GitReference; -use crate::core::PackageId; use crate::core::SourceKind; use crate::sources::registry::CRATES_IO_HTTP_INDEX; use crate::sources::source::Source; @@ -389,12 +388,9 @@ impl SourceId { /// Creates an implementation of `Source` corresponding to this ID. /// - /// * `yanked_whitelist` --- Packages allowed to be used, even if they are yanked. - pub fn load<'a>( - self, - gctx: &'a GlobalContext, - yanked_whitelist: &HashSet, - ) -> CargoResult> { + /// To allow yanked packages through queries, + /// call [`Source::add_to_yanked_whitelist`] on the returned source. + pub fn load<'a>(self, gctx: &'a GlobalContext) -> CargoResult> { trace!("loading SourceId; {}", self); match self.inner.kind { SourceKind::Git(..) => Ok(Box::new(GitSource::new(self, gctx)?)), @@ -409,21 +405,16 @@ impl SourceId { } Ok(Box::new(PathSource::new(&path, self, gctx))) } - SourceKind::Registry | SourceKind::SparseRegistry => Ok(Box::new( - RegistrySource::remote(self, yanked_whitelist, gctx)?, - )), + SourceKind::Registry | SourceKind::SparseRegistry => { + Ok(Box::new(RegistrySource::remote(self, gctx)?)) + } SourceKind::LocalRegistry => { let path = self .inner .url .to_file_path() .expect("path sources cannot be remote"); - Ok(Box::new(RegistrySource::local( - self, - &path, - yanked_whitelist, - gctx, - ))) + Ok(Box::new(RegistrySource::local(self, &path, gctx))) } SourceKind::Directory => { let path = self diff --git a/src/cargo/ops/cargo_install.rs b/src/cargo/ops/cargo_install.rs index 14c22a2201c..0cdba3aa558 100644 --- a/src/cargo/ops/cargo_install.rs +++ b/src/cargo/ops/cargo_install.rs @@ -1,4 +1,6 @@ -use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::{env, fmt, fs}; @@ -184,7 +186,7 @@ impl<'gctx> InstallablePackage<'gctx> { current_rust_version, )? } else if let Some(dep) = dep { - let mut source = map.load(source_id, &HashSet::new())?; + let mut source = map.load(source_id)?; if let Ok(Some(pkg)) = installed_exact_package( dep.clone(), &mut *source, diff --git a/src/cargo/ops/registry/mod.rs b/src/cargo/ops/registry/mod.rs index 62320da2527..cc63efb2df7 100644 --- a/src/cargo/ops/registry/mod.rs +++ b/src/cargo/ops/registry/mod.rs @@ -135,7 +135,7 @@ fn registry<'gctx>( auth::cache_token_from_commandline(gctx, &source_ids.original, token); } - let src = RegistrySource::remote(source_ids.replacement, &HashSet::new(), gctx)?; + let src = RegistrySource::remote(source_ids.replacement, gctx)?; let cfg = { let _lock = gctx.acquire_package_cache_lock(CacheLockMode::DownloadExclusive)?; // Only update the index if `force_update` is set. @@ -265,11 +265,9 @@ fn get_replacement_source_ids( sid: SourceId, ) -> CargoResult<(SourceId, SourceId)> { let builtin_replacement_sid = SourceConfigMap::empty(gctx)? - .load(sid, &HashSet::new())? - .replaced_source_id(); - let replacement_sid = SourceConfigMap::new(gctx)? - .load(sid, &HashSet::new())? + .load(sid)? .replaced_source_id(); + let replacement_sid = SourceConfigMap::new(gctx)?.load(sid)?.replaced_source_id(); Ok((builtin_replacement_sid, replacement_sid)) } @@ -279,7 +277,7 @@ fn is_replacement_for_package_source( package_source_id: SourceId, ) -> CargoResult { let pkg_source_replacement_sid = SourceConfigMap::new(gctx)? - .load(package_source_id, &HashSet::new())? + .load(package_source_id)? .replaced_source_id(); Ok(pkg_source_replacement_sid == sid) } diff --git a/src/cargo/ops/registry/publish.rs b/src/cargo/ops/registry/publish.rs index 48908d02e73..599b3be5453 100644 --- a/src/cargo/ops/registry/publish.rs +++ b/src/cargo/ops/registry/publish.rs @@ -5,7 +5,6 @@ use std::collections::BTreeMap; use std::collections::BTreeSet; use std::collections::HashMap; -use std::collections::HashSet; use std::fs::File; use std::io::Seek; use std::io::SeekFrom; @@ -390,7 +389,7 @@ fn wait_for_any_publish_confirmation( pkgs: &BTreeSet, timeout: Duration, ) -> CargoResult> { - let mut source = SourceConfigMap::empty(gctx)?.load(registry_src, &HashSet::new())?; + let mut source = SourceConfigMap::empty(gctx)?.load(registry_src)?; // Disable the source's built-in progress bars. Repeatedly showing a bunch // of independent progress bars can be a little confusing. There is an // overall progress bar managed here. diff --git a/src/cargo/ops/vendor.rs b/src/cargo/ops/vendor.rs index 87080ff0862..41f352661a5 100644 --- a/src/cargo/ops/vendor.rs +++ b/src/cargo/ops/vendor.rs @@ -110,7 +110,7 @@ impl SourceReplacementCache<'_> { match self.cache.entry(id) { Entry::Occupied(e) => Ok(e.get().clone()), Entry::Vacant(e) => { - let replaced = self.map.load(id, &HashSet::new())?.replaced_source_id(); + let replaced = self.map.load(id)?.replaced_source_id(); Ok(e.insert(replaced).clone()) } } @@ -246,11 +246,11 @@ fn sync( // we'll do a direct extraction into the vendor directory. let registry = match sid.kind() { SourceKind::Registry | SourceKind::SparseRegistry => { - RegistrySource::remote(sid, &Default::default(), gctx)? + RegistrySource::remote(sid, gctx)? } SourceKind::LocalRegistry => { let path = sid.url().to_file_path().expect("local path"); - RegistrySource::local(sid, &path, &Default::default(), gctx) + RegistrySource::local(sid, &path, gctx) } _ => unreachable!("not registry source: {sid}"), }; diff --git a/src/cargo/sources/config.rs b/src/cargo/sources/config.rs index 9dbfe56f2e4..3b77ab4d5d1 100644 --- a/src/cargo/sources/config.rs +++ b/src/cargo/sources/config.rs @@ -4,15 +4,18 @@ //! structure usable by Cargo itself. Currently, this is primarily used to map //! sources to one another via the `replace-with` key in `.cargo/config`. -use crate::core::{GitReference, PackageId, SourceId}; +use std::collections::HashMap; + +use crate::core::GitReference; +use crate::core::SourceId; use crate::sources::overlay::DependencyConfusionThreatOverlaySource; use crate::sources::source::Source; use crate::sources::{CRATES_IO_REGISTRY, ReplacedSource}; use crate::util::context::{self, ConfigRelativePath, OptValue}; use crate::util::errors::CargoResult; use crate::util::{GlobalContext, IntoUrl}; + use anyhow::{Context as _, bail}; -use std::collections::{HashMap, HashSet}; use tracing::debug; use url::Url; @@ -142,17 +145,11 @@ impl<'gctx> SourceConfigMap<'gctx> { } /// Gets the [`Source`] for a given [`SourceId`]. - /// - /// * `yanked_whitelist` --- Packages allowed to be used, even if they are yanked. - pub fn load( - &self, - id: SourceId, - yanked_whitelist: &HashSet, - ) -> CargoResult> { + pub fn load(&self, id: SourceId) -> CargoResult> { debug!("loading: {}", id); let Some(mut name) = self.id2name.get(&id) else { - return self.load_overlaid(id, yanked_whitelist); + return self.load_overlaid(id); }; let mut cfg_loc = ""; let orig_name = name; @@ -177,7 +174,7 @@ impl<'gctx> SourceConfigMap<'gctx> { name = s; cfg_loc = c; } - None if id == cfg.id => return self.load_overlaid(id, yanked_whitelist), + None if id == cfg.id => return self.load_overlaid(id), None => { break cfg.id.with_precise_from(id); } @@ -194,14 +191,8 @@ impl<'gctx> SourceConfigMap<'gctx> { } }; - let new_src = self.load_overlaid( - new_id, - &yanked_whitelist - .iter() - .map(|p| p.map_source(id, new_id)) - .collect(), - )?; - let old_src = id.load(self.gctx, yanked_whitelist)?; + let new_src = self.load_overlaid(new_id)?; + let old_src = id.load(self.gctx)?; if !new_src.supports_checksums() && old_src.supports_checksums() { bail!( "\ @@ -232,14 +223,10 @@ restore the source replacement configuration to continue the build } /// Gets the [`Source`] for a given [`SourceId`] without performing any source replacement. - fn load_overlaid( - &self, - id: SourceId, - yanked_whitelist: &HashSet, - ) -> CargoResult> { - let src = id.load(self.gctx, yanked_whitelist)?; + fn load_overlaid(&self, id: SourceId) -> CargoResult> { + let src = id.load(self.gctx)?; if let Some(overlay_id) = self.overlays.get(&id) { - let overlay = overlay_id.load(self.gctx(), yanked_whitelist)?; + let overlay = overlay_id.load(self.gctx())?; Ok(Box::new(DependencyConfusionThreatOverlaySource::new( overlay, src, ))) diff --git a/src/cargo/sources/registry/mod.rs b/src/cargo/sources/registry/mod.rs index 441278fc058..bd38dc1c195 100644 --- a/src/cargo/sources/registry/mod.rs +++ b/src/cargo/sources/registry/mod.rs @@ -480,11 +480,8 @@ impl<'gctx> RegistrySource<'gctx> { /// Creates a [`Source`] of a "remote" registry. /// It could be either an HTTP-based [`http_remote::HttpRegistry`] or /// a Git-based [`remote::RemoteRegistry`]. - /// - /// * `yanked_whitelist` --- Packages allowed to be used, even if they are yanked. pub fn remote( source_id: SourceId, - yanked_whitelist: &HashSet, gctx: &'gctx GlobalContext, ) -> CargoResult> { assert!(source_id.is_remote_registry()); @@ -501,28 +498,20 @@ impl<'gctx> RegistrySource<'gctx> { Box::new(remote::RemoteRegistry::new(source_id, gctx, &name)) as Box<_> }; - Ok(RegistrySource::new( - source_id, - gctx, - &name, - ops, - yanked_whitelist, - )) + Ok(RegistrySource::new(source_id, gctx, &name, ops)) } /// Creates a [`Source`] of a local registry, with [`local::LocalRegistry`] under the hood. /// /// * `path` --- The root path of a local registry on the file system. - /// * `yanked_whitelist` --- Packages allowed to be used, even if they are yanked. pub fn local( source_id: SourceId, path: &Path, - yanked_whitelist: &HashSet, gctx: &'gctx GlobalContext, ) -> RegistrySource<'gctx> { let name = short_name(source_id, false); let ops = local::LocalRegistry::new(path, gctx, &name); - RegistrySource::new(source_id, gctx, &name, Box::new(ops), yanked_whitelist) + RegistrySource::new(source_id, gctx, &name, Box::new(ops)) } /// Creates a source of a registry. This is a inner helper function. @@ -530,13 +519,11 @@ impl<'gctx> RegistrySource<'gctx> { /// * `name` --- Name of a path segment which may affect where `.crate` /// tarballs, the registry index and cache are stored. Expect to be unique. /// * `ops` --- The underlying [`RegistryData`] type. - /// * `yanked_whitelist` --- Packages allowed to be used, even if they are yanked. fn new( source_id: SourceId, gctx: &'gctx GlobalContext, name: &str, ops: Box, - yanked_whitelist: &HashSet, ) -> RegistrySource<'gctx> { // Before starting to work on the registry, make sure that // `/registry` is marked as excluded from indexing and @@ -559,7 +546,7 @@ impl<'gctx> RegistrySource<'gctx> { gctx, source_id, index: index::RegistryIndex::new(source_id, ops.index_path(), gctx), - yanked_whitelist: RefCell::new(yanked_whitelist.clone()), + yanked_whitelist: RefCell::new(HashSet::new()), ops, selected_precise_yanked: RefCell::new(HashSet::new()), } From c9e126647e0660321ab7729b51f5a93e63ab18f1 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Tue, 19 May 2026 01:03:10 +0200 Subject: [PATCH 04/11] refactor(gctx): a global invocation time So we have a global shaed invocation for `-Zbuild-analysis` and other upcoming features like `-Zmin-publish-age`. --- src/cargo/util/context/mod.rs | 23 +++++++++++++++++++++++ src/cargo/util/logger.rs | 6 +++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index bcf095f0154..0dc65ae7b89 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -252,6 +252,10 @@ pub struct GlobalContext { cache_rustc_info: bool, /// Creation time of this config, used to output the total build time creation_time: Instant, + /// Wall-clock time of this cargo invocation. + /// + /// Currently used as the reference time for `min-publish-age` and `-Zbuild-analysis`. + invocation_time: jiff::Timestamp, /// Target Directory via resolved Cli parameter target_dir: Option, /// Environment variable snapshot. @@ -358,6 +362,15 @@ impl GlobalContext { _ => true, }; + #[expect( + clippy::disallowed_methods, + reason = "testing only, no reason for config support" + )] + let invocation_time = match env::var("__CARGO_TEST_INVOCATION_TIME") { + Ok(now) => now.parse().unwrap(), + Err(_) => jiff::Timestamp::now(), + }; + GlobalContext { home_path: Filesystem::new(homedir), shell: Mutex::new(shell), @@ -379,6 +392,7 @@ impl GlobalContext { crates_io_source_id: Default::default(), cache_rustc_info, creation_time: Instant::now(), + invocation_time, target_dir: None, env, updated_sources: Default::default(), @@ -2058,6 +2072,15 @@ impl GlobalContext { self.creation_time } + /// Returns the wall-clock time of this cargo invocation. + /// + /// See the [`invocation_time`] field doc for details. + /// + /// [`invocation_time`]: GlobalContext::invocation_time + pub fn invocation_time(&self) -> jiff::Timestamp { + self.invocation_time + } + /// Retrieves a config variable. /// /// This supports most serde `Deserialize` types. Examples: diff --git a/src/cargo/util/logger.rs b/src/cargo/util/logger.rs index 0ced79778ef..10665d2f5ec 100644 --- a/src/cargo/util/logger.rs +++ b/src/cargo/util/logger.rs @@ -124,7 +124,7 @@ impl BuildLogger { /// Generates a unique run ID. pub fn generate_run_id(ws: &Workspace<'_>) -> RunId { - RunId::new(&ws.root()) + RunId::new(&ws.root(), ws.gctx().invocation_time()) } /// Returns the run ID for this build session. @@ -166,9 +166,9 @@ pub struct RunId { impl RunId { const FORMAT: &str = "%Y%m%dT%H%M%S%3fZ"; - pub fn new(h: &H) -> RunId { + pub fn new(h: &H, timestamp: jiff::Timestamp) -> RunId { RunId { - timestamp: jiff::Timestamp::now(), + timestamp, hash: short_hash(h), } } From e26bc255cd1b8706be892f2cca497e327df1a5ef Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 7 May 2026 14:43:29 -0400 Subject: [PATCH 05/11] test: `-Zmin-publish-age` feature gate --- tests/testsuite/main.rs | 1 + tests/testsuite/min_publish_age.rs | 172 +++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 tests/testsuite/min_publish_age.rs diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index 16971afb716..cfe0335dc4e 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -134,6 +134,7 @@ mod message_format; mod messages; mod metabuild; mod metadata; +mod min_publish_age; mod minimal_versions; mod multitarget; mod net_config; diff --git a/tests/testsuite/min_publish_age.rs b/tests/testsuite/min_publish_age.rs new file mode 100644 index 00000000000..36c1b9e97ea --- /dev/null +++ b/tests/testsuite/min_publish_age.rs @@ -0,0 +1,172 @@ +//! Tests for the `min-publish-age` feature (RFC 3923). + +use cargo_test_support::compare::assert_e2e; +use cargo_test_support::project; +use cargo_test_support::registry::Package; +use cargo_test_support::str; + +use crate::prelude::*; + +/// Mocked "now" for deterministic publish-age comparisons. +const NOW: &str = "2006-08-08T00:00:00Z"; + +fn setup_packages() { + // 14 days before `NOW` + Package::new("bar", "1.0.0") + .pubtime("2006-07-25T00:00:00Z") + .publish(); + // 2 days before `NOW` + Package::new("bar", "1.1.0") + .pubtime("2006-08-06T00:00:00Z") + .publish(); +} + +#[cargo_test] +fn feature_gated() { + setup_packages(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + edition = "2021" + + [dependencies] + bar = "1.0" + "#, + ) + .file( + ".cargo/config.toml", + r#" + [registry] + global-min-publish-age = "7 days" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile") + .masquerade_as_nightly_cargo(&["min-publish-age"]) + .env("__CARGO_TEST_INVOCATION_TIME", NOW) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version + +"#]]) + .run(); + + assert_e2e().eq( + p.read_lockfile(), + str![[r##" +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bar" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2216902cacb5611f65e00da63d917a6b9f4dbb10ddc39ee24c853322ab44b041" + +[[package]] +name = "foo" +version = "0.0.0" +dependencies = [ + "bar", +] + +"##]], + ); +} + +#[cargo_test] +fn feature_gated_env() { + setup_packages(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + edition = "2021" + + [dependencies] + bar = "1.0" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile") + .env("CARGO_REGISTRY_GLOBAL_MIN_PUBLISH_AGE", "7 days") + .masquerade_as_nightly_cargo(&["min-publish-age"]) + .env("__CARGO_TEST_INVOCATION_TIME", NOW) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version + +"#]]) + .run(); + + assert_e2e().eq( + p.read_lockfile(), + str![[r##" +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bar" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2216902cacb5611f65e00da63d917a6b9f4dbb10ddc39ee24c853322ab44b041" + +[[package]] +name = "foo" +version = "0.0.0" +dependencies = [ + "bar", +] + +"##]], + ); +} + +#[cargo_test] +fn warns_on_resolver_config_without_flag() { + setup_packages(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + edition = "2021" + + [dependencies] + bar = "1.0" + "#, + ) + .file( + ".cargo/config.toml", + r#" + [resolver] + incompatible-publish-age = "deny" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile") + .with_stderr_data(str![[r#" +[WARNING] unused config key `resolver.incompatible-publish-age` in `[ROOT]/foo/.cargo/config.toml` +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version + +"#]]) + .run(); +} From 13551852155c26512fee7aa8bd8373f8b2e054bf Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 7 May 2026 14:43:29 -0400 Subject: [PATCH 06/11] feat: `-Zmin-publish-age` feature gate --- src/cargo/core/features.rs | 2 + tests/testsuite/cargo/z_help/stdout.term.svg | 60 ++++++++++---------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index f7c8a75bea4..af8fbb8b2cf 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -887,6 +887,7 @@ unstable_cli_options!( gitoxide: Option = ("Use gitoxide for the given git interactions, or all of them if no argument is given"), 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"), @@ -1431,6 +1432,7 @@ 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)?, "next-lockfile-bump" => self.next_lockfile_bump = parse_empty(k, v)?, "minimal-versions" => self.minimal_versions = parse_empty(k, v)?, "msrv-policy" => self.msrv_policy = parse_empty(k, v)?, diff --git a/tests/testsuite/cargo/z_help/stdout.term.svg b/tests/testsuite/cargo/z_help/stdout.term.svg index b5725124eba..c79aee4d39e 100644 --- a/tests/testsuite/cargo/z_help/stdout.term.svg +++ b/tests/testsuite/cargo/z_help/stdout.term.svg @@ -1,4 +1,4 @@ - +