Skip to content

Commit edd3172

Browse files
committed
fix: prefer using patched dependencies in lockfile
We've tracked patched depencencies in Cargo.lock with the `patched+` protocol. However, we don't really make use of it when rebuilding from a lockfile. This PR fixes that. You can see the test cases now won't update registry becaseu it starts resuing locked patch dependencies.
1 parent e78ccb1 commit edd3172

File tree

4 files changed

+135
-14
lines changed

4 files changed

+135
-14
lines changed

crates/cargo-util-schemas/src/core/source_kind.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@ impl PatchInfo {
288288
PatchInfo::Deferred { patches } => patches.as_slice(),
289289
}
290290
}
291+
292+
pub fn is_resolved(&self) -> bool {
293+
matches!(self, PatchInfo::Resolved { .. })
294+
}
291295
}
292296

293297
/// A [`PatchInfo`] that can be `Display`ed as URL query string.

src/cargo/core/registry.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ pub type PatchDependency<'a> = (&'a Dependency, Option<LockedPatchDependency>);
180180

181181
/// Argument to [`PackageRegistry::patch`] which is information about a `[patch]`
182182
/// directive that we found in a lockfile, if present.
183+
#[derive(Debug)]
183184
pub struct LockedPatchDependency {
184185
/// The original `Dependency` directive, except "locked" so it's version
185186
/// requirement is Locked to `foo` and its `SourceId` has a "precise" listed.

src/cargo/ops/resolve.rs

Lines changed: 130 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ pub fn resolve_with_previous<'gctx>(
337337
register_patches: bool,
338338
) -> CargoResult<Resolve> {
339339
let mut patches = if register_patches {
340-
Some(PatchEntries::from_workspace(ws)?)
340+
Some(PatchEntries::from_workspace(ws, previous)?)
341341
} else {
342342
None
343343
};
@@ -364,7 +364,7 @@ pub fn resolve_with_previous<'gctx>(
364364

365365
if let Some(patches) = patches.as_ref() {
366366
emit_warnings_of_unused_patches(ws, &resolved, registry)?;
367-
emit_warnings_of_unresolved_patches(ws, patches)?;
367+
emit_warnings_of_unresolved_patches(ws, &resolved, patches)?;
368368
}
369369

370370
return Ok(resolved);
@@ -843,16 +843,45 @@ fn emit_warnings_of_unused_patches(
843843
/// [`emit_warnings_of_unused_patches`] which requires summaries.
844844
fn emit_warnings_of_unresolved_patches(
845845
ws: &Workspace<'_>,
846+
resolve: &Resolve,
846847
patches: &PatchEntries,
847848
) -> CargoResult<()> {
849+
let resolved_patched_pkg_ids: Vec<_> = resolve
850+
.iter()
851+
.filter(|pkg_id| pkg_id.source_id().is_patched())
852+
.collect();
848853
for (url, patches) in &patches.unresolved {
854+
let canonical_url = CanonicalUrl::new(url)?;
849855
for patch in patches {
856+
let maybe_already_used = resolved_patched_pkg_ids
857+
.iter()
858+
.filter(|id| {
859+
let source_id = id.source_id();
860+
let url = source_id.canonical_url().raw_canonicalized_url();
861+
// To check if the patch is for the same source,
862+
// we need strip `patched` prefix here,
863+
// as CanonicalUrl preserves that.
864+
let url = url.as_str().strip_prefix("patched+").unwrap();
865+
url == canonical_url.raw_canonicalized_url().as_str()
866+
})
867+
.any(|id| patch.matches_ignoring_source(*id));
868+
869+
if maybe_already_used {
870+
continue;
871+
}
872+
850873
let url = if url.as_str() == CRATES_IO_INDEX {
851874
"crates-io"
852875
} else {
853876
url.as_str()
854877
};
855-
let msg = format!("unused patch for `{}` in `{url}`", patch.name_in_toml());
878+
879+
let msg = format!(
880+
"unused patch `{}@{}` {} in `{url}`",
881+
patch.name_in_toml(),
882+
patch.version_req(),
883+
patch.source_id(),
884+
);
856885
ws.gctx().shell().warn(msg)?;
857886
}
858887
}
@@ -880,8 +909,8 @@ fn register_patch_entries(
880909

881910
registry.clear_patch();
882911

883-
for (url, patches) in patches {
884-
for patch in patches {
912+
for (url, patches) in patches.iter() {
913+
for patch in patches.iter() {
885914
version_prefs.prefer_dependency(patch.clone());
886915
}
887916
let Some(previous) = previous else {
@@ -900,7 +929,7 @@ fn register_patch_entries(
900929
// previous resolve graph, which is primarily what's done here to
901930
// build the `registrations` list.
902931
let mut registrations = Vec::new();
903-
for dep in patches {
932+
for dep in patches.iter() {
904933
let candidates = || {
905934
previous
906935
.iter()
@@ -1029,14 +1058,16 @@ struct PatchEntries {
10291058
/// resolved dependency graph.
10301059
unresolved: Vec<(Url, Vec<Dependency>)>,
10311060

1061+
from_previous: HashMap<Url, Vec<LockedPatchDependency>>,
1062+
10321063
/// How many times [`PatchEntries::resolves`] has been called.
10331064
resolve_times: u32,
10341065
}
10351066

10361067
impl PatchEntries {
10371068
const RESOLVE_TIMES_LIMIT: u32 = 20;
10381069

1039-
fn from_workspace(ws: &Workspace<'_>) -> CargoResult<PatchEntries> {
1070+
fn from_workspace(ws: &Workspace<'_>, previous: Option<&Resolve>) -> CargoResult<PatchEntries> {
10401071
let mut ready_patches = HashMap::new();
10411072
let mut unresolved = Vec::new();
10421073
for (url, patches) in ws.root_patch()?.into_iter() {
@@ -1050,9 +1081,60 @@ impl PatchEntries {
10501081
unresolved.push((url, file_patches));
10511082
}
10521083
}
1084+
1085+
let mut from_previous = HashMap::<Url, Vec<_>>::new();
1086+
let resolved_patched_pkg_ids: Vec<_> = previous
1087+
.iter()
1088+
.flat_map(|previous| {
1089+
previous
1090+
.iter()
1091+
.chain(previous.unused_patches().iter().cloned())
1092+
})
1093+
.filter(|pkg_id| {
1094+
pkg_id
1095+
.source_id()
1096+
.patch_info()
1097+
.map(|info| info.is_resolved())
1098+
.unwrap_or_default()
1099+
})
1100+
.collect();
1101+
for (url, patches) in &unresolved {
1102+
let mut ready_patches = Vec::new();
1103+
let canonical_url = CanonicalUrl::new(url)?;
1104+
for patch in patches {
1105+
for pkg_id in resolved_patched_pkg_ids.iter().filter(|id| {
1106+
let source_id = id.source_id();
1107+
let url = source_id.canonical_url().raw_canonicalized_url();
1108+
// To check if the patch is for the same source,
1109+
// we need strip `patched` prefix here,
1110+
// as CanonicalUrl preserves that.
1111+
let url = url.as_str().strip_prefix("patched+").unwrap();
1112+
url == canonical_url.raw_canonicalized_url().as_str()
1113+
}) {
1114+
if patch.matches_ignoring_source(*pkg_id) {
1115+
let mut patch = patch.clone();
1116+
patch.set_source_id(pkg_id.source_id());
1117+
patch.lock_to(*pkg_id);
1118+
ready_patches.push(LockedPatchDependency {
1119+
dependency: patch,
1120+
package_id: *pkg_id,
1121+
alt_package_id: None,
1122+
});
1123+
}
1124+
}
1125+
}
1126+
if !ready_patches.is_empty() {
1127+
from_previous
1128+
.entry(url.clone())
1129+
.or_default()
1130+
.extend(ready_patches);
1131+
}
1132+
}
1133+
10531134
Ok(PatchEntries {
10541135
ready_patches,
10551136
unresolved,
1137+
from_previous,
10561138
resolve_times: 0,
10571139
})
10581140
}
@@ -1102,6 +1184,16 @@ impl PatchEntries {
11021184
*unresolved = pending_patches;
11031185

11041186
if !ready_patches.is_empty() {
1187+
if let Some(from_previous) = self.from_previous.get_mut(url) {
1188+
for patch in &ready_patches {
1189+
if let Some(index) = from_previous
1190+
.iter()
1191+
.position(|locked| patch.matches_ignoring_source(locked.package_id))
1192+
{
1193+
from_previous.swap_remove(index);
1194+
}
1195+
}
1196+
}
11051197
self.ready_patches
11061198
.entry(url.clone())
11071199
.or_default()
@@ -1112,13 +1204,39 @@ impl PatchEntries {
11121204
self.resolve_times += 1;
11131205
Ok(has_new_patches)
11141206
}
1207+
1208+
fn iter(&self) -> Box<dyn Iterator<Item = (&Url, Deps<'_>)> + '_> {
1209+
if self.ready_patches.is_empty() {
1210+
Box::new(self.from_previous.iter().map(|(url, deps)| {
1211+
let deps = Deps {
1212+
deps: None,
1213+
locked_deps: Some(deps),
1214+
};
1215+
(url, deps)
1216+
}))
1217+
} else {
1218+
Box::new(self.ready_patches.iter().map(|(url, deps)| {
1219+
let deps = Deps {
1220+
deps: Some(deps),
1221+
locked_deps: self.from_previous.get(url),
1222+
};
1223+
(url, deps)
1224+
}))
1225+
}
1226+
}
11151227
}
11161228

1117-
impl<'a> IntoIterator for &'a PatchEntries {
1118-
type Item = (&'a Url, &'a Vec<Dependency>);
1119-
type IntoIter = std::collections::hash_map::Iter<'a, Url, Vec<Dependency>>;
1229+
struct Deps<'a> {
1230+
deps: Option<&'a Vec<Dependency>>,
1231+
locked_deps: Option<&'a Vec<LockedPatchDependency>>,
1232+
}
11201233

1121-
fn into_iter(self) -> Self::IntoIter {
1122-
self.ready_patches.iter()
1234+
impl<'a> Deps<'a> {
1235+
fn iter(&self) -> impl Iterator<Item = &'a Dependency> {
1236+
let locked_deps = self
1237+
.locked_deps
1238+
.into_iter()
1239+
.flat_map(|deps| deps.into_iter().map(|locked| &locked.dependency));
1240+
self.deps.into_iter().flatten().chain(locked_deps)
11231241
}
11241242
}

tests/testsuite/patch_files.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,6 @@ Hello, patched!
11241124
p.cargo("run -v")
11251125
.masquerade_as_nightly_cargo(&["patch-files"])
11261126
.with_stderr_data(str![[r#"
1127-
[UPDATING] `dummy-registry` index
11281127
[FRESH] bar v1.0.0 ([email protected] with 1 patch file)
11291128
[FRESH] foo v0.0.0 ([ROOT]/foo)
11301129
[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s
@@ -1177,7 +1176,6 @@ Hello, patched!
11771176
p.cargo("run")
11781177
.masquerade_as_nightly_cargo(&["patch-files"])
11791178
.with_stderr_data(str![[r#"
1180-
[UPDATING] `dummy-registry` index
11811179
[PATCHING] bar v1.0.0
11821180
[COMPILING] bar v1.0.0 ([email protected] with 1 patch file)
11831181
[COMPILING] foo v0.0.0 ([ROOT]/foo)

0 commit comments

Comments
 (0)