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