Make patches automatically update if updated.

This commit is contained in:
Eric Huss 2020-05-19 18:01:23 -07:00
parent b78cb373b8
commit afa3acedf0
3 changed files with 494 additions and 111 deletions

View file

@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
use anyhow::bail;
use log::{debug, trace};
use semver::{Version, VersionReq};
use semver::VersionReq;
use url::Url;
use crate::core::PackageSet;
@ -233,13 +233,24 @@ impl<'cfg> PackageRegistry<'cfg> {
/// Note that the patch list specified here *will not* be available to
/// `query` until `lock_patches` is called below, which should be called
/// once all patches have been added.
///
/// The return value is a `Vec` of patches that should *not* be locked.
/// This happens when the patch is locked, but the patch has been updated
/// so the locked value is no longer correct.
pub fn patch(
&mut self,
url: &Url,
deps: &[(&Dependency, Option<Dependency>)],
) -> CargoResult<()> {
deps: &[(&Dependency, Option<(Dependency, PackageId)>)],
) -> CargoResult<Vec<(Dependency, PackageId)>> {
// NOTE: None of this code is aware of required features. If a patch
// is missing a required feature, you end up with an "unused patch"
// warning, which is very hard to understand. Ideally the warning
// would be tailored to indicate *why* it is unused.
let canonical = CanonicalUrl::new(url)?;
// Return value of patches that shouldn't be locked.
let mut unlock_patches = Vec::new();
// First up we need to actually resolve each `deps` specification to
// precisely one summary. We're not using the `query` method below as it
// internally uses maps we're building up as part of this method
@ -251,8 +262,15 @@ impl<'cfg> PackageRegistry<'cfg> {
// of summaries which should be the same length as `deps` above.
let unlocked_summaries = deps
.iter()
.map(|(orig_dep, locked_dep)| {
let dep = locked_dep.as_ref().unwrap_or(orig_dep);
.map(|(orig_patch, locked)| {
// Remove double reference in orig_patch. Is there maybe a
// magic pattern that could avoid this?
let orig_patch = *orig_patch;
// Use the locked patch if it exists, otherwise use the original.
let dep = match locked {
Some((locked_patch, _locked_id)) => locked_patch,
None => orig_patch,
};
debug!(
"registering a patch for `{}` with `{}`",
url,
@ -275,7 +293,21 @@ impl<'cfg> PackageRegistry<'cfg> {
.get_mut(dep.source_id())
.expect("loaded source not present");
let summaries = source.query_vec(dep)?;
let summary = summary_for_patch(orig_dep, locked_dep, url, summaries, source)?;
let (summary, should_unlock) =
summary_for_patch(orig_patch, &locked, summaries, source).chain_err(|| {
format!(
"patch for `{}` in `{}` did not resolve to any crates",
orig_patch.package_name(),
url,
)
})?;
debug!(
"patch summary is {:?} should_unlock={:?}",
summary, should_unlock
);
if let Some(unlock_id) = should_unlock {
unlock_patches.push((orig_patch.clone(), unlock_id));
}
if *summary.package_id().source_id().canonical_url() == canonical {
anyhow::bail!(
@ -313,7 +345,7 @@ impl<'cfg> PackageRegistry<'cfg> {
self.patches_available.insert(canonical.clone(), ids);
self.patches.insert(canonical, unlocked_summaries);
Ok(())
Ok(unlock_patches)
}
/// Lock all patch summaries added via `patch`, making them available to
@ -327,6 +359,7 @@ impl<'cfg> PackageRegistry<'cfg> {
assert!(!self.patches_locked);
for summaries in self.patches.values_mut() {
for summary in summaries {
debug!("locking patch {:?}", summary);
*summary = lock(&self.locked, &self.patches_available, summary.clone());
}
}
@ -711,60 +744,68 @@ fn lock(
})
}
/// This is a helper for generating a user-friendly error message for a bad patch.
/// This is a helper for selecting the summary, or generating a helpful error message.
fn summary_for_patch(
orig_patch: &Dependency,
locked_patch: &Option<Dependency>,
url: &Url,
locked: &Option<(Dependency, PackageId)>,
mut summaries: Vec<Summary>,
source: &mut dyn Source,
) -> CargoResult<Summary> {
) -> CargoResult<(Summary, Option<PackageId>)> {
if summaries.len() == 1 {
return Ok(summaries.pop().unwrap());
return Ok((summaries.pop().unwrap(), None));
}
// Helpers to create a comma-separated string of versions.
let versions = |versions: &mut [&Version]| -> String {
versions.sort();
let versions: Vec<_> = versions.into_iter().map(|v| v.to_string()).collect();
versions.join(", ")
};
let summary_versions = |summaries: &[Summary]| -> String {
let mut vers: Vec<_> = summaries.iter().map(|summary| summary.version()).collect();
versions(&mut vers)
let best_summary = |summaries: &mut Vec<Summary>| -> Summary {
// TODO: This could maybe honor -Zminimal-versions?
summaries.sort_by(|a, b| a.version().cmp(b.version()));
summaries.pop().unwrap()
};
if summaries.len() > 1 {
anyhow::bail!(
"patch for `{}` in `{}` resolved to more than one candidate\n\
Found versions: {}\n\
Update the patch definition to select only one package, \
or remove the extras from the patch location.",
orig_patch.package_name(),
url,
summary_versions(&summaries)
);
let summary = best_summary(&mut summaries);
if let Some((_dep, lock_id)) = locked {
// I can't think of a scenario where this might happen (locked by
// definition should only match at most one summary). Maybe if the
// source is broken?
return Ok((summary, Some(*lock_id)));
} else {
return Ok((summary, None));
}
}
assert!(summaries.is_empty());
// No summaries found, try to help the user figure out what is wrong.
let extra = if let Some(locked_patch) = locked_patch {
let found = match source.query_vec(orig_patch) {
Ok(unlocked_summaries) => format!(" (found {})", summary_versions(&unlocked_summaries)),
if let Some((locked_patch, locked_id)) = locked {
// Since the locked patch did not match anything, try the unlocked one.
let mut orig_matches = match source.query_vec(orig_patch) {
Ok(summaries) => summaries,
Err(e) => {
log::warn!(
"could not determine unlocked summaries for dep {:?}: {:?}",
orig_patch,
e
);
"".to_string()
Vec::new()
}
};
format!(
"The patch is locked to {} in Cargo.lock, \
but the version in the patch location does not match{}.\n\
Make sure the patch points to the correct version.\n\
If it does, run `cargo update -p {}` to update Cargo.lock.",
locked_patch.version_req(),
found,
locked_patch.package_name(),
)
if orig_matches.is_empty() {
// This should be relatively unusual. For example, a patch of
// {version="0.1.2", ...} and the patch location no longer contains a
// version that matches "0.1.2". It is unusual to explicitly write a
// version in the patch.
anyhow::bail!(
"The patch is locked to {} in Cargo.lock, but the version in the \
patch location does not match any packages in the patch location.\n\
Make sure the patch points to the correct version.",
locked_patch.version_req(),
);
}
let summary = best_summary(&mut orig_matches);
debug!(
"locked patch no longer matches, but unlocked version should work. \
locked={:?} unlocked={:?} summary={:?}",
locked, orig_patch, summary
);
// The unlocked version found a match. This returns a value to
// indicate that this entry should be unlocked.
return Ok((summary, Some(*locked_id)));
} else {
// Try checking if there are *any* packages that match this by name.
let name_only_dep =
@ -777,8 +818,12 @@ fn summary_for_patch(
.collect::<Vec<_>>();
match vers.len() {
0 => format!(""),
1 => format!("version `{}`", versions(&mut vers)),
_ => format!("versions `{}`", versions(&mut vers)),
1 => format!("version `{}`", vers[0]),
_ => {
vers.sort();
let strs: Vec<_> = vers.into_iter().map(|v| v.to_string()).collect();
format!("versions `{}`", strs.join(", "))
}
}
}
Err(e) => {
@ -791,13 +836,13 @@ fn summary_for_patch(
}
};
if found.is_empty() {
format!(
anyhow::bail!(
"The patch location does not appear to contain any packages \
matching the name `{}`.",
orig_patch.package_name()
)
);
} else {
format!(
anyhow::bail!(
"The patch location contains a `{}` package with {}, but the patch \
definition requires `{}`.\n\
Check that the version in the patch location is what you expect, \
@ -805,13 +850,7 @@ fn summary_for_patch(
orig_patch.package_name(),
found,
orig_patch.version_req()
)
);
}
};
anyhow::bail!(
"patch for `{}` in `{}` did not resolve to any crates.\n{}",
orig_patch.package_name(),
url,
extra
);
}
}

View file

@ -20,7 +20,7 @@ use crate::core::{PackageId, PackageIdSpec, PackageSet, Source, SourceId, Worksp
use crate::ops;
use crate::sources::PathSource;
use crate::util::errors::{CargoResult, CargoResultExt};
use crate::util::profile;
use crate::util::{profile, CanonicalUrl};
use log::{debug, trace};
use std::collections::HashSet;
@ -224,7 +224,7 @@ pub fn resolve_with_previous<'cfg>(
);
}
let keep = |p: &PackageId| {
let pre_patch_keep = |p: &PackageId| {
!to_avoid_sources.contains(&p.source_id())
&& match to_avoid {
Some(set) => !set.contains(p),
@ -232,6 +232,55 @@ pub fn resolve_with_previous<'cfg>(
}
};
// This is a set of PackageIds of `[patch]` entries that should not be
// locked.
let mut avoid_patch_ids = HashSet::new();
if register_patches {
for (url, patches) in ws.root_patch() {
let previous = match previous {
Some(r) => r,
None => {
let patches: Vec<_> = patches.iter().map(|p| (p, None)).collect();
let unlock_ids = registry.patch(url, &patches)?;
// Since nothing is locked, this shouldn't possibly return anything.
assert!(unlock_ids.is_empty());
continue;
}
};
let patches = patches
.iter()
.map(|dep| {
let unused = previous.unused_patches().iter().cloned();
let candidates = previous.iter().chain(unused);
match candidates
.filter(pre_patch_keep)
.find(|&id| dep.matches_id(id))
{
Some(id) => {
let mut locked_dep = dep.clone();
locked_dep.lock_to(id);
(dep, Some((locked_dep, id)))
}
None => (dep, None),
}
})
.collect::<Vec<_>>();
let canonical = CanonicalUrl::new(url)?;
for (orig_patch, unlock_id) in registry.patch(url, &patches)? {
// Avoid the locked patch ID.
avoid_patch_ids.insert(unlock_id);
// Also avoid the thing it is patching.
avoid_patch_ids.extend(previous.iter().filter(|id| {
orig_patch.matches_ignoring_source(*id)
&& *id.source_id().canonical_url() == canonical
}));
}
}
}
debug!("avoid_patch_ids={:?}", avoid_patch_ids);
let keep = |p: &PackageId| pre_patch_keep(p) && !avoid_patch_ids.contains(p);
// In the case where a previous instance of resolve is available, we
// want to lock as many packages as possible to the previous version
// without disturbing the graph structure.
@ -249,33 +298,6 @@ pub fn resolve_with_previous<'cfg>(
}
if register_patches {
for (url, patches) in ws.root_patch() {
let previous = match previous {
Some(r) => r,
None => {
let patches: Vec<_> = patches.iter().map(|p| (p, None)).collect();
registry.patch(url, &patches)?;
continue;
}
};
let patches = patches
.iter()
.map(|dep| {
let unused = previous.unused_patches().iter().cloned();
let candidates = previous.iter().chain(unused);
match candidates.filter(keep).find(|&id| dep.matches_id(id)) {
Some(id) => {
let mut locked_dep = dep.clone();
locked_dep.lock_to(id);
(dep, Some(locked_dep))
}
None => (dep, None),
}
})
.collect::<Vec<_>>();
registry.patch(url, &patches)?;
}
registry.lock_patches();
}

View file

@ -1498,25 +1498,25 @@ fn update_unused_new_version() {
// Create a backup so we can test it with different options.
fs::copy(p.root().join("Cargo.lock"), p.root().join("Cargo.lock.bak")).unwrap();
// Try to build again.
// Try to build again, this should automatically update Cargo.lock.
p.cargo("build")
.with_status(101)
.with_stderr(
"\
[ERROR] failed to resolve patches for `https://github.com/rust-lang/crates.io-index`
Caused by:
patch for `bar` in `https://github.com/rust-lang/crates.io-index` did not \
resolve to any crates.
The patch is locked to = 0.1.4 in Cargo.lock, but the version in the patch \
location does not match (found 0.1.6).
Make sure the patch points to the correct version.
If it does, run `cargo update -p bar` to update Cargo.lock.
[UPDATING] `[..]/registry` index
[COMPILING] bar v0.1.6 ([..]/bar)
[COMPILING] foo v0.0.1 ([..]/foo)
[FINISHED] [..]
",
)
.run();
// This should not update any registry.
p.cargo("build").with_stderr("[FINISHED] [..]").run();
assert!(!p.read_lockfile().contains("unused"));
// Oh, OK, try `update -p`.
// Restore the lock file, and see if `update` will work, too.
fs::copy(p.root().join("Cargo.lock.bak"), p.root().join("Cargo.lock")).unwrap();
// Try `update -p`.
p.cargo("update -p bar")
.with_stderr(
"\
@ -1565,17 +1565,19 @@ fn too_many_matches() {
.file("src/lib.rs", "")
.build();
// Picks 0.1.1, the most recent version.
p.cargo("check")
.with_status(101)
.with_stderr("\
[UPDATING] `[..]alternative-registry` index
[ERROR] failed to resolve patches for `https://github.com/rust-lang/crates.io-index`
Caused by:
patch for `bar` in `https://github.com/rust-lang/crates.io-index` resolved to more than one candidate
Found versions: 0.1.0, 0.1.1
Update the patch definition to select only one package, or remove the extras from the patch location.
")
.with_stderr(
"\
[UPDATING] `[..]/alternative-registry` index
[UPDATING] `[..]/registry` index
[DOWNLOADING] crates ...
[DOWNLOADED] bar v0.1.1 (registry `[..]/alternative-registry`)
[CHECKING] bar v0.1.1 (registry `[..]/alternative-registry`)
[CHECKING] foo v0.1.0 ([..]/foo)
[FINISHED] [..]
",
)
.run();
}
@ -1609,8 +1611,10 @@ fn no_matches() {
error: failed to resolve patches for `https://github.com/rust-lang/crates.io-index`
Caused by:
patch for `bar` in `https://github.com/rust-lang/crates.io-index` did not resolve to any crates.
The patch location does not appear to contain any packages matching the name `bar`.
patch for `bar` in `https://github.com/rust-lang/crates.io-index` did not resolve to any crates
Caused by:
The patch location does not appear to contain any packages matching the name `bar`.
",
)
.run();
@ -1646,8 +1650,10 @@ fn mismatched_version() {
[ERROR] failed to resolve patches for `https://github.com/rust-lang/crates.io-index`
Caused by:
patch for `bar` in `https://github.com/rust-lang/crates.io-index` did not resolve to any crates.
The patch location contains a `bar` package with version `0.1.0`, \
patch for `bar` in `https://github.com/rust-lang/crates.io-index` did not resolve to any crates
Caused by:
The patch location contains a `bar` package with version `0.1.0`, \
but the patch definition requires `^0.1.1`.
Check that the version in the patch location is what you expect, \
and update the patch definition to match.
@ -1655,3 +1661,319 @@ and update the patch definition to match.
)
.run();
}
#[cargo_test]
fn patch_walks_backwards() {
// Starting with a locked patch, change the patch so it points to an older version.
Package::new("bar", "0.1.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = "0.1"
[patch.crates-io]
bar = {path="bar"}
"#,
)
.file("src/lib.rs", "")
.file("bar/Cargo.toml", &basic_manifest("bar", "0.1.1"))
.file("bar/src/lib.rs", "")
.build();
p.cargo("check")
.with_stderr(
"\
[UPDATING] `[..]/registry` index
[CHECKING] bar v0.1.1 ([..]/foo/bar)
[CHECKING] foo v0.1.0 ([..]/foo)
[FINISHED] [..]
",
)
.run();
// Somehow the user changes the version backwards.
p.change_file("bar/Cargo.toml", &basic_manifest("bar", "0.1.0"));
p.cargo("check")
.with_stderr(
"\
[UPDATING] `[..]/registry` index
[CHECKING] bar v0.1.0 ([..]/foo/bar)
[CHECKING] foo v0.1.0 ([..]/foo)
[FINISHED] [..]
",
)
.run();
}
#[cargo_test]
fn patch_walks_backwards_restricted() {
// This is the same as `patch_walks_backwards`, but the patch contains a
// `version` qualifier. This is unusual, just checking a strange edge case.
Package::new("bar", "0.1.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = "0.1"
[patch.crates-io]
bar = {path="bar", version="0.1.1"}
"#,
)
.file("src/lib.rs", "")
.file("bar/Cargo.toml", &basic_manifest("bar", "0.1.1"))
.file("bar/src/lib.rs", "")
.build();
p.cargo("check")
.with_stderr(
"\
[UPDATING] `[..]/registry` index
[CHECKING] bar v0.1.1 ([..]/foo/bar)
[CHECKING] foo v0.1.0 ([..]/foo)
[FINISHED] [..]
",
)
.run();
// Somehow the user changes the version backwards.
p.change_file("bar/Cargo.toml", &basic_manifest("bar", "0.1.0"));
p.cargo("check")
.with_status(101)
.with_stderr(
"\
error: failed to resolve patches for `https://github.com/rust-lang/crates.io-index`
Caused by:
patch for `bar` in `https://github.com/rust-lang/crates.io-index` did not resolve to any crates
Caused by:
The patch is locked to = 0.1.1 in Cargo.lock, but the version in the patch location does not match any packages in the patch location.
Make sure the patch points to the correct version.
",
)
.run();
}
#[cargo_test]
fn patched_dep_new_version() {
// What happens when a patch is locked, and then one of the patched
// dependencies needs to be updated. In this case, the baz requirement
// gets updated from 0.1.0 to 0.1.1.
Package::new("bar", "0.1.0").dep("baz", "0.1.0").publish();
Package::new("baz", "0.1.0").publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = "0.1"
[patch.crates-io]
bar = {path="bar"}
"#,
)
.file("src/lib.rs", "")
.file(
"bar/Cargo.toml",
r#"
[package]
name = "bar"
version = "0.1.0"
[dependencies]
baz = "0.1"
"#,
)
.file("bar/src/lib.rs", "")
.build();
// Lock everything.
p.cargo("check")
.with_stderr(
"\
[UPDATING] `[..]/registry` index
[DOWNLOADING] crates ...
[DOWNLOADED] baz v0.1.0 [..]
[CHECKING] baz v0.1.0
[CHECKING] bar v0.1.0 ([..]/foo/bar)
[CHECKING] foo v0.1.0 ([..]/foo)
[FINISHED] [..]
",
)
.run();
Package::new("baz", "0.1.1").publish();
// Just the presence of the new version should not have changed anything.
p.cargo("check").with_stderr("[FINISHED] [..]").run();
// Modify the patch so it requires the new version.
p.change_file(
"bar/Cargo.toml",
r#"
[package]
name = "bar"
version = "0.1.0"
[dependencies]
baz = "0.1.1"
"#,
);
// Should unlock and update cleanly.
p.cargo("check")
.with_stderr(
"\
[UPDATING] `[..]/registry` index
[DOWNLOADING] crates ...
[DOWNLOADED] baz v0.1.1 (registry `[..]/registry`)
[CHECKING] baz v0.1.1
[CHECKING] bar v0.1.0 ([..]/foo/bar)
[CHECKING] foo v0.1.0 ([..]/foo)
[FINISHED] [..]
",
)
.run();
}
#[cargo_test]
fn patch_update_doesnt_update_other_sources() {
// Very extreme edge case, make sure a patch update doesn't update other
// sources.
Package::new("bar", "0.1.0").publish();
Package::new("bar", "0.1.0").alternative(true).publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = "0.1"
bar_alt = { version = "0.1", registry = "alternative", package = "bar" }
[patch.crates-io]
bar = { path = "bar" }
"#,
)
.file("src/lib.rs", "")
.file("bar/Cargo.toml", &basic_manifest("bar", "0.1.0"))
.file("bar/src/lib.rs", "")
.build();
p.cargo("check")
.with_stderr_unordered(
"\
[UPDATING] `[..]/registry` index
[UPDATING] `[..]/alternative-registry` index
[DOWNLOADING] crates ...
[DOWNLOADED] bar v0.1.0 (registry `[..]/alternative-registry`)
[CHECKING] bar v0.1.0 (registry `[..]/alternative-registry`)
[CHECKING] bar v0.1.0 ([..]/foo/bar)
[CHECKING] foo v0.1.0 ([..]/foo)
[FINISHED] [..]
",
)
.run();
// Publish new versions in both sources.
Package::new("bar", "0.1.1").publish();
Package::new("bar", "0.1.1").alternative(true).publish();
// Since it is locked, nothing should change.
p.cargo("check").with_stderr("[FINISHED] [..]").run();
// Require new version on crates.io.
p.change_file("bar/Cargo.toml", &basic_manifest("bar", "0.1.1"));
// This should not update bar_alt.
p.cargo("check")
.with_stderr(
"\
[UPDATING] `[..]/registry` index
[CHECKING] bar v0.1.1 ([..]/foo/bar)
[CHECKING] foo v0.1.0 ([..]/foo)
[FINISHED] [..]
",
)
.run();
}
#[cargo_test]
fn can_update_with_alt_reg() {
// A patch to an alt reg can update.
Package::new("bar", "0.1.0").publish();
Package::new("bar", "0.1.0").alternative(true).publish();
Package::new("bar", "0.1.1").alternative(true).publish();
let p = project()
.file(
"Cargo.toml",
r#"
[package]
name = "foo"
version = "0.1.0"
[dependencies]
bar = "0.1"
[patch.crates-io]
bar = { version = "0.1.1", registry = "alternative" }
"#,
)
.file("src/lib.rs", "")
.build();
p.cargo("check")
.with_stderr(
"\
[UPDATING] `[..]/alternative-registry` index
[UPDATING] `[..]/registry` index
[DOWNLOADING] crates ...
[DOWNLOADED] bar v0.1.1 (registry `[..]/alternative-registry`)
[CHECKING] bar v0.1.1 (registry `[..]/alternative-registry`)
[CHECKING] foo v0.1.0 ([..]/foo)
[FINISHED] [..]
",
)
.run();
Package::new("bar", "0.1.2").alternative(true).publish();
// Should remain locked.
p.cargo("check").with_stderr("[FINISHED] [..]").run();
p.cargo("update -p bar")
.with_stderr(
"\
[UPDATING] `[..]/alternative-registry` index
[UPDATING] `[..]/registry` index
[UPDATING] bar v0.1.1 (registry `[..]/alternative-registry`) -> v0.1.2
",
)
.run();
}