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