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