Skip to main content

libwally/
resolution.rs

1use std::cmp::Ordering;
2use std::collections::{BTreeMap, BTreeSet, VecDeque};
3
4use anyhow::bail;
5use anyhow::format_err;
6use semver::Version;
7use serde::Serialize;
8
9use crate::manifest::{Manifest, Realm};
10use crate::package_id::PackageId;
11use crate::package_req::PackageReq;
12use crate::package_source::{PackageSourceId, PackageSourceMap, PackageSourceProvider};
13
14/// A completely resolved graph of packages returned by `resolve`.
15///
16/// State here is stored in multiple maps, all keyed by PackageId, to facilitate
17/// concurrent mutable access to unrelated information about different packages.
18#[derive(Debug, Default, Serialize, Clone)]
19pub struct Resolve {
20    /// Set of all packages that have been chosen to be part of the package
21    /// graph.
22    pub activated: BTreeSet<PackageId>,
23
24    /// Metadata stored about each package that does not need to be accessed
25    /// concurrently to other information.
26    pub metadata: BTreeMap<PackageId, ResolvePackageMetadata>,
27
28    /// Graph of all dependencies originating from the "shared" dependency realm.
29    pub shared_dependencies: BTreeMap<PackageId, BTreeMap<String, PackageId>>,
30
31    /// Graph of all dependencies originating from the "server" dependency realm.
32    pub server_dependencies: BTreeMap<PackageId, BTreeMap<String, PackageId>>,
33
34    /// Graph of all dependencies originating from the "dev" dependency realm.
35    pub dev_dependencies: BTreeMap<PackageId, BTreeMap<String, PackageId>>,
36}
37
38impl Resolve {
39    fn activate(&mut self, source: PackageId, dep_name: String, dep_realm: Realm, dep: PackageId) {
40        self.activated.insert(dep.clone());
41
42        let dependencies = match dep_realm {
43            Realm::Shared => self.shared_dependencies.entry(source).or_default(),
44            Realm::Server => self.server_dependencies.entry(source).or_default(),
45            Realm::Dev => self.dev_dependencies.entry(source).or_default(),
46        };
47        dependencies.insert(dep_name, dep);
48    }
49}
50
51/// A single node in the package resolution graph.
52/// Origin realm is the "most restrictive" realm the package can still be dependended
53/// upon. It is where the package gets placed during install.
54/// See [ origin_realm clarification ]. In the resolve function for more info.
55#[derive(Debug, Serialize, Clone)]
56pub struct ResolvePackageMetadata {
57    pub realm: Realm,
58    pub origin_realm: Realm,
59    pub source_registry: PackageSourceId,
60}
61
62pub fn resolve(
63    root_manifest: &Manifest,
64    try_to_use: &BTreeSet<PackageId>,
65    package_sources: &PackageSourceMap,
66) -> anyhow::Result<Resolve> {
67    let mut resolve = Resolve::default();
68
69    // Insert root project into graph and activated dependencies, as it'll
70    // always be present.
71    resolve.activated.insert(root_manifest.package_id());
72    resolve.metadata.insert(
73        root_manifest.package_id(),
74        ResolvePackageMetadata {
75            realm: root_manifest.package.realm,
76            origin_realm: root_manifest.package.realm,
77            source_registry: PackageSourceId::DefaultRegistry,
78        },
79    );
80
81    // Queue of all dependency requests that need to be resolved.
82    let mut packages_to_visit = VecDeque::new();
83
84    for (alias, req) in &root_manifest.dependencies {
85        packages_to_visit.push_back(DependencyRequest {
86            request_source: root_manifest.package_id(),
87            request_realm: Realm::Shared,
88            origin_realm: Realm::Shared,
89            package_alias: alias.clone(),
90            package_req: req.clone(),
91        });
92    }
93
94    for (alias, req) in &root_manifest.server_dependencies {
95        packages_to_visit.push_back(DependencyRequest {
96            request_source: root_manifest.package_id(),
97            request_realm: Realm::Server,
98            origin_realm: Realm::Server,
99            package_alias: alias.clone(),
100            package_req: req.clone(),
101        });
102    }
103
104    for (alias, req) in &root_manifest.dev_dependencies {
105        packages_to_visit.push_back(DependencyRequest {
106            request_source: root_manifest.package_id(),
107            request_realm: Realm::Dev,
108            origin_realm: Realm::Dev,
109            package_alias: alias.clone(),
110            package_req: req.clone(),
111        });
112    }
113
114    // Workhorse loop: resolve all dependencies, depth-first.
115    'outer: while let Some(dependency_request) = packages_to_visit.pop_front() {
116        // Locate all already-activated packages that might match this
117        // dependency request.
118        let mut matching_activated: Vec<_> = resolve
119            .activated
120            .iter()
121            .filter(|package_id| package_id.name() == dependency_request.package_req.name())
122            .cloned()
123            .collect();
124
125        // Sort our list of candidates by descending version so that we can pick
126        // newest candidates first.
127        matching_activated.sort_by(|a, b| b.version().cmp(a.version()));
128
129        // Check for the highest version already-activated package that matches
130        // our constraints.
131        for package_id in &matching_activated {
132            if dependency_request.package_req.matches_id(package_id) {
133                let metadata = resolve
134                    .metadata
135                    .get_mut(package_id)
136                    .expect("activated package was missing metadata");
137
138                // [ origin_realm clarification ]
139                // We want to set the origin to the most restrictive origin possible.
140                // For example we want to keep packages in the dev realm unless a dependency
141                // with a shared/server origin requires it. This way server/shared dependencies
142                // which only originate from dev dependencies get put into the dev folder even
143                // if they usually belong to another realm. Likewise we want to keep shared
144                // dependencies in the server realm unless they are explicitly required as a
145                // shared dependency.
146                let realm_match = match (metadata.origin_realm, dependency_request.origin_realm) {
147                    (_, Realm::Shared) => Realm::Shared,
148                    (Realm::Shared, _) => Realm::Shared,
149                    (_, Realm::Server) => Realm::Server,
150                    (Realm::Server, _) => Realm::Server,
151                    (Realm::Dev, Realm::Dev) => Realm::Dev,
152                };
153
154                metadata.origin_realm = realm_match;
155
156                resolve.activate(
157                    dependency_request.request_source.clone(),
158                    dependency_request.package_alias.clone(),
159                    realm_match,
160                    package_id.clone(),
161                );
162
163                continue 'outer;
164            }
165        }
166
167        // Look through all our packages sources in order of priority
168        let (source_registry, mut candidates) = package_sources
169            .source_order()
170            .iter()
171            .find_map(|source| {
172                let registry = package_sources.get(source).unwrap();
173
174                // Pull all of the possible candidate versions of the package we're
175                // looking for from the highest priority source which has them.
176                match registry.query(&dependency_request.package_req) {
177                    Ok(manifests) => Some((source, manifests)),
178                    Err(_) => None,
179                }
180            })
181            .ok_or_else(|| {
182                format_err!(
183                    "Failed to find a source for {}",
184                    dependency_request.package_req
185                )
186            })?;
187
188        // Sort our candidate packages by descending version, so that we try the
189        // highest versions first.
190        //
191        // Additionally, if there were any packages that were previously used by
192        // our lockfile (in `try_to_use`), prioritize those first. This
193        // technique is the one used by Cargo.
194        candidates.sort_by(|a, b| {
195            let contains_a = try_to_use.contains(&a.package_id());
196            let contains_b = try_to_use.contains(&b.package_id());
197
198            match (contains_a, contains_b) {
199                (true, false) => Ordering::Less,
200                (false, true) => Ordering::Greater,
201                _ => b.package.version.cmp(&a.package.version),
202            }
203        });
204
205        let filtered_candidates = candidates.iter().filter(|candidate| {
206            Realm::is_dependency_valid(dependency_request.request_realm, candidate.package.realm)
207        });
208
209        let mut conflicting = Vec::new();
210
211        for candidate in filtered_candidates {
212            // Conflicts occur if two packages are SemVer compatible. We choose
213            // to only allow one compatible copy of a given package to prevent
214            // common user errors.
215
216            let has_conflicting = matching_activated
217                .iter()
218                .any(|activated| compatible(&candidate.package.version, activated.version()));
219
220            if has_conflicting {
221                // This is a matching candidate, but it conflicts with a
222                // candidate we already selected before. We'll note that this
223                // happened. If there are no other matching versions that don't
224                // conflict, we'll report this in an error.
225
226                conflicting.push(candidate.package_id());
227                continue;
228            }
229
230            let candidate_id = PackageId::new(
231                candidate.package.name.clone(),
232                candidate.package.version.clone(),
233            );
234
235            resolve.activate(
236                dependency_request.request_source.clone(),
237                dependency_request.package_alias.to_owned(),
238                dependency_request.origin_realm,
239                candidate_id.clone(),
240            );
241
242            resolve.metadata.insert(
243                candidate_id.clone(),
244                ResolvePackageMetadata {
245                    realm: candidate.package.realm,
246                    origin_realm: dependency_request.origin_realm,
247                    source_registry: source_registry.clone(),
248                },
249            );
250
251            for (alias, req) in &candidate.dependencies {
252                packages_to_visit.push_back(DependencyRequest {
253                    request_source: candidate_id.clone(),
254                    request_realm: Realm::Shared,
255                    origin_realm: dependency_request.origin_realm,
256                    package_alias: alias.clone(),
257                    package_req: req.clone(),
258                })
259            }
260
261            for (alias, req) in &candidate.server_dependencies {
262                packages_to_visit.push_back(DependencyRequest {
263                    request_source: candidate_id.clone(),
264                    request_realm: Realm::Server,
265                    origin_realm: dependency_request.origin_realm,
266                    package_alias: alias.clone(),
267                    package_req: req.clone(),
268                })
269            }
270
271            continue 'outer;
272        }
273
274        if conflicting.is_empty() {
275            bail!(
276                "No packages were found that matched ({req_realm:?}) {req}.\
277                \nAre you sure this is a {req_realm:?} dependency?",
278                req_realm = dependency_request.request_realm,
279                req = dependency_request.package_req,
280            );
281        } else {
282            let conflicting_debug: Vec<_> = conflicting
283                .into_iter()
284                .map(|id| format!("{:?}", id))
285                .collect();
286
287            bail!(
288                "All possible candidates for package {req} ({req_realm:?}) \
289                 conflicted with other packages that were already installed. \
290                 These packages were previously selected: {conflicting}",
291                req = dependency_request.package_req,
292                req_realm = dependency_request.request_realm,
293                conflicting = conflicting_debug.join(", "),
294            );
295        }
296    }
297
298    Ok(resolve)
299}
300
301fn compatible(a: &Version, b: &Version) -> bool {
302    if a == b {
303        return true;
304    }
305
306    if a.major == 0 && b.major == 0 {
307        a.minor == b.minor
308    } else {
309        a.major == b.major
310    }
311}
312
313pub struct DependencyRequest {
314    request_source: PackageId,
315    request_realm: Realm,
316    origin_realm: Realm,
317    package_alias: String,
318    package_req: PackageReq,
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    use crate::{
326        package_name::PackageName, package_source::InMemoryRegistry, test_package::PackageBuilder,
327    };
328
329    fn test_project(registry: InMemoryRegistry, package: PackageBuilder) -> anyhow::Result<()> {
330        let package_sources = PackageSourceMap::new(Box::new(registry.source()));
331        let manifest = package.into_manifest();
332        let resolve = resolve(&manifest, &Default::default(), &package_sources)?;
333        insta::assert_yaml_snapshot!(resolve);
334        Ok(())
335    }
336
337    #[test]
338    fn minimal() -> anyhow::Result<()> {
339        let registry = InMemoryRegistry::new();
340
341        let root = PackageBuilder::new("biff/minimal@0.1.0");
342        test_project(registry, root)
343    }
344
345    #[test]
346    fn one_dependency() -> anyhow::Result<()> {
347        let registry = InMemoryRegistry::new();
348        registry.publish(PackageBuilder::new("biff/minimal@0.1.0"));
349        registry.publish(PackageBuilder::new("biff/minimal@0.2.0"));
350
351        let root = PackageBuilder::new("biff/one-dependency@0.1.0")
352            .with_dep("Minimal", "biff/minimal@0.1.0");
353        test_project(registry, root)
354    }
355
356    #[test]
357    fn transitive_dependency() -> anyhow::Result<()> {
358        let registry = InMemoryRegistry::new();
359        registry.publish(PackageBuilder::new("biff/minimal@0.1.0"));
360        registry.publish(
361            PackageBuilder::new("biff/one-dependency@0.1.0")
362                .with_dep("Minimal", "biff/minimal@0.1.0"),
363        );
364
365        let root = PackageBuilder::new("biff/transitive-dependency@0.1.0")
366            .with_dep("OneDependency", "biff/one-dependency@0.1.0");
367        test_project(registry, root)
368    }
369
370    /// When there are shared dependencies, Wally should select the same
371    /// dependency. Here, A depends on B and C, which both in turn depend on D.
372    #[test]
373    fn unified_dependencies() -> anyhow::Result<()> {
374        let registry = InMemoryRegistry::new();
375        registry.publish(PackageBuilder::new("biff/b@1.0.0").with_dep("D", "biff/d@1.0.0"));
376        registry.publish(PackageBuilder::new("biff/c@1.0.0").with_dep("D", "biff/d@1.0.0"));
377        registry.publish(PackageBuilder::new("biff/d@1.0.0"));
378
379        let root = PackageBuilder::new("biff/a@1.0.0")
380            .with_dep("B", "biff/b@1.0.0")
381            .with_dep("C", "biff/c@1.0.0");
382
383        test_project(registry, root)
384    }
385
386    /// Server dependencies are allowed to depend on shared dependencies. If a
387    /// shared dependency is only depended on by server dependencies, it should
388    /// be marked as server-only.
389    #[test]
390    fn server_to_shared() -> anyhow::Result<()> {
391        let registry = InMemoryRegistry::new();
392        registry.publish(PackageBuilder::new("biff/shared@1.0.0"));
393        registry.publish(
394            PackageBuilder::new("biff/server@1.0.0")
395                .with_realm(Realm::Server)
396                .with_dep("Shared", "biff/shared@1.0.0"),
397        );
398
399        let root =
400            PackageBuilder::new("biff/root@1.0.0").with_server_dep("Server", "biff/server@1.0.0");
401
402        test_project(registry, root)
403    }
404
405    /// but... if that shared dependency is required by another shared dependency,
406    /// (while not being also server-only) it's not server-only anymore.
407    #[test]
408    fn server_to_shared_and_shared_to_shared() -> anyhow::Result<()> {
409        let registry = InMemoryRegistry::new();
410        registry.publish(PackageBuilder::new("biff/shared@1.0.0"));
411        registry.publish(
412            PackageBuilder::new("biff/server@1.0.0")
413                .with_realm(Realm::Server)
414                .with_dep("Shared", "biff/shared@1.0.0"),
415        );
416
417        let root = PackageBuilder::new("biff/root@1.0.0")
418            .with_server_dep("Server", "biff/server@1.0.0")
419            .with_dep("Shared", "biff/shared@1.0.0");
420
421        test_project(registry, root)
422    }
423
424    /// Shared dependencies are allowed to depend on server dependencies. Server
425    /// dependencies should always be marked as server-only.
426    #[test]
427    fn shared_to_server() -> anyhow::Result<()> {
428        let registry = InMemoryRegistry::new();
429        registry.publish(PackageBuilder::new("biff/server@1.0.0").with_realm(Realm::Server));
430
431        let root =
432            PackageBuilder::new("biff/root@1.0.0").with_server_dep("Server", "biff/server@1.0.0");
433
434        test_project(registry, root)
435    }
436
437    #[test]
438    fn fail_server_in_shared() {
439        let registry = InMemoryRegistry::new();
440        registry.publish(PackageBuilder::new("biff/server@1.0.0").with_realm(Realm::Server));
441
442        let root = PackageBuilder::new("biff/root@1.0.0").with_dep("Server", "biff/server@1.0.0");
443
444        let package_sources = PackageSourceMap::new(Box::new(registry.source()));
445        let err = resolve(root.manifest(), &Default::default(), &package_sources).unwrap_err();
446        insta::assert_display_snapshot!(err);
447    }
448
449    /// Tests the simple one dependency case, except that a new version of the
450    /// dependency will be published after the initial resolve. By persisting
451    /// the set of activated packages from the initial install, we signal that
452    /// the dependency should not be upgraded.
453    #[test]
454    fn one_dependency_no_upgrade() -> anyhow::Result<()> {
455        let registry = InMemoryRegistry::new();
456        registry.publish(PackageBuilder::new("biff/minimal@1.0.0"));
457
458        let root = PackageBuilder::new("biff/one-dependency@1.0.0")
459            .with_dep("Minimal", "biff/minimal@1.0.0");
460
461        let package_sources = PackageSourceMap::new(Box::new(registry.source()));
462
463        let resolved = resolve(root.manifest(), &Default::default(), &package_sources)?;
464        insta::assert_yaml_snapshot!("one_dependency_no_upgrade", resolved);
465
466        registry.publish(PackageBuilder::new("biff/minimal@1.1.0"));
467        let new_resolved = resolve(root.manifest(), &resolved.activated, &package_sources)?;
468        insta::assert_yaml_snapshot!("one_dependency_no_upgrade", new_resolved);
469
470        Ok(())
471    }
472
473    #[test]
474    fn one_dependency_yes_upgrade() -> anyhow::Result<()> {
475        let registry = InMemoryRegistry::new();
476        registry.publish(PackageBuilder::new("biff/minimal@1.0.0"));
477
478        let root = PackageBuilder::new("biff/one-dependency@1.0.0")
479            .with_dep("Minimal", "biff/minimal@1.0.0");
480
481        let package_sources = PackageSourceMap::new(Box::new(registry.source()));
482
483        let resolved = resolve(root.manifest(), &Default::default(), &package_sources)?;
484        insta::assert_yaml_snapshot!(resolved);
485
486        // We can indicate that we'd like to upgrade a package by just removing
487        // it from the try_to_use set!
488        let remove_this: PackageName = "biff/minimal".parse().unwrap();
489        let try_to_use = resolved
490            .activated
491            .into_iter()
492            .filter(|id| id.name() != &remove_this)
493            .collect();
494
495        registry.publish(PackageBuilder::new("biff/minimal@1.1.0"));
496        let new_resolved = resolve(root.manifest(), &try_to_use, &package_sources)?;
497        insta::assert_yaml_snapshot!(new_resolved);
498
499        Ok(())
500    }
501}