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