wasmer_wasix/runtime/resolver/
resolve.rs

1use std::{
2    collections::{BTreeMap, HashSet, VecDeque},
3    path::PathBuf,
4};
5
6use petgraph::{
7    graph::{DiGraph, NodeIndex},
8    visit::EdgeRef,
9};
10use semver::Version;
11use wasmer_config::package::{PackageId, PackageSource};
12
13use crate::runtime::resolver::{
14    DependencyGraph, ItemLocation, PackageInfo, PackageSummary, QueryError, Resolution,
15    ResolvedPackage, Source,
16    outputs::{Edge, Node},
17};
18
19use super::ResolvedFileSystemMapping;
20
21/// Given the [`PackageInfo`] for a root package, resolve its dependency graph
22/// and figure out how it could be executed.
23#[tracing::instrument(level = "debug", skip_all)]
24pub async fn resolve(
25    root_id: &PackageId,
26    root: &PackageInfo,
27    source: &dyn Source,
28) -> Result<Resolution, ResolveError> {
29    let graph = resolve_dependency_graph(root_id, root, source).await?;
30    let package = resolve_package(&graph)?;
31
32    Ok(Resolution { graph, package })
33}
34
35#[derive(Debug, thiserror::Error)]
36pub enum ResolveError {
37    #[error("{}", registry_error_message(.package))]
38    Registry {
39        package: PackageSource,
40        #[source]
41        error: QueryError,
42    },
43    #[error("Dependency cycle detected: {}", print_cycle(_0))]
44    Cycle(Vec<PackageId>),
45    #[error(
46        "Multiple versions of {package_name} were found {}",
47        versions.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", "),
48    )]
49    DuplicateVersions {
50        package_name: String,
51        versions: Vec<Version>,
52    },
53}
54
55fn registry_error_message(specifier: &PackageSource) -> String {
56    match specifier {
57        PackageSource::Ident(id) => {
58            format!("Unable to find \"{id}\" in the registry")
59        }
60        PackageSource::Url(url) => format!("Unable to resolve \"{url}\""),
61        PackageSource::Path(path) => {
62            format!("Unable to load \"{path}\" from disk")
63        }
64    }
65}
66
67impl ResolveError {
68    pub fn as_cycle(&self) -> Option<&[PackageId]> {
69        match self {
70            ResolveError::Cycle(cycle) => Some(cycle),
71            _ => None,
72        }
73    }
74}
75
76fn print_cycle(packages: &[PackageId]) -> String {
77    packages
78        .iter()
79        .map(|pkg_id| pkg_id.to_string())
80        .collect::<Vec<_>>()
81        .join(" → ")
82}
83
84async fn resolve_dependency_graph(
85    root_id: &PackageId,
86    root: &PackageInfo,
87    source: &dyn Source,
88) -> Result<DependencyGraph, ResolveError> {
89    let DiscoveredPackages {
90        root,
91        graph,
92        indices,
93        packages,
94    } = discover_dependencies(root_id, root, source).await?;
95
96    check_for_duplicate_versions(indices.iter().copied().map(|ix| &graph[ix].id))?;
97    log_dependencies(&graph, root);
98
99    let graph = DependencyGraph::new(root, graph, packages);
100
101    Ok(graph)
102}
103
104async fn discover_dependencies(
105    root_id: &PackageId,
106    root: &PackageInfo,
107    source: &dyn Source,
108) -> Result<DiscoveredPackages, ResolveError> {
109    let mut nodes: BTreeMap<PackageId, NodeIndex> = BTreeMap::new();
110    let mut graph: DiGraph<Node, Edge> = DiGraph::new();
111
112    let root_index = graph.add_node(Node {
113        id: root_id.clone(),
114        pkg: root.clone(),
115        dist: None,
116    });
117    nodes.insert(root_id.clone(), root_index);
118
119    let mut to_visit = VecDeque::new();
120    to_visit.push_back(root_index);
121
122    while let Some(index) = to_visit.pop_front() {
123        let mut to_add = Vec::new();
124
125        for dep in &graph[index].pkg.dependencies {
126            // Get the latest version that satisfies our requirement. If we were
127            // doing this more rigorously, we would be narrowing the version
128            // down using existing requirements and trying to reuse the same
129            // dependency when possible.
130            let dep_summary =
131                source
132                    .latest(&dep.pkg)
133                    .await
134                    .map_err(|error| ResolveError::Registry {
135                        package: dep.pkg.clone(),
136                        error,
137                    })?;
138            let dep_id = dep_summary.package_id().clone();
139
140            let PackageSummary { pkg, dist } = dep_summary;
141
142            let alias = dep.alias().to_string();
143            let node = Node {
144                id: dep_id.clone(),
145                pkg,
146                dist: Some(dist),
147            };
148            // Note: We can't add the node to the graph directly because we're
149            // still iterating over it.
150            to_add.push((alias, node));
151        }
152
153        for (alias, node) in to_add {
154            let dep_id = node.id.clone();
155
156            let dep_index = match nodes.get(&dep_id) {
157                Some(&ix) => ix,
158                None => {
159                    // Create a new node and schedule its dependencies to be
160                    // retrieved
161                    let ix = graph.add_node(node);
162                    nodes.insert(dep_id, ix);
163                    to_visit.push_back(ix);
164                    ix
165                }
166            };
167
168            graph.add_edge(index, dep_index, Edge { alias });
169        }
170    }
171
172    let sorted_indices = petgraph::algo::toposort(&graph, None).map_err(|_| cycle_error(&graph))?;
173
174    Ok(DiscoveredPackages {
175        root: root_index,
176        graph,
177        indices: sorted_indices,
178        packages: nodes,
179    })
180}
181
182fn cycle_error(graph: &petgraph::Graph<Node, Edge>) -> ResolveError {
183    // We know the graph has at least one cycle, so use SCC to find it.
184    let mut cycle = petgraph::algo::kosaraju_scc(graph)
185        .into_iter()
186        .find(|cycle| cycle.len() > 1)
187        .expect("We know there is at least one cycle");
188
189    // we want the loop's starting node to be deterministic (for tests), and
190    // nodes with lower indices are normally closer to the root of the
191    // dependency tree.
192    let lowest_index_node = cycle.iter().copied().min().expect("Cycle is non-empty");
193
194    // We want the cycle vector to start with that node, so let's do a bit of
195    // shuffling
196    let offset = cycle
197        .iter()
198        .position(|&node| node == lowest_index_node)
199        .unwrap();
200    cycle.rotate_left(offset);
201
202    // Don't forget to make the cycle start and end with the same node
203    cycle.push(lowest_index_node);
204
205    let package_ids = cycle.into_iter().map(|ix| graph[ix].id.clone()).collect();
206    ResolveError::Cycle(package_ids)
207}
208
209#[derive(Debug)]
210struct DiscoveredPackages {
211    root: NodeIndex,
212    graph: DiGraph<Node, Edge>,
213    /// All node indices, in topologically sorted order.
214    indices: Vec<NodeIndex>,
215    packages: BTreeMap<PackageId, NodeIndex>,
216}
217
218#[tracing::instrument(level = "debug", name = "dependencies", skip_all)]
219fn log_dependencies(graph: &DiGraph<Node, Edge>, root: NodeIndex) {
220    tracing::debug!(
221        root = root.index(),
222        dependency_count = graph.node_count(),
223        "Resolved dependencies",
224    );
225
226    if tracing::enabled!(tracing::Level::TRACE) {
227        petgraph::visit::depth_first_search(graph, [root], |event| {
228            if let petgraph::visit::DfsEvent::Discover(n, _) = event {
229                let package = &graph[n].id;
230                let dependencies: BTreeMap<_, _> = graph
231                    .edges(n)
232                    .map(|edge_ref| (&edge_ref.weight().alias, &graph[edge_ref.target()].id))
233                    .collect();
234
235                tracing::trace!(%package, ?dependencies);
236            }
237        });
238    }
239}
240
241/// As a workaround for the lack of "proper" dependency merging, we'll make sure
242/// only one copy of each package is in the dependency tree. If the same package
243/// is included in the tree multiple times, they all need to use the exact same
244/// version otherwise it's an error.
245fn check_for_duplicate_versions<'a, I>(package_ids: I) -> Result<(), ResolveError>
246where
247    I: Iterator<Item = &'a PackageId>,
248{
249    let mut package_versions: BTreeMap<&str, HashSet<&Version>> = BTreeMap::new();
250
251    for id in package_ids {
252        let Some(id) = id.as_named() else {
253            continue;
254        };
255        package_versions
256            .entry(&id.full_name)
257            .or_default()
258            .insert(&id.version);
259    }
260
261    for (package_name, versions) in package_versions {
262        if versions.len() > 1 {
263            let mut versions: Vec<_> = versions.into_iter().cloned().collect();
264            versions.sort();
265            return Err(ResolveError::DuplicateVersions {
266                package_name: package_name.to_string(),
267                versions,
268            });
269        }
270    }
271
272    Ok(())
273}
274
275/// Given some [`DiscoveredPackages`], figure out how the resulting "package"
276/// would look when loaded at runtime.
277fn resolve_package(dependency_graph: &DependencyGraph) -> Result<ResolvedPackage, ResolveError> {
278    // FIXME: This code is all super naive and will break the moment there
279    // are any conflicts or duplicate names.
280    tracing::trace!("Resolving the package");
281
282    let mut commands = BTreeMap::new();
283    let mut filesystem = Vec::new();
284
285    let mut entrypoint = dependency_graph.root_info().entrypoint.clone();
286
287    for index in petgraph::algo::toposort(dependency_graph.graph(), None).expect("acyclic") {
288        let node = &dependency_graph[index];
289        let id = &node.id;
290        let pkg = &node.pkg;
291
292        // update the entrypoint, if necessary
293        if entrypoint.is_none() {
294            if let Some(entry) = &pkg.entrypoint {
295                tracing::trace!(
296                    entrypoint = entry.as_str(),
297                    parent=%id,
298                    "Inheriting the entrypoint",
299                );
300
301                entrypoint = Some(entry.clone());
302            }
303        }
304
305        for cmd in &pkg.commands {
306            // Note: We are traversing in topological order with the root at the
307            // start, so if we ever see any duplicates we should prefer the
308            // earlier copy and skip the later one.
309
310            match commands.entry(cmd.name.clone()) {
311                std::collections::btree_map::Entry::Vacant(entry) => {
312                    let resolved = ItemLocation {
313                        name: cmd.name.clone(),
314                        package: id.clone(),
315                    };
316                    entry.insert(resolved);
317                    tracing::trace!(
318                        command.name=cmd.name.as_str(),
319                        pkg=%id,
320                        "Discovered command",
321                    );
322                }
323                std::collections::btree_map::Entry::Occupied(_) => {
324                    tracing::trace!(
325                        command.name=cmd.name.as_str(),
326                        pkg=%id,
327                        "Ignoring duplicate command",
328                    );
329                }
330            }
331        }
332
333        for mapping in &pkg.filesystem {
334            let dep = match &mapping.dependency_name {
335                Some(name) => {
336                    let dep_index = dependency_graph
337                        .graph()
338                        .edges(index)
339                        .find(|edge| edge.weight().alias == *name)
340                        .unwrap()
341                        .target();
342                    &dependency_graph[dep_index].id
343                }
344                None => id,
345            };
346            filesystem.push(ResolvedFileSystemMapping {
347                mount_path: PathBuf::from(&mapping.mount_path),
348                original_path: mapping.original_path.clone(),
349                volume_name: mapping.volume_name.clone(),
350                package: dep.clone(),
351            })
352        }
353    }
354
355    if entrypoint.is_none() {
356        // We *still* haven't been able to figure out what the entrypoint for the
357        // resolved package should be. If there is only one command in the main
358        // package, let's assume they want to use that.
359        //
360        // This works around packages like saghul/quickjs and syrusakbary/cowsay
361        // which don't specify their entrypoints explicitly.
362        if let [cmd] = dependency_graph.root_info().commands.as_slice() {
363            tracing::debug!(
364                command = cmd.name.as_str(),
365                "No entrypoint specified. Falling back to the root package's only command.",
366            );
367            entrypoint = Some(cmd.name.clone());
368        }
369    }
370
371    tracing::debug!("resolved filesystem: {:?}", &filesystem);
372
373    Ok(ResolvedPackage {
374        root_package: dependency_graph.id().clone(),
375        commands,
376        entrypoint,
377        filesystem,
378    })
379}
380
381#[cfg(test)]
382mod tests {
383    use std::path::PathBuf;
384
385    use wasmer_config::package::NamedPackageIdent;
386
387    use crate::runtime::resolver::{
388        Dependency, InMemorySource, MultiSource,
389        inputs::{DistributionInfo, FileSystemMapping, PackageInfo},
390    };
391
392    use super::*;
393
394    struct RegistryBuilder(InMemorySource);
395
396    impl RegistryBuilder {
397        fn new() -> Self {
398            RegistryBuilder(InMemorySource::new())
399        }
400
401        fn register(&mut self, name: &str, version: &str) -> AddPackageVersion<'_> {
402            let pkg = PackageInfo {
403                id: PackageId::new_named(name, version.parse().unwrap()),
404                dependencies: Vec::new(),
405                commands: Vec::new(),
406                entrypoint: None,
407                filesystem: Vec::new(),
408            };
409            let dist = DistributionInfo {
410                webc: format!("http://localhost/{name}@{version}")
411                    .parse()
412                    .unwrap(),
413                webc_sha256: [0; 32].into(),
414            };
415            let summary = PackageSummary { pkg, dist };
416
417            AddPackageVersion {
418                builder: &mut self.0,
419                summary,
420            }
421        }
422
423        fn finish(&self) -> MultiSource {
424            let mut registry = MultiSource::default();
425            registry.add_source(self.0.clone());
426            registry
427        }
428
429        fn get(&self, id: &PackageId) -> &PackageSummary {
430            self.0.get(id).unwrap()
431        }
432
433        // fn get_named(&self, name: &str, version: &str) -> &PackageSummary {
434        //     let id = PackageId::new_named(name, version.parse().unwrap());
435        //     self.get(&id)
436        // }
437
438        fn start_dependency_graph(&self) -> DependencyGraphBuilder<'_> {
439            DependencyGraphBuilder {
440                dependencies: BTreeMap::new(),
441                source: &self.0,
442            }
443        }
444    }
445
446    #[derive(Debug)]
447    struct AddPackageVersion<'builder> {
448        builder: &'builder mut InMemorySource,
449        summary: PackageSummary,
450    }
451
452    impl AddPackageVersion<'_> {
453        fn with_dependency(&mut self, name: &str, version_constraint: &str) -> &mut Self {
454            self.with_aliased_dependency(name, name, version_constraint)
455        }
456
457        fn with_aliased_dependency(
458            &mut self,
459            alias: &str,
460            name: &str,
461            version_constraint: &str,
462        ) -> &mut Self {
463            let pkg = PackageSource::from(
464                NamedPackageIdent::try_from_full_name_and_version(name, version_constraint)
465                    .unwrap(),
466            );
467
468            self.summary.pkg.dependencies.push(Dependency {
469                alias: alias.to_string(),
470                pkg,
471            });
472
473            self
474        }
475
476        fn with_command(&mut self, name: &str) -> &mut Self {
477            self.summary
478                .pkg
479                .commands
480                .push(crate::runtime::resolver::Command {
481                    name: name.to_string(),
482                });
483            self
484        }
485
486        fn with_entrypoint(&mut self, name: &str) -> &mut Self {
487            self.summary.pkg.entrypoint = Some(name.to_string());
488            self
489        }
490
491        fn with_fs_mapping(
492            &mut self,
493            volume_name: &str,
494            original_path: &str,
495            mount_path: &str,
496        ) -> &mut Self {
497            self.summary.pkg.filesystem.push(FileSystemMapping {
498                volume_name: volume_name.to_string(),
499                mount_path: mount_path.to_string(),
500                original_path: Some(original_path.to_string()),
501                dependency_name: None,
502            });
503            self
504        }
505
506        fn with_fs_mapping_from_dependency(
507            &mut self,
508            volume_name: &str,
509            mount_path: &str,
510            original_path: &str,
511            dependency: &str,
512        ) -> &mut Self {
513            self.summary.pkg.filesystem.push(FileSystemMapping {
514                volume_name: volume_name.to_string(),
515                mount_path: mount_path.to_string(),
516                original_path: Some(original_path.to_string()),
517                dependency_name: Some(dependency.to_string()),
518            });
519            self
520        }
521    }
522
523    impl Drop for AddPackageVersion<'_> {
524        fn drop(&mut self) {
525            let summary = self.summary.clone();
526            self.builder.add(summary);
527        }
528    }
529
530    #[derive(Debug)]
531    struct DependencyGraphBuilder<'source> {
532        dependencies: BTreeMap<PackageId, BTreeMap<String, PackageId>>,
533        source: &'source InMemorySource,
534    }
535
536    impl<'source> DependencyGraphBuilder<'source> {
537        fn insert(&mut self, id: PackageId) -> DependencyGraphEntryBuilder<'source, '_> {
538            let _ = self.source.get(&id).unwrap();
539            DependencyGraphEntryBuilder {
540                builder: self,
541                pkg_id: id,
542                dependencies: BTreeMap::new(),
543            }
544        }
545
546        fn finish(self) -> BTreeMap<PackageId, BTreeMap<String, PackageId>> {
547            self.dependencies
548        }
549
550        /// Using the dependency mapping that we've been building up, construct
551        /// a dependency graph using the specified root package.
552        fn graph(self, root_id: PackageId) -> DependencyGraph {
553            let _ = self.source.get(&root_id).unwrap();
554
555            let mut graph = DiGraph::new();
556            let mut nodes = BTreeMap::new();
557
558            for id in self.dependencies.keys() {
559                let PackageSummary { pkg, dist } = self.source.get(id).unwrap();
560                let index = graph.add_node(Node {
561                    id: pkg.id(),
562                    pkg: pkg.clone(),
563                    dist: Some(dist.clone()),
564                });
565                nodes.insert(id.clone(), index);
566            }
567
568            for (id, deps) in &self.dependencies {
569                let index = nodes[id];
570                for (dep_name, dep_id) in deps {
571                    let dep_index = nodes[dep_id];
572                    graph.add_edge(
573                        index,
574                        dep_index,
575                        Edge {
576                            alias: dep_name.clone(),
577                        },
578                    );
579                }
580            }
581
582            let root_index = nodes[&root_id];
583
584            DependencyGraph::new(root_index, graph, nodes)
585        }
586    }
587
588    #[derive(Debug)]
589    struct DependencyGraphEntryBuilder<'source, 'builder> {
590        builder: &'builder mut DependencyGraphBuilder<'source>,
591        pkg_id: PackageId,
592        dependencies: BTreeMap<String, PackageId>,
593    }
594
595    impl DependencyGraphEntryBuilder<'_, '_> {
596        fn with_dependency(&mut self, id: &PackageId) -> &mut Self {
597            let name = &id.as_named().unwrap().full_name;
598            self.with_aliased_dependency(name, id)
599        }
600
601        fn with_aliased_dependency(&mut self, alias: &str, id: &PackageId) -> &mut Self {
602            let dep_id = self.builder.source.get(id).unwrap().package_id();
603            self.dependencies.insert(alias.to_string(), dep_id);
604            self
605        }
606    }
607
608    impl Drop for DependencyGraphEntryBuilder<'_, '_> {
609        fn drop(&mut self) {
610            self.builder
611                .dependencies
612                .insert(self.pkg_id.clone(), self.dependencies.clone());
613        }
614    }
615
616    macro_rules! map {
617        (
618            $(
619                $key:expr => $value:expr
620            ),*
621            $(,)?
622        ) => {
623            vec![
624                $( ($key.into(), $value.into()) ),*
625            ]
626            .into_iter()
627            .collect()
628        }
629    }
630
631    fn deps(resolution: &Resolution) -> BTreeMap<PackageId, BTreeMap<String, PackageId>> {
632        resolution
633            .graph
634            .iter_dependencies()
635            .map(|(id, deps)| {
636                let deps = deps
637                    .into_iter()
638                    .map(|(name, dep_id)| (name.to_string(), dep_id.clone()))
639                    .collect();
640                (id.clone(), deps)
641            })
642            .collect()
643    }
644
645    #[tokio::test]
646    async fn no_deps_and_no_commands() {
647        let mut builder = RegistryBuilder::new();
648        builder.register("root", "1.0.0");
649        let registry = builder.finish();
650        let id = PackageId::new_named("root", Version::parse("1.0.0").unwrap());
651        let root = builder.get(&id);
652
653        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
654            .await
655            .unwrap();
656
657        let mut dependency_graph = builder.start_dependency_graph();
658        dependency_graph.insert(id);
659        assert_eq!(deps(&resolution), dependency_graph.finish());
660        assert_eq!(
661            resolution.package,
662            ResolvedPackage {
663                root_package: root.package_id(),
664                commands: BTreeMap::new(),
665                entrypoint: None,
666                filesystem: Vec::new(),
667            }
668        );
669    }
670
671    #[tokio::test]
672    async fn no_deps_one_command() {
673        let mut builder = RegistryBuilder::new();
674        builder.register("root", "1.0.0").with_command("asdf");
675        let registry = builder.finish();
676        let id = PackageId::new_named("root", "1.0.0".parse().unwrap());
677        let root = builder.get(&id);
678
679        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
680            .await
681            .unwrap();
682
683        let mut dependency_graph = builder.start_dependency_graph();
684        dependency_graph.insert(id.clone());
685        assert_eq!(deps(&resolution), dependency_graph.finish());
686        assert_eq!(
687            resolution.package,
688            ResolvedPackage {
689                root_package: root.package_id(),
690                commands: map! {
691                    "asdf" => ItemLocation {
692                        name: "asdf".to_string(),
693                        package: root.package_id(),
694                    },
695                },
696                entrypoint: Some("asdf".to_string()),
697                filesystem: Vec::new(),
698            }
699        );
700    }
701
702    #[tokio::test]
703    async fn single_dependency() {
704        let mut builder = RegistryBuilder::new();
705        builder
706            .register("root", "1.0.0")
707            .with_dependency("dep", "=1.0.0");
708        builder.register("dep", "1.0.0");
709        let registry = builder.finish();
710        let id = PackageId::new_named("root", "1.0.0".parse().unwrap());
711        let root = builder.get(&id);
712
713        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
714            .await
715            .unwrap();
716        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
717
718        let mut dependency_graph = builder.start_dependency_graph();
719        dependency_graph.insert(id.clone()).with_dependency(&dep_id);
720        dependency_graph.insert(dep_id.clone());
721        assert_eq!(deps(&resolution), dependency_graph.finish());
722        assert_eq!(
723            resolution.package,
724            ResolvedPackage {
725                root_package: root.package_id(),
726                commands: BTreeMap::new(),
727                entrypoint: None,
728                filesystem: Vec::new(),
729            }
730        );
731    }
732
733    #[tokio::test]
734    async fn linear_dependency_chain() {
735        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
736        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
737        let third_id = PackageId::new_named("third", "1.0.0".parse().unwrap());
738
739        let mut builder = RegistryBuilder::new();
740        builder
741            .register("first", "1.0.0")
742            .with_dependency("second", "=1.0.0");
743        builder
744            .register("second", "1.0.0")
745            .with_dependency("third", "=1.0.0");
746        builder.register("third", "1.0.0");
747        let registry = builder.finish();
748        let root = builder.get(&first_id);
749
750        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
751            .await
752            .unwrap();
753
754        let mut dependency_graph = builder.start_dependency_graph();
755        dependency_graph
756            .insert(first_id.clone())
757            .with_dependency(&second_id);
758        dependency_graph
759            .insert(second_id.clone())
760            .with_dependency(&third_id);
761        dependency_graph.insert(third_id.clone());
762        assert_eq!(deps(&resolution), dependency_graph.finish());
763        assert_eq!(
764            resolution.package,
765            ResolvedPackage {
766                root_package: root.package_id(),
767                commands: BTreeMap::new(),
768                entrypoint: None,
769                filesystem: Vec::new(),
770            }
771        );
772    }
773
774    #[tokio::test]
775    async fn pick_the_latest_dependency_when_multiple_are_possible() {
776        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
777        let mut builder = RegistryBuilder::new();
778        builder
779            .register("root", "1.0.0")
780            .with_dependency("dep", "^1.0.0");
781        builder.register("dep", "1.0.0");
782        builder.register("dep", "1.0.1");
783        builder.register("dep", "1.0.2");
784        let registry = builder.finish();
785        let root = builder.get(&root_id);
786
787        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
788            .await
789            .unwrap();
790        let dep_id = PackageId::new_named("dep", "1.0.2".parse().unwrap());
791
792        let mut dependency_graph = builder.start_dependency_graph();
793        dependency_graph
794            .insert(root_id.clone())
795            .with_dependency(&dep_id);
796        dependency_graph.insert(dep_id.clone());
797        assert_eq!(deps(&resolution), dependency_graph.finish());
798        assert_eq!(
799            resolution.package,
800            ResolvedPackage {
801                root_package: root.package_id(),
802                commands: BTreeMap::new(),
803                entrypoint: None,
804                filesystem: Vec::new(),
805            }
806        );
807    }
808
809    #[tokio::test]
810    async fn version_merging_isnt_implemented_yet() {
811        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
812        let mut builder = RegistryBuilder::new();
813        builder
814            .register("root", "1.0.0")
815            .with_dependency("first", "=1.0.0")
816            .with_dependency("second", "=1.0.0");
817        builder
818            .register("first", "1.0.0")
819            .with_dependency("common", "^1.0.0");
820        builder
821            .register("second", "1.0.0")
822            .with_dependency("common", ">1.1,<1.3");
823        builder.register("common", "1.0.0");
824        builder.register("common", "1.1.0");
825        builder.register("common", "1.2.0");
826        builder.register("common", "1.5.0");
827        let registry = builder.finish();
828        let root = builder.get(&root_id);
829
830        let result = resolve(&root.package_id(), &root.pkg, &registry).await;
831
832        match result {
833            Err(ResolveError::DuplicateVersions {
834                package_name,
835                versions,
836            }) => {
837                assert_eq!(package_name, "common");
838                assert_eq!(
839                    versions,
840                    [
841                        Version::parse("1.2.0").unwrap(),
842                        Version::parse("1.5.0").unwrap(),
843                    ]
844                );
845            }
846            _ => unreachable!("Expected a duplicate versions error, found {:?}", result),
847        }
848    }
849
850    #[tokio::test]
851    #[ignore = "Version merging isn't implemented"]
852    async fn merge_compatible_versions() {
853        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
854        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
855        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
856        let common_id = PackageId::new_named("common", "1.2.0".parse().unwrap());
857
858        let mut builder = RegistryBuilder::new();
859        builder
860            .register("root", "1.0.0")
861            .with_dependency("first", "=1.0.0")
862            .with_dependency("second", "=1.0.0");
863        builder
864            .register("first", "1.0.0")
865            .with_dependency("common", "^1.0.0");
866        builder
867            .register("second", "1.0.0")
868            .with_dependency("common", ">1.1,<1.3");
869        builder.register("common", "1.0.0");
870        builder.register("common", "1.1.0");
871        builder.register("common", "1.2.0");
872        builder.register("common", "1.5.0");
873        let registry = builder.finish();
874        let root = builder.get(&root_id);
875
876        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
877            .await
878            .unwrap();
879
880        let mut dependency_graph = builder.start_dependency_graph();
881        dependency_graph
882            .insert(root_id.clone())
883            .with_dependency(&first_id)
884            .with_dependency(&second_id);
885        dependency_graph
886            .insert(first_id.clone())
887            .with_dependency(&common_id);
888        dependency_graph
889            .insert(second_id.clone())
890            .with_dependency(&common_id);
891        dependency_graph.insert(common_id.clone());
892        assert_eq!(deps(&resolution), dependency_graph.finish());
893        assert_eq!(
894            resolution.package,
895            ResolvedPackage {
896                root_package: root.package_id(),
897                commands: BTreeMap::new(),
898                entrypoint: None,
899                filesystem: Vec::new(),
900            }
901        );
902    }
903
904    #[tokio::test]
905    async fn commands_from_dependencies_end_up_in_the_package() {
906        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
907        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
908        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
909        let mut builder = RegistryBuilder::new();
910        builder
911            .register("root", "1.0.0")
912            .with_dependency("first", "=1.0.0")
913            .with_dependency("second", "=1.0.0");
914        builder
915            .register("first", "1.0.0")
916            .with_command("first-command");
917        builder
918            .register("second", "1.0.0")
919            .with_command("second-command");
920        let registry = builder.finish();
921        let root = builder.get(&root_id);
922
923        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
924            .await
925            .unwrap();
926
927        let mut dependency_graph = builder.start_dependency_graph();
928        dependency_graph
929            .insert(root_id.clone())
930            .with_dependency(&first_id)
931            .with_dependency(&second_id);
932        dependency_graph.insert(first_id.clone());
933        dependency_graph.insert(second_id.clone());
934        assert_eq!(deps(&resolution), dependency_graph.finish());
935        assert_eq!(
936            resolution.package,
937            ResolvedPackage {
938                root_package: root.package_id(),
939                commands: map! {
940                    "first-command" => ItemLocation {
941                        name: "first-command".to_string(),
942                        package: builder.get(&first_id).package_id(),
943                     },
944                    "second-command" => ItemLocation {
945                        name: "second-command".to_string(),
946                        package: builder.get(&second_id).package_id(),
947                     },
948                },
949                entrypoint: None,
950                filesystem: Vec::new(),
951            }
952        );
953    }
954
955    #[tokio::test]
956    #[ignore = "TODO: Re-order the way commands are resolved"]
957    async fn commands_in_root_shadow_their_dependencies() {
958        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
959        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
960        let mut builder = RegistryBuilder::new();
961        builder
962            .register("root", "1.0.0")
963            .with_dependency("dep", "=1.0.0")
964            .with_command("command");
965        builder.register("dep", "1.0.0").with_command("command");
966        let registry = builder.finish();
967        let root = builder.get(&root_id);
968
969        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
970            .await
971            .unwrap();
972
973        let mut dependency_graph = builder.start_dependency_graph();
974        dependency_graph
975            .insert(root_id.clone())
976            .with_dependency(&dep_id);
977        dependency_graph.insert(dep_id.clone());
978        assert_eq!(deps(&resolution), dependency_graph.finish());
979        assert_eq!(
980            resolution.package,
981            ResolvedPackage {
982                root_package: root.package_id(),
983                commands: map! {
984                    "command" => ItemLocation {
985                        name: "command".to_string(),
986                        package: builder.get(&root_id).package_id(),
987                     },
988                },
989                entrypoint: None,
990                filesystem: Vec::new(),
991            }
992        );
993    }
994
995    #[tokio::test]
996    async fn cyclic_dependencies() {
997        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
998        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
999
1000        let mut builder = RegistryBuilder::new();
1001        builder
1002            .register("root", "1.0.0")
1003            .with_dependency("dep", "=1.0.0");
1004        builder
1005            .register("dep", "1.0.0")
1006            .with_dependency("root", "=1.0.0");
1007        let registry = builder.finish();
1008        let root = builder.get(&root_id);
1009
1010        let err = resolve(&root.package_id(), &root.pkg, &registry)
1011            .await
1012            .unwrap_err();
1013
1014        let cycle = err.as_cycle().unwrap().to_vec();
1015        assert_eq!(
1016            cycle,
1017            [
1018                builder.get(&root_id).package_id(),
1019                builder.get(&dep_id).package_id(),
1020                builder.get(&root_id).package_id(),
1021            ]
1022        );
1023    }
1024
1025    #[tokio::test]
1026    async fn entrypoint_is_inherited() {
1027        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1028        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
1029
1030        let mut builder = RegistryBuilder::new();
1031        builder
1032            .register("root", "1.0.0")
1033            .with_dependency("dep", "=1.0.0");
1034        builder
1035            .register("dep", "1.0.0")
1036            .with_command("entry")
1037            .with_entrypoint("entry");
1038        let registry = builder.finish();
1039        let root = builder.get(&root_id);
1040
1041        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1042            .await
1043            .unwrap();
1044
1045        assert_eq!(
1046            resolution.package,
1047            ResolvedPackage {
1048                root_package: root.package_id(),
1049                commands: map! {
1050                    "entry" => ItemLocation {
1051                        name: "entry".to_string(),
1052                        package: builder.get(&dep_id).package_id(),
1053                     },
1054                },
1055                entrypoint: Some("entry".to_string()),
1056                filesystem: Vec::new(),
1057            }
1058        );
1059    }
1060
1061    #[tokio::test]
1062    async fn infer_entrypoint_if_unspecified_and_only_one_command_in_root_package() {
1063        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1064        let mut builder = RegistryBuilder::new();
1065        builder
1066            .register("root", "1.0.0")
1067            .with_command("root-cmd")
1068            .with_dependency("dep", "=1.0.0");
1069        builder.register("dep", "1.0.0").with_command("entry");
1070        let registry = builder.finish();
1071        let root = builder.get(&root_id);
1072
1073        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1074            .await
1075            .unwrap();
1076
1077        assert_eq!(resolution.package.entrypoint.as_deref(), Some("root-cmd"));
1078    }
1079
1080    #[test]
1081    fn cyclic_error_message() {
1082        let cycle = [
1083            PackageId::new_named("root", "1.0.0".parse().unwrap()),
1084            PackageId::new_named("dep", "1.0.0".parse().unwrap()),
1085            PackageId::new_named("root", "1.0.0".parse().unwrap()),
1086        ];
1087
1088        let message = print_cycle(&cycle);
1089
1090        assert_eq!(message, "root@1.0.0 → dep@1.0.0 → root@1.0.0");
1091    }
1092
1093    #[test]
1094    fn filesystem_with_one_package_and_no_fs_tables() {
1095        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1096        let mut builder = RegistryBuilder::new();
1097        builder.register("root", "1.0.0");
1098        let mut dep_builder = builder.start_dependency_graph();
1099        dep_builder.insert(root_id.clone());
1100        let graph = dep_builder.graph(root_id.clone());
1101
1102        let pkg = resolve_package(&graph).unwrap();
1103
1104        assert!(pkg.filesystem.is_empty());
1105    }
1106
1107    #[test]
1108    fn filesystem_with_one_package_and_one_fs_tables() {
1109        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1110        let mut builder = RegistryBuilder::new();
1111        builder
1112            .register("root", "1.0.0")
1113            .with_fs_mapping("atom", "/publisher/lib", "/lib");
1114        let mut dep_builder = builder.start_dependency_graph();
1115        dep_builder.insert(root_id.clone());
1116        let graph = dep_builder.graph(root_id.clone());
1117
1118        let pkg = resolve_package(&graph).unwrap();
1119
1120        assert_eq!(
1121            pkg.filesystem,
1122            vec![ResolvedFileSystemMapping {
1123                mount_path: PathBuf::from("/lib"),
1124                original_path: Some("/publisher/lib".to_string()),
1125                volume_name: "atom".to_string(),
1126                package: builder.get(&root_id).package_id(),
1127            }]
1128        );
1129    }
1130
1131    #[test]
1132    fn merge_fs_mappings_from_multiple_packages() {
1133        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1134        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
1135        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
1136
1137        let mut builder = RegistryBuilder::new();
1138        builder
1139            .register("root", "1.0.0")
1140            .with_dependency("first", "=1.0.0")
1141            .with_dependency("second", "=1.0.0")
1142            .with_fs_mapping("atom", "/root", "/root");
1143        builder.register("first", "1.0.0").with_fs_mapping(
1144            "atom",
1145            "/usr/local/lib/first",
1146            "/usr/local/lib/first",
1147        );
1148        builder.register("second", "1.0.0").with_fs_mapping(
1149            "atom",
1150            "/usr/local/lib/second",
1151            "/usr/local/lib/second",
1152        );
1153        let mut dep_builder = builder.start_dependency_graph();
1154        dep_builder
1155            .insert(root_id.clone())
1156            .with_dependency(&first_id)
1157            .with_dependency(&second_id);
1158        dep_builder.insert(first_id.clone());
1159        dep_builder.insert(second_id.clone());
1160        let graph = dep_builder.graph(root_id.clone());
1161
1162        let pkg = resolve_package(&graph).unwrap();
1163
1164        assert_eq!(
1165            pkg.filesystem,
1166            vec![
1167                ResolvedFileSystemMapping {
1168                    mount_path: PathBuf::from("/root"),
1169                    original_path: Some("/root".to_string()),
1170                    volume_name: "atom".to_string(),
1171                    package: builder.get(&root_id).package_id(),
1172                },
1173                ResolvedFileSystemMapping {
1174                    mount_path: PathBuf::from("/usr/local/lib/second"),
1175                    original_path: Some("/usr/local/lib/second".to_string()),
1176                    volume_name: "atom".to_string(),
1177                    package: builder.get(&second_id).package_id(),
1178                },
1179                ResolvedFileSystemMapping {
1180                    mount_path: PathBuf::from("/usr/local/lib/first"),
1181                    volume_name: "atom".to_string(),
1182                    original_path: Some("/usr/local/lib/first".to_string()),
1183                    package: builder.get(&first_id).package_id(),
1184                }
1185            ]
1186        );
1187    }
1188
1189    #[test]
1190    fn use_fs_mapping_from_dependency() {
1191        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1192        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
1193        let mut builder = RegistryBuilder::new();
1194        builder
1195            .register("root", "1.0.0")
1196            .with_dependency("dep", "=1.0.0")
1197            .with_fs_mapping_from_dependency("dep-volume", "/root", "/root", "dep");
1198        builder.register("dep", "1.0.0");
1199        let mut dep_builder = builder.start_dependency_graph();
1200        dep_builder.insert(root_id.clone()).with_dependency(&dep_id);
1201        dep_builder.insert(dep_id.clone());
1202        let graph = dep_builder.graph(root_id.clone());
1203
1204        let pkg = resolve_package(&graph).unwrap();
1205
1206        assert_eq!(
1207            pkg.filesystem,
1208            vec![ResolvedFileSystemMapping {
1209                mount_path: PathBuf::from("/root"),
1210                original_path: Some("/root".to_string()),
1211                volume_name: "dep-volume".to_string(),
1212                package: builder.get(&dep_id).package_id(),
1213            }]
1214        );
1215    }
1216}