Skip to content

Commit ba37dac

Browse files
committed
Auto merge of #7303 - alexcrichton:duplicate-patch, r=ehuss
Fixes around multiple `[patch]` per crate This commit fixes a few bugs in handling of `[patch]` where multiple version of the same crate name have been patched in. Two sub-issues were discovered when investigating #7264: * The first issue is that the source of the assertion, the logic in `lock`, revealed a fundamental flaw in the logic. The `lock` function serves the purpose of applying a lock file to a dependency candidate and ensure that it stays within locked versions of dependencies, if they're previously found. The logic here to handle `[patch]`, however, happened a bit too late and was a bit too zealous to simply lock a dependency by name instead of accounting for the version as well. The updated logic is to move the locking of dependencies here to during the normal iteration over the list of dependencies. Adjacent to `matches_id` we check `matches_ignoring_source`. If the latter returns `true` then we double-check that the previous dependency is still in `[patch]`, and then we let it through. This means that patches with multiple versions should be correctly handled where edges drawn with `[patch]` are preserved. * The second issue, after fixing this, was found where if the exact same version was listed in `[patch]` multiple times then we would continuously update the original source since one of the replacements gets lost along the way. This commit adds a first-class warning disallowing patches pointing to the exact same crate version, since we don't have a way to prioritize amongst them anyway. Closes #7264
2 parents 57986ea + 803bf32 commit ba37dac

File tree

2 files changed

+241
-35
lines changed

2 files changed

+241
-35
lines changed

src/cargo/core/registry.rs

Lines changed: 82 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::collections::{HashMap, HashSet};
22

3+
use failure::bail;
34
use log::{debug, trace};
45
use semver::VersionReq;
56
use url::Url;
@@ -79,7 +80,28 @@ pub struct PackageRegistry<'cfg> {
7980
patches_available: HashMap<Url, Vec<PackageId>>,
8081
}
8182

82-
type LockedMap = HashMap<SourceId, HashMap<String, Vec<(PackageId, Vec<PackageId>)>>>;
83+
/// A map of all "locked packages" which is filled in when parsing a lock file
84+
/// and is used to guide dependency resolution by altering summaries as they're
85+
/// queried from this source.
86+
///
87+
/// This map can be thought of as a glorified `Vec<MySummary>` where `MySummary`
88+
/// has a `PackageId` for which package it represents as well as a list of
89+
/// `PackageId` for the resolved dependencies. The hash map is otherwise
90+
/// structured though for easy access throughout this registry.
91+
type LockedMap = HashMap<
92+
// The first level of key-ing done in this hash map is the source that
93+
// dependencies come from, identified by a `SourceId`.
94+
SourceId,
95+
HashMap<
96+
// This next level is keyed by the name of the package...
97+
String,
98+
// ... and the value here is a list of tuples. The first element of each
99+
// tuple is a package which has the source/name used to get to this
100+
// point. The second element of each tuple is the list of locked
101+
// dependencies that the first element has.
102+
Vec<(PackageId, Vec<PackageId>)>,
103+
>,
104+
>;
83105

84106
#[derive(PartialEq, Eq, Clone, Copy)]
85107
enum Kind {
@@ -275,6 +297,20 @@ impl<'cfg> PackageRegistry<'cfg> {
275297
.collect::<CargoResult<Vec<_>>>()
276298
.chain_err(|| failure::format_err!("failed to resolve patches for `{}`", url))?;
277299

300+
let mut name_and_version = HashSet::new();
301+
for summary in unlocked_summaries.iter() {
302+
let name = summary.package_id().name();
303+
let version = summary.package_id().version();
304+
if !name_and_version.insert((name, version)) {
305+
bail!(
306+
"cannot have two `[patch]` entries which both resolve \
307+
to `{} v{}`",
308+
name,
309+
version
310+
);
311+
}
312+
}
313+
278314
// Note that we do not use `lock` here to lock summaries! That step
279315
// happens later once `lock_patches` is invoked. In the meantime though
280316
// we want to fill in the `patches_available` map (later used in the
@@ -579,7 +615,7 @@ fn lock(locked: &LockedMap, patches: &HashMap<Url, Vec<PackageId>>, summary: Sum
579615

580616
// Lock the summary's ID if possible
581617
let summary = match pair {
582-
Some(&(ref precise, _)) => summary.override_id(precise.clone()),
618+
Some((precise, _)) => summary.override_id(precise.clone()),
583619
None => summary,
584620
};
585621
summary.map_dependencies(|dep| {
@@ -603,18 +639,56 @@ fn lock(locked: &LockedMap, patches: &HashMap<Url, Vec<PackageId>>, summary: Sum
603639
// locked version because the dependency needs to be
604640
// re-resolved.
605641
//
606-
// 3. We don't have a lock entry for this dependency, in which
642+
// 3. We have a lock entry for this dependency, but it's from a
643+
// different source than what's listed. This lock though happens
644+
// through `[patch]`, so we want to preserve it.
645+
//
646+
// 4. We don't have a lock entry for this dependency, in which
607647
// case it was likely an optional dependency which wasn't
608648
// included previously so we just pass it through anyway.
609649
//
610-
// Cases 1/2 are handled by `matches_id` and case 3 is handled by
611-
// falling through to the logic below.
612-
if let Some(&(_, ref locked_deps)) = pair {
613-
let locked = locked_deps.iter().find(|&&id| dep.matches_id(id));
650+
// Cases 1/2 are handled by `matches_id`, case 3 is handled specially,
651+
// and case 4 is handled by falling through to the logic below.
652+
if let Some((_, locked_deps)) = pair {
653+
let locked = locked_deps.iter().find(|&&id| {
654+
// If the dependency matches the package id exactly then we've
655+
// found a match, this is the id the dependency was previously
656+
// locked to.
657+
if dep.matches_id(id) {
658+
return true;
659+
}
660+
661+
// If the name/version doesn't match, then we definitely don't
662+
// have a match whatsoever. Otherwise we need to check
663+
// `[patch]`...
664+
if !dep.matches_ignoring_source(id) {
665+
return false;
666+
}
667+
668+
// ... so here we look up the dependency url in the patches
669+
// map, and we see if `id` is contained in the list of patches
670+
// for that url. If it is then this lock is still valid,
671+
// otherwise the lock is no longer valid.
672+
match patches.get(dep.source_id().url()) {
673+
Some(list) => list.contains(&id),
674+
None => false,
675+
}
676+
});
677+
614678
if let Some(&locked) = locked {
615679
trace!("\tfirst hit on {}", locked);
616680
let mut dep = dep;
617-
dep.lock_to(locked);
681+
682+
// If we found a locked version where the sources match, then
683+
// we can `lock_to` to get an exact lock on this dependency.
684+
// Otherwise we got a lock via `[patch]` so we only lock the
685+
// version requirement, not the source.
686+
if locked.source_id() == dep.source_id() {
687+
dep.lock_to(locked);
688+
} else {
689+
let req = VersionReq::exact(locked.version());
690+
dep.set_version_req(req);
691+
}
618692
return dep;
619693
}
620694
}
@@ -633,33 +707,6 @@ fn lock(locked: &LockedMap, patches: &HashMap<Url, Vec<PackageId>>, summary: Sum
633707
return dep;
634708
}
635709

636-
// Finally we check to see if any registered patches correspond to
637-
// this dependency.
638-
let v = patches.get(dep.source_id().url()).map(|vec| {
639-
let dep2 = dep.clone();
640-
let mut iter = vec
641-
.iter()
642-
.filter(move |&&p| dep2.matches_ignoring_source(p));
643-
(iter.next(), iter)
644-
});
645-
if let Some((Some(patch_id), mut remaining)) = v {
646-
assert!(remaining.next().is_none());
647-
let patch_source = patch_id.source_id();
648-
let patch_locked = locked
649-
.get(&patch_source)
650-
.and_then(|m| m.get(&*patch_id.name()))
651-
.map(|list| list.iter().any(|&(ref id, _)| id == patch_id))
652-
.unwrap_or(false);
653-
654-
if patch_locked {
655-
trace!("\tthird hit on {}", patch_id);
656-
let req = VersionReq::exact(patch_id.version());
657-
let mut dep = dep;
658-
dep.set_version_req(req);
659-
return dep;
660-
}
661-
}
662-
663710
trace!("\tnope, unlocked");
664711
dep
665712
})

tests/testsuite/patch.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,3 +1179,162 @@ fn multipatch() {
11791179

11801180
p.cargo("build").run();
11811181
}
1182+
1183+
#[cargo_test]
1184+
fn patch_same_version() {
1185+
let bar = git::repo(&paths::root().join("override"))
1186+
.file("Cargo.toml", &basic_manifest("bar", "0.1.0"))
1187+
.file("src/lib.rs", "")
1188+
.build();
1189+
1190+
crate::support::registry::init();
1191+
1192+
let p = project()
1193+
.file(
1194+
"Cargo.toml",
1195+
&format!(
1196+
r#"
1197+
[package]
1198+
name = "foo"
1199+
version = "0.0.1"
1200+
[dependencies]
1201+
bar = "0.1"
1202+
[patch.crates-io]
1203+
bar = {{ path = "bar" }}
1204+
bar2 = {{ git = '{}', package = 'bar' }}
1205+
"#,
1206+
bar.url(),
1207+
),
1208+
)
1209+
.file("src/lib.rs", "")
1210+
.file(
1211+
"bar/Cargo.toml",
1212+
r#"
1213+
[package]
1214+
name = "bar"
1215+
version = "0.1.0"
1216+
"#,
1217+
)
1218+
.file("bar/src/lib.rs", "")
1219+
.build();
1220+
1221+
p.cargo("build")
1222+
.with_status(101)
1223+
.with_stderr(
1224+
"\
1225+
[UPDATING] [..]
1226+
error: cannot have two `[patch]` entries which both resolve to `bar v0.1.0`
1227+
",
1228+
)
1229+
.run();
1230+
}
1231+
1232+
#[cargo_test]
1233+
fn two_semver_compatible() {
1234+
let bar = git::repo(&paths::root().join("override"))
1235+
.file("Cargo.toml", &basic_manifest("bar", "0.1.1"))
1236+
.file("src/lib.rs", "")
1237+
.build();
1238+
1239+
crate::support::registry::init();
1240+
1241+
let p = project()
1242+
.file(
1243+
"Cargo.toml",
1244+
&format!(
1245+
r#"
1246+
[package]
1247+
name = "foo"
1248+
version = "0.0.1"
1249+
[dependencies]
1250+
bar = "0.1"
1251+
[patch.crates-io]
1252+
bar = {{ path = "bar" }}
1253+
bar2 = {{ git = '{}', package = 'bar' }}
1254+
"#,
1255+
bar.url(),
1256+
),
1257+
)
1258+
.file("src/lib.rs", "pub fn foo() { bar::foo() }")
1259+
.file(
1260+
"bar/Cargo.toml",
1261+
r#"
1262+
[package]
1263+
name = "bar"
1264+
version = "0.1.2"
1265+
"#,
1266+
)
1267+
.file("bar/src/lib.rs", "pub fn foo() {}")
1268+
.build();
1269+
1270+
// assert the build succeeds and doesn't panic anywhere, and then afterwards
1271+
// assert that the build succeeds again without updating anything or
1272+
// building anything else.
1273+
p.cargo("build").run();
1274+
p.cargo("build")
1275+
.with_stderr(
1276+
"\
1277+
warning: Patch `bar v0.1.1 [..]` was not used in the crate graph.
1278+
Check that [..]
1279+
with the [..]
1280+
what is [..]
1281+
version. [..]
1282+
[FINISHED] [..]",
1283+
)
1284+
.run();
1285+
}
1286+
1287+
#[cargo_test]
1288+
fn multipatch_select_big() {
1289+
let bar = git::repo(&paths::root().join("override"))
1290+
.file("Cargo.toml", &basic_manifest("bar", "0.1.0"))
1291+
.file("src/lib.rs", "")
1292+
.build();
1293+
1294+
crate::support::registry::init();
1295+
1296+
let p = project()
1297+
.file(
1298+
"Cargo.toml",
1299+
&format!(
1300+
r#"
1301+
[package]
1302+
name = "foo"
1303+
version = "0.0.1"
1304+
[dependencies]
1305+
bar = "*"
1306+
[patch.crates-io]
1307+
bar = {{ path = "bar" }}
1308+
bar2 = {{ git = '{}', package = 'bar' }}
1309+
"#,
1310+
bar.url(),
1311+
),
1312+
)
1313+
.file("src/lib.rs", "pub fn foo() { bar::foo() }")
1314+
.file(
1315+
"bar/Cargo.toml",
1316+
r#"
1317+
[package]
1318+
name = "bar"
1319+
version = "0.2.0"
1320+
"#,
1321+
)
1322+
.file("bar/src/lib.rs", "pub fn foo() {}")
1323+
.build();
1324+
1325+
// assert the build succeeds, which is only possible if 0.2.0 is selected
1326+
// since 0.1.0 is missing the function we need. Afterwards assert that the
1327+
// build succeeds again without updating anything or building anything else.
1328+
p.cargo("build").run();
1329+
p.cargo("build")
1330+
.with_stderr(
1331+
"\
1332+
warning: Patch `bar v0.1.0 [..]` was not used in the crate graph.
1333+
Check that [..]
1334+
with the [..]
1335+
what is [..]
1336+
version. [..]
1337+
[FINISHED] [..]",
1338+
)
1339+
.run();
1340+
}

0 commit comments

Comments
 (0)