Skip to content

Commit 6109ea8

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 2e3cfd6 commit 6109ea8

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
@@ -334,7 +334,7 @@ pub fn resolve_with_previous<'gctx>(
334334
register_patches: bool,
335335
) -> CargoResult<Resolve> {
336336
let mut patches = if register_patches {
337-
Some(PatchEntries::from_workspace(ws)?)
337+
Some(PatchEntries::from_workspace(ws, previous)?)
338338
} else {
339339
None
340340
};
@@ -361,7 +361,7 @@ pub fn resolve_with_previous<'gctx>(
361361

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

367367
return Ok(resolved);
@@ -850,16 +850,45 @@ fn emit_warnings_of_unused_patches(
850850
/// [`emit_warnings_of_unused_patches`] which requires summaries.
851851
fn emit_warnings_of_unresolved_patches(
852852
ws: &Workspace<'_>,
853+
resolve: &Resolve,
853854
patches: &PatchEntries,
854855
) -> CargoResult<()> {
856+
let resolved_patched_pkg_ids: Vec<_> = resolve
857+
.iter()
858+
.filter(|pkg_id| pkg_id.source_id().is_patched())
859+
.collect();
855860
for (url, patches) in &patches.unresolved {
861+
let canonical_url = CanonicalUrl::new(url)?;
856862
for patch in patches {
863+
let maybe_already_used = resolved_patched_pkg_ids
864+
.iter()
865+
.filter(|id| {
866+
let source_id = id.source_id();
867+
let url = source_id.canonical_url().raw_canonicalized_url();
868+
// To check if the patch is for the same source,
869+
// we need strip `patched` prefix here,
870+
// as CanonicalUrl preserves that.
871+
let url = url.as_str().strip_prefix("patched+").unwrap();
872+
url == canonical_url.raw_canonicalized_url().as_str()
873+
})
874+
.any(|id| patch.matches_ignoring_source(*id));
875+
876+
if maybe_already_used {
877+
continue;
878+
}
879+
857880
let url = if url.as_str() == CRATES_IO_INDEX {
858881
"crates-io"
859882
} else {
860883
url.as_str()
861884
};
862-
let msg = format!("unused patch for `{}` in `{url}`", patch.name_in_toml());
885+
886+
let msg = format!(
887+
"unused patch `{}@{}` {} in `{url}`",
888+
patch.name_in_toml(),
889+
patch.version_req(),
890+
patch.source_id(),
891+
);
863892
ws.gctx().shell().warn(msg)?;
864893
}
865894
}
@@ -887,8 +916,8 @@ fn register_patch_entries(
887916

888917
registry.clear_patch();
889918

890-
for (url, patches) in patches {
891-
for patch in patches {
919+
for (url, patches) in patches.iter() {
920+
for patch in patches.iter() {
892921
version_prefs.prefer_dependency(patch.clone());
893922
}
894923
let Some(previous) = previous else {
@@ -907,7 +936,7 @@ fn register_patch_entries(
907936
// previous resolve graph, which is primarily what's done here to
908937
// build the `registrations` list.
909938
let mut registrations = Vec::new();
910-
for dep in patches {
939+
for dep in patches.iter() {
911940
let candidates = || {
912941
previous
913942
.iter()
@@ -1036,14 +1065,16 @@ struct PatchEntries {
10361065
/// resolved dependency graph.
10371066
unresolved: Vec<(Url, Vec<Dependency>)>,
10381067

1068+
from_previous: HashMap<Url, Vec<LockedPatchDependency>>,
1069+
10391070
/// How many times [`PatchEntries::requires_reresolve`] has been called.
10401071
resolve_times: u32,
10411072
}
10421073

10431074
impl PatchEntries {
10441075
const RESOLVE_TIMES_LIMIT: u32 = 20;
10451076

1046-
fn from_workspace(ws: &Workspace<'_>) -> CargoResult<PatchEntries> {
1077+
fn from_workspace(ws: &Workspace<'_>, previous: Option<&Resolve>) -> CargoResult<PatchEntries> {
10471078
let mut ready_patches = HashMap::new();
10481079
let mut unresolved = Vec::new();
10491080
for (url, patches) in ws.root_patch()?.into_iter() {
@@ -1057,9 +1088,60 @@ impl PatchEntries {
10571088
unresolved.push((url, file_patches));
10581089
}
10591090
}
1091+
1092+
let mut from_previous = HashMap::<Url, Vec<_>>::new();
1093+
let resolved_patched_pkg_ids: Vec<_> = previous
1094+
.iter()
1095+
.flat_map(|previous| {
1096+
previous
1097+
.iter()
1098+
.chain(previous.unused_patches().iter().cloned())
1099+
})
1100+
.filter(|pkg_id| {
1101+
pkg_id
1102+
.source_id()
1103+
.patch_info()
1104+
.map(|info| info.is_resolved())
1105+
.unwrap_or_default()
1106+
})
1107+
.collect();
1108+
for (url, patches) in &unresolved {
1109+
let mut ready_patches = Vec::new();
1110+
let canonical_url = CanonicalUrl::new(url)?;
1111+
for patch in patches {
1112+
for pkg_id in resolved_patched_pkg_ids.iter().filter(|id| {
1113+
let source_id = id.source_id();
1114+
let url = source_id.canonical_url().raw_canonicalized_url();
1115+
// To check if the patch is for the same source,
1116+
// we need strip `patched` prefix here,
1117+
// as CanonicalUrl preserves that.
1118+
let url = url.as_str().strip_prefix("patched+").unwrap();
1119+
url == canonical_url.raw_canonicalized_url().as_str()
1120+
}) {
1121+
if patch.matches_ignoring_source(*pkg_id) {
1122+
let mut patch = patch.clone();
1123+
patch.set_source_id(pkg_id.source_id());
1124+
patch.lock_to(*pkg_id);
1125+
ready_patches.push(LockedPatchDependency {
1126+
dependency: patch,
1127+
package_id: *pkg_id,
1128+
alt_package_id: None,
1129+
});
1130+
}
1131+
}
1132+
}
1133+
if !ready_patches.is_empty() {
1134+
from_previous
1135+
.entry(url.clone())
1136+
.or_default()
1137+
.extend(ready_patches);
1138+
}
1139+
}
1140+
10601141
Ok(PatchEntries {
10611142
ready_patches,
10621143
unresolved,
1144+
from_previous,
10631145
resolve_times: 0,
10641146
})
10651147
}
@@ -1109,6 +1191,16 @@ impl PatchEntries {
11091191
*unresolved = pending_patches;
11101192

11111193
if !ready_patches.is_empty() {
1194+
if let Some(from_previous) = self.from_previous.get_mut(url) {
1195+
for patch in &ready_patches {
1196+
if let Some(index) = from_previous
1197+
.iter()
1198+
.position(|locked| patch.matches_ignoring_source(locked.package_id))
1199+
{
1200+
from_previous.swap_remove(index);
1201+
}
1202+
}
1203+
}
11121204
self.ready_patches
11131205
.entry(url.clone())
11141206
.or_default()
@@ -1119,13 +1211,39 @@ impl PatchEntries {
11191211
self.resolve_times += 1;
11201212
Ok(has_new_patches)
11211213
}
1214+
1215+
fn iter(&self) -> Box<dyn Iterator<Item = (&Url, Deps<'_>)> + '_> {
1216+
if self.ready_patches.is_empty() {
1217+
Box::new(self.from_previous.iter().map(|(url, deps)| {
1218+
let deps = Deps {
1219+
deps: None,
1220+
locked_deps: Some(deps),
1221+
};
1222+
(url, deps)
1223+
}))
1224+
} else {
1225+
Box::new(self.ready_patches.iter().map(|(url, deps)| {
1226+
let deps = Deps {
1227+
deps: Some(deps),
1228+
locked_deps: self.from_previous.get(url),
1229+
};
1230+
(url, deps)
1231+
}))
1232+
}
1233+
}
11221234
}
11231235

1124-
impl<'a> IntoIterator for &'a PatchEntries {
1125-
type Item = (&'a Url, &'a Vec<Dependency>);
1126-
type IntoIter = std::collections::hash_map::Iter<'a, Url, Vec<Dependency>>;
1236+
struct Deps<'a> {
1237+
deps: Option<&'a Vec<Dependency>>,
1238+
locked_deps: Option<&'a Vec<LockedPatchDependency>>,
1239+
}
11271240

1128-
fn into_iter(self) -> Self::IntoIter {
1129-
self.ready_patches.iter()
1241+
impl<'a> Deps<'a> {
1242+
fn iter(&self) -> impl Iterator<Item = &'a Dependency> {
1243+
let locked_deps = self
1244+
.locked_deps
1245+
.into_iter()
1246+
.flat_map(|deps| deps.into_iter().map(|locked| &locked.dependency));
1247+
self.deps.into_iter().flatten().chain(locked_deps)
11301248
}
11311249
}

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 (bar@1.0.0 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 (bar@1.0.0 with 1 patch file)
11831181
[COMPILING] foo v0.0.0 ([ROOT]/foo)

0 commit comments

Comments
 (0)