wasmer_wasix/runtime/resolver/
resolve.rs

1use std::{
2    collections::{BTreeMap, BTreeSet, HashSet, VecDeque},
3    path::PathBuf,
4};
5
6use petgraph::{
7    graph::{DiGraph, NodeIndex},
8    visit::EdgeRef,
9};
10use semver::Version;
11use wasmer_config::package::{NamedPackageIdent, PackageId, PackageSource};
12
13use crate::runtime::resolver::{
14    Dependency, DependencyGraph, ItemLocation, PackageInfo, PackageSummary, QueryError, Resolution,
15    ResolvedPackage, Source,
16    outputs::{Edge, Node},
17    utils::cmp_version_precedence,
18};
19
20use super::ResolvedFileSystemMapping;
21
22const MAX_DISCOVERED_PACKAGES: usize = 1_000;
23const MAX_PENDING_DEPENDENCIES: usize = 4_000;
24const MAX_BACKTRACK_STATES: usize = 10_000;
25
26/// Given the [`PackageInfo`] for a root package, resolve its dependency graph
27/// and figure out how it could be executed.
28#[tracing::instrument(level = "debug", skip_all)]
29pub async fn resolve(
30    root_id: &PackageId,
31    root: &PackageInfo,
32    source: &dyn Source,
33) -> Result<Resolution, ResolveError> {
34    let graph = resolve_dependency_graph(root_id, root, source).await?;
35    validate_dependency_graph(&graph)?;
36    let package = resolve_package(&graph)?;
37
38    Ok(Resolution { graph, package })
39}
40
41#[derive(Debug, thiserror::Error)]
42pub enum ResolveError {
43    #[error("{}", registry_error_message(.package))]
44    Registry {
45        package: PackageSource,
46        #[source]
47        error: QueryError,
48    },
49    #[error("Dependency cycle detected: {}", print_cycle(_0))]
50    Cycle(Vec<PackageId>),
51    #[error(
52        "Multiple versions of {package_name} were found {}",
53        versions.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "),
54    )]
55    DuplicateVersions {
56        package_name: String,
57        versions: Vec<Version>,
58    },
59    #[error("Dependency resolution exceeded safety limits ({resource}={value}, limit={limit})")]
60    TooComplex {
61        resource: &'static str,
62        value: usize,
63        limit: usize,
64    },
65}
66
67fn registry_error_message(specifier: &PackageSource) -> String {
68    match specifier {
69        PackageSource::Ident(id) => {
70            format!("Unable to find \"{id}\" in the registry")
71        }
72        PackageSource::Url(url) => format!("Unable to resolve \"{url}\""),
73        PackageSource::Path(path) => {
74            format!("Unable to load \"{path}\" from disk")
75        }
76    }
77}
78
79impl ResolveError {
80    pub fn as_cycle(&self) -> Option<&[PackageId]> {
81        match self {
82            ResolveError::Cycle(cycle) => Some(cycle),
83            _ => None,
84        }
85    }
86}
87
88fn print_cycle(packages: &[PackageId]) -> String {
89    packages
90        .iter()
91        .map(|pkg_id| pkg_id.to_string())
92        .collect::<Vec<_>>()
93        .join(" → ")
94}
95
96/// Given the [`PackageInfo`] for a root package, discover its dependency graph.
97///
98/// Unlike [`resolve()`], this only queries and links packages. It does not apply
99/// runtime compatibility checks such as rejecting duplicate package versions.
100#[tracing::instrument(level = "debug", skip_all)]
101pub async fn resolve_dependency_graph(
102    root_id: &PackageId,
103    root: &PackageInfo,
104    source: &dyn Source,
105) -> Result<DependencyGraph, ResolveError> {
106    let DiscoveredPackages {
107        root,
108        graph,
109        packages,
110        ..
111    } = discover_dependencies(root_id, root, source).await?;
112
113    log_dependencies(&graph, root);
114
115    let graph = DependencyGraph::new(root, graph, packages);
116
117    Ok(graph)
118}
119
120/// Validate whether a dependency graph can be unified into a runnable package.
121pub fn validate_dependency_graph(graph: &DependencyGraph) -> Result<(), ResolveError> {
122    let sorted =
123        petgraph::algo::toposort(graph.graph(), None).map_err(|_| cycle_error(graph.graph()))?;
124
125    check_for_duplicate_versions(sorted.iter().map(|ix| &graph[*ix].id))
126}
127
128async fn discover_dependencies(
129    root_id: &PackageId,
130    root: &PackageInfo,
131    source: &dyn Source,
132) -> Result<DiscoveredPackages, ResolveError> {
133    // First try a fast fixpoint pass that merges obviously compatible versions.
134    let heuristic = discover_merged_dependencies(root_id, root, source).await?;
135    if discovered_packages_are_unified(&heuristic) {
136        return Ok(heuristic);
137    }
138
139    // If the greedy pass still leaves duplicate named packages behind, search
140    // alternate version choices so lower direct versions can win when they are
141    // the only way to keep the full graph consistent.
142    if let Some(discovered) = discover_dependencies_with_backtracking(root_id, root, source).await?
143    {
144        return Ok(discovered);
145    }
146
147    Ok(heuristic)
148}
149
150async fn discover_merged_dependencies(
151    root_id: &PackageId,
152    root: &PackageInfo,
153    source: &dyn Source,
154) -> Result<DiscoveredPackages, ResolveError> {
155    let mut selected_named = BTreeMap::new();
156    let mut candidate_cache = BTreeMap::new();
157    let mut seen_selections = BTreeSet::from([named_selection_signature(&selected_named)]);
158
159    loop {
160        let discovered = discover_dependencies_once(root_id, root, source, &selected_named).await?;
161        let constraints = named_dependency_constraints(&discovered.graph);
162        let next_selected =
163            select_named_dependencies(constraints, source, &mut candidate_cache).await?;
164
165        if same_named_selection(&selected_named, &next_selected) {
166            return Ok(discovered);
167        }
168
169        if !seen_selections.insert(named_selection_signature(&next_selected)) {
170            return Ok(discovered);
171        }
172
173        selected_named = next_selected;
174    }
175}
176
177async fn discover_dependencies_once(
178    root_id: &PackageId,
179    root: &PackageInfo,
180    source: &dyn Source,
181    selected_named: &BTreeMap<String, PackageSummary>,
182) -> Result<DiscoveredPackages, ResolveError> {
183    let mut nodes: BTreeMap<PackageId, NodeIndex> = BTreeMap::new();
184    let mut graph: DiGraph<Node, Edge> = DiGraph::new();
185
186    let root_index = graph.add_node(Node {
187        id: root_id.clone(),
188        pkg: root.clone(),
189        dist: None,
190    });
191    nodes.insert(root_id.clone(), root_index);
192
193    let mut to_visit = VecDeque::new();
194    to_visit.push_back(root_index);
195
196    while let Some(index) = to_visit.pop_front() {
197        let mut to_add = Vec::new();
198
199        for dep in &graph[index].pkg.dependencies {
200            let dep_summary = resolve_dependency(dep, source, selected_named)
201                .await
202                .map_err(|error| ResolveError::Registry {
203                    package: dep.pkg.clone(),
204                    error,
205                })?;
206            let dep_id = dep_summary.package_id();
207
208            let PackageSummary { pkg, dist } = dep_summary;
209
210            let alias = dep.alias().to_string();
211            let node = Node {
212                id: dep_id.clone(),
213                pkg,
214                dist: Some(dist),
215            };
216            // Note: We can't add the node to the graph directly because we're
217            // still iterating over it.
218            to_add.push((alias, node));
219        }
220
221        for (alias, node) in to_add {
222            let dep_id = node.id.clone();
223
224            let dep_index = match nodes.get(&dep_id) {
225                Some(&ix) => ix,
226                None => {
227                    if nodes.len() >= MAX_DISCOVERED_PACKAGES {
228                        return Err(ResolveError::TooComplex {
229                            resource: "packages",
230                            value: nodes.len() + 1,
231                            limit: MAX_DISCOVERED_PACKAGES,
232                        });
233                    }
234                    // Create a new node and schedule its dependencies to be
235                    // retrieved
236                    let ix = graph.add_node(node);
237                    nodes.insert(dep_id, ix);
238                    to_visit.push_back(ix);
239                    if to_visit.len() > MAX_PENDING_DEPENDENCIES {
240                        return Err(ResolveError::TooComplex {
241                            resource: "pending_dependencies",
242                            value: to_visit.len(),
243                            limit: MAX_PENDING_DEPENDENCIES,
244                        });
245                    }
246                    ix
247                }
248            };
249
250            graph.add_edge(index, dep_index, Edge { alias });
251        }
252    }
253
254    petgraph::algo::toposort(&graph, None).map_err(|_| cycle_error(&graph))?;
255
256    Ok(DiscoveredPackages {
257        root: root_index,
258        graph,
259        packages: nodes,
260    })
261}
262
263async fn resolve_dependency(
264    dep: &Dependency,
265    source: &dyn Source,
266    selected_named: &BTreeMap<String, PackageSummary>,
267) -> Result<PackageSummary, QueryError> {
268    let candidates = source.query(&dep.pkg).await?;
269
270    match dep.pkg.as_named() {
271        Some(named) => {
272            if let Some(selected) = selected_named.get(&named.full_name())
273                && let Some(id) = selected.pkg.id.as_named()
274                && named.matches_id(id)
275            {
276                return Ok(selected.clone());
277            }
278
279            select_latest_named_dependency(candidates, dep)
280        }
281        None => candidates
282            .into_iter()
283            .next()
284            .ok_or_else(|| QueryError::NotFound {
285                query: dep.pkg.clone(),
286            }),
287    }
288}
289
290fn select_latest_named_dependency(
291    candidates: Vec<PackageSummary>,
292    dep: &Dependency,
293) -> Result<PackageSummary, QueryError> {
294    candidates
295        .into_iter()
296        .max_by(|left, right| {
297            let left_version = left.pkg.id.as_named().map(|id| &id.version);
298            let right_version = right.pkg.id.as_named().map(|id| &id.version);
299
300            cmp_version_precedence(left_version, right_version)
301        })
302        .ok_or_else(|| QueryError::NoMatches {
303            query: dep.pkg.clone(),
304            archived_versions: Vec::new(),
305        })
306}
307
308fn named_dependency_constraints(
309    graph: &DiGraph<Node, Edge>,
310) -> BTreeMap<String, Vec<NamedPackageIdent>> {
311    let mut constraints: BTreeMap<String, Vec<NamedPackageIdent>> = BTreeMap::new();
312
313    graph
314        .node_weights()
315        .flat_map(|node| &node.pkg.dependencies)
316        .filter_map(|dep| dep.pkg.as_named())
317        .for_each(|named| {
318            constraints
319                .entry(named.full_name())
320                .or_default()
321                .push(named.clone());
322        });
323
324    constraints
325}
326
327async fn select_named_dependencies(
328    constraints: BTreeMap<String, Vec<NamedPackageIdent>>,
329    source: &dyn Source,
330    candidate_cache: &mut BTreeMap<String, Vec<PackageSummary>>,
331) -> Result<BTreeMap<String, PackageSummary>, ResolveError> {
332    let mut selected = BTreeMap::new();
333
334    for (full_name, constraints) in constraints {
335        let Some(summary) =
336            select_unified_named_dependency(&constraints, source, candidate_cache).await?
337        else {
338            continue;
339        };
340        selected.insert(full_name, summary);
341    }
342
343    Ok(selected)
344}
345
346async fn select_unified_named_dependency(
347    constraints: &[NamedPackageIdent],
348    source: &dyn Source,
349    candidate_cache: &mut BTreeMap<String, Vec<PackageSummary>>,
350) -> Result<Option<PackageSummary>, ResolveError> {
351    let [first, remaining @ ..] = constraints else {
352        return Ok(None);
353    };
354
355    let mut candidates = named_dependency_candidates(first, source, candidate_cache).await?;
356
357    for constraint in remaining {
358        let matching_ids: HashSet<_> =
359            named_dependency_candidates(constraint, source, candidate_cache)
360                .await?
361                .into_iter()
362                .map(|summary| summary.package_id())
363                .collect();
364
365        candidates.retain(|candidate| matching_ids.contains(&candidate.package_id()));
366    }
367
368    Ok(candidates.into_iter().max_by(|left, right| {
369        let left_version = left.pkg.id.as_named().map(|id| &id.version);
370        let right_version = right.pkg.id.as_named().map(|id| &id.version);
371
372        cmp_version_precedence(left_version, right_version)
373    }))
374}
375
376fn same_named_selection(
377    left: &BTreeMap<String, PackageSummary>,
378    right: &BTreeMap<String, PackageSummary>,
379) -> bool {
380    left.len() == right.len()
381        && left.iter().all(|(name, summary)| {
382            right
383                .get(name)
384                .is_some_and(|other| summary.package_id() == other.package_id())
385        })
386}
387
388fn named_selection_signature(
389    selected: &BTreeMap<String, PackageSummary>,
390) -> Vec<(String, PackageId)> {
391    selected
392        .iter()
393        .map(|(name, summary)| (name.clone(), summary.package_id()))
394        .collect()
395}
396
397fn discovered_packages_are_unified(discovered: &DiscoveredPackages) -> bool {
398    let Ok(sorted) = petgraph::algo::toposort(&discovered.graph, None) else {
399        return false;
400    };
401
402    check_for_duplicate_versions(sorted.iter().map(|ix| &discovered.graph[*ix].id)).is_ok()
403}
404
405async fn discover_dependencies_with_backtracking(
406    root_id: &PackageId,
407    root: &PackageInfo,
408    source: &dyn Source,
409) -> Result<Option<DiscoveredPackages>, ResolveError> {
410    let mut candidate_cache = BTreeMap::new();
411    let mut stack = vec![PartialDiscovery::new(root_id, root)];
412    let mut states_explored = 0usize;
413
414    while let Some(state) = stack.pop() {
415        states_explored += 1;
416        if states_explored > MAX_BACKTRACK_STATES {
417            return Err(ResolveError::TooComplex {
418                resource: "backtrack_states",
419                value: states_explored,
420                limit: MAX_BACKTRACK_STATES,
421            });
422        }
423
424        let Some((task, remaining)) = state.pending.split_first() else {
425            if petgraph::algo::toposort(&state.graph, None).is_ok() {
426                let discovered = state.finish();
427                if discovered_packages_are_unified(&discovered) {
428                    return Ok(Some(discovered));
429                }
430            }
431            continue;
432        };
433
434        let candidates = dependency_candidates(
435            task.dep(),
436            source,
437            &state.selected_named,
438            &mut candidate_cache,
439        )
440        .await?;
441
442        for candidate in candidates.into_iter().rev() {
443            // This still clones the graph state per candidate, but the clone is
444            // bounded by the state, package, and pending-dependency limits.
445            let mut next = state.clone();
446            next.pending = remaining.to_vec();
447            next.add_dependency(task.parent(), task.dep(), candidate)?;
448            stack.push(next);
449        }
450    }
451
452    Ok(None)
453}
454
455async fn dependency_candidates(
456    dep: &Dependency,
457    source: &dyn Source,
458    selected_named: &BTreeMap<String, PackageSummary>,
459    candidate_cache: &mut BTreeMap<String, Vec<PackageSummary>>,
460) -> Result<Vec<PackageSummary>, ResolveError> {
461    match dep.pkg.as_named() {
462        Some(named) => {
463            if let Some(selected) = selected_named.get(&named.full_name()) {
464                let matches = if named
465                    .tag
466                    .as_ref()
467                    .is_some_and(|tag| tag.as_named().is_some())
468                {
469                    named_dependency_candidates(named, source, candidate_cache)
470                        .await?
471                        .into_iter()
472                        .any(|candidate| candidate.package_id() == selected.package_id())
473                } else {
474                    selected
475                        .pkg
476                        .id
477                        .as_named()
478                        .is_some_and(|id| named.matches_id(id))
479                };
480                return Ok(if matches {
481                    vec![selected.clone()]
482                } else {
483                    Vec::new()
484                });
485            }
486
487            let candidates = named_dependency_candidates(named, source, candidate_cache).await?;
488            Ok(candidates
489                .into_iter()
490                .filter(|candidate| candidate_matches_named_query(named, candidate))
491                .collect())
492        }
493        None => Ok(vec![
494            source
495                .query(&dep.pkg)
496                .await
497                .map_err(|error| ResolveError::Registry {
498                    package: dep.pkg.clone(),
499                    error,
500                })?
501                .into_iter()
502                .next()
503                .ok_or_else(|| ResolveError::Registry {
504                    package: dep.pkg.clone(),
505                    error: QueryError::NotFound {
506                        query: dep.pkg.clone(),
507                    },
508                })?,
509        ]),
510    }
511}
512
513async fn named_dependency_candidates(
514    named: &NamedPackageIdent,
515    source: &dyn Source,
516    candidate_cache: &mut BTreeMap<String, Vec<PackageSummary>>,
517) -> Result<Vec<PackageSummary>, ResolveError> {
518    let cache_key = named.build();
519
520    let candidates = match candidate_cache.get(&cache_key) {
521        Some(candidates) => candidates.clone(),
522        None => {
523            let query = PackageSource::from(named.clone());
524
525            let mut candidates =
526                source
527                    .query(&query)
528                    .await
529                    .map_err(|error| ResolveError::Registry {
530                        package: query.clone(),
531                        error,
532                    })?;
533            sort_named_candidates_desc(&mut candidates);
534            candidate_cache.insert(cache_key, candidates.clone());
535            candidates
536        }
537    };
538
539    Ok(candidates)
540}
541
542fn candidate_matches_named_query(named: &NamedPackageIdent, candidate: &PackageSummary) -> bool {
543    let Some(id) = candidate.pkg.id.as_named() else {
544        return false;
545    };
546
547    if named
548        .tag
549        .as_ref()
550        .is_some_and(|tag| tag.as_named().is_some())
551    {
552        named.full_name() == id.full_name
553    } else {
554        named.matches_id(id)
555    }
556}
557
558fn sort_named_candidates_desc(candidates: &mut [PackageSummary]) {
559    candidates.sort_by(|left, right| {
560        let left_version = left.pkg.id.as_named().map(|id| &id.version);
561        let right_version = right.pkg.id.as_named().map(|id| &id.version);
562
563        cmp_version_precedence(right_version, left_version)
564    });
565}
566
567fn cycle_error(graph: &petgraph::Graph<Node, Edge>) -> ResolveError {
568    // We know the graph has at least one cycle, so use SCC to find it.
569    let mut cycle = petgraph::algo::kosaraju_scc(graph)
570        .into_iter()
571        .find(|cycle| {
572            cycle.len() > 1
573                || cycle
574                    .first()
575                    .is_some_and(|node| graph.edges(*node).any(|edge| edge.target() == *node))
576        })
577        .expect("We know there is at least one cycle");
578
579    // we want the loop's starting node to be deterministic (for tests), and
580    // nodes with lower indices are normally closer to the root of the
581    // dependency tree.
582    let lowest_index_node = cycle.iter().copied().min().expect("Cycle is non-empty");
583
584    // We want the cycle vector to start with that node, so let's do a bit of
585    // shuffling
586    let offset = cycle
587        .iter()
588        .position(|&node| node == lowest_index_node)
589        .unwrap();
590    cycle.rotate_left(offset);
591
592    // Don't forget to make the cycle start and end with the same node
593    cycle.push(lowest_index_node);
594
595    let package_ids = cycle.into_iter().map(|ix| graph[ix].id.clone()).collect();
596    ResolveError::Cycle(package_ids)
597}
598
599#[derive(Debug)]
600struct DiscoveredPackages {
601    root: NodeIndex,
602    graph: DiGraph<Node, Edge>,
603    packages: BTreeMap<PackageId, NodeIndex>,
604}
605
606#[derive(Debug, Clone)]
607struct PartialDiscovery {
608    root: NodeIndex,
609    graph: DiGraph<Node, Edge>,
610    packages: BTreeMap<PackageId, NodeIndex>,
611    pending: Vec<PendingDependency>,
612    selected_named: BTreeMap<String, PackageSummary>,
613}
614
615impl PartialDiscovery {
616    fn new(root_id: &PackageId, root: &PackageInfo) -> Self {
617        let mut graph = DiGraph::new();
618        let root_index = graph.add_node(Node {
619            id: root_id.clone(),
620            pkg: root.clone(),
621            dist: None,
622        });
623
624        let packages = BTreeMap::from([(root_id.clone(), root_index)]);
625        let pending = root
626            .dependencies
627            .iter()
628            .cloned()
629            .map(|dep| PendingDependency {
630                parent: root_index,
631                dep,
632            })
633            .collect();
634
635        PartialDiscovery {
636            root: root_index,
637            graph,
638            packages,
639            pending,
640            selected_named: BTreeMap::new(),
641        }
642    }
643
644    fn add_dependency(
645        &mut self,
646        parent: NodeIndex,
647        dep: &Dependency,
648        summary: PackageSummary,
649    ) -> Result<(), ResolveError> {
650        let package_id = summary.package_id();
651        let PackageSummary { pkg, dist } = summary.clone();
652
653        if let Some(id) = pkg.id.as_named() {
654            self.selected_named
655                .entry(id.full_name.clone())
656                .or_insert(summary);
657        }
658
659        let child = match self.packages.get(&package_id) {
660            Some(&index) => index,
661            None => {
662                if self.packages.len() >= MAX_DISCOVERED_PACKAGES {
663                    return Err(ResolveError::TooComplex {
664                        resource: "packages",
665                        value: self.packages.len() + 1,
666                        limit: MAX_DISCOVERED_PACKAGES,
667                    });
668                }
669
670                let pending_len = self.pending.len() + pkg.dependencies.len();
671                if pending_len > MAX_PENDING_DEPENDENCIES {
672                    return Err(ResolveError::TooComplex {
673                        resource: "pending_dependencies",
674                        value: pending_len,
675                        limit: MAX_PENDING_DEPENDENCIES,
676                    });
677                }
678
679                let index = self.graph.add_node(Node {
680                    id: package_id.clone(),
681                    pkg: pkg.clone(),
682                    dist: Some(dist),
683                });
684                self.packages.insert(package_id, index);
685                self.pending.extend(
686                    pkg.dependencies
687                        .iter()
688                        .cloned()
689                        .map(|dep| PendingDependency { parent: index, dep }),
690                );
691                index
692            }
693        };
694
695        self.graph.add_edge(
696            parent,
697            child,
698            Edge {
699                alias: dep.alias().to_string(),
700            },
701        );
702
703        Ok(())
704    }
705
706    fn finish(self) -> DiscoveredPackages {
707        DiscoveredPackages {
708            root: self.root,
709            graph: self.graph,
710            packages: self.packages,
711        }
712    }
713}
714
715#[derive(Debug, Clone)]
716struct PendingDependency {
717    parent: NodeIndex,
718    dep: Dependency,
719}
720
721impl PendingDependency {
722    fn parent(&self) -> NodeIndex {
723        self.parent
724    }
725
726    fn dep(&self) -> &Dependency {
727        &self.dep
728    }
729}
730
731#[tracing::instrument(level = "debug", name = "dependencies", skip_all)]
732fn log_dependencies(graph: &DiGraph<Node, Edge>, root: NodeIndex) {
733    tracing::debug!(
734        root = root.index(),
735        dependency_count = graph.node_count(),
736        "Resolved dependencies",
737    );
738
739    if tracing::enabled!(tracing::Level::TRACE) {
740        petgraph::visit::depth_first_search(graph, [root], |event| {
741            if let petgraph::visit::DfsEvent::Discover(n, _) = event {
742                let package = &graph[n].id;
743                let dependencies: BTreeMap<_, _> = graph
744                    .edges(n)
745                    .map(|edge_ref| (&edge_ref.weight().alias, &graph[edge_ref.target()].id))
746                    .collect();
747
748                tracing::trace!(%package, ?dependencies);
749            }
750        });
751    }
752}
753
754/// As a workaround for the lack of "proper" dependency merging, we'll make sure
755/// only one copy of each package is in the dependency tree. If the same package
756/// is included in the tree multiple times, they all need to use the exact same
757/// version otherwise it's an error.
758fn check_for_duplicate_versions<'a, I>(package_ids: I) -> Result<(), ResolveError>
759where
760    I: Iterator<Item = &'a PackageId>,
761{
762    let mut package_versions: BTreeMap<&str, HashSet<&Version>> = BTreeMap::new();
763
764    for id in package_ids {
765        let Some(id) = id.as_named() else {
766            continue;
767        };
768        package_versions
769            .entry(&id.full_name)
770            .or_default()
771            .insert(&id.version);
772    }
773
774    for (package_name, versions) in package_versions {
775        if versions.len() > 1 {
776            let mut versions: Vec<_> = versions.into_iter().cloned().collect();
777            versions.sort();
778            return Err(ResolveError::DuplicateVersions {
779                package_name: package_name.to_string(),
780                versions,
781            });
782        }
783    }
784
785    Ok(())
786}
787
788/// Given some [`DiscoveredPackages`], figure out how the resulting "package"
789/// would look when loaded at runtime.
790fn resolve_package(dependency_graph: &DependencyGraph) -> Result<ResolvedPackage, ResolveError> {
791    // FIXME: This code is all super naive and will break the moment there
792    // are any conflicts or duplicate names.
793    tracing::trace!("Resolving the package");
794
795    let mut commands = BTreeMap::new();
796    let mut filesystem = Vec::new();
797
798    let mut entrypoint = dependency_graph.root_info().entrypoint.clone();
799
800    for index in petgraph::algo::toposort(dependency_graph.graph(), None).expect("acyclic") {
801        let node = &dependency_graph[index];
802        let id = &node.id;
803        let pkg = &node.pkg;
804
805        // update the entrypoint, if necessary
806        if entrypoint.is_none()
807            && let Some(entry) = &pkg.entrypoint
808        {
809            tracing::trace!(
810                entrypoint = entry.as_str(),
811                parent=%id,
812                "Inheriting the entrypoint",
813            );
814
815            entrypoint = Some(entry.clone());
816        }
817
818        for cmd in &pkg.commands {
819            // Note: We are traversing in topological order with the root at the
820            // start, so if we ever see any duplicates we should prefer the
821            // earlier copy and skip the later one.
822
823            match commands.entry(cmd.name.clone()) {
824                std::collections::btree_map::Entry::Vacant(entry) => {
825                    let resolved = ItemLocation {
826                        name: cmd.name.clone(),
827                        package: id.clone(),
828                    };
829                    entry.insert(resolved);
830                    tracing::trace!(
831                        command.name=cmd.name.as_str(),
832                        pkg=%id,
833                        "Discovered command",
834                    );
835                }
836                std::collections::btree_map::Entry::Occupied(_) => {
837                    tracing::trace!(
838                        command.name=cmd.name.as_str(),
839                        pkg=%id,
840                        "Ignoring duplicate command",
841                    );
842                }
843            }
844        }
845
846        for mapping in &pkg.filesystem {
847            let dep = match &mapping.dependency_name {
848                Some(name) => {
849                    let dep_index = dependency_graph
850                        .graph()
851                        .edges(index)
852                        .find(|edge| edge.weight().alias == *name)
853                        .unwrap()
854                        .target();
855                    &dependency_graph[dep_index].id
856                }
857                None => id,
858            };
859            filesystem.push(ResolvedFileSystemMapping {
860                mount_path: PathBuf::from(&mapping.mount_path),
861                original_path: mapping.original_path.clone(),
862                volume_name: mapping.volume_name.clone(),
863                package: dep.clone(),
864            })
865        }
866    }
867
868    if entrypoint.is_none() {
869        // We *still* haven't been able to figure out what the entrypoint for the
870        // resolved package should be. If there is only one command in the main
871        // package, let's assume they want to use that.
872        //
873        // This works around packages like saghul/quickjs and syrusakbary/cowsay
874        // which don't specify their entrypoints explicitly.
875        if let [cmd] = dependency_graph.root_info().commands.as_slice() {
876            tracing::debug!(
877                command = cmd.name.as_str(),
878                "No entrypoint specified. Falling back to the root package's only command.",
879            );
880            entrypoint = Some(cmd.name.clone());
881        }
882    }
883
884    tracing::debug!("resolved filesystem: {:?}", &filesystem);
885
886    Ok(ResolvedPackage {
887        root_package: dependency_graph.id().clone(),
888        commands,
889        entrypoint,
890        filesystem,
891    })
892}
893
894#[cfg(test)]
895mod tests {
896    use std::{path::PathBuf, time::Duration};
897
898    use wasmer_config::package::{NamedPackageIdent, PackageIdent, Tag};
899
900    use crate::runtime::resolver::{
901        Dependency, InMemorySource, MultiSource,
902        inputs::{DistributionInfo, FileSystemMapping, PackageInfo},
903    };
904
905    use super::*;
906
907    struct RegistryBuilder(InMemorySource);
908
909    impl RegistryBuilder {
910        fn new() -> Self {
911            RegistryBuilder(InMemorySource::new())
912        }
913
914        fn register(&mut self, name: &str, version: &str) -> AddPackageVersion<'_> {
915            let pkg = PackageInfo {
916                id: PackageId::new_named(name, version.parse().unwrap()),
917                dependencies: Vec::new(),
918                commands: Vec::new(),
919                entrypoint: None,
920                filesystem: Vec::new(),
921            };
922            let dist = DistributionInfo {
923                webc: format!("http://localhost/{name}@{version}")
924                    .parse()
925                    .unwrap(),
926                webc_sha256: [0; 32].into(),
927            };
928            let summary = PackageSummary { pkg, dist };
929
930            AddPackageVersion {
931                builder: &mut self.0,
932                summary,
933            }
934        }
935
936        fn finish(&self) -> MultiSource {
937            let mut registry = MultiSource::default();
938            registry.add_source(self.0.clone());
939            registry
940        }
941
942        fn finish_with_tags(&self, tags: BTreeMap<(String, String), String>) -> TagResolvingSource {
943            TagResolvingSource {
944                inner: self.finish(),
945                tags,
946            }
947        }
948
949        fn get(&self, id: &PackageId) -> &PackageSummary {
950            self.0.get(id).unwrap()
951        }
952
953        // fn get_named(&self, name: &str, version: &str) -> &PackageSummary {
954        //     let id = PackageId::new_named(name, version.parse().unwrap());
955        //     self.get(&id)
956        // }
957
958        fn start_dependency_graph(&self) -> DependencyGraphBuilder<'_> {
959            DependencyGraphBuilder {
960                dependencies: BTreeMap::new(),
961                source: &self.0,
962            }
963        }
964    }
965
966    #[derive(Debug)]
967    struct AddPackageVersion<'builder> {
968        builder: &'builder mut InMemorySource,
969        summary: PackageSummary,
970    }
971
972    impl AddPackageVersion<'_> {
973        fn with_dependency(&mut self, name: &str, version_constraint: &str) -> &mut Self {
974            self.with_aliased_dependency(name, name, version_constraint)
975        }
976
977        fn with_tagged_dependency(&mut self, name: &str, tag: &str) -> &mut Self {
978            let pkg = PackageSource::from(NamedPackageIdent {
979                registry: None,
980                namespace: None,
981                name: name.to_string(),
982                tag: Some(Tag::Named(tag.to_string())),
983            });
984
985            self.summary.pkg.dependencies.push(Dependency {
986                alias: name.to_string(),
987                pkg,
988            });
989
990            self
991        }
992
993        fn with_aliased_dependency(
994            &mut self,
995            alias: &str,
996            name: &str,
997            version_constraint: &str,
998        ) -> &mut Self {
999            let pkg = PackageSource::from(
1000                NamedPackageIdent::try_from_full_name_and_version(name, version_constraint)
1001                    .unwrap(),
1002            );
1003
1004            self.summary.pkg.dependencies.push(Dependency {
1005                alias: alias.to_string(),
1006                pkg,
1007            });
1008
1009            self
1010        }
1011
1012        fn with_command(&mut self, name: &str) -> &mut Self {
1013            self.summary
1014                .pkg
1015                .commands
1016                .push(crate::runtime::resolver::Command {
1017                    name: name.to_string(),
1018                });
1019            self
1020        }
1021
1022        fn with_entrypoint(&mut self, name: &str) -> &mut Self {
1023            self.summary.pkg.entrypoint = Some(name.to_string());
1024            self
1025        }
1026
1027        fn with_fs_mapping(
1028            &mut self,
1029            volume_name: &str,
1030            original_path: &str,
1031            mount_path: &str,
1032        ) -> &mut Self {
1033            self.summary.pkg.filesystem.push(FileSystemMapping {
1034                volume_name: volume_name.to_string(),
1035                mount_path: mount_path.to_string(),
1036                original_path: Some(original_path.to_string()),
1037                dependency_name: None,
1038            });
1039            self
1040        }
1041
1042        fn with_fs_mapping_from_dependency(
1043            &mut self,
1044            volume_name: &str,
1045            mount_path: &str,
1046            original_path: &str,
1047            dependency: &str,
1048        ) -> &mut Self {
1049            self.summary.pkg.filesystem.push(FileSystemMapping {
1050                volume_name: volume_name.to_string(),
1051                mount_path: mount_path.to_string(),
1052                original_path: Some(original_path.to_string()),
1053                dependency_name: Some(dependency.to_string()),
1054            });
1055            self
1056        }
1057    }
1058
1059    #[derive(Debug)]
1060    struct TagResolvingSource {
1061        inner: MultiSource,
1062        tags: BTreeMap<(String, String), String>,
1063    }
1064
1065    #[async_trait::async_trait]
1066    impl Source for TagResolvingSource {
1067        async fn query(&self, package: &PackageSource) -> Result<Vec<PackageSummary>, QueryError> {
1068            let rewritten = match package {
1069                PackageSource::Ident(PackageIdent::Named(named)) => {
1070                    match named.tag.as_ref().and_then(|tag| tag.as_named()) {
1071                        Some(tag) => {
1072                            let version = self
1073                                .tags
1074                                .get(&(named.full_name(), tag.clone()))
1075                                .ok_or_else(|| QueryError::NotFound {
1076                                    query: package.clone(),
1077                                })?;
1078                            PackageSource::from(
1079                                NamedPackageIdent::try_from_full_name_and_version(
1080                                    &named.full_name(),
1081                                    &format!("={version}"),
1082                                )
1083                                .unwrap(),
1084                            )
1085                        }
1086                        None => package.clone(),
1087                    }
1088                }
1089                _ => package.clone(),
1090            };
1091
1092            self.inner.query(&rewritten).await
1093        }
1094    }
1095
1096    impl Drop for AddPackageVersion<'_> {
1097        fn drop(&mut self) {
1098            let summary = self.summary.clone();
1099            self.builder.add(summary);
1100        }
1101    }
1102
1103    #[derive(Debug)]
1104    struct DependencyGraphBuilder<'source> {
1105        dependencies: BTreeMap<PackageId, BTreeMap<String, PackageId>>,
1106        source: &'source InMemorySource,
1107    }
1108
1109    impl<'source> DependencyGraphBuilder<'source> {
1110        fn insert(&mut self, id: PackageId) -> DependencyGraphEntryBuilder<'source, '_> {
1111            let _ = self.source.get(&id).unwrap();
1112            DependencyGraphEntryBuilder {
1113                builder: self,
1114                pkg_id: id,
1115                dependencies: BTreeMap::new(),
1116            }
1117        }
1118
1119        fn finish(self) -> BTreeMap<PackageId, BTreeMap<String, PackageId>> {
1120            self.dependencies
1121        }
1122
1123        /// Using the dependency mapping that we've been building up, construct
1124        /// a dependency graph using the specified root package.
1125        fn graph(self, root_id: PackageId) -> DependencyGraph {
1126            let _ = self.source.get(&root_id).unwrap();
1127
1128            let mut graph = DiGraph::new();
1129            let mut nodes = BTreeMap::new();
1130
1131            for id in self.dependencies.keys() {
1132                let PackageSummary { pkg, dist } = self.source.get(id).unwrap();
1133                let index = graph.add_node(Node {
1134                    id: pkg.id(),
1135                    pkg: pkg.clone(),
1136                    dist: Some(dist.clone()),
1137                });
1138                nodes.insert(id.clone(), index);
1139            }
1140
1141            for (id, deps) in &self.dependencies {
1142                let index = nodes[id];
1143                for (dep_name, dep_id) in deps {
1144                    let dep_index = nodes[dep_id];
1145                    graph.add_edge(
1146                        index,
1147                        dep_index,
1148                        Edge {
1149                            alias: dep_name.clone(),
1150                        },
1151                    );
1152                }
1153            }
1154
1155            let root_index = nodes[&root_id];
1156
1157            DependencyGraph::new(root_index, graph, nodes)
1158        }
1159    }
1160
1161    #[derive(Debug)]
1162    struct DependencyGraphEntryBuilder<'source, 'builder> {
1163        builder: &'builder mut DependencyGraphBuilder<'source>,
1164        pkg_id: PackageId,
1165        dependencies: BTreeMap<String, PackageId>,
1166    }
1167
1168    impl DependencyGraphEntryBuilder<'_, '_> {
1169        fn with_dependency(&mut self, id: &PackageId) -> &mut Self {
1170            let name = &id.as_named().unwrap().full_name;
1171            self.with_aliased_dependency(name, id)
1172        }
1173
1174        fn with_aliased_dependency(&mut self, alias: &str, id: &PackageId) -> &mut Self {
1175            let dep_id = self.builder.source.get(id).unwrap().package_id();
1176            self.dependencies.insert(alias.to_string(), dep_id);
1177            self
1178        }
1179    }
1180
1181    impl Drop for DependencyGraphEntryBuilder<'_, '_> {
1182        fn drop(&mut self) {
1183            self.builder
1184                .dependencies
1185                .insert(self.pkg_id.clone(), self.dependencies.clone());
1186        }
1187    }
1188
1189    macro_rules! map {
1190        (
1191            $(
1192                $key:expr => $value:expr
1193            ),*
1194            $(,)?
1195        ) => {
1196            vec![
1197                $( ($key.into(), $value.into()) ),*
1198            ]
1199            .into_iter()
1200            .collect()
1201        }
1202    }
1203
1204    fn deps(resolution: &Resolution) -> BTreeMap<PackageId, BTreeMap<String, PackageId>> {
1205        resolution
1206            .graph
1207            .iter_dependencies()
1208            .map(|(id, deps)| {
1209                let deps = deps
1210                    .into_iter()
1211                    .map(|(name, dep_id)| (name.to_string(), dep_id.clone()))
1212                    .collect();
1213                (id.clone(), deps)
1214            })
1215            .collect()
1216    }
1217
1218    #[tokio::test]
1219    async fn no_deps_and_no_commands() {
1220        let mut builder = RegistryBuilder::new();
1221        builder.register("root", "1.0.0");
1222        let registry = builder.finish();
1223        let id = PackageId::new_named("root", Version::parse("1.0.0").unwrap());
1224        let root = builder.get(&id);
1225
1226        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1227            .await
1228            .unwrap();
1229
1230        let mut dependency_graph = builder.start_dependency_graph();
1231        dependency_graph.insert(id);
1232        assert_eq!(deps(&resolution), dependency_graph.finish());
1233        assert_eq!(
1234            resolution.package,
1235            ResolvedPackage {
1236                root_package: root.package_id(),
1237                commands: BTreeMap::new(),
1238                entrypoint: None,
1239                filesystem: Vec::new(),
1240            }
1241        );
1242    }
1243
1244    #[tokio::test]
1245    async fn no_deps_one_command() {
1246        let mut builder = RegistryBuilder::new();
1247        builder.register("root", "1.0.0").with_command("asdf");
1248        let registry = builder.finish();
1249        let id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1250        let root = builder.get(&id);
1251
1252        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1253            .await
1254            .unwrap();
1255
1256        let mut dependency_graph = builder.start_dependency_graph();
1257        dependency_graph.insert(id.clone());
1258        assert_eq!(deps(&resolution), dependency_graph.finish());
1259        assert_eq!(
1260            resolution.package,
1261            ResolvedPackage {
1262                root_package: root.package_id(),
1263                commands: map! {
1264                    "asdf" => ItemLocation {
1265                        name: "asdf".to_string(),
1266                        package: root.package_id(),
1267                    },
1268                },
1269                entrypoint: Some("asdf".to_string()),
1270                filesystem: Vec::new(),
1271            }
1272        );
1273    }
1274
1275    #[tokio::test]
1276    async fn single_dependency() {
1277        let mut builder = RegistryBuilder::new();
1278        builder
1279            .register("root", "1.0.0")
1280            .with_dependency("dep", "=1.0.0");
1281        builder.register("dep", "1.0.0");
1282        let registry = builder.finish();
1283        let id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1284        let root = builder.get(&id);
1285
1286        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1287            .await
1288            .unwrap();
1289        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
1290
1291        let mut dependency_graph = builder.start_dependency_graph();
1292        dependency_graph.insert(id.clone()).with_dependency(&dep_id);
1293        dependency_graph.insert(dep_id.clone());
1294        assert_eq!(deps(&resolution), dependency_graph.finish());
1295        assert_eq!(
1296            resolution.package,
1297            ResolvedPackage {
1298                root_package: root.package_id(),
1299                commands: BTreeMap::new(),
1300                entrypoint: None,
1301                filesystem: Vec::new(),
1302            }
1303        );
1304    }
1305
1306    #[tokio::test]
1307    async fn linear_dependency_chain() {
1308        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
1309        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
1310        let third_id = PackageId::new_named("third", "1.0.0".parse().unwrap());
1311
1312        let mut builder = RegistryBuilder::new();
1313        builder
1314            .register("first", "1.0.0")
1315            .with_dependency("second", "=1.0.0");
1316        builder
1317            .register("second", "1.0.0")
1318            .with_dependency("third", "=1.0.0");
1319        builder.register("third", "1.0.0");
1320        let registry = builder.finish();
1321        let root = builder.get(&first_id);
1322
1323        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1324            .await
1325            .unwrap();
1326
1327        let mut dependency_graph = builder.start_dependency_graph();
1328        dependency_graph
1329            .insert(first_id.clone())
1330            .with_dependency(&second_id);
1331        dependency_graph
1332            .insert(second_id.clone())
1333            .with_dependency(&third_id);
1334        dependency_graph.insert(third_id.clone());
1335        assert_eq!(deps(&resolution), dependency_graph.finish());
1336        assert_eq!(
1337            resolution.package,
1338            ResolvedPackage {
1339                root_package: root.package_id(),
1340                commands: BTreeMap::new(),
1341                entrypoint: None,
1342                filesystem: Vec::new(),
1343            }
1344        );
1345    }
1346
1347    #[tokio::test]
1348    async fn pick_the_latest_dependency_when_multiple_are_possible() {
1349        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1350        let mut builder = RegistryBuilder::new();
1351        builder
1352            .register("root", "1.0.0")
1353            .with_dependency("dep", "^1.0.0");
1354        builder.register("dep", "1.0.0");
1355        builder.register("dep", "1.0.1");
1356        builder.register("dep", "1.0.2");
1357        let registry = builder.finish();
1358        let root = builder.get(&root_id);
1359
1360        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1361            .await
1362            .unwrap();
1363        let dep_id = PackageId::new_named("dep", "1.0.2".parse().unwrap());
1364
1365        let mut dependency_graph = builder.start_dependency_graph();
1366        dependency_graph
1367            .insert(root_id.clone())
1368            .with_dependency(&dep_id);
1369        dependency_graph.insert(dep_id.clone());
1370        assert_eq!(deps(&resolution), dependency_graph.finish());
1371        assert_eq!(
1372            resolution.package,
1373            ResolvedPackage {
1374                root_package: root.package_id(),
1375                commands: BTreeMap::new(),
1376                entrypoint: None,
1377                filesystem: Vec::new(),
1378            }
1379        );
1380    }
1381
1382    #[tokio::test]
1383    async fn merge_compatible_versions() {
1384        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1385        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
1386        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
1387        let common_id = PackageId::new_named("common", "1.2.0".parse().unwrap());
1388
1389        let mut builder = RegistryBuilder::new();
1390        builder
1391            .register("root", "1.0.0")
1392            .with_dependency("first", "=1.0.0")
1393            .with_dependency("second", "=1.0.0");
1394        builder
1395            .register("first", "1.0.0")
1396            .with_dependency("common", "^1.0.0");
1397        builder
1398            .register("second", "1.0.0")
1399            .with_dependency("common", ">1.1,<1.3");
1400        builder.register("common", "1.0.0");
1401        builder.register("common", "1.1.0");
1402        builder.register("common", "1.2.0");
1403        builder.register("common", "1.5.0");
1404        let registry = builder.finish();
1405        let root = builder.get(&root_id);
1406
1407        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1408            .await
1409            .unwrap();
1410
1411        let mut dependency_graph = builder.start_dependency_graph();
1412        dependency_graph
1413            .insert(root_id.clone())
1414            .with_dependency(&first_id)
1415            .with_dependency(&second_id);
1416        dependency_graph
1417            .insert(first_id.clone())
1418            .with_dependency(&common_id);
1419        dependency_graph
1420            .insert(second_id.clone())
1421            .with_dependency(&common_id);
1422        dependency_graph.insert(common_id.clone());
1423        assert_eq!(deps(&resolution), dependency_graph.finish());
1424        assert_eq!(
1425            resolution.package,
1426            ResolvedPackage {
1427                root_package: root.package_id(),
1428                commands: BTreeMap::new(),
1429                entrypoint: None,
1430                filesystem: Vec::new(),
1431            }
1432        );
1433    }
1434
1435    #[tokio::test]
1436    async fn merge_compatible_versions_when_constraints_appear_later() {
1437        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1438        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
1439        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
1440        let mid_id = PackageId::new_named("mid", "1.0.0".parse().unwrap());
1441        let common_id = PackageId::new_named("common", "1.2.0".parse().unwrap());
1442
1443        let mut builder = RegistryBuilder::new();
1444        builder
1445            .register("root", "1.0.0")
1446            .with_dependency("first", "=1.0.0")
1447            .with_dependency("second", "=1.0.0");
1448        builder
1449            .register("first", "1.0.0")
1450            .with_dependency("common", "^1.0.0");
1451        builder
1452            .register("second", "1.0.0")
1453            .with_dependency("mid", "=1.0.0");
1454        builder
1455            .register("mid", "1.0.0")
1456            .with_dependency("common", "<=1.2.0");
1457        builder.register("common", "1.2.0");
1458        builder.register("common", "1.5.0");
1459        let registry = builder.finish();
1460        let root = builder.get(&root_id);
1461
1462        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1463            .await
1464            .unwrap();
1465
1466        let mut dependency_graph = builder.start_dependency_graph();
1467        dependency_graph
1468            .insert(root_id.clone())
1469            .with_dependency(&first_id)
1470            .with_dependency(&second_id);
1471        dependency_graph
1472            .insert(first_id.clone())
1473            .with_dependency(&common_id);
1474        dependency_graph
1475            .insert(second_id.clone())
1476            .with_dependency(&mid_id);
1477        dependency_graph
1478            .insert(mid_id.clone())
1479            .with_dependency(&common_id);
1480        dependency_graph.insert(common_id.clone());
1481        assert_eq!(deps(&resolution), dependency_graph.finish());
1482    }
1483
1484    #[tokio::test]
1485    async fn pick_a_lower_direct_dependency_to_preserve_transitive_unification() {
1486        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1487        let feature_id = PackageId::new_named("feature", "1.0.0".parse().unwrap());
1488        let common_id = PackageId::new_named("common", "1.5.0".parse().unwrap());
1489        let shared_id = PackageId::new_named("shared", "1.0.0".parse().unwrap());
1490
1491        let mut builder = RegistryBuilder::new();
1492        builder
1493            .register("root", "1.0.0")
1494            .with_dependency("feature", "^1.0.0")
1495            .with_dependency("shared", "=1.0.0");
1496        builder
1497            .register("feature", "1.0.0")
1498            .with_dependency("common", "^1.0.0");
1499        builder
1500            .register("feature", "1.1.0")
1501            .with_dependency("common", "^2.0.0");
1502        builder
1503            .register("shared", "1.0.0")
1504            .with_dependency("common", "=1.5.0");
1505        builder.register("common", "1.5.0");
1506        builder.register("common", "2.1.0");
1507        let registry = builder.finish();
1508        let root = builder.get(&root_id);
1509
1510        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1511            .await
1512            .unwrap();
1513
1514        let mut dependency_graph = builder.start_dependency_graph();
1515        dependency_graph
1516            .insert(root_id.clone())
1517            .with_dependency(&feature_id)
1518            .with_dependency(&shared_id);
1519        dependency_graph
1520            .insert(feature_id.clone())
1521            .with_dependency(&common_id);
1522        dependency_graph
1523            .insert(shared_id.clone())
1524            .with_dependency(&common_id);
1525        dependency_graph.insert(common_id.clone());
1526        assert_eq!(deps(&resolution), dependency_graph.finish());
1527    }
1528
1529    #[tokio::test]
1530    async fn pick_a_lower_dependency_after_branching_transitive_constraints() {
1531        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1532        let feature_id = PackageId::new_named("feature", "1.0.0".parse().unwrap());
1533        let aggregator_id = PackageId::new_named("aggregator", "1.0.0".parse().unwrap());
1534        let leaf_id = PackageId::new_named("leaf", "1.0.0".parse().unwrap());
1535        let common_id = PackageId::new_named("common", "1.4.0".parse().unwrap());
1536
1537        let mut builder = RegistryBuilder::new();
1538        builder
1539            .register("root", "1.0.0")
1540            .with_dependency("feature", "^1.0.0")
1541            .with_dependency("aggregator", "=1.0.0");
1542        builder
1543            .register("feature", "1.0.0")
1544            .with_dependency("common", "^1.0.0");
1545        builder
1546            .register("feature", "1.1.0")
1547            .with_dependency("common", "^2.0.0");
1548        builder
1549            .register("aggregator", "1.0.0")
1550            .with_dependency("leaf", "=1.0.0");
1551        builder
1552            .register("leaf", "1.0.0")
1553            .with_dependency("common", "<=1.4.0");
1554        builder.register("common", "1.4.0");
1555        builder.register("common", "2.0.0");
1556        let registry = builder.finish();
1557        let root = builder.get(&root_id);
1558
1559        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1560            .await
1561            .unwrap();
1562
1563        let mut dependency_graph = builder.start_dependency_graph();
1564        dependency_graph
1565            .insert(root_id.clone())
1566            .with_dependency(&feature_id)
1567            .with_dependency(&aggregator_id);
1568        dependency_graph
1569            .insert(feature_id.clone())
1570            .with_dependency(&common_id);
1571        dependency_graph
1572            .insert(aggregator_id.clone())
1573            .with_dependency(&leaf_id);
1574        dependency_graph
1575            .insert(leaf_id.clone())
1576            .with_dependency(&common_id);
1577        dependency_graph.insert(common_id.clone());
1578        assert_eq!(deps(&resolution), dependency_graph.finish());
1579    }
1580
1581    #[tokio::test]
1582    async fn backtracking_skips_acyclic_graphs_that_are_not_unified() {
1583        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1584        let feature_id = PackageId::new_named("feature", "1.0.0".parse().unwrap());
1585
1586        let mut builder = RegistryBuilder::new();
1587        builder
1588            .register("root", "1.0.0")
1589            .with_dependency("feature", "^1.0.0");
1590        builder.register("root", "1.1.0");
1591        builder.register("feature", "1.0.0");
1592        builder
1593            .register("feature", "1.1.0")
1594            .with_dependency("root", "^1.0.0");
1595        let registry = builder.finish();
1596        let root = builder.get(&root_id);
1597
1598        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1599            .await
1600            .unwrap();
1601
1602        let mut dependency_graph = builder.start_dependency_graph();
1603        dependency_graph
1604            .insert(root_id.clone())
1605            .with_dependency(&feature_id);
1606        dependency_graph.insert(feature_id.clone());
1607        assert_eq!(deps(&resolution), dependency_graph.finish());
1608    }
1609
1610    #[tokio::test]
1611    async fn oscillating_greedy_selection_falls_back_to_backtracking() {
1612        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1613        let a_id = PackageId::new_named("a", "1.0.0".parse().unwrap());
1614
1615        let mut builder = RegistryBuilder::new();
1616        builder
1617            .register("root", "1.0.0")
1618            .with_dependency("a", "^1.0.0");
1619        builder.register("a", "1.0.0");
1620        builder
1621            .register("a", "2.0.0")
1622            .with_dependency("x", "=1.0.0");
1623        builder
1624            .register("x", "1.0.0")
1625            .with_dependency("a", "=1.0.0");
1626        let registry = builder.finish();
1627        let root = builder.get(&root_id);
1628
1629        tokio::time::timeout(
1630            Duration::from_secs(1),
1631            discover_merged_dependencies(&root.package_id(), &root.pkg, &registry),
1632        )
1633        .await
1634        .expect("greedy dependency discovery did not terminate")
1635        .unwrap();
1636
1637        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1638            .await
1639            .unwrap();
1640
1641        let mut dependency_graph = builder.start_dependency_graph();
1642        dependency_graph
1643            .insert(root_id.clone())
1644            .with_dependency(&a_id);
1645        dependency_graph.insert(a_id.clone());
1646        assert_eq!(deps(&resolution), dependency_graph.finish());
1647    }
1648
1649    #[tokio::test]
1650    async fn named_tags_are_resolved_through_the_source_during_backtracking() {
1651        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1652        let feature_id = PackageId::new_named("feature", "1.0.0".parse().unwrap());
1653        let common_id = PackageId::new_named("common", "1.5.0".parse().unwrap());
1654        let shared_id = PackageId::new_named("shared", "1.0.0".parse().unwrap());
1655
1656        let mut builder = RegistryBuilder::new();
1657        builder
1658            .register("root", "1.0.0")
1659            .with_dependency("feature", "^1.0.0")
1660            .with_dependency("shared", "=1.0.0");
1661        builder
1662            .register("feature", "1.0.0")
1663            .with_tagged_dependency("common", "stable");
1664        builder
1665            .register("feature", "1.1.0")
1666            .with_dependency("common", "^2.0.0");
1667        builder
1668            .register("shared", "1.0.0")
1669            .with_dependency("common", "=1.5.0");
1670        builder.register("common", "1.5.0");
1671        builder.register("common", "2.1.0");
1672        let registry = builder.finish_with_tags(BTreeMap::from([(
1673            ("common".to_string(), "stable".to_string()),
1674            "1.5.0".to_string(),
1675        )]));
1676        let root = builder.get(&root_id);
1677
1678        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1679            .await
1680            .unwrap();
1681
1682        let mut dependency_graph = builder.start_dependency_graph();
1683        dependency_graph
1684            .insert(root_id.clone())
1685            .with_dependency(&feature_id)
1686            .with_dependency(&shared_id);
1687        dependency_graph
1688            .insert(feature_id.clone())
1689            .with_dependency(&common_id);
1690        dependency_graph
1691            .insert(shared_id.clone())
1692            .with_dependency(&common_id);
1693        dependency_graph.insert(common_id.clone());
1694        assert_eq!(deps(&resolution), dependency_graph.finish());
1695    }
1696
1697    #[tokio::test]
1698    async fn incompatible_versions_still_report_duplicates() {
1699        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1700
1701        let mut builder = RegistryBuilder::new();
1702        builder
1703            .register("root", "1.0.0")
1704            .with_dependency("first", "=1.0.0")
1705            .with_dependency("second", "=1.0.0");
1706        builder
1707            .register("first", "1.0.0")
1708            .with_dependency("common", "=1.0.0");
1709        builder
1710            .register("second", "1.0.0")
1711            .with_dependency("common", "=2.0.0");
1712        builder.register("common", "1.0.0");
1713        builder.register("common", "2.0.0");
1714        let registry = builder.finish();
1715        let root = builder.get(&root_id);
1716
1717        let result = resolve(&root.package_id(), &root.pkg, &registry).await;
1718
1719        match result {
1720            Err(ResolveError::DuplicateVersions {
1721                package_name,
1722                versions,
1723            }) => {
1724                assert_eq!(package_name, "common");
1725                assert_eq!(
1726                    versions,
1727                    [
1728                        Version::parse("1.0.0").unwrap(),
1729                        Version::parse("2.0.0").unwrap(),
1730                    ]
1731                );
1732            }
1733            _ => unreachable!("Expected a duplicate versions error, found {:?}", result),
1734        }
1735    }
1736
1737    #[tokio::test]
1738    async fn incompatible_parent_versions_still_report_duplicates_when_no_solution_exists() {
1739        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1740
1741        let mut builder = RegistryBuilder::new();
1742        builder
1743            .register("root", "1.0.0")
1744            .with_dependency("feature", "^1.0.0")
1745            .with_dependency("shared", "=1.0.0");
1746        builder
1747            .register("feature", "1.0.0")
1748            .with_dependency("common", "^1.0.0");
1749        builder
1750            .register("feature", "1.1.0")
1751            .with_dependency("common", "^2.0.0");
1752        builder
1753            .register("shared", "1.0.0")
1754            .with_dependency("common", "=3.0.0");
1755        builder.register("common", "1.5.0");
1756        builder.register("common", "2.1.0");
1757        builder.register("common", "3.0.0");
1758        let registry = builder.finish();
1759        let root = builder.get(&root_id);
1760
1761        let result = resolve(&root.package_id(), &root.pkg, &registry).await;
1762
1763        match result {
1764            Err(ResolveError::DuplicateVersions {
1765                package_name,
1766                versions,
1767            }) => {
1768                assert_eq!(package_name, "common");
1769                assert_eq!(
1770                    versions,
1771                    [
1772                        Version::parse("2.1.0").unwrap(),
1773                        Version::parse("3.0.0").unwrap(),
1774                    ]
1775                );
1776            }
1777            _ => unreachable!("Expected a duplicate versions error, found {:?}", result),
1778        }
1779    }
1780
1781    #[tokio::test]
1782    async fn very_large_dependency_graphs_fail_with_a_clear_limit_error() {
1783        let root_id = PackageId::new_named("pkg0", "1.0.0".parse().unwrap());
1784
1785        let mut builder = RegistryBuilder::new();
1786        for ix in 0..=MAX_DISCOVERED_PACKAGES {
1787            let name = format!("pkg{ix}");
1788            let version = "1.0.0";
1789            let mut pkg = builder.register(&name, version);
1790            if ix < MAX_DISCOVERED_PACKAGES {
1791                let dep_name = format!("pkg{}", ix + 1);
1792                pkg.with_dependency(&dep_name, "=1.0.0");
1793            }
1794        }
1795
1796        let registry = builder.finish();
1797        let root = builder.get(&root_id);
1798
1799        let result = resolve(&root.package_id(), &root.pkg, &registry).await;
1800
1801        match result {
1802            Err(ResolveError::TooComplex {
1803                resource,
1804                value,
1805                limit,
1806            }) => {
1807                assert_eq!(resource, "packages");
1808                assert_eq!(limit, MAX_DISCOVERED_PACKAGES);
1809                assert!(value > limit);
1810            }
1811            _ => unreachable!("Expected a complexity limit error, found {:?}", result),
1812        }
1813    }
1814
1815    #[test]
1816    fn validate_dependency_graph_reports_cycles_without_panicking() {
1817        let mut builder = RegistryBuilder::new();
1818        builder.register("root", "1.0.0");
1819        builder.register("dep", "1.0.0");
1820
1821        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1822        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
1823
1824        let root = builder.get(&root_id);
1825        let dep = builder.get(&dep_id);
1826
1827        let mut graph = DiGraph::new();
1828        let root_ix = graph.add_node(Node {
1829            id: root_id.clone(),
1830            pkg: root.pkg.clone(),
1831            dist: Some(root.dist.clone()),
1832        });
1833        let dep_ix = graph.add_node(Node {
1834            id: dep_id.clone(),
1835            pkg: dep.pkg.clone(),
1836            dist: Some(dep.dist.clone()),
1837        });
1838
1839        graph.add_edge(
1840            root_ix,
1841            dep_ix,
1842            Edge {
1843                alias: "dep".to_string(),
1844            },
1845        );
1846        graph.add_edge(
1847            dep_ix,
1848            root_ix,
1849            Edge {
1850                alias: "root".to_string(),
1851            },
1852        );
1853
1854        let graph = DependencyGraph::new(
1855            root_ix,
1856            graph,
1857            BTreeMap::from([(root_id, root_ix), (dep_id, dep_ix)]),
1858        );
1859
1860        assert!(matches!(
1861            validate_dependency_graph(&graph),
1862            Err(ResolveError::Cycle(_))
1863        ));
1864    }
1865
1866    #[test]
1867    fn backtracking_partial_discovery_enforces_pending_dependency_limit() {
1868        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1869        let child_id = PackageId::new_named("child", "1.0.0".parse().unwrap());
1870
1871        let root = PackageInfo {
1872            id: root_id.clone(),
1873            dependencies: vec![Dependency {
1874                alias: "child".to_string(),
1875                pkg: PackageSource::from(
1876                    NamedPackageIdent::try_from_full_name_and_version("child", "=1.0.0").unwrap(),
1877                ),
1878            }],
1879            commands: Vec::new(),
1880            entrypoint: None,
1881            filesystem: Vec::new(),
1882        };
1883        let mut child = PackageInfo {
1884            id: child_id.clone(),
1885            dependencies: Vec::new(),
1886            commands: Vec::new(),
1887            entrypoint: None,
1888            filesystem: Vec::new(),
1889        };
1890        for ix in 0..=MAX_PENDING_DEPENDENCIES {
1891            let name = format!("leaf{ix}");
1892            child.dependencies.push(Dependency {
1893                alias: name.clone(),
1894                pkg: PackageSource::from(
1895                    NamedPackageIdent::try_from_full_name_and_version(&name, "=1.0.0").unwrap(),
1896                ),
1897            });
1898        }
1899        let dist = DistributionInfo {
1900            webc: "http://localhost/child@1.0.0".parse().unwrap(),
1901            webc_sha256: [0; 32].into(),
1902        };
1903
1904        let mut discovery = PartialDiscovery::new(&root_id, &root);
1905        let task = discovery.pending.remove(0);
1906        let result = discovery.add_dependency(
1907            task.parent(),
1908            task.dep(),
1909            PackageSummary { pkg: child, dist },
1910        );
1911
1912        match result {
1913            Err(ResolveError::TooComplex {
1914                resource,
1915                value,
1916                limit,
1917            }) => {
1918                assert_eq!(resource, "pending_dependencies");
1919                assert_eq!(limit, MAX_PENDING_DEPENDENCIES);
1920                assert!(value > limit);
1921            }
1922            _ => unreachable!("Expected a complexity limit error, found {:?}", result),
1923        }
1924    }
1925
1926    #[tokio::test]
1927    async fn backtracking_handles_deep_incompatible_candidate_tree_with_state_limit() {
1928        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1929
1930        let mut builder = RegistryBuilder::new();
1931        builder
1932            .register("root", "1.0.0")
1933            .with_dependency("branch0", "^1.0.0")
1934            .with_dependency("pinner", "=1.0.0");
1935
1936        for branch in 0..8 {
1937            for version in 0..8 {
1938                let name = format!("branch{branch}");
1939                let version = format!("1.{version}.0");
1940                let mut pkg = builder.register(&name, &version);
1941                pkg.with_dependency("common", "=2.0.0");
1942                if branch < 7 {
1943                    pkg.with_dependency(&format!("branch{}", branch + 1), "^1.0.0");
1944                }
1945            }
1946        }
1947
1948        builder
1949            .register("pinner", "1.0.0")
1950            .with_dependency("common", "=1.0.0");
1951        builder.register("common", "1.0.0");
1952        builder.register("common", "2.0.0");
1953
1954        let registry = builder.finish();
1955        let root = builder.get(&root_id);
1956
1957        let result = tokio::time::timeout(
1958            Duration::from_secs(1),
1959            resolve(&root.package_id(), &root.pkg, &registry),
1960        )
1961        .await
1962        .expect("backtracking did not finish within the test budget");
1963
1964        assert!(matches!(
1965            result,
1966            Err(ResolveError::DuplicateVersions { .. })
1967                | Err(ResolveError::TooComplex {
1968                    resource: "backtrack_states",
1969                    ..
1970                })
1971        ));
1972    }
1973
1974    #[tokio::test]
1975    async fn commands_from_dependencies_end_up_in_the_package() {
1976        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1977        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
1978        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
1979        let mut builder = RegistryBuilder::new();
1980        builder
1981            .register("root", "1.0.0")
1982            .with_dependency("first", "=1.0.0")
1983            .with_dependency("second", "=1.0.0");
1984        builder
1985            .register("first", "1.0.0")
1986            .with_command("first-command");
1987        builder
1988            .register("second", "1.0.0")
1989            .with_command("second-command");
1990        let registry = builder.finish();
1991        let root = builder.get(&root_id);
1992
1993        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1994            .await
1995            .unwrap();
1996
1997        let mut dependency_graph = builder.start_dependency_graph();
1998        dependency_graph
1999            .insert(root_id.clone())
2000            .with_dependency(&first_id)
2001            .with_dependency(&second_id);
2002        dependency_graph.insert(first_id.clone());
2003        dependency_graph.insert(second_id.clone());
2004        assert_eq!(deps(&resolution), dependency_graph.finish());
2005        assert_eq!(
2006            resolution.package,
2007            ResolvedPackage {
2008                root_package: root.package_id(),
2009                commands: map! {
2010                    "first-command" => ItemLocation {
2011                        name: "first-command".to_string(),
2012                        package: builder.get(&first_id).package_id(),
2013                     },
2014                    "second-command" => ItemLocation {
2015                        name: "second-command".to_string(),
2016                        package: builder.get(&second_id).package_id(),
2017                     },
2018                },
2019                entrypoint: None,
2020                filesystem: Vec::new(),
2021            }
2022        );
2023    }
2024
2025    #[tokio::test]
2026    async fn commands_in_root_shadow_their_dependencies() {
2027        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2028        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
2029        let mut builder = RegistryBuilder::new();
2030        builder
2031            .register("root", "1.0.0")
2032            .with_dependency("dep", "=1.0.0")
2033            .with_command("command");
2034        builder.register("dep", "1.0.0").with_command("command");
2035        let registry = builder.finish();
2036        let root = builder.get(&root_id);
2037
2038        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
2039            .await
2040            .unwrap();
2041
2042        let mut dependency_graph = builder.start_dependency_graph();
2043        dependency_graph
2044            .insert(root_id.clone())
2045            .with_dependency(&dep_id);
2046        dependency_graph.insert(dep_id.clone());
2047        assert_eq!(deps(&resolution), dependency_graph.finish());
2048        assert_eq!(
2049            resolution.package,
2050            ResolvedPackage {
2051                root_package: root.package_id(),
2052                commands: map! {
2053                    "command" => ItemLocation {
2054                        name: "command".to_string(),
2055                        package: builder.get(&root_id).package_id(),
2056                     },
2057                },
2058                entrypoint: Some("command".to_string()),
2059                filesystem: Vec::new(),
2060            }
2061        );
2062    }
2063
2064    #[tokio::test]
2065    async fn cyclic_dependencies() {
2066        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2067        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
2068
2069        let mut builder = RegistryBuilder::new();
2070        builder
2071            .register("root", "1.0.0")
2072            .with_dependency("dep", "=1.0.0");
2073        builder
2074            .register("dep", "1.0.0")
2075            .with_dependency("root", "=1.0.0");
2076        let registry = builder.finish();
2077        let root = builder.get(&root_id);
2078
2079        let err = resolve(&root.package_id(), &root.pkg, &registry)
2080            .await
2081            .unwrap_err();
2082
2083        let cycle = err.as_cycle().unwrap().to_vec();
2084        assert_eq!(
2085            cycle,
2086            [
2087                builder.get(&root_id).package_id(),
2088                builder.get(&dep_id).package_id(),
2089                builder.get(&root_id).package_id(),
2090            ]
2091        );
2092    }
2093
2094    #[tokio::test]
2095    async fn entrypoint_is_inherited() {
2096        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2097        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
2098
2099        let mut builder = RegistryBuilder::new();
2100        builder
2101            .register("root", "1.0.0")
2102            .with_dependency("dep", "=1.0.0");
2103        builder
2104            .register("dep", "1.0.0")
2105            .with_command("entry")
2106            .with_entrypoint("entry");
2107        let registry = builder.finish();
2108        let root = builder.get(&root_id);
2109
2110        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
2111            .await
2112            .unwrap();
2113
2114        assert_eq!(
2115            resolution.package,
2116            ResolvedPackage {
2117                root_package: root.package_id(),
2118                commands: map! {
2119                    "entry" => ItemLocation {
2120                        name: "entry".to_string(),
2121                        package: builder.get(&dep_id).package_id(),
2122                     },
2123                },
2124                entrypoint: Some("entry".to_string()),
2125                filesystem: Vec::new(),
2126            }
2127        );
2128    }
2129
2130    #[tokio::test]
2131    async fn infer_entrypoint_if_unspecified_and_only_one_command_in_root_package() {
2132        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2133        let mut builder = RegistryBuilder::new();
2134        builder
2135            .register("root", "1.0.0")
2136            .with_command("root-cmd")
2137            .with_dependency("dep", "=1.0.0");
2138        builder.register("dep", "1.0.0").with_command("entry");
2139        let registry = builder.finish();
2140        let root = builder.get(&root_id);
2141
2142        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
2143            .await
2144            .unwrap();
2145
2146        assert_eq!(resolution.package.entrypoint.as_deref(), Some("root-cmd"));
2147    }
2148
2149    #[test]
2150    fn cyclic_error_message() {
2151        let cycle = [
2152            PackageId::new_named("root", "1.0.0".parse().unwrap()),
2153            PackageId::new_named("dep", "1.0.0".parse().unwrap()),
2154            PackageId::new_named("root", "1.0.0".parse().unwrap()),
2155        ];
2156
2157        let message = print_cycle(&cycle);
2158
2159        assert_eq!(message, "root@1.0.0 → dep@1.0.0 → root@1.0.0");
2160    }
2161
2162    #[test]
2163    fn filesystem_with_one_package_and_no_fs_tables() {
2164        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2165        let mut builder = RegistryBuilder::new();
2166        builder.register("root", "1.0.0");
2167        let mut dep_builder = builder.start_dependency_graph();
2168        dep_builder.insert(root_id.clone());
2169        let graph = dep_builder.graph(root_id.clone());
2170
2171        let pkg = resolve_package(&graph).unwrap();
2172
2173        assert!(pkg.filesystem.is_empty());
2174    }
2175
2176    #[test]
2177    fn filesystem_with_one_package_and_one_fs_tables() {
2178        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2179        let mut builder = RegistryBuilder::new();
2180        builder
2181            .register("root", "1.0.0")
2182            .with_fs_mapping("atom", "/publisher/lib", "/lib");
2183        let mut dep_builder = builder.start_dependency_graph();
2184        dep_builder.insert(root_id.clone());
2185        let graph = dep_builder.graph(root_id.clone());
2186
2187        let pkg = resolve_package(&graph).unwrap();
2188
2189        assert_eq!(
2190            pkg.filesystem,
2191            vec![ResolvedFileSystemMapping {
2192                mount_path: PathBuf::from("/lib"),
2193                original_path: Some("/publisher/lib".to_string()),
2194                volume_name: "atom".to_string(),
2195                package: builder.get(&root_id).package_id(),
2196            }]
2197        );
2198    }
2199
2200    #[test]
2201    fn merge_fs_mappings_from_multiple_packages() {
2202        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2203        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
2204        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
2205
2206        let mut builder = RegistryBuilder::new();
2207        builder
2208            .register("root", "1.0.0")
2209            .with_dependency("first", "=1.0.0")
2210            .with_dependency("second", "=1.0.0")
2211            .with_fs_mapping("atom", "/root", "/root");
2212        builder.register("first", "1.0.0").with_fs_mapping(
2213            "atom",
2214            "/usr/local/lib/first",
2215            "/usr/local/lib/first",
2216        );
2217        builder.register("second", "1.0.0").with_fs_mapping(
2218            "atom",
2219            "/usr/local/lib/second",
2220            "/usr/local/lib/second",
2221        );
2222        let mut dep_builder = builder.start_dependency_graph();
2223        dep_builder
2224            .insert(root_id.clone())
2225            .with_dependency(&first_id)
2226            .with_dependency(&second_id);
2227        dep_builder.insert(first_id.clone());
2228        dep_builder.insert(second_id.clone());
2229        let graph = dep_builder.graph(root_id.clone());
2230
2231        let pkg = resolve_package(&graph).unwrap();
2232
2233        assert_eq!(
2234            pkg.filesystem,
2235            vec![
2236                ResolvedFileSystemMapping {
2237                    mount_path: PathBuf::from("/root"),
2238                    original_path: Some("/root".to_string()),
2239                    volume_name: "atom".to_string(),
2240                    package: builder.get(&root_id).package_id(),
2241                },
2242                ResolvedFileSystemMapping {
2243                    mount_path: PathBuf::from("/usr/local/lib/second"),
2244                    original_path: Some("/usr/local/lib/second".to_string()),
2245                    volume_name: "atom".to_string(),
2246                    package: builder.get(&second_id).package_id(),
2247                },
2248                ResolvedFileSystemMapping {
2249                    mount_path: PathBuf::from("/usr/local/lib/first"),
2250                    volume_name: "atom".to_string(),
2251                    original_path: Some("/usr/local/lib/first".to_string()),
2252                    package: builder.get(&first_id).package_id(),
2253                }
2254            ]
2255        );
2256    }
2257
2258    #[test]
2259    fn use_fs_mapping_from_dependency() {
2260        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2261        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
2262        let mut builder = RegistryBuilder::new();
2263        builder
2264            .register("root", "1.0.0")
2265            .with_dependency("dep", "=1.0.0")
2266            .with_fs_mapping_from_dependency("dep-volume", "/root", "/root", "dep");
2267        builder.register("dep", "1.0.0");
2268        let mut dep_builder = builder.start_dependency_graph();
2269        dep_builder.insert(root_id.clone()).with_dependency(&dep_id);
2270        dep_builder.insert(dep_id.clone());
2271        let graph = dep_builder.graph(root_id.clone());
2272
2273        let pkg = resolve_package(&graph).unwrap();
2274
2275        assert_eq!(
2276            pkg.filesystem,
2277            vec![ResolvedFileSystemMapping {
2278                mount_path: PathBuf::from("/root"),
2279                original_path: Some("/root".to_string()),
2280                volume_name: "dep-volume".to_string(),
2281                package: builder.get(&dep_id).package_id(),
2282            }]
2283        );
2284    }
2285}