Skip to main content

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    // Only a command-less root may inherit an entrypoint from a dependency.
801    // Otherwise a dependency like wasmer/bash, which declares an entrypoint,
802    // would hijack a root package that ships commands of its own. A root with
803    // commands falls through to the "only command" fallback below instead, and
804    // is left without an entrypoint if that can't pick one unambiguously.
805    let may_inherit_entrypoint = dependency_graph.root_info().commands.is_empty();
806
807    for index in petgraph::algo::toposort(dependency_graph.graph(), None).expect("acyclic") {
808        let node = &dependency_graph[index];
809        let id = &node.id;
810        let pkg = &node.pkg;
811
812        // update the entrypoint, if necessary
813        if entrypoint.is_none()
814            && may_inherit_entrypoint
815            && let Some(entry) = &pkg.entrypoint
816        {
817            tracing::trace!(
818                entrypoint = entry.as_str(),
819                parent=%id,
820                "Inheriting the entrypoint",
821            );
822
823            entrypoint = Some(entry.clone());
824        }
825
826        for cmd in &pkg.commands {
827            // Note: We are traversing in topological order with the root at the
828            // start, so if we ever see any duplicates we should prefer the
829            // earlier copy and skip the later one.
830
831            match commands.entry(cmd.name.clone()) {
832                std::collections::btree_map::Entry::Vacant(entry) => {
833                    let resolved = ItemLocation {
834                        name: cmd.name.clone(),
835                        package: id.clone(),
836                    };
837                    entry.insert(resolved);
838                    tracing::trace!(
839                        command.name=cmd.name.as_str(),
840                        pkg=%id,
841                        "Discovered command",
842                    );
843                }
844                std::collections::btree_map::Entry::Occupied(_) => {
845                    tracing::trace!(
846                        command.name=cmd.name.as_str(),
847                        pkg=%id,
848                        "Ignoring duplicate command",
849                    );
850                }
851            }
852        }
853
854        for mapping in &pkg.filesystem {
855            let dep = match &mapping.dependency_name {
856                Some(name) => {
857                    let dep_index = dependency_graph
858                        .graph()
859                        .edges(index)
860                        .find(|edge| edge.weight().alias == *name)
861                        .unwrap()
862                        .target();
863                    &dependency_graph[dep_index].id
864                }
865                None => id,
866            };
867            filesystem.push(ResolvedFileSystemMapping {
868                mount_path: PathBuf::from(&mapping.mount_path),
869                original_path: mapping.original_path.clone(),
870                volume_name: mapping.volume_name.clone(),
871                package: dep.clone(),
872            })
873        }
874    }
875
876    if entrypoint.is_none() {
877        // We *still* haven't been able to figure out what the entrypoint for the
878        // resolved package should be. If there is only one command in the main
879        // package, let's assume they want to use that.
880        //
881        // This works around packages like saghul/quickjs and syrusakbary/cowsay
882        // which don't specify their entrypoints explicitly.
883        if let [cmd] = dependency_graph.root_info().commands.as_slice() {
884            tracing::debug!(
885                command = cmd.name.as_str(),
886                "No entrypoint specified. Falling back to the root package's only command.",
887            );
888            entrypoint = Some(cmd.name.clone());
889        }
890    }
891
892    tracing::debug!("resolved filesystem: {:?}", &filesystem);
893
894    Ok(ResolvedPackage {
895        root_package: dependency_graph.id().clone(),
896        commands,
897        entrypoint,
898        filesystem,
899    })
900}
901
902#[cfg(test)]
903mod tests {
904    use std::{path::PathBuf, time::Duration};
905
906    use wasmer_config::package::{NamedPackageIdent, PackageIdent, Tag};
907
908    use crate::runtime::resolver::{
909        Dependency, InMemorySource, MultiSource,
910        inputs::{DistributionInfo, FileSystemMapping, PackageInfo},
911    };
912
913    use super::*;
914
915    struct RegistryBuilder(InMemorySource);
916
917    impl RegistryBuilder {
918        fn new() -> Self {
919            RegistryBuilder(InMemorySource::new())
920        }
921
922        fn register(&mut self, name: &str, version: &str) -> AddPackageVersion<'_> {
923            let pkg = PackageInfo {
924                id: PackageId::new_named(name, version.parse().unwrap()),
925                dependencies: Vec::new(),
926                commands: Vec::new(),
927                entrypoint: None,
928                filesystem: Vec::new(),
929            };
930            let dist = DistributionInfo {
931                webc: format!("http://localhost/{name}@{version}")
932                    .parse()
933                    .unwrap(),
934                webc_sha256: [0; 32].into(),
935            };
936            let summary = PackageSummary { pkg, dist };
937
938            AddPackageVersion {
939                builder: &mut self.0,
940                summary,
941            }
942        }
943
944        fn finish(&self) -> MultiSource {
945            let mut registry = MultiSource::default();
946            registry.add_source(self.0.clone());
947            registry
948        }
949
950        fn finish_with_tags(&self, tags: BTreeMap<(String, String), String>) -> TagResolvingSource {
951            TagResolvingSource {
952                inner: self.finish(),
953                tags,
954            }
955        }
956
957        fn get(&self, id: &PackageId) -> &PackageSummary {
958            self.0.get(id).unwrap()
959        }
960
961        // fn get_named(&self, name: &str, version: &str) -> &PackageSummary {
962        //     let id = PackageId::new_named(name, version.parse().unwrap());
963        //     self.get(&id)
964        // }
965
966        fn start_dependency_graph(&self) -> DependencyGraphBuilder<'_> {
967            DependencyGraphBuilder {
968                dependencies: BTreeMap::new(),
969                source: &self.0,
970            }
971        }
972    }
973
974    #[derive(Debug)]
975    struct AddPackageVersion<'builder> {
976        builder: &'builder mut InMemorySource,
977        summary: PackageSummary,
978    }
979
980    impl AddPackageVersion<'_> {
981        fn with_dependency(&mut self, name: &str, version_constraint: &str) -> &mut Self {
982            self.with_aliased_dependency(name, name, version_constraint)
983        }
984
985        fn with_tagged_dependency(&mut self, name: &str, tag: &str) -> &mut Self {
986            let pkg = PackageSource::from(NamedPackageIdent {
987                registry: None,
988                namespace: None,
989                name: name.to_string(),
990                tag: Some(Tag::Named(tag.to_string())),
991            });
992
993            self.summary.pkg.dependencies.push(Dependency {
994                alias: name.to_string(),
995                pkg,
996            });
997
998            self
999        }
1000
1001        fn with_aliased_dependency(
1002            &mut self,
1003            alias: &str,
1004            name: &str,
1005            version_constraint: &str,
1006        ) -> &mut Self {
1007            let pkg = PackageSource::from(
1008                NamedPackageIdent::try_from_full_name_and_version(name, version_constraint)
1009                    .unwrap(),
1010            );
1011
1012            self.summary.pkg.dependencies.push(Dependency {
1013                alias: alias.to_string(),
1014                pkg,
1015            });
1016
1017            self
1018        }
1019
1020        fn with_command(&mut self, name: &str) -> &mut Self {
1021            self.summary
1022                .pkg
1023                .commands
1024                .push(crate::runtime::resolver::Command {
1025                    name: name.to_string(),
1026                });
1027            self
1028        }
1029
1030        fn with_entrypoint(&mut self, name: &str) -> &mut Self {
1031            self.summary.pkg.entrypoint = Some(name.to_string());
1032            self
1033        }
1034
1035        fn with_fs_mapping(
1036            &mut self,
1037            volume_name: &str,
1038            original_path: &str,
1039            mount_path: &str,
1040        ) -> &mut Self {
1041            self.summary.pkg.filesystem.push(FileSystemMapping {
1042                volume_name: volume_name.to_string(),
1043                mount_path: mount_path.to_string(),
1044                original_path: Some(original_path.to_string()),
1045                dependency_name: None,
1046            });
1047            self
1048        }
1049
1050        fn with_fs_mapping_from_dependency(
1051            &mut self,
1052            volume_name: &str,
1053            mount_path: &str,
1054            original_path: &str,
1055            dependency: &str,
1056        ) -> &mut Self {
1057            self.summary.pkg.filesystem.push(FileSystemMapping {
1058                volume_name: volume_name.to_string(),
1059                mount_path: mount_path.to_string(),
1060                original_path: Some(original_path.to_string()),
1061                dependency_name: Some(dependency.to_string()),
1062            });
1063            self
1064        }
1065    }
1066
1067    #[derive(Debug)]
1068    struct TagResolvingSource {
1069        inner: MultiSource,
1070        tags: BTreeMap<(String, String), String>,
1071    }
1072
1073    #[async_trait::async_trait]
1074    impl Source for TagResolvingSource {
1075        async fn query(&self, package: &PackageSource) -> Result<Vec<PackageSummary>, QueryError> {
1076            let rewritten = match package {
1077                PackageSource::Ident(PackageIdent::Named(named)) => {
1078                    match named.tag.as_ref().and_then(|tag| tag.as_named()) {
1079                        Some(tag) => {
1080                            let version = self
1081                                .tags
1082                                .get(&(named.full_name(), tag.clone()))
1083                                .ok_or_else(|| QueryError::NotFound {
1084                                    query: package.clone(),
1085                                })?;
1086                            PackageSource::from(
1087                                NamedPackageIdent::try_from_full_name_and_version(
1088                                    &named.full_name(),
1089                                    &format!("={version}"),
1090                                )
1091                                .unwrap(),
1092                            )
1093                        }
1094                        None => package.clone(),
1095                    }
1096                }
1097                _ => package.clone(),
1098            };
1099
1100            self.inner.query(&rewritten).await
1101        }
1102    }
1103
1104    impl Drop for AddPackageVersion<'_> {
1105        fn drop(&mut self) {
1106            let summary = self.summary.clone();
1107            self.builder.add(summary);
1108        }
1109    }
1110
1111    #[derive(Debug)]
1112    struct DependencyGraphBuilder<'source> {
1113        dependencies: BTreeMap<PackageId, BTreeMap<String, PackageId>>,
1114        source: &'source InMemorySource,
1115    }
1116
1117    impl<'source> DependencyGraphBuilder<'source> {
1118        fn insert(&mut self, id: PackageId) -> DependencyGraphEntryBuilder<'source, '_> {
1119            let _ = self.source.get(&id).unwrap();
1120            DependencyGraphEntryBuilder {
1121                builder: self,
1122                pkg_id: id,
1123                dependencies: BTreeMap::new(),
1124            }
1125        }
1126
1127        fn finish(self) -> BTreeMap<PackageId, BTreeMap<String, PackageId>> {
1128            self.dependencies
1129        }
1130
1131        /// Using the dependency mapping that we've been building up, construct
1132        /// a dependency graph using the specified root package.
1133        fn graph(self, root_id: PackageId) -> DependencyGraph {
1134            let _ = self.source.get(&root_id).unwrap();
1135
1136            let mut graph = DiGraph::new();
1137            let mut nodes = BTreeMap::new();
1138
1139            for id in self.dependencies.keys() {
1140                let PackageSummary { pkg, dist } = self.source.get(id).unwrap();
1141                let index = graph.add_node(Node {
1142                    id: pkg.id(),
1143                    pkg: pkg.clone(),
1144                    dist: Some(dist.clone()),
1145                });
1146                nodes.insert(id.clone(), index);
1147            }
1148
1149            for (id, deps) in &self.dependencies {
1150                let index = nodes[id];
1151                for (dep_name, dep_id) in deps {
1152                    let dep_index = nodes[dep_id];
1153                    graph.add_edge(
1154                        index,
1155                        dep_index,
1156                        Edge {
1157                            alias: dep_name.clone(),
1158                        },
1159                    );
1160                }
1161            }
1162
1163            let root_index = nodes[&root_id];
1164
1165            DependencyGraph::new(root_index, graph, nodes)
1166        }
1167    }
1168
1169    #[derive(Debug)]
1170    struct DependencyGraphEntryBuilder<'source, 'builder> {
1171        builder: &'builder mut DependencyGraphBuilder<'source>,
1172        pkg_id: PackageId,
1173        dependencies: BTreeMap<String, PackageId>,
1174    }
1175
1176    impl DependencyGraphEntryBuilder<'_, '_> {
1177        fn with_dependency(&mut self, id: &PackageId) -> &mut Self {
1178            let name = &id.as_named().unwrap().full_name;
1179            self.with_aliased_dependency(name, id)
1180        }
1181
1182        fn with_aliased_dependency(&mut self, alias: &str, id: &PackageId) -> &mut Self {
1183            let dep_id = self.builder.source.get(id).unwrap().package_id();
1184            self.dependencies.insert(alias.to_string(), dep_id);
1185            self
1186        }
1187    }
1188
1189    impl Drop for DependencyGraphEntryBuilder<'_, '_> {
1190        fn drop(&mut self) {
1191            self.builder
1192                .dependencies
1193                .insert(self.pkg_id.clone(), self.dependencies.clone());
1194        }
1195    }
1196
1197    macro_rules! map {
1198        (
1199            $(
1200                $key:expr => $value:expr
1201            ),*
1202            $(,)?
1203        ) => {
1204            vec![
1205                $( ($key.into(), $value.into()) ),*
1206            ]
1207            .into_iter()
1208            .collect()
1209        }
1210    }
1211
1212    fn deps(resolution: &Resolution) -> BTreeMap<PackageId, BTreeMap<String, PackageId>> {
1213        resolution
1214            .graph
1215            .iter_dependencies()
1216            .map(|(id, deps)| {
1217                let deps = deps
1218                    .into_iter()
1219                    .map(|(name, dep_id)| (name.to_string(), dep_id.clone()))
1220                    .collect();
1221                (id.clone(), deps)
1222            })
1223            .collect()
1224    }
1225
1226    #[tokio::test]
1227    async fn no_deps_and_no_commands() {
1228        let mut builder = RegistryBuilder::new();
1229        builder.register("root", "1.0.0");
1230        let registry = builder.finish();
1231        let id = PackageId::new_named("root", Version::parse("1.0.0").unwrap());
1232        let root = builder.get(&id);
1233
1234        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1235            .await
1236            .unwrap();
1237
1238        let mut dependency_graph = builder.start_dependency_graph();
1239        dependency_graph.insert(id);
1240        assert_eq!(deps(&resolution), dependency_graph.finish());
1241        assert_eq!(
1242            resolution.package,
1243            ResolvedPackage {
1244                root_package: root.package_id(),
1245                commands: BTreeMap::new(),
1246                entrypoint: None,
1247                filesystem: Vec::new(),
1248            }
1249        );
1250    }
1251
1252    #[tokio::test]
1253    async fn no_deps_one_command() {
1254        let mut builder = RegistryBuilder::new();
1255        builder.register("root", "1.0.0").with_command("asdf");
1256        let registry = builder.finish();
1257        let id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1258        let root = builder.get(&id);
1259
1260        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1261            .await
1262            .unwrap();
1263
1264        let mut dependency_graph = builder.start_dependency_graph();
1265        dependency_graph.insert(id.clone());
1266        assert_eq!(deps(&resolution), dependency_graph.finish());
1267        assert_eq!(
1268            resolution.package,
1269            ResolvedPackage {
1270                root_package: root.package_id(),
1271                commands: map! {
1272                    "asdf" => ItemLocation {
1273                        name: "asdf".to_string(),
1274                        package: root.package_id(),
1275                    },
1276                },
1277                entrypoint: Some("asdf".to_string()),
1278                filesystem: Vec::new(),
1279            }
1280        );
1281    }
1282
1283    #[tokio::test]
1284    async fn single_dependency() {
1285        let mut builder = RegistryBuilder::new();
1286        builder
1287            .register("root", "1.0.0")
1288            .with_dependency("dep", "=1.0.0");
1289        builder.register("dep", "1.0.0");
1290        let registry = builder.finish();
1291        let id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1292        let root = builder.get(&id);
1293
1294        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1295            .await
1296            .unwrap();
1297        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
1298
1299        let mut dependency_graph = builder.start_dependency_graph();
1300        dependency_graph.insert(id.clone()).with_dependency(&dep_id);
1301        dependency_graph.insert(dep_id.clone());
1302        assert_eq!(deps(&resolution), dependency_graph.finish());
1303        assert_eq!(
1304            resolution.package,
1305            ResolvedPackage {
1306                root_package: root.package_id(),
1307                commands: BTreeMap::new(),
1308                entrypoint: None,
1309                filesystem: Vec::new(),
1310            }
1311        );
1312    }
1313
1314    #[tokio::test]
1315    async fn linear_dependency_chain() {
1316        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
1317        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
1318        let third_id = PackageId::new_named("third", "1.0.0".parse().unwrap());
1319
1320        let mut builder = RegistryBuilder::new();
1321        builder
1322            .register("first", "1.0.0")
1323            .with_dependency("second", "=1.0.0");
1324        builder
1325            .register("second", "1.0.0")
1326            .with_dependency("third", "=1.0.0");
1327        builder.register("third", "1.0.0");
1328        let registry = builder.finish();
1329        let root = builder.get(&first_id);
1330
1331        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1332            .await
1333            .unwrap();
1334
1335        let mut dependency_graph = builder.start_dependency_graph();
1336        dependency_graph
1337            .insert(first_id.clone())
1338            .with_dependency(&second_id);
1339        dependency_graph
1340            .insert(second_id.clone())
1341            .with_dependency(&third_id);
1342        dependency_graph.insert(third_id.clone());
1343        assert_eq!(deps(&resolution), dependency_graph.finish());
1344        assert_eq!(
1345            resolution.package,
1346            ResolvedPackage {
1347                root_package: root.package_id(),
1348                commands: BTreeMap::new(),
1349                entrypoint: None,
1350                filesystem: Vec::new(),
1351            }
1352        );
1353    }
1354
1355    #[tokio::test]
1356    async fn pick_the_latest_dependency_when_multiple_are_possible() {
1357        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1358        let mut builder = RegistryBuilder::new();
1359        builder
1360            .register("root", "1.0.0")
1361            .with_dependency("dep", "^1.0.0");
1362        builder.register("dep", "1.0.0");
1363        builder.register("dep", "1.0.1");
1364        builder.register("dep", "1.0.2");
1365        let registry = builder.finish();
1366        let root = builder.get(&root_id);
1367
1368        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1369            .await
1370            .unwrap();
1371        let dep_id = PackageId::new_named("dep", "1.0.2".parse().unwrap());
1372
1373        let mut dependency_graph = builder.start_dependency_graph();
1374        dependency_graph
1375            .insert(root_id.clone())
1376            .with_dependency(&dep_id);
1377        dependency_graph.insert(dep_id.clone());
1378        assert_eq!(deps(&resolution), dependency_graph.finish());
1379        assert_eq!(
1380            resolution.package,
1381            ResolvedPackage {
1382                root_package: root.package_id(),
1383                commands: BTreeMap::new(),
1384                entrypoint: None,
1385                filesystem: Vec::new(),
1386            }
1387        );
1388    }
1389
1390    #[tokio::test]
1391    async fn merge_compatible_versions() {
1392        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1393        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
1394        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
1395        let common_id = PackageId::new_named("common", "1.2.0".parse().unwrap());
1396
1397        let mut builder = RegistryBuilder::new();
1398        builder
1399            .register("root", "1.0.0")
1400            .with_dependency("first", "=1.0.0")
1401            .with_dependency("second", "=1.0.0");
1402        builder
1403            .register("first", "1.0.0")
1404            .with_dependency("common", "^1.0.0");
1405        builder
1406            .register("second", "1.0.0")
1407            .with_dependency("common", ">1.1,<1.3");
1408        builder.register("common", "1.0.0");
1409        builder.register("common", "1.1.0");
1410        builder.register("common", "1.2.0");
1411        builder.register("common", "1.5.0");
1412        let registry = builder.finish();
1413        let root = builder.get(&root_id);
1414
1415        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1416            .await
1417            .unwrap();
1418
1419        let mut dependency_graph = builder.start_dependency_graph();
1420        dependency_graph
1421            .insert(root_id.clone())
1422            .with_dependency(&first_id)
1423            .with_dependency(&second_id);
1424        dependency_graph
1425            .insert(first_id.clone())
1426            .with_dependency(&common_id);
1427        dependency_graph
1428            .insert(second_id.clone())
1429            .with_dependency(&common_id);
1430        dependency_graph.insert(common_id.clone());
1431        assert_eq!(deps(&resolution), dependency_graph.finish());
1432        assert_eq!(
1433            resolution.package,
1434            ResolvedPackage {
1435                root_package: root.package_id(),
1436                commands: BTreeMap::new(),
1437                entrypoint: None,
1438                filesystem: Vec::new(),
1439            }
1440        );
1441    }
1442
1443    #[tokio::test]
1444    async fn merge_compatible_versions_when_constraints_appear_later() {
1445        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1446        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
1447        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
1448        let mid_id = PackageId::new_named("mid", "1.0.0".parse().unwrap());
1449        let common_id = PackageId::new_named("common", "1.2.0".parse().unwrap());
1450
1451        let mut builder = RegistryBuilder::new();
1452        builder
1453            .register("root", "1.0.0")
1454            .with_dependency("first", "=1.0.0")
1455            .with_dependency("second", "=1.0.0");
1456        builder
1457            .register("first", "1.0.0")
1458            .with_dependency("common", "^1.0.0");
1459        builder
1460            .register("second", "1.0.0")
1461            .with_dependency("mid", "=1.0.0");
1462        builder
1463            .register("mid", "1.0.0")
1464            .with_dependency("common", "<=1.2.0");
1465        builder.register("common", "1.2.0");
1466        builder.register("common", "1.5.0");
1467        let registry = builder.finish();
1468        let root = builder.get(&root_id);
1469
1470        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1471            .await
1472            .unwrap();
1473
1474        let mut dependency_graph = builder.start_dependency_graph();
1475        dependency_graph
1476            .insert(root_id.clone())
1477            .with_dependency(&first_id)
1478            .with_dependency(&second_id);
1479        dependency_graph
1480            .insert(first_id.clone())
1481            .with_dependency(&common_id);
1482        dependency_graph
1483            .insert(second_id.clone())
1484            .with_dependency(&mid_id);
1485        dependency_graph
1486            .insert(mid_id.clone())
1487            .with_dependency(&common_id);
1488        dependency_graph.insert(common_id.clone());
1489        assert_eq!(deps(&resolution), dependency_graph.finish());
1490    }
1491
1492    #[tokio::test]
1493    async fn pick_a_lower_direct_dependency_to_preserve_transitive_unification() {
1494        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1495        let feature_id = PackageId::new_named("feature", "1.0.0".parse().unwrap());
1496        let common_id = PackageId::new_named("common", "1.5.0".parse().unwrap());
1497        let shared_id = PackageId::new_named("shared", "1.0.0".parse().unwrap());
1498
1499        let mut builder = RegistryBuilder::new();
1500        builder
1501            .register("root", "1.0.0")
1502            .with_dependency("feature", "^1.0.0")
1503            .with_dependency("shared", "=1.0.0");
1504        builder
1505            .register("feature", "1.0.0")
1506            .with_dependency("common", "^1.0.0");
1507        builder
1508            .register("feature", "1.1.0")
1509            .with_dependency("common", "^2.0.0");
1510        builder
1511            .register("shared", "1.0.0")
1512            .with_dependency("common", "=1.5.0");
1513        builder.register("common", "1.5.0");
1514        builder.register("common", "2.1.0");
1515        let registry = builder.finish();
1516        let root = builder.get(&root_id);
1517
1518        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1519            .await
1520            .unwrap();
1521
1522        let mut dependency_graph = builder.start_dependency_graph();
1523        dependency_graph
1524            .insert(root_id.clone())
1525            .with_dependency(&feature_id)
1526            .with_dependency(&shared_id);
1527        dependency_graph
1528            .insert(feature_id.clone())
1529            .with_dependency(&common_id);
1530        dependency_graph
1531            .insert(shared_id.clone())
1532            .with_dependency(&common_id);
1533        dependency_graph.insert(common_id.clone());
1534        assert_eq!(deps(&resolution), dependency_graph.finish());
1535    }
1536
1537    #[tokio::test]
1538    async fn pick_a_lower_dependency_after_branching_transitive_constraints() {
1539        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1540        let feature_id = PackageId::new_named("feature", "1.0.0".parse().unwrap());
1541        let aggregator_id = PackageId::new_named("aggregator", "1.0.0".parse().unwrap());
1542        let leaf_id = PackageId::new_named("leaf", "1.0.0".parse().unwrap());
1543        let common_id = PackageId::new_named("common", "1.4.0".parse().unwrap());
1544
1545        let mut builder = RegistryBuilder::new();
1546        builder
1547            .register("root", "1.0.0")
1548            .with_dependency("feature", "^1.0.0")
1549            .with_dependency("aggregator", "=1.0.0");
1550        builder
1551            .register("feature", "1.0.0")
1552            .with_dependency("common", "^1.0.0");
1553        builder
1554            .register("feature", "1.1.0")
1555            .with_dependency("common", "^2.0.0");
1556        builder
1557            .register("aggregator", "1.0.0")
1558            .with_dependency("leaf", "=1.0.0");
1559        builder
1560            .register("leaf", "1.0.0")
1561            .with_dependency("common", "<=1.4.0");
1562        builder.register("common", "1.4.0");
1563        builder.register("common", "2.0.0");
1564        let registry = builder.finish();
1565        let root = builder.get(&root_id);
1566
1567        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1568            .await
1569            .unwrap();
1570
1571        let mut dependency_graph = builder.start_dependency_graph();
1572        dependency_graph
1573            .insert(root_id.clone())
1574            .with_dependency(&feature_id)
1575            .with_dependency(&aggregator_id);
1576        dependency_graph
1577            .insert(feature_id.clone())
1578            .with_dependency(&common_id);
1579        dependency_graph
1580            .insert(aggregator_id.clone())
1581            .with_dependency(&leaf_id);
1582        dependency_graph
1583            .insert(leaf_id.clone())
1584            .with_dependency(&common_id);
1585        dependency_graph.insert(common_id.clone());
1586        assert_eq!(deps(&resolution), dependency_graph.finish());
1587    }
1588
1589    #[tokio::test]
1590    async fn backtracking_skips_acyclic_graphs_that_are_not_unified() {
1591        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1592        let feature_id = PackageId::new_named("feature", "1.0.0".parse().unwrap());
1593
1594        let mut builder = RegistryBuilder::new();
1595        builder
1596            .register("root", "1.0.0")
1597            .with_dependency("feature", "^1.0.0");
1598        builder.register("root", "1.1.0");
1599        builder.register("feature", "1.0.0");
1600        builder
1601            .register("feature", "1.1.0")
1602            .with_dependency("root", "^1.0.0");
1603        let registry = builder.finish();
1604        let root = builder.get(&root_id);
1605
1606        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1607            .await
1608            .unwrap();
1609
1610        let mut dependency_graph = builder.start_dependency_graph();
1611        dependency_graph
1612            .insert(root_id.clone())
1613            .with_dependency(&feature_id);
1614        dependency_graph.insert(feature_id.clone());
1615        assert_eq!(deps(&resolution), dependency_graph.finish());
1616    }
1617
1618    #[tokio::test]
1619    async fn oscillating_greedy_selection_falls_back_to_backtracking() {
1620        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1621        let a_id = PackageId::new_named("a", "1.0.0".parse().unwrap());
1622
1623        let mut builder = RegistryBuilder::new();
1624        builder
1625            .register("root", "1.0.0")
1626            .with_dependency("a", "^1.0.0");
1627        builder.register("a", "1.0.0");
1628        builder
1629            .register("a", "2.0.0")
1630            .with_dependency("x", "=1.0.0");
1631        builder
1632            .register("x", "1.0.0")
1633            .with_dependency("a", "=1.0.0");
1634        let registry = builder.finish();
1635        let root = builder.get(&root_id);
1636
1637        tokio::time::timeout(
1638            Duration::from_secs(1),
1639            discover_merged_dependencies(&root.package_id(), &root.pkg, &registry),
1640        )
1641        .await
1642        .expect("greedy dependency discovery did not terminate")
1643        .unwrap();
1644
1645        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1646            .await
1647            .unwrap();
1648
1649        let mut dependency_graph = builder.start_dependency_graph();
1650        dependency_graph
1651            .insert(root_id.clone())
1652            .with_dependency(&a_id);
1653        dependency_graph.insert(a_id.clone());
1654        assert_eq!(deps(&resolution), dependency_graph.finish());
1655    }
1656
1657    #[tokio::test]
1658    async fn named_tags_are_resolved_through_the_source_during_backtracking() {
1659        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1660        let feature_id = PackageId::new_named("feature", "1.0.0".parse().unwrap());
1661        let common_id = PackageId::new_named("common", "1.5.0".parse().unwrap());
1662        let shared_id = PackageId::new_named("shared", "1.0.0".parse().unwrap());
1663
1664        let mut builder = RegistryBuilder::new();
1665        builder
1666            .register("root", "1.0.0")
1667            .with_dependency("feature", "^1.0.0")
1668            .with_dependency("shared", "=1.0.0");
1669        builder
1670            .register("feature", "1.0.0")
1671            .with_tagged_dependency("common", "stable");
1672        builder
1673            .register("feature", "1.1.0")
1674            .with_dependency("common", "^2.0.0");
1675        builder
1676            .register("shared", "1.0.0")
1677            .with_dependency("common", "=1.5.0");
1678        builder.register("common", "1.5.0");
1679        builder.register("common", "2.1.0");
1680        let registry = builder.finish_with_tags(BTreeMap::from([(
1681            ("common".to_string(), "stable".to_string()),
1682            "1.5.0".to_string(),
1683        )]));
1684        let root = builder.get(&root_id);
1685
1686        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1687            .await
1688            .unwrap();
1689
1690        let mut dependency_graph = builder.start_dependency_graph();
1691        dependency_graph
1692            .insert(root_id.clone())
1693            .with_dependency(&feature_id)
1694            .with_dependency(&shared_id);
1695        dependency_graph
1696            .insert(feature_id.clone())
1697            .with_dependency(&common_id);
1698        dependency_graph
1699            .insert(shared_id.clone())
1700            .with_dependency(&common_id);
1701        dependency_graph.insert(common_id.clone());
1702        assert_eq!(deps(&resolution), dependency_graph.finish());
1703    }
1704
1705    #[tokio::test]
1706    async fn incompatible_versions_still_report_duplicates() {
1707        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1708
1709        let mut builder = RegistryBuilder::new();
1710        builder
1711            .register("root", "1.0.0")
1712            .with_dependency("first", "=1.0.0")
1713            .with_dependency("second", "=1.0.0");
1714        builder
1715            .register("first", "1.0.0")
1716            .with_dependency("common", "=1.0.0");
1717        builder
1718            .register("second", "1.0.0")
1719            .with_dependency("common", "=2.0.0");
1720        builder.register("common", "1.0.0");
1721        builder.register("common", "2.0.0");
1722        let registry = builder.finish();
1723        let root = builder.get(&root_id);
1724
1725        let result = resolve(&root.package_id(), &root.pkg, &registry).await;
1726
1727        match result {
1728            Err(ResolveError::DuplicateVersions {
1729                package_name,
1730                versions,
1731            }) => {
1732                assert_eq!(package_name, "common");
1733                assert_eq!(
1734                    versions,
1735                    [
1736                        Version::parse("1.0.0").unwrap(),
1737                        Version::parse("2.0.0").unwrap(),
1738                    ]
1739                );
1740            }
1741            _ => unreachable!("Expected a duplicate versions error, found {:?}", result),
1742        }
1743    }
1744
1745    #[tokio::test]
1746    async fn incompatible_parent_versions_still_report_duplicates_when_no_solution_exists() {
1747        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1748
1749        let mut builder = RegistryBuilder::new();
1750        builder
1751            .register("root", "1.0.0")
1752            .with_dependency("feature", "^1.0.0")
1753            .with_dependency("shared", "=1.0.0");
1754        builder
1755            .register("feature", "1.0.0")
1756            .with_dependency("common", "^1.0.0");
1757        builder
1758            .register("feature", "1.1.0")
1759            .with_dependency("common", "^2.0.0");
1760        builder
1761            .register("shared", "1.0.0")
1762            .with_dependency("common", "=3.0.0");
1763        builder.register("common", "1.5.0");
1764        builder.register("common", "2.1.0");
1765        builder.register("common", "3.0.0");
1766        let registry = builder.finish();
1767        let root = builder.get(&root_id);
1768
1769        let result = resolve(&root.package_id(), &root.pkg, &registry).await;
1770
1771        match result {
1772            Err(ResolveError::DuplicateVersions {
1773                package_name,
1774                versions,
1775            }) => {
1776                assert_eq!(package_name, "common");
1777                assert_eq!(
1778                    versions,
1779                    [
1780                        Version::parse("2.1.0").unwrap(),
1781                        Version::parse("3.0.0").unwrap(),
1782                    ]
1783                );
1784            }
1785            _ => unreachable!("Expected a duplicate versions error, found {:?}", result),
1786        }
1787    }
1788
1789    #[tokio::test]
1790    async fn very_large_dependency_graphs_fail_with_a_clear_limit_error() {
1791        let root_id = PackageId::new_named("pkg0", "1.0.0".parse().unwrap());
1792
1793        let mut builder = RegistryBuilder::new();
1794        for ix in 0..=MAX_DISCOVERED_PACKAGES {
1795            let name = format!("pkg{ix}");
1796            let version = "1.0.0";
1797            let mut pkg = builder.register(&name, version);
1798            if ix < MAX_DISCOVERED_PACKAGES {
1799                let dep_name = format!("pkg{}", ix + 1);
1800                pkg.with_dependency(&dep_name, "=1.0.0");
1801            }
1802        }
1803
1804        let registry = builder.finish();
1805        let root = builder.get(&root_id);
1806
1807        let result = resolve(&root.package_id(), &root.pkg, &registry).await;
1808
1809        match result {
1810            Err(ResolveError::TooComplex {
1811                resource,
1812                value,
1813                limit,
1814            }) => {
1815                assert_eq!(resource, "packages");
1816                assert_eq!(limit, MAX_DISCOVERED_PACKAGES);
1817                assert!(value > limit);
1818            }
1819            _ => unreachable!("Expected a complexity limit error, found {:?}", result),
1820        }
1821    }
1822
1823    #[test]
1824    fn validate_dependency_graph_reports_cycles_without_panicking() {
1825        let mut builder = RegistryBuilder::new();
1826        builder.register("root", "1.0.0");
1827        builder.register("dep", "1.0.0");
1828
1829        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1830        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
1831
1832        let root = builder.get(&root_id);
1833        let dep = builder.get(&dep_id);
1834
1835        let mut graph = DiGraph::new();
1836        let root_ix = graph.add_node(Node {
1837            id: root_id.clone(),
1838            pkg: root.pkg.clone(),
1839            dist: Some(root.dist.clone()),
1840        });
1841        let dep_ix = graph.add_node(Node {
1842            id: dep_id.clone(),
1843            pkg: dep.pkg.clone(),
1844            dist: Some(dep.dist.clone()),
1845        });
1846
1847        graph.add_edge(
1848            root_ix,
1849            dep_ix,
1850            Edge {
1851                alias: "dep".to_string(),
1852            },
1853        );
1854        graph.add_edge(
1855            dep_ix,
1856            root_ix,
1857            Edge {
1858                alias: "root".to_string(),
1859            },
1860        );
1861
1862        let graph = DependencyGraph::new(
1863            root_ix,
1864            graph,
1865            BTreeMap::from([(root_id, root_ix), (dep_id, dep_ix)]),
1866        );
1867
1868        assert!(matches!(
1869            validate_dependency_graph(&graph),
1870            Err(ResolveError::Cycle(_))
1871        ));
1872    }
1873
1874    #[test]
1875    fn backtracking_partial_discovery_enforces_pending_dependency_limit() {
1876        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1877        let child_id = PackageId::new_named("child", "1.0.0".parse().unwrap());
1878
1879        let root = PackageInfo {
1880            id: root_id.clone(),
1881            dependencies: vec![Dependency {
1882                alias: "child".to_string(),
1883                pkg: PackageSource::from(
1884                    NamedPackageIdent::try_from_full_name_and_version("child", "=1.0.0").unwrap(),
1885                ),
1886            }],
1887            commands: Vec::new(),
1888            entrypoint: None,
1889            filesystem: Vec::new(),
1890        };
1891        let mut child = PackageInfo {
1892            id: child_id.clone(),
1893            dependencies: Vec::new(),
1894            commands: Vec::new(),
1895            entrypoint: None,
1896            filesystem: Vec::new(),
1897        };
1898        for ix in 0..=MAX_PENDING_DEPENDENCIES {
1899            let name = format!("leaf{ix}");
1900            child.dependencies.push(Dependency {
1901                alias: name.clone(),
1902                pkg: PackageSource::from(
1903                    NamedPackageIdent::try_from_full_name_and_version(&name, "=1.0.0").unwrap(),
1904                ),
1905            });
1906        }
1907        let dist = DistributionInfo {
1908            webc: "http://localhost/child@1.0.0".parse().unwrap(),
1909            webc_sha256: [0; 32].into(),
1910        };
1911
1912        let mut discovery = PartialDiscovery::new(&root_id, &root);
1913        let task = discovery.pending.remove(0);
1914        let result = discovery.add_dependency(
1915            task.parent(),
1916            task.dep(),
1917            PackageSummary { pkg: child, dist },
1918        );
1919
1920        match result {
1921            Err(ResolveError::TooComplex {
1922                resource,
1923                value,
1924                limit,
1925            }) => {
1926                assert_eq!(resource, "pending_dependencies");
1927                assert_eq!(limit, MAX_PENDING_DEPENDENCIES);
1928                assert!(value > limit);
1929            }
1930            _ => unreachable!("Expected a complexity limit error, found {:?}", result),
1931        }
1932    }
1933
1934    #[tokio::test]
1935    async fn backtracking_handles_deep_incompatible_candidate_tree_with_state_limit() {
1936        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1937
1938        let mut builder = RegistryBuilder::new();
1939        builder
1940            .register("root", "1.0.0")
1941            .with_dependency("branch0", "^1.0.0")
1942            .with_dependency("pinner", "=1.0.0");
1943
1944        for branch in 0..8 {
1945            for version in 0..8 {
1946                let name = format!("branch{branch}");
1947                let version = format!("1.{version}.0");
1948                let mut pkg = builder.register(&name, &version);
1949                pkg.with_dependency("common", "=2.0.0");
1950                if branch < 7 {
1951                    pkg.with_dependency(&format!("branch{}", branch + 1), "^1.0.0");
1952                }
1953            }
1954        }
1955
1956        builder
1957            .register("pinner", "1.0.0")
1958            .with_dependency("common", "=1.0.0");
1959        builder.register("common", "1.0.0");
1960        builder.register("common", "2.0.0");
1961
1962        let registry = builder.finish();
1963        let root = builder.get(&root_id);
1964
1965        let result = tokio::time::timeout(
1966            Duration::from_secs(1),
1967            resolve(&root.package_id(), &root.pkg, &registry),
1968        )
1969        .await
1970        .expect("backtracking did not finish within the test budget");
1971
1972        assert!(matches!(
1973            result,
1974            Err(ResolveError::DuplicateVersions { .. })
1975                | Err(ResolveError::TooComplex {
1976                    resource: "backtrack_states",
1977                    ..
1978                })
1979        ));
1980    }
1981
1982    #[tokio::test]
1983    async fn commands_from_dependencies_end_up_in_the_package() {
1984        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1985        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
1986        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
1987        let mut builder = RegistryBuilder::new();
1988        builder
1989            .register("root", "1.0.0")
1990            .with_dependency("first", "=1.0.0")
1991            .with_dependency("second", "=1.0.0");
1992        builder
1993            .register("first", "1.0.0")
1994            .with_command("first-command");
1995        builder
1996            .register("second", "1.0.0")
1997            .with_command("second-command");
1998        let registry = builder.finish();
1999        let root = builder.get(&root_id);
2000
2001        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
2002            .await
2003            .unwrap();
2004
2005        let mut dependency_graph = builder.start_dependency_graph();
2006        dependency_graph
2007            .insert(root_id.clone())
2008            .with_dependency(&first_id)
2009            .with_dependency(&second_id);
2010        dependency_graph.insert(first_id.clone());
2011        dependency_graph.insert(second_id.clone());
2012        assert_eq!(deps(&resolution), dependency_graph.finish());
2013        assert_eq!(
2014            resolution.package,
2015            ResolvedPackage {
2016                root_package: root.package_id(),
2017                commands: map! {
2018                    "first-command" => ItemLocation {
2019                        name: "first-command".to_string(),
2020                        package: builder.get(&first_id).package_id(),
2021                     },
2022                    "second-command" => ItemLocation {
2023                        name: "second-command".to_string(),
2024                        package: builder.get(&second_id).package_id(),
2025                     },
2026                },
2027                entrypoint: None,
2028                filesystem: Vec::new(),
2029            }
2030        );
2031    }
2032
2033    #[tokio::test]
2034    async fn commands_in_root_shadow_their_dependencies() {
2035        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2036        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
2037        let mut builder = RegistryBuilder::new();
2038        builder
2039            .register("root", "1.0.0")
2040            .with_dependency("dep", "=1.0.0")
2041            .with_command("command");
2042        builder.register("dep", "1.0.0").with_command("command");
2043        let registry = builder.finish();
2044        let root = builder.get(&root_id);
2045
2046        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
2047            .await
2048            .unwrap();
2049
2050        let mut dependency_graph = builder.start_dependency_graph();
2051        dependency_graph
2052            .insert(root_id.clone())
2053            .with_dependency(&dep_id);
2054        dependency_graph.insert(dep_id.clone());
2055        assert_eq!(deps(&resolution), dependency_graph.finish());
2056        assert_eq!(
2057            resolution.package,
2058            ResolvedPackage {
2059                root_package: root.package_id(),
2060                commands: map! {
2061                    "command" => ItemLocation {
2062                        name: "command".to_string(),
2063                        package: builder.get(&root_id).package_id(),
2064                     },
2065                },
2066                entrypoint: Some("command".to_string()),
2067                filesystem: Vec::new(),
2068            }
2069        );
2070    }
2071
2072    #[tokio::test]
2073    async fn cyclic_dependencies() {
2074        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2075        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
2076
2077        let mut builder = RegistryBuilder::new();
2078        builder
2079            .register("root", "1.0.0")
2080            .with_dependency("dep", "=1.0.0");
2081        builder
2082            .register("dep", "1.0.0")
2083            .with_dependency("root", "=1.0.0");
2084        let registry = builder.finish();
2085        let root = builder.get(&root_id);
2086
2087        let err = resolve(&root.package_id(), &root.pkg, &registry)
2088            .await
2089            .unwrap_err();
2090
2091        let cycle = err.as_cycle().unwrap().to_vec();
2092        assert_eq!(
2093            cycle,
2094            [
2095                builder.get(&root_id).package_id(),
2096                builder.get(&dep_id).package_id(),
2097                builder.get(&root_id).package_id(),
2098            ]
2099        );
2100    }
2101
2102    /// A root with commands of its own and no explicit entrypoint must not fall
2103    /// back to a dependency's entrypoint.
2104    ///
2105    /// See https://github.com/wasmerio/wasmer/issues/6870.
2106    #[tokio::test]
2107    async fn entrypoint_is_not_inherited_when_root_has_its_own_commands() {
2108        let root_id = PackageId::new_named("anybuild", "1.0.0".parse().unwrap());
2109        let mut builder = RegistryBuilder::new();
2110        builder
2111            .register("anybuild", "1.0.0")
2112            .with_command("anybuild")
2113            .with_command("shipit")
2114            .with_dependency("bash", "=1.0.0");
2115        builder
2116            .register("bash", "1.0.0")
2117            .with_command("bash")
2118            .with_entrypoint("bash");
2119        let registry = builder.finish();
2120        let root = builder.get(&root_id);
2121
2122        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
2123            .await
2124            .unwrap();
2125
2126        // "anybuild" and "shipit" are equally good candidates and neither is
2127        // declared as the entrypoint, so the caller has to error out rather
2128        // than guess -- and it certainly must not reach for "bash".
2129        assert_eq!(resolution.package.entrypoint, None);
2130    }
2131
2132    /// Same as above, but with a single root command, so the "root package's
2133    /// only command" fallback has an unambiguous answer to give.
2134    #[tokio::test]
2135    async fn root_package_only_command_wins_over_dependency_entrypoint() {
2136        let root_id = PackageId::new_named("anybuild", "1.0.0".parse().unwrap());
2137        let mut builder = RegistryBuilder::new();
2138        builder
2139            .register("anybuild", "1.0.0")
2140            .with_command("anybuild")
2141            .with_dependency("bash", "=1.0.0");
2142        builder
2143            .register("bash", "1.0.0")
2144            .with_command("bash")
2145            .with_entrypoint("bash");
2146        let registry = builder.finish();
2147        let root = builder.get(&root_id);
2148
2149        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
2150            .await
2151            .unwrap();
2152
2153        assert_eq!(
2154            resolution.package.entrypoint.as_deref(),
2155            Some("anybuild"),
2156            "the root package's own only command should win over a dependency's \
2157             entrypoint",
2158        );
2159    }
2160
2161    #[tokio::test]
2162    async fn entrypoint_is_inherited() {
2163        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2164        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
2165
2166        let mut builder = RegistryBuilder::new();
2167        builder
2168            .register("root", "1.0.0")
2169            .with_dependency("dep", "=1.0.0");
2170        builder
2171            .register("dep", "1.0.0")
2172            .with_command("entry")
2173            .with_entrypoint("entry");
2174        let registry = builder.finish();
2175        let root = builder.get(&root_id);
2176
2177        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
2178            .await
2179            .unwrap();
2180
2181        assert_eq!(
2182            resolution.package,
2183            ResolvedPackage {
2184                root_package: root.package_id(),
2185                commands: map! {
2186                    "entry" => ItemLocation {
2187                        name: "entry".to_string(),
2188                        package: builder.get(&dep_id).package_id(),
2189                     },
2190                },
2191                entrypoint: Some("entry".to_string()),
2192                filesystem: Vec::new(),
2193            }
2194        );
2195    }
2196
2197    #[tokio::test]
2198    async fn infer_entrypoint_if_unspecified_and_only_one_command_in_root_package() {
2199        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2200        let mut builder = RegistryBuilder::new();
2201        builder
2202            .register("root", "1.0.0")
2203            .with_command("root-cmd")
2204            .with_dependency("dep", "=1.0.0");
2205        builder.register("dep", "1.0.0").with_command("entry");
2206        let registry = builder.finish();
2207        let root = builder.get(&root_id);
2208
2209        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
2210            .await
2211            .unwrap();
2212
2213        assert_eq!(resolution.package.entrypoint.as_deref(), Some("root-cmd"));
2214    }
2215
2216    #[test]
2217    fn cyclic_error_message() {
2218        let cycle = [
2219            PackageId::new_named("root", "1.0.0".parse().unwrap()),
2220            PackageId::new_named("dep", "1.0.0".parse().unwrap()),
2221            PackageId::new_named("root", "1.0.0".parse().unwrap()),
2222        ];
2223
2224        let message = print_cycle(&cycle);
2225
2226        assert_eq!(message, "root@1.0.0 → dep@1.0.0 → root@1.0.0");
2227    }
2228
2229    #[test]
2230    fn filesystem_with_one_package_and_no_fs_tables() {
2231        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2232        let mut builder = RegistryBuilder::new();
2233        builder.register("root", "1.0.0");
2234        let mut dep_builder = builder.start_dependency_graph();
2235        dep_builder.insert(root_id.clone());
2236        let graph = dep_builder.graph(root_id.clone());
2237
2238        let pkg = resolve_package(&graph).unwrap();
2239
2240        assert!(pkg.filesystem.is_empty());
2241    }
2242
2243    #[test]
2244    fn filesystem_with_one_package_and_one_fs_tables() {
2245        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2246        let mut builder = RegistryBuilder::new();
2247        builder
2248            .register("root", "1.0.0")
2249            .with_fs_mapping("atom", "/publisher/lib", "/lib");
2250        let mut dep_builder = builder.start_dependency_graph();
2251        dep_builder.insert(root_id.clone());
2252        let graph = dep_builder.graph(root_id.clone());
2253
2254        let pkg = resolve_package(&graph).unwrap();
2255
2256        assert_eq!(
2257            pkg.filesystem,
2258            vec![ResolvedFileSystemMapping {
2259                mount_path: PathBuf::from("/lib"),
2260                original_path: Some("/publisher/lib".to_string()),
2261                volume_name: "atom".to_string(),
2262                package: builder.get(&root_id).package_id(),
2263            }]
2264        );
2265    }
2266
2267    #[test]
2268    fn merge_fs_mappings_from_multiple_packages() {
2269        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2270        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
2271        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
2272
2273        let mut builder = RegistryBuilder::new();
2274        builder
2275            .register("root", "1.0.0")
2276            .with_dependency("first", "=1.0.0")
2277            .with_dependency("second", "=1.0.0")
2278            .with_fs_mapping("atom", "/root", "/root");
2279        builder.register("first", "1.0.0").with_fs_mapping(
2280            "atom",
2281            "/usr/local/lib/first",
2282            "/usr/local/lib/first",
2283        );
2284        builder.register("second", "1.0.0").with_fs_mapping(
2285            "atom",
2286            "/usr/local/lib/second",
2287            "/usr/local/lib/second",
2288        );
2289        let mut dep_builder = builder.start_dependency_graph();
2290        dep_builder
2291            .insert(root_id.clone())
2292            .with_dependency(&first_id)
2293            .with_dependency(&second_id);
2294        dep_builder.insert(first_id.clone());
2295        dep_builder.insert(second_id.clone());
2296        let graph = dep_builder.graph(root_id.clone());
2297
2298        let pkg = resolve_package(&graph).unwrap();
2299
2300        assert_eq!(
2301            pkg.filesystem,
2302            vec![
2303                ResolvedFileSystemMapping {
2304                    mount_path: PathBuf::from("/root"),
2305                    original_path: Some("/root".to_string()),
2306                    volume_name: "atom".to_string(),
2307                    package: builder.get(&root_id).package_id(),
2308                },
2309                ResolvedFileSystemMapping {
2310                    mount_path: PathBuf::from("/usr/local/lib/second"),
2311                    original_path: Some("/usr/local/lib/second".to_string()),
2312                    volume_name: "atom".to_string(),
2313                    package: builder.get(&second_id).package_id(),
2314                },
2315                ResolvedFileSystemMapping {
2316                    mount_path: PathBuf::from("/usr/local/lib/first"),
2317                    volume_name: "atom".to_string(),
2318                    original_path: Some("/usr/local/lib/first".to_string()),
2319                    package: builder.get(&first_id).package_id(),
2320                }
2321            ]
2322        );
2323    }
2324
2325    #[test]
2326    fn use_fs_mapping_from_dependency() {
2327        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
2328        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
2329        let mut builder = RegistryBuilder::new();
2330        builder
2331            .register("root", "1.0.0")
2332            .with_dependency("dep", "=1.0.0")
2333            .with_fs_mapping_from_dependency("dep-volume", "/root", "/root", "dep");
2334        builder.register("dep", "1.0.0");
2335        let mut dep_builder = builder.start_dependency_graph();
2336        dep_builder.insert(root_id.clone()).with_dependency(&dep_id);
2337        dep_builder.insert(dep_id.clone());
2338        let graph = dep_builder.graph(root_id.clone());
2339
2340        let pkg = resolve_package(&graph).unwrap();
2341
2342        assert_eq!(
2343            pkg.filesystem,
2344            vec![ResolvedFileSystemMapping {
2345                mount_path: PathBuf::from("/root"),
2346                original_path: Some("/root".to_string()),
2347                volume_name: "dep-volume".to_string(),
2348                package: builder.get(&dep_id).package_id(),
2349            }]
2350        );
2351    }
2352}