Skip to content

Commit b78cb37

Browse files
committed
Provide better error messages for a bad patch.
1 parent 13bded9 commit b78cb37

File tree

3 files changed

+266
-34
lines changed

3 files changed

+266
-34
lines changed

src/cargo/core/registry.rs

Lines changed: 125 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
22

33
use anyhow::bail;
44
use log::{debug, trace};
5-
use semver::VersionReq;
5+
use semver::{Version, VersionReq};
66
use url::Url;
77

88
use crate::core::PackageSet;
@@ -222,14 +222,22 @@ impl<'cfg> PackageRegistry<'cfg> {
222222
/// the manifest.
223223
///
224224
/// Here the `deps` will be resolved to a precise version and stored
225-
/// internally for future calls to `query` below. It's expected that `deps`
226-
/// have had `lock_to` call already, if applicable. (e.g., if a lock file was
227-
/// already present).
225+
/// internally for future calls to `query` below. `deps` should be a tuple
226+
/// where the first element is the patch definition straight from the
227+
/// manifest, and the second element is an optional variant where the
228+
/// patch has been locked. This locked patch is the patch locked to
229+
/// a specific version found in Cargo.lock. This will be `None` if
230+
/// `Cargo.lock` doesn't exist, or the patch did not match any existing
231+
/// entries in `Cargo.lock`.
228232
///
229233
/// Note that the patch list specified here *will not* be available to
230234
/// `query` until `lock_patches` is called below, which should be called
231235
/// once all patches have been added.
232-
pub fn patch(&mut self, url: &Url, deps: &[Dependency]) -> CargoResult<()> {
236+
pub fn patch(
237+
&mut self,
238+
url: &Url,
239+
deps: &[(&Dependency, Option<Dependency>)],
240+
) -> CargoResult<()> {
233241
let canonical = CanonicalUrl::new(url)?;
234242

235243
// First up we need to actually resolve each `deps` specification to
@@ -243,7 +251,8 @@ impl<'cfg> PackageRegistry<'cfg> {
243251
// of summaries which should be the same length as `deps` above.
244252
let unlocked_summaries = deps
245253
.iter()
246-
.map(|dep| {
254+
.map(|(orig_dep, locked_dep)| {
255+
let dep = locked_dep.as_ref().unwrap_or(orig_dep);
247256
debug!(
248257
"registering a patch for `{}` with `{}`",
249258
url,
@@ -261,30 +270,13 @@ impl<'cfg> PackageRegistry<'cfg> {
261270
)
262271
})?;
263272

264-
let mut summaries = self
273+
let source = self
265274
.sources
266275
.get_mut(dep.source_id())
267-
.expect("loaded source not present")
268-
.query_vec(dep)?
269-
.into_iter();
270-
271-
let summary = match summaries.next() {
272-
Some(summary) => summary,
273-
None => anyhow::bail!(
274-
"patch for `{}` in `{}` did not resolve to any crates. If this is \
275-
unexpected, you may wish to consult: \
276-
https://github.com/rust-lang/cargo/issues/4678",
277-
dep.package_name(),
278-
url
279-
),
280-
};
281-
if summaries.next().is_some() {
282-
anyhow::bail!(
283-
"patch for `{}` in `{}` resolved to more than one candidate",
284-
dep.package_name(),
285-
url
286-
)
287-
}
276+
.expect("loaded source not present");
277+
let summaries = source.query_vec(dep)?;
278+
let summary = summary_for_patch(orig_dep, locked_dep, url, summaries, source)?;
279+
288280
if *summary.package_id().source_id().canonical_url() == canonical {
289281
anyhow::bail!(
290282
"patch for `{}` in `{}` points to the same source, but \
@@ -718,3 +710,108 @@ fn lock(
718710
dep
719711
})
720712
}
713+
714+
/// This is a helper for generating a user-friendly error message for a bad patch.
715+
fn summary_for_patch(
716+
orig_patch: &Dependency,
717+
locked_patch: &Option<Dependency>,
718+
url: &Url,
719+
mut summaries: Vec<Summary>,
720+
source: &mut dyn Source,
721+
) -> CargoResult<Summary> {
722+
if summaries.len() == 1 {
723+
return Ok(summaries.pop().unwrap());
724+
}
725+
// Helpers to create a comma-separated string of versions.
726+
let versions = |versions: &mut [&Version]| -> String {
727+
versions.sort();
728+
let versions: Vec<_> = versions.into_iter().map(|v| v.to_string()).collect();
729+
versions.join(", ")
730+
};
731+
let summary_versions = |summaries: &[Summary]| -> String {
732+
let mut vers: Vec<_> = summaries.iter().map(|summary| summary.version()).collect();
733+
versions(&mut vers)
734+
};
735+
if summaries.len() > 1 {
736+
anyhow::bail!(
737+
"patch for `{}` in `{}` resolved to more than one candidate\n\
738+
Found versions: {}\n\
739+
Update the patch definition to select only one package, \
740+
or remove the extras from the patch location.",
741+
orig_patch.package_name(),
742+
url,
743+
summary_versions(&summaries)
744+
);
745+
}
746+
// No summaries found, try to help the user figure out what is wrong.
747+
let extra = if let Some(locked_patch) = locked_patch {
748+
let found = match source.query_vec(orig_patch) {
749+
Ok(unlocked_summaries) => format!(" (found {})", summary_versions(&unlocked_summaries)),
750+
Err(e) => {
751+
log::warn!(
752+
"could not determine unlocked summaries for dep {:?}: {:?}",
753+
orig_patch,
754+
e
755+
);
756+
"".to_string()
757+
}
758+
};
759+
format!(
760+
"The patch is locked to {} in Cargo.lock, \
761+
but the version in the patch location does not match{}.\n\
762+
Make sure the patch points to the correct version.\n\
763+
If it does, run `cargo update -p {}` to update Cargo.lock.",
764+
locked_patch.version_req(),
765+
found,
766+
locked_patch.package_name(),
767+
)
768+
} else {
769+
// Try checking if there are *any* packages that match this by name.
770+
let name_only_dep =
771+
Dependency::new_override(orig_patch.package_name(), orig_patch.source_id());
772+
let found = match source.query_vec(&name_only_dep) {
773+
Ok(name_summaries) => {
774+
let mut vers = name_summaries
775+
.iter()
776+
.map(|summary| summary.version())
777+
.collect::<Vec<_>>();
778+
match vers.len() {
779+
0 => format!(""),
780+
1 => format!("version `{}`", versions(&mut vers)),
781+
_ => format!("versions `{}`", versions(&mut vers)),
782+
}
783+
}
784+
Err(e) => {
785+
log::warn!(
786+
"failed to do name-only summary query for {:?}: {:?}",
787+
name_only_dep,
788+
e
789+
);
790+
"".to_string()
791+
}
792+
};
793+
if found.is_empty() {
794+
format!(
795+
"The patch location does not appear to contain any packages \
796+
matching the name `{}`.",
797+
orig_patch.package_name()
798+
)
799+
} else {
800+
format!(
801+
"The patch location contains a `{}` package with {}, but the patch \
802+
definition requires `{}`.\n\
803+
Check that the version in the patch location is what you expect, \
804+
and update the patch definition to match.",
805+
orig_patch.package_name(),
806+
found,
807+
orig_patch.version_req()
808+
)
809+
}
810+
};
811+
anyhow::bail!(
812+
"patch for `{}` in `{}` did not resolve to any crates.\n{}",
813+
orig_patch.package_name(),
814+
url,
815+
extra
816+
);
817+
}

src/cargo/ops/resolve.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ pub fn resolve_with_previous<'cfg>(
253253
let previous = match previous {
254254
Some(r) => r,
255255
None => {
256-
registry.patch(url, patches)?;
256+
let patches: Vec<_> = patches.iter().map(|p| (p, None)).collect();
257+
registry.patch(url, &patches)?;
257258
continue;
258259
}
259260
};
@@ -264,11 +265,11 @@ pub fn resolve_with_previous<'cfg>(
264265
let candidates = previous.iter().chain(unused);
265266
match candidates.filter(keep).find(|&id| dep.matches_id(id)) {
266267
Some(id) => {
267-
let mut dep = dep.clone();
268-
dep.lock_to(id);
269-
dep
268+
let mut locked_dep = dep.clone();
269+
locked_dep.lock_to(id);
270+
(dep, Some(locked_dep))
270271
}
271-
None => dep.clone(),
272+
None => (dep, None),
272273
}
273274
})
274275
.collect::<Vec<_>>();

tests/testsuite/patch.rs

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1498,7 +1498,25 @@ fn update_unused_new_version() {
14981498
// Create a backup so we can test it with different options.
14991499
fs::copy(p.root().join("Cargo.lock"), p.root().join("Cargo.lock.bak")).unwrap();
15001500

1501-
// Try with `-p`.
1501+
// Try to build again.
1502+
p.cargo("build")
1503+
.with_status(101)
1504+
.with_stderr(
1505+
"\
1506+
[ERROR] failed to resolve patches for `https://github.com/rust-lang/crates.io-index`
1507+
1508+
Caused by:
1509+
patch for `bar` in `https://github.com/rust-lang/crates.io-index` did not \
1510+
resolve to any crates.
1511+
The patch is locked to = 0.1.4 in Cargo.lock, but the version in the patch \
1512+
location does not match (found 0.1.6).
1513+
Make sure the patch points to the correct version.
1514+
If it does, run `cargo update -p bar` to update Cargo.lock.
1515+
",
1516+
)
1517+
.run();
1518+
1519+
// Oh, OK, try `update -p`.
15021520
p.cargo("update -p bar")
15031521
.with_stderr(
15041522
"\
@@ -1521,3 +1539,119 @@ fn update_unused_new_version() {
15211539
)
15221540
.run();
15231541
}
1542+
1543+
#[cargo_test]
1544+
fn too_many_matches() {
1545+
// The patch locations has multiple versions that match.
1546+
Package::new("bar", "0.1.0").publish();
1547+
Package::new("bar", "0.1.0").alternative(true).publish();
1548+
Package::new("bar", "0.1.1").alternative(true).publish();
1549+
1550+
let p = project()
1551+
.file(
1552+
"Cargo.toml",
1553+
r#"
1554+
[package]
1555+
name = "foo"
1556+
version = "0.1.0"
1557+
1558+
[dependencies]
1559+
bar = "0.1"
1560+
1561+
[patch.crates-io]
1562+
bar = { version = "0.1", registry = "alternative" }
1563+
"#,
1564+
)
1565+
.file("src/lib.rs", "")
1566+
.build();
1567+
1568+
p.cargo("check")
1569+
.with_status(101)
1570+
.with_stderr("\
1571+
[UPDATING] `[..]alternative-registry` index
1572+
[ERROR] failed to resolve patches for `https://github.com/rust-lang/crates.io-index`
1573+
1574+
Caused by:
1575+
patch for `bar` in `https://github.com/rust-lang/crates.io-index` resolved to more than one candidate
1576+
Found versions: 0.1.0, 0.1.1
1577+
Update the patch definition to select only one package, or remove the extras from the patch location.
1578+
")
1579+
.run();
1580+
}
1581+
1582+
#[cargo_test]
1583+
fn no_matches() {
1584+
// A patch to a location that does not contain the named package.
1585+
let p = project()
1586+
.file(
1587+
"Cargo.toml",
1588+
r#"
1589+
[package]
1590+
name = "foo"
1591+
version = "0.1.0"
1592+
1593+
[dependencies]
1594+
bar = "0.1"
1595+
1596+
[patch.crates-io]
1597+
bar = { path = "bar" }
1598+
"#,
1599+
)
1600+
.file("src/lib.rs", "")
1601+
.file("bar/Cargo.toml", &basic_manifest("abc", "0.1.0"))
1602+
.file("bar/src/lib.rs", "")
1603+
.build();
1604+
1605+
p.cargo("check")
1606+
.with_status(101)
1607+
.with_stderr(
1608+
"\
1609+
error: failed to resolve patches for `https://github.com/rust-lang/crates.io-index`
1610+
1611+
Caused by:
1612+
patch for `bar` in `https://github.com/rust-lang/crates.io-index` did not resolve to any crates.
1613+
The patch location does not appear to contain any packages matching the name `bar`.
1614+
",
1615+
)
1616+
.run();
1617+
}
1618+
1619+
#[cargo_test]
1620+
fn mismatched_version() {
1621+
// A patch to a location that has an old version.
1622+
let p = project()
1623+
.file(
1624+
"Cargo.toml",
1625+
r#"
1626+
[package]
1627+
name = "foo"
1628+
version = "0.1.0"
1629+
1630+
[dependencies]
1631+
bar = "0.1.1"
1632+
1633+
[patch.crates-io]
1634+
bar = { path = "bar", version = "0.1.1" }
1635+
"#,
1636+
)
1637+
.file("src/lib.rs", "")
1638+
.file("bar/Cargo.toml", &basic_manifest("bar", "0.1.0"))
1639+
.file("bar/src/lib.rs", "")
1640+
.build();
1641+
1642+
p.cargo("check")
1643+
.with_status(101)
1644+
.with_stderr(
1645+
"\
1646+
[ERROR] failed to resolve patches for `https://github.com/rust-lang/crates.io-index`
1647+
1648+
Caused by:
1649+
patch for `bar` in `https://github.com/rust-lang/crates.io-index` did not resolve to any crates.
1650+
The patch location contains a `bar` package with version `0.1.0`, \
1651+
but the patch definition requires `^0.1.1`.
1652+
Check that the version in the patch location is what you expect, \
1653+
and update the patch definition to match.
1654+
",
1655+
)
1656+
.run();
1657+
}

0 commit comments

Comments
 (0)