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            && let Some(entry) = &pkg.entrypoint
295        {
296            tracing::trace!(
297                entrypoint = entry.as_str(),
298                parent=%id,
299                "Inheriting the entrypoint",
300            );
301
302            entrypoint = Some(entry.clone());
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    async fn commands_in_root_shadow_their_dependencies() {
957        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
958        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
959        let mut builder = RegistryBuilder::new();
960        builder
961            .register("root", "1.0.0")
962            .with_dependency("dep", "=1.0.0")
963            .with_command("command");
964        builder.register("dep", "1.0.0").with_command("command");
965        let registry = builder.finish();
966        let root = builder.get(&root_id);
967
968        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
969            .await
970            .unwrap();
971
972        let mut dependency_graph = builder.start_dependency_graph();
973        dependency_graph
974            .insert(root_id.clone())
975            .with_dependency(&dep_id);
976        dependency_graph.insert(dep_id.clone());
977        assert_eq!(deps(&resolution), dependency_graph.finish());
978        assert_eq!(
979            resolution.package,
980            ResolvedPackage {
981                root_package: root.package_id(),
982                commands: map! {
983                    "command" => ItemLocation {
984                        name: "command".to_string(),
985                        package: builder.get(&root_id).package_id(),
986                     },
987                },
988                entrypoint: Some("command".to_string()),
989                filesystem: Vec::new(),
990            }
991        );
992    }
993
994    #[tokio::test]
995    async fn cyclic_dependencies() {
996        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
997        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
998
999        let mut builder = RegistryBuilder::new();
1000        builder
1001            .register("root", "1.0.0")
1002            .with_dependency("dep", "=1.0.0");
1003        builder
1004            .register("dep", "1.0.0")
1005            .with_dependency("root", "=1.0.0");
1006        let registry = builder.finish();
1007        let root = builder.get(&root_id);
1008
1009        let err = resolve(&root.package_id(), &root.pkg, &registry)
1010            .await
1011            .unwrap_err();
1012
1013        let cycle = err.as_cycle().unwrap().to_vec();
1014        assert_eq!(
1015            cycle,
1016            [
1017                builder.get(&root_id).package_id(),
1018                builder.get(&dep_id).package_id(),
1019                builder.get(&root_id).package_id(),
1020            ]
1021        );
1022    }
1023
1024    #[tokio::test]
1025    async fn entrypoint_is_inherited() {
1026        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1027        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
1028
1029        let mut builder = RegistryBuilder::new();
1030        builder
1031            .register("root", "1.0.0")
1032            .with_dependency("dep", "=1.0.0");
1033        builder
1034            .register("dep", "1.0.0")
1035            .with_command("entry")
1036            .with_entrypoint("entry");
1037        let registry = builder.finish();
1038        let root = builder.get(&root_id);
1039
1040        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1041            .await
1042            .unwrap();
1043
1044        assert_eq!(
1045            resolution.package,
1046            ResolvedPackage {
1047                root_package: root.package_id(),
1048                commands: map! {
1049                    "entry" => ItemLocation {
1050                        name: "entry".to_string(),
1051                        package: builder.get(&dep_id).package_id(),
1052                     },
1053                },
1054                entrypoint: Some("entry".to_string()),
1055                filesystem: Vec::new(),
1056            }
1057        );
1058    }
1059
1060    #[tokio::test]
1061    async fn infer_entrypoint_if_unspecified_and_only_one_command_in_root_package() {
1062        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1063        let mut builder = RegistryBuilder::new();
1064        builder
1065            .register("root", "1.0.0")
1066            .with_command("root-cmd")
1067            .with_dependency("dep", "=1.0.0");
1068        builder.register("dep", "1.0.0").with_command("entry");
1069        let registry = builder.finish();
1070        let root = builder.get(&root_id);
1071
1072        let resolution = resolve(&root.package_id(), &root.pkg, &registry)
1073            .await
1074            .unwrap();
1075
1076        assert_eq!(resolution.package.entrypoint.as_deref(), Some("root-cmd"));
1077    }
1078
1079    #[test]
1080    fn cyclic_error_message() {
1081        let cycle = [
1082            PackageId::new_named("root", "1.0.0".parse().unwrap()),
1083            PackageId::new_named("dep", "1.0.0".parse().unwrap()),
1084            PackageId::new_named("root", "1.0.0".parse().unwrap()),
1085        ];
1086
1087        let message = print_cycle(&cycle);
1088
1089        assert_eq!(message, "root@1.0.0 → dep@1.0.0 → root@1.0.0");
1090    }
1091
1092    #[test]
1093    fn filesystem_with_one_package_and_no_fs_tables() {
1094        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1095        let mut builder = RegistryBuilder::new();
1096        builder.register("root", "1.0.0");
1097        let mut dep_builder = builder.start_dependency_graph();
1098        dep_builder.insert(root_id.clone());
1099        let graph = dep_builder.graph(root_id.clone());
1100
1101        let pkg = resolve_package(&graph).unwrap();
1102
1103        assert!(pkg.filesystem.is_empty());
1104    }
1105
1106    #[test]
1107    fn filesystem_with_one_package_and_one_fs_tables() {
1108        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1109        let mut builder = RegistryBuilder::new();
1110        builder
1111            .register("root", "1.0.0")
1112            .with_fs_mapping("atom", "/publisher/lib", "/lib");
1113        let mut dep_builder = builder.start_dependency_graph();
1114        dep_builder.insert(root_id.clone());
1115        let graph = dep_builder.graph(root_id.clone());
1116
1117        let pkg = resolve_package(&graph).unwrap();
1118
1119        assert_eq!(
1120            pkg.filesystem,
1121            vec![ResolvedFileSystemMapping {
1122                mount_path: PathBuf::from("/lib"),
1123                original_path: Some("/publisher/lib".to_string()),
1124                volume_name: "atom".to_string(),
1125                package: builder.get(&root_id).package_id(),
1126            }]
1127        );
1128    }
1129
1130    #[test]
1131    fn merge_fs_mappings_from_multiple_packages() {
1132        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1133        let first_id = PackageId::new_named("first", "1.0.0".parse().unwrap());
1134        let second_id = PackageId::new_named("second", "1.0.0".parse().unwrap());
1135
1136        let mut builder = RegistryBuilder::new();
1137        builder
1138            .register("root", "1.0.0")
1139            .with_dependency("first", "=1.0.0")
1140            .with_dependency("second", "=1.0.0")
1141            .with_fs_mapping("atom", "/root", "/root");
1142        builder.register("first", "1.0.0").with_fs_mapping(
1143            "atom",
1144            "/usr/local/lib/first",
1145            "/usr/local/lib/first",
1146        );
1147        builder.register("second", "1.0.0").with_fs_mapping(
1148            "atom",
1149            "/usr/local/lib/second",
1150            "/usr/local/lib/second",
1151        );
1152        let mut dep_builder = builder.start_dependency_graph();
1153        dep_builder
1154            .insert(root_id.clone())
1155            .with_dependency(&first_id)
1156            .with_dependency(&second_id);
1157        dep_builder.insert(first_id.clone());
1158        dep_builder.insert(second_id.clone());
1159        let graph = dep_builder.graph(root_id.clone());
1160
1161        let pkg = resolve_package(&graph).unwrap();
1162
1163        assert_eq!(
1164            pkg.filesystem,
1165            vec![
1166                ResolvedFileSystemMapping {
1167                    mount_path: PathBuf::from("/root"),
1168                    original_path: Some("/root".to_string()),
1169                    volume_name: "atom".to_string(),
1170                    package: builder.get(&root_id).package_id(),
1171                },
1172                ResolvedFileSystemMapping {
1173                    mount_path: PathBuf::from("/usr/local/lib/second"),
1174                    original_path: Some("/usr/local/lib/second".to_string()),
1175                    volume_name: "atom".to_string(),
1176                    package: builder.get(&second_id).package_id(),
1177                },
1178                ResolvedFileSystemMapping {
1179                    mount_path: PathBuf::from("/usr/local/lib/first"),
1180                    volume_name: "atom".to_string(),
1181                    original_path: Some("/usr/local/lib/first".to_string()),
1182                    package: builder.get(&first_id).package_id(),
1183                }
1184            ]
1185        );
1186    }
1187
1188    #[test]
1189    fn use_fs_mapping_from_dependency() {
1190        let root_id = PackageId::new_named("root", "1.0.0".parse().unwrap());
1191        let dep_id = PackageId::new_named("dep", "1.0.0".parse().unwrap());
1192        let mut builder = RegistryBuilder::new();
1193        builder
1194            .register("root", "1.0.0")
1195            .with_dependency("dep", "=1.0.0")
1196            .with_fs_mapping_from_dependency("dep-volume", "/root", "/root", "dep");
1197        builder.register("dep", "1.0.0");
1198        let mut dep_builder = builder.start_dependency_graph();
1199        dep_builder.insert(root_id.clone()).with_dependency(&dep_id);
1200        dep_builder.insert(dep_id.clone());
1201        let graph = dep_builder.graph(root_id.clone());
1202
1203        let pkg = resolve_package(&graph).unwrap();
1204
1205        assert_eq!(
1206            pkg.filesystem,
1207            vec![ResolvedFileSystemMapping {
1208                mount_path: PathBuf::from("/root"),
1209                original_path: Some("/root".to_string()),
1210                volume_name: "dep-volume".to_string(),
1211                package: builder.get(&dep_id).package_id(),
1212            }]
1213        );
1214    }
1215}