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#[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 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 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 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 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 let lowest_index_node = cycle.iter().copied().min().expect("Cycle is non-empty");
193
194 let offset = cycle
197 .iter()
198 .position(|&node| node == lowest_index_node)
199 .unwrap();
200 cycle.rotate_left(offset);
201
202 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 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
241fn 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
275fn resolve_package(dependency_graph: &DependencyGraph) -> Result<ResolvedPackage, ResolveError> {
278 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 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 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 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 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 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, ®istry)
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, ®istry)
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, ®istry)
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, ®istry)
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, ®istry)
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, ®istry).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, ®istry)
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, ®istry)
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, ®istry)
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, ®istry)
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, ®istry)
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, ®istry)
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}