Skip to content

Commit 4fdeb78

Browse files
authored
Automatically upgrade locked versions on conflicts (#2218)
fix #2031
1 parent 5229477 commit 4fdeb78

File tree

6 files changed

+110
-35
lines changed

6 files changed

+110
-35
lines changed

scarb/src/core/lockfile.rs

+69-12
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
use std::collections::BTreeSet;
1+
use std::collections::{BTreeMap, BTreeSet};
22
use std::str::FromStr;
33

44
use anyhow::{Context, Result, anyhow};
5+
use itertools::Itertools;
56
use semver::Version;
67
use serde::{Deserialize, Serialize};
78
use serde_repr::{Deserialize_repr, Serialize_repr};
9+
use smallvec::SmallVec;
810
use toml_edit::DocumentMut;
911
use typed_builder::TypedBuilder;
1012

@@ -23,12 +25,44 @@ pub enum LockVersion {
2325

2426
#[derive(Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize)]
2527
#[serde(rename_all = "kebab-case")]
28+
#[serde(from = "serdex::Lockfile", into = "serdex::Lockfile")]
2629
pub struct Lockfile {
27-
pub version: LockVersion,
28-
#[serde(rename = "package")]
29-
#[serde(default = "BTreeSet::new")]
30-
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
31-
pub packages: BTreeSet<PackageLock>,
30+
version: LockVersion,
31+
// Note that the current compilation model will not allow Scarb to emit lock files with multiple
32+
// entries for the same package name. This structure allows this for the sake of future-proofing
33+
// and being more defensive against lock files tinkered with by hand. Thus, we use `SmallVec`
34+
// to optimise for the common case of a single entry only.
35+
packages: BTreeMap<PackageName, SmallVec<[PackageLock; 1]>>,
36+
}
37+
38+
mod serdex {
39+
use crate::core::lockfile::{LockVersion, PackageLock};
40+
use serde::{Deserialize, Serialize};
41+
use std::collections::BTreeSet;
42+
43+
#[derive(Serialize, Deserialize)]
44+
pub struct Lockfile {
45+
version: LockVersion,
46+
#[serde(rename = "package")]
47+
#[serde(default = "BTreeSet::new")]
48+
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
49+
packages: BTreeSet<PackageLock>,
50+
}
51+
52+
impl From<Lockfile> for super::Lockfile {
53+
fn from(value: Lockfile) -> Self {
54+
Self::new(value.packages).with_version(value.version)
55+
}
56+
}
57+
58+
impl From<super::Lockfile> for Lockfile {
59+
fn from(value: super::Lockfile) -> Self {
60+
Self {
61+
version: value.version,
62+
packages: value.packages.into_values().flatten().collect(),
63+
}
64+
}
65+
}
3266
}
3367

3468
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, TypedBuilder)]
@@ -57,12 +91,24 @@ fn skip_path_source_id(sid: &Option<SourceId>) -> bool {
5791

5892
impl Lockfile {
5993
pub fn new(packages: impl IntoIterator<Item = PackageLock>) -> Self {
94+
let packages = packages
95+
.into_iter()
96+
.map(|lock| (lock.name.clone(), lock))
97+
.chunk_by(|(name, _)| name.clone());
98+
let packages = packages
99+
.into_iter()
100+
.map(|(name, locks)| (name, locks.into_iter().map(|(_, lock)| lock).collect()))
101+
.collect();
60102
Self {
61103
version: Default::default(),
62-
packages: packages.into_iter().collect(),
104+
packages,
63105
}
64106
}
65107

108+
fn with_version(self, version: LockVersion) -> Self {
109+
Self { version, ..self }
110+
}
111+
66112
pub fn from_resolve(resolve: &Resolve) -> Self {
67113
let include_package = |package_id: &PackageId| !package_id.source_id.is_std();
68114
let packages = resolve
@@ -87,13 +133,24 @@ impl Lockfile {
87133
}
88134

89135
pub fn packages(&self) -> impl Iterator<Item = &PackageLock> {
90-
self.packages.iter()
136+
self.packages.values().flatten()
137+
}
138+
139+
pub fn packages_by_name<'a>(
140+
&'a self,
141+
name: &PackageName,
142+
) -> Box<dyn Iterator<Item = &'a PackageLock> + 'a> {
143+
if let Some(locks) = self.packages.get(name) {
144+
Box::new(locks.iter())
145+
} else {
146+
Box::new(std::iter::empty())
147+
}
91148
}
92149

93-
pub fn packages_matching(&self, dependency: ManifestDependency) -> Option<Result<PackageId>> {
94-
self.packages()
95-
.filter(|p| dependency.matches_name_and_version(&p.name, &p.version))
96-
.find(|p| {
150+
pub fn package_matching(&self, dependency: ManifestDependency) -> Option<Result<PackageId>> {
151+
self.packages_by_name(&dependency.name)
152+
.find(|p| dependency.matches_name_and_version(&p.name, &p.version))
153+
.filter(|p| {
97154
p.source
98155
.map(|sid| sid.can_lock_source_id(dependency.source_id))
99156
// No locking occurs on path sources.

scarb/src/core/resolver.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ impl Resolve {
196196
/// In all of these cases, we want to report an error to indicate that something is awry.
197197
/// Normal execution (esp. just using the default registry) should never run into this.
198198
pub fn check_checksums(&self, lockfile: &Lockfile) -> Result<()> {
199-
for package_lock in &lockfile.packages {
199+
for package_lock in lockfile.packages() {
200200
let (locked, source_id) = match (package_lock.checksum.as_ref(), package_lock.source) {
201201
(None, None) => continue,
202202
(Some(_), None) => {

scarb/src/resolver/algorithm/mod.rs

+2-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::core::registry::Registry;
33
use crate::core::registry::patch_map::PatchMap;
44
use crate::core::{PackageId, Resolve, Summary};
55
use crate::resolver::algorithm::provider::{
6-
DependencyProviderError, PubGrubDependencyProvider, PubGrubPackage, rewrite_locked_dependency,
6+
DependencyProviderError, PubGrubDependencyProvider, PubGrubPackage, lock_dependency,
77
};
88
use crate::resolver::algorithm::solution::{build_resolve, validate_solution};
99
use crate::resolver::algorithm::state::{Request, ResolverState};
@@ -86,12 +86,7 @@ pub async fn resolve(
8686
for summary in summaries {
8787
for dep in summary.full_dependencies() {
8888
let dep = patch_map.lookup(dep);
89-
let locked_package_id = lockfile.packages_matching(dep.clone());
90-
let dep = if let Some(locked_package_id) = locked_package_id {
91-
rewrite_locked_dependency(dep.clone(), locked_package_id?)
92-
} else {
93-
dep.clone()
94-
};
89+
let dep = lock_dependency(&lockfile, dep.clone())?;
9590
if state.index.packages().register(dep.clone()) {
9691
request_sink.send(Request::Package(dep.clone())).await?;
9792
}

scarb/src/resolver/algorithm/provider.rs

+37-7
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ impl DependencyProvider for PubGrubDependencyProvider {
249249
.unwrap();
250250
}
251251

252-
// Prioritize by ordering from root.
252+
// Prioritize by ordering from the root.
253253
let priority = self.priority.read().unwrap().get(package).copied();
254254
if let Some(priority) = priority {
255255
return Some(PubGrubPriority::Unspecified(Reverse(priority)));
@@ -268,13 +268,38 @@ impl DependencyProvider for PubGrubDependencyProvider {
268268
// Query available versions.
269269
let dependency: ManifestDependency = package.to_dependency(range.clone());
270270
let summaries = self.wait_for_summaries(dependency)?;
271+
let summaries = summaries
272+
.into_iter()
273+
.filter(|summary| range.contains(&summary.package_id.version))
274+
.sorted_by_key(|summary| summary.package_id.version.clone())
275+
.collect_vec();
271276

272277
// Choose version.
273-
let summary = summaries
274-
.into_iter()
275-
.find(|summary| range.contains(&summary.package_id.version));
278+
let locked = self.lockfile.packages_by_name(&package.name).find(|p| {
279+
p.name == package.name
280+
&& range.contains(&p.version)
281+
&& p.source
282+
.map(|source| {
283+
// Git sources are rewritten to the locked source before fetching summaries.
284+
source.is_registry() && source.can_lock_source_id(package.source_id)
285+
})
286+
.unwrap_or_default()
287+
});
288+
let summary = locked
289+
.and_then(|locked| {
290+
summaries
291+
.iter()
292+
.find(|summary| {
293+
summary.package_id.name == locked.name
294+
&& summary.package_id.version == locked.version
295+
&& summary.package_id.source_id == locked.source.expect("source set to `None` is filtered out when searching the lockfile")
296+
})
297+
.cloned()
298+
})
299+
// No version locked - using the highest matching summary.
300+
.or_else(|| summaries.last().cloned());
276301

277-
// Store retrieved summary for selected version.
302+
// Store retrieved summary for the selected version.
278303
if let Some(summary) = summary.as_ref() {
279304
self.packages
280305
.write()
@@ -364,12 +389,17 @@ impl From<DependencyVersionReq> for SemverPubgrub {
364389

365390
/// Check lockfile for a matching package.
366391
/// Rewrite the dependency if a matching package is found.
367-
fn lock_dependency(
392+
pub fn lock_dependency(
368393
lockfile: &Lockfile,
369394
dep: ManifestDependency,
370395
) -> Result<ManifestDependency, DependencyProviderError> {
396+
if dep.source_id.is_registry() {
397+
// We do not rewrite to the locked version for registry dependencies
398+
// because they will be locked in the `choose_version` step of pubgrub.
399+
return Ok(dep);
400+
}
371401
lockfile
372-
.packages_matching(dep.clone())
402+
.package_matching(dep.clone())
373403
.map(|locked_package_id| Ok(rewrite_locked_dependency(dep.clone(), locked_package_id?)))
374404
.unwrap_or(Ok(dep))
375405
}

scarb/src/resolver/mod.rs

-7
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,6 @@ mod tests {
548548
}
549549

550550
#[test]
551-
#[ignore = "TODO(#2031): upgrade lock that matches only part of the deps"]
552551
fn lock_matching_only_some_deps_is_upgraded() {
553552
check_with_lock(
554553
registry![("foo v0.1.0", []), ("foo v0.1.1", [])],
@@ -567,12 +566,6 @@ mod tests {
567566
// with the following error.
568567
// Ideally, the lock should be upgraded instead.
569568
Ok(pkgs!["foo v0.1.1"]),
570-
// Current error message:
571-
// Err(indoc! { r#"
572-
// version solving failed:
573-
// Because root_2 1.0.0 depends on foo >=0.1.1, <0.2.0 and root_1 1.0.0 depends on foo >=0.1.0, <0.1.1, root_2 1.0.0, root_1 1.0.0 are incompatible.
574-
// And because we are solving dependencies of root_2 1.0.0, root_1 1.0.0 is forbidden.
575-
// "#}),
576569
);
577570
}
578571

scarb/src/resolver/primitive.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ pub async fn resolve(
7171
for dep in summary.filtered_full_dependencies(dep_filter) {
7272
let dep = rewrite_dependency_source_id(registry, &package_id, dep).await?;
7373

74-
let locked_package_id = lockfile.packages_matching(dep.clone());
74+
let locked_package_id = lockfile.package_matching(dep.clone());
7575
let dep = if let Some(locked_package_id) = locked_package_id {
7676
rewrite_locked_dependency(dep.clone(), locked_package_id?)
7777
} else {

0 commit comments

Comments
 (0)