1use std::collections::BTreeSet;
2use std::collections::VecDeque;
3use std::collections::hash_map::Entry;
4use std::path::Path;
5use std::sync::Arc;
6
7use either::Either;
8use itertools::Itertools;
9use petgraph::Graph;
10use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
11
12use uv_configuration::{
13 BuildOptions, DependencyGroupsWithDefaults, ExtrasSpecification,
14 ExtrasSpecificationWithDefaults, InstallOptions,
15};
16use uv_distribution_types::{Edge, Node, Resolution, ResolvedDist};
17use uv_normalize::{DefaultExtras, ExtraName, GroupName, PackageName};
18use uv_platform_tags::Tags;
19use uv_pypi_types::{ConflictKind, ConflictSet, ResolverMarkerEnvironment};
20
21use crate::lock::{
22 Dependency, DependencySelectionContext, HashedDist, LockErrorKind, Package, PackageId,
23 SelectedDependency, TagPolicy,
24};
25use crate::{Lock, LockError, UniversalMarker};
26
27fn newly_activated_extras<'lock>(
28 dep: &'lock Dependency,
29 activated_extras: &[(&'lock PackageName, &'lock ExtraName)],
30) -> Vec<(&'lock PackageName, &'lock ExtraName)> {
31 dep.extra
32 .iter()
33 .filter_map(|extra| {
34 let key = (&dep.package_id.name, extra);
35 (!activated_extras.contains(&key)).then_some(key)
36 })
37 .collect()
38}
39
40fn add_reachability<'lock>(
44 reachability: &mut FxHashMap<(&'lock PackageId, Option<&'lock ExtraName>), UniversalMarker>,
45 key: (&'lock PackageId, Option<&'lock ExtraName>),
46 marker: UniversalMarker,
47) -> bool {
48 match reachability.entry(key) {
49 Entry::Occupied(mut entry) => {
50 let mut combined = *entry.get();
51 combined.or(marker);
52 if combined == *entry.get() {
53 false
54 } else {
55 entry.insert(combined);
56 true
57 }
58 }
59 Entry::Vacant(entry) => {
60 entry.insert(marker);
61 true
62 }
63 }
64}
65
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub enum InstallableRootKind {
69 Production,
71 DependencyGroups,
73}
74
75pub trait Installable<'lock> {
76 fn install_path(&self) -> &'lock Path;
78
79 fn lock(&self) -> &'lock Lock;
81
82 fn roots(&self) -> impl Iterator<Item = &PackageName>;
84
85 fn group_root(&self, _groups: &DependencyGroupsWithDefaults) -> Option<&PackageName> {
87 None
88 }
89
90 fn includes_group(
94 &self,
95 _package: Option<&PackageName>,
96 group: &GroupName,
97 groups: &DependencyGroupsWithDefaults,
98 ) -> bool {
99 groups.contains(group)
100 }
101
102 fn project_name(&self) -> Option<&PackageName>;
104
105 fn to_resolution(
107 &self,
108 marker_env: &ResolverMarkerEnvironment,
109 tags: &Tags,
110 extras: &ExtrasSpecificationWithDefaults,
111 groups: &DependencyGroupsWithDefaults,
112 build_options: &BuildOptions,
113 install_options: &InstallOptions,
114 ) -> Result<Resolution, LockError> {
115 let resolve_root = |root_name: &PackageName| {
116 self.lock()
117 .find_by_name(root_name)
118 .map_err(|_| LockErrorKind::MultipleRootPackages {
119 name: root_name.clone(),
120 })?
121 .ok_or_else(|| {
122 LockError::from(LockErrorKind::MissingRootPackage {
123 name: root_name.clone(),
124 })
125 })
126 };
127 let roots = self
128 .roots()
129 .map(&resolve_root)
130 .collect::<Result<Vec<_>, LockError>>()?;
131 let group_root = self.group_root(groups).map(resolve_root).transpose()?;
132
133 InstallableExt::to_resolution_from_packages(
134 self,
135 &roots,
136 group_root,
137 true,
138 DependencySelectionContext::None,
139 marker_env,
140 tags,
141 extras,
142 groups,
143 build_options,
144 install_options,
145 )
146 }
147
148 fn installable_node(
150 &self,
151 package: &Package,
152 tags: &Tags,
153 marker_env: &ResolverMarkerEnvironment,
154 build_options: &BuildOptions,
155 ) -> Result<Node, LockError> {
156 let tag_policy = TagPolicy::Required(tags);
157 let HashedDist { dist, hashes } =
158 package.to_dist(self.install_path(), tag_policy, build_options, marker_env)?;
159 let version = package.version().cloned();
160 let dist = ResolvedDist::Installable {
161 dist: Arc::new(dist),
162 version,
163 };
164 Ok(Node::Dist {
165 dist,
166 hashes,
167 install: true,
168 })
169 }
170
171 fn non_installable_node(
173 &self,
174 package: &Package,
175 tags: &Tags,
176 marker_env: &ResolverMarkerEnvironment,
177 ) -> Result<Node, LockError> {
178 let HashedDist { dist, .. } = package.to_dist(
179 self.install_path(),
180 TagPolicy::Preferred(tags),
181 &BuildOptions::default(),
182 marker_env,
183 )?;
184 let version = package.version().cloned();
185 let dist = ResolvedDist::Installable {
186 dist: Arc::new(dist),
187 version,
188 };
189 let hashes = package.hashes();
190 Ok(Node::Dist {
191 dist,
192 hashes,
193 install: false,
194 })
195 }
196
197 fn package_to_node(
199 &self,
200 package: &Package,
201 tags: &Tags,
202 build_options: &BuildOptions,
203 install_options: &InstallOptions,
204 marker_env: &ResolverMarkerEnvironment,
205 ) -> Result<Node, LockError> {
206 if install_options.include_package(
207 package.as_install_target(),
208 self.project_name(),
209 self.lock().members(),
210 ) {
211 self.installable_node(package, tags, marker_env, build_options)
212 } else {
213 self.non_installable_node(package, tags, marker_env)
214 }
215 }
216}
217
218trait InstallableExt<'lock>: Installable<'lock> {
220 fn to_resolution_from_packages(
225 &self,
226 roots: &[&Package],
227 group_root: Option<&Package>,
228 include_manifest: bool,
229 selection_context: DependencySelectionContext<'lock>,
230 marker_env: &ResolverMarkerEnvironment,
231 tags: &Tags,
232 extras: &ExtrasSpecificationWithDefaults,
233 groups: &DependencyGroupsWithDefaults,
234 build_options: &BuildOptions,
235 install_options: &InstallOptions,
236 ) -> Result<Resolution, LockError> {
237 let size_guess = self.lock().packages.len();
238 let mut petgraph = Graph::with_capacity(size_guess, size_guess);
239 let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
240
241 let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
242 let mut seen = FxHashSet::default();
243 let mut conflict_reachability = FxHashMap::default();
244 let mut activated_projects: Vec<&PackageName> = vec![];
245 let mut activated_extras: Vec<(&PackageName, &ExtraName)> = vec![];
246 let mut activated_groups: Vec<(&PackageName, &GroupName)> = vec![];
247 let has_conflicts = !self.lock().conflicts().is_empty();
248 let validate_conflicts = !include_manifest && has_conflicts;
249 let mut dependencies_for_conflict_validation = vec![];
250
251 let root = petgraph.add_node(Node::Root);
252
253 match selection_context {
254 DependencySelectionContext::None => {}
255 DependencySelectionContext::Production(project) => {
256 activated_projects.push(project);
257 }
258 DependencySelectionContext::Group(project, group) => {
259 activated_groups.push((project, group));
260 }
261 }
262
263 if has_conflicts {
286 for dist in roots.iter().copied() {
287 if groups.prod() {
289 activated_projects.push(&dist.id.name);
290 for extra in extras.extra_names(dist.optional_dependencies.keys()) {
291 activated_extras.push((&dist.id.name, extra));
292 }
293 }
294 }
295
296 for dist in roots.iter().copied().chain(group_root) {
297 for group in dist
298 .dependency_groups
299 .keys()
300 .filter(|group| self.includes_group(Some(&dist.id.name), group, groups))
301 {
302 activated_groups.push((&dist.id.name, group));
303 }
304 }
305 }
306
307 let mut initialized_roots = vec![];
309 for (dist, root_kind) in roots
310 .iter()
311 .copied()
312 .map(|dist| (dist, InstallableRootKind::Production))
313 .chain(group_root.map(|dist| (dist, InstallableRootKind::DependencyGroups)))
314 {
315 let index = petgraph.add_node(
317 if root_kind == InstallableRootKind::Production && groups.prod() {
318 self.package_to_node(dist, tags, build_options, install_options, marker_env)?
319 } else {
320 self.non_installable_node(dist, tags, marker_env)?
321 },
322 );
323 inverse.insert(&dist.id, index);
324
325 petgraph.add_edge(root, index, Edge::Prod);
327
328 initialized_roots.push((dist, index, root_kind));
330 }
331
332 for (dist, index, root_kind) in initialized_roots {
334 if root_kind == InstallableRootKind::Production && groups.prod() {
335 queue.push_back((dist, None));
337 add_reachability(
338 &mut conflict_reachability,
339 (&dist.id, None),
340 UniversalMarker::TRUE,
341 );
342 for extra in extras.extra_names(dist.optional_dependencies.keys()) {
343 queue.push_back((dist, Some(extra)));
344 add_reachability(
345 &mut conflict_reachability,
346 (&dist.id, Some(extra)),
347 UniversalMarker::TRUE,
348 );
349 }
350 }
351
352 for (group, dep) in dist
354 .dependency_groups
355 .iter()
356 .filter_map(|(group, deps)| {
357 if self.includes_group(Some(&dist.id.name), group, groups) {
358 Some(deps.iter().map(move |dep| (group, dep)))
359 } else {
360 None
361 }
362 })
363 .flatten()
364 {
365 if validate_conflicts && dep.complexified_marker.has_conflict_marker() {
366 dependencies_for_conflict_validation.push((dist, dep));
367 }
368 let additional_activated_extras = newly_activated_extras(dep, &activated_extras);
369 if !dep.complexified_marker.evaluate(
370 marker_env,
371 activated_projects.iter().copied(),
372 activated_extras
373 .iter()
374 .chain(additional_activated_extras.iter())
375 .copied(),
376 activated_groups.iter().copied(),
377 ) {
378 continue;
379 }
380
381 let dep_dist = self.lock().find_by_id(&dep.package_id);
382
383 let dep_index = match inverse.entry(&dep.package_id) {
385 Entry::Vacant(entry) => {
386 let index = petgraph.add_node(self.package_to_node(
387 dep_dist,
388 tags,
389 build_options,
390 install_options,
391 marker_env,
392 )?);
393 entry.insert(index);
394 index
395 }
396 Entry::Occupied(entry) => {
397 let index = *entry.get();
401 let node = &mut petgraph[index];
402 if !groups.prod() || matches!(node, Node::Dist { install: false, .. }) {
403 *node = self.package_to_node(
404 dep_dist,
405 tags,
406 build_options,
407 install_options,
408 marker_env,
409 )?;
410 }
411 index
412 }
413 };
414
415 petgraph.add_edge(
416 index,
417 dep_index,
418 Edge::Dev(group.clone()),
423 );
424
425 for key in additional_activated_extras {
430 activated_extras.push(key);
431 }
432
433 add_reachability(
435 &mut conflict_reachability,
436 (&dep.package_id, None),
437 dep.complexified_marker,
438 );
439 if seen.insert((&dep.package_id, None)) {
440 queue.push_back((dep_dist, None));
441 }
442 for extra in &dep.extra {
443 add_reachability(
444 &mut conflict_reachability,
445 (&dep.package_id, Some(extra)),
446 dep.complexified_marker,
447 );
448 if seen.insert((&dep.package_id, Some(extra))) {
449 queue.push_back((dep_dist, Some(extra)));
450 }
451 }
452 }
453 }
454
455 if include_manifest {
456 for dependency in self.lock().requirements() {
459 if !dependency.marker.evaluate(marker_env, &[]) {
460 continue;
461 }
462
463 let root_name = &dependency.name;
464 let dist = self
465 .lock()
466 .find_by_markers(root_name, marker_env)
467 .map_err(|_| LockErrorKind::MultipleRootPackages {
468 name: root_name.clone(),
469 })?
470 .ok_or_else(|| LockErrorKind::MissingRootPackage {
471 name: root_name.clone(),
472 })?;
473
474 let index = petgraph.add_node(if groups.prod() {
476 self.package_to_node(dist, tags, build_options, install_options, marker_env)?
477 } else {
478 self.non_installable_node(dist, tags, marker_env)?
479 });
480 inverse.insert(&dist.id, index);
481
482 petgraph.add_edge(root, index, Edge::Prod);
484
485 add_reachability(
487 &mut conflict_reachability,
488 (&dist.id, None),
489 UniversalMarker::TRUE,
490 );
491 if seen.insert((&dist.id, None)) {
492 queue.push_back((dist, None));
493 }
494 for extra in &dependency.extras {
495 add_reachability(
496 &mut conflict_reachability,
497 (&dist.id, Some(extra)),
498 UniversalMarker::TRUE,
499 );
500 if seen.insert((&dist.id, Some(extra))) {
501 queue.push_back((dist, Some(extra)));
502 }
503 }
504 }
505
506 for (group, dependency) in self
509 .lock()
510 .dependency_groups()
511 .iter()
512 .filter_map(|(group, deps)| {
513 if self.includes_group(None, group, groups) {
514 Some(deps.iter().map(move |dep| (group, dep)))
515 } else {
516 None
517 }
518 })
519 .flatten()
520 {
521 if !dependency.marker.evaluate(marker_env, &[]) {
522 continue;
523 }
524
525 let root_name = &dependency.name;
526 let dist = self
527 .lock()
528 .find_by_markers(root_name, marker_env)
529 .map_err(|_| LockErrorKind::MultipleRootPackages {
530 name: root_name.clone(),
531 })?
532 .ok_or_else(|| LockErrorKind::MissingRootPackage {
533 name: root_name.clone(),
534 })?;
535
536 let index = match inverse.entry(&dist.id) {
538 Entry::Vacant(entry) => {
539 let index = petgraph.add_node(self.package_to_node(
540 dist,
541 tags,
542 build_options,
543 install_options,
544 marker_env,
545 )?);
546 entry.insert(index);
547 index
548 }
549 Entry::Occupied(entry) => {
550 let index = *entry.get();
554 let node = &mut petgraph[index];
555 if !groups.prod() {
556 *node = self.package_to_node(
557 dist,
558 tags,
559 build_options,
560 install_options,
561 marker_env,
562 )?;
563 }
564 index
565 }
566 };
567
568 petgraph.add_edge(root, index, Edge::Dev(group.clone()));
570
571 for extra in &dependency.extras {
576 let key = (&dist.id.name, extra);
577 if !activated_extras.contains(&key) {
578 activated_extras.push(key);
579 }
580 }
581
582 add_reachability(
584 &mut conflict_reachability,
585 (&dist.id, None),
586 UniversalMarker::TRUE,
587 );
588 if seen.insert((&dist.id, None)) {
589 queue.push_back((dist, None));
590 }
591 for extra in &dependency.extras {
592 add_reachability(
593 &mut conflict_reachability,
594 (&dist.id, Some(extra)),
595 UniversalMarker::TRUE,
596 );
597 if seen.insert((&dist.id, Some(extra))) {
598 queue.push_back((dist, Some(extra)));
599 }
600 }
601 }
602 }
603
604 if has_conflicts {
633 let mut activated_extras_set: BTreeSet<(&PackageName, &ExtraName)> =
634 activated_extras.iter().copied().collect();
635 let mut queue = queue.clone();
636 let mut reachability = conflict_reachability;
637 while let Some((package, extra)) = queue.pop_front() {
638 let Some(parent_reachability) = reachability.get(&(&package.id, extra)).copied()
639 else {
640 continue;
641 };
642 let deps = if let Some(extra) = extra {
643 Either::Left(
644 package
645 .optional_dependencies
646 .get(extra)
647 .into_iter()
648 .flatten(),
649 )
650 } else {
651 Either::Right(package.dependencies.iter())
652 };
653 for dep in deps {
654 let mut dep_reachability = dep.complexified_marker;
655 dep_reachability.and(parent_reachability);
656 let additional_activated_extras =
657 newly_activated_extras(dep, &activated_extras);
658 if !dep_reachability.evaluate(
659 marker_env,
660 activated_projects.iter().copied(),
661 activated_extras
662 .iter()
663 .chain(additional_activated_extras.iter())
664 .copied(),
665 activated_groups.iter().copied(),
666 ) {
667 continue;
668 }
669 for key in additional_activated_extras {
678 activated_extras_set.insert(key);
679 activated_extras.push(key);
680 }
681 let dep_dist = self.lock().find_by_id(&dep.package_id);
682 if add_reachability(
684 &mut reachability,
685 (&dep.package_id, None),
686 dep_reachability,
687 ) {
688 queue.push_back((dep_dist, None));
689 }
690 for extra in &dep.extra {
691 if add_reachability(
692 &mut reachability,
693 (&dep.package_id, Some(extra)),
694 dep_reachability,
695 ) {
696 queue.push_back((dep_dist, Some(extra)));
697 }
698 }
699 }
700 }
701 for set in self.lock().conflicts().iter() {
713 for ((pkg1, extra1), (pkg2, extra2)) in
714 activated_extras_set.iter().tuple_combinations()
715 {
716 if set.contains(pkg1, *extra1) && set.contains(pkg2, *extra2) {
717 return Err(LockErrorKind::ConflictingExtra {
718 package1: (*pkg1).clone(),
719 extra1: (*extra1).clone(),
720 package2: (*pkg2).clone(),
721 extra2: (*extra2).clone(),
722 }
723 .into());
724 }
725 }
726 }
727 }
728
729 while let Some((package, extra)) = queue.pop_front() {
730 let deps = if let Some(extra) = extra {
731 Either::Left(
732 package
733 .optional_dependencies
734 .get(extra)
735 .into_iter()
736 .flatten(),
737 )
738 } else {
739 Either::Right(package.dependencies.iter())
740 };
741 for dep in deps {
742 if validate_conflicts && dep.complexified_marker.has_conflict_marker() {
743 dependencies_for_conflict_validation.push((package, dep));
744 }
745 if !dep.complexified_marker.evaluate(
746 marker_env,
747 activated_projects.iter().copied(),
748 activated_extras.iter().copied(),
749 activated_groups.iter().copied(),
750 ) {
751 continue;
752 }
753
754 let dep_dist = self.lock().find_by_id(&dep.package_id);
755
756 let dep_index = match inverse.entry(&dep.package_id) {
758 Entry::Vacant(entry) => {
759 let index = petgraph.add_node(self.package_to_node(
760 dep_dist,
761 tags,
762 build_options,
763 install_options,
764 marker_env,
765 )?);
766 entry.insert(index);
767 index
768 }
769 Entry::Occupied(entry) => {
770 let index = *entry.get();
771 if matches!(&petgraph[index], Node::Dist { install: false, .. }) {
772 petgraph[index] = self.package_to_node(
773 dep_dist,
774 tags,
775 build_options,
776 install_options,
777 marker_env,
778 )?;
779 }
780 index
781 }
782 };
783
784 let index = inverse[&package.id];
786 petgraph.add_edge(
787 index,
788 dep_index,
789 if let Some(extra) = extra {
790 Edge::Optional(extra.clone())
791 } else {
792 Edge::Prod
793 },
794 );
795
796 if seen.insert((&dep.package_id, None)) {
798 queue.push_back((dep_dist, None));
799 }
800 for extra in &dep.extra {
801 if seen.insert((&dep.package_id, Some(extra))) {
802 queue.push_back((dep_dist, Some(extra)));
803 }
804 }
805 }
806 }
807
808 if !dependencies_for_conflict_validation.is_empty() {
811 let subgraph_packages = inverse
812 .keys()
813 .map(|package_id| &package_id.name)
814 .collect::<FxHashSet<_>>();
815 let selection_context_package = selection_context.package();
816
817 let mut validated_markers = FxHashSet::default();
820 for (package, dependency) in dependencies_for_conflict_validation {
821 if !validated_markers.insert(dependency.complexified_marker) {
822 continue;
823 }
824 let mut marker = dependency.complexified_marker;
825 for item in self.lock().conflicts().iter().flat_map(ConflictSet::iter) {
826 if selection_context_package != Some(item.package())
827 && !subgraph_packages.contains(item.package())
828 {
829 continue;
830 }
831
832 let active = match item.kind() {
833 ConflictKind::Project => activated_projects.contains(&item.package()),
834 ConflictKind::Extra(extra) => {
835 activated_extras.contains(&(item.package(), extra))
836 }
837 ConflictKind::Group(group) => {
838 activated_groups.contains(&(item.package(), group))
839 }
840 };
841 if active {
842 marker.assume_conflict_item(item);
843 } else {
844 marker.assume_not_conflict_item(item);
845 }
846 }
847
848 let conflict = marker.conflict_for_environment(marker_env);
849 if !conflict.is_constant() {
852 return Err(LockErrorKind::DependencyConflictOutsideSubgraph {
853 package: package.id.clone(),
854 dependency: dependency.package_id.clone(),
855 }
856 .into());
857 }
858 }
859 }
860
861 Ok(Resolution::new(petgraph))
862 }
863}
864
865impl<'lock, T> InstallableExt<'lock> for T where T: Installable<'lock> + ?Sized {}
866
867struct LockedPackages<'lock> {
869 lock: &'lock Lock,
870 install_path: &'lock Path,
871 project_name: Option<&'lock PackageName>,
872}
873
874impl<'lock> Installable<'lock> for LockedPackages<'lock> {
875 fn install_path(&self) -> &'lock Path {
876 self.install_path
877 }
878
879 fn lock(&self) -> &'lock Lock {
880 self.lock
881 }
882
883 fn roots(&self) -> impl Iterator<Item = &PackageName> {
884 std::iter::empty()
885 }
886
887 fn project_name(&self) -> Option<&PackageName> {
888 self.project_name
889 }
890}
891
892impl Lock {
893 pub fn to_resolution_from_dependency<'lock>(
899 &'lock self,
900 install_path: &'lock Path,
901 dependency: &SelectedDependency<'lock>,
902 project_name: Option<&'lock PackageName>,
903 marker_env: &ResolverMarkerEnvironment,
904 tags: &Tags,
905 build_options: &BuildOptions,
906 install_options: &InstallOptions,
907 ) -> Result<Resolution, LockError> {
908 let selected_package = dependency.package();
909 let Some(index) = self.by_id.get(&selected_package.id) else {
910 return Err(LockErrorKind::RootPackageMissingFromLock {
911 id: selected_package.id.clone(),
912 }
913 .into());
914 };
915 let Some(package) = self.packages.get(*index) else {
916 return Err(LockErrorKind::RootPackageMissingFromLock {
917 id: selected_package.id.clone(),
918 }
919 .into());
920 };
921 let extras = ExtrasSpecification::from_extra(dependency.extras().cloned().collect())
922 .with_defaults(DefaultExtras::default());
923 let groups = DependencyGroupsWithDefaults::none();
924
925 LockedPackages {
926 lock: self,
927 install_path,
928 project_name,
929 }
930 .to_resolution_from_packages(
931 &[package],
932 None,
933 false,
934 dependency.context(),
935 marker_env,
936 tags,
937 &extras,
938 &groups,
939 build_options,
940 install_options,
941 )
942 }
943
944 pub fn to_resolution<'lock>(
959 &'lock self,
960 install_path: &'lock Path,
961 roots: impl IntoIterator<Item = &'lock Package>,
962 project_name: Option<&'lock PackageName>,
963 marker_env: &ResolverMarkerEnvironment,
964 tags: &Tags,
965 extras: &ExtrasSpecificationWithDefaults,
966 groups: &DependencyGroupsWithDefaults,
967 build_options: &BuildOptions,
968 install_options: &InstallOptions,
969 ) -> Result<Resolution, LockError> {
970 let mut seen = FxHashSet::default();
971 let mut concrete_roots = Vec::new();
972 for root in roots {
973 let Some(index) = self.by_id.get(&root.id) else {
974 return Err(LockErrorKind::RootPackageMissingFromLock {
975 id: root.id.clone(),
976 }
977 .into());
978 };
979 if seen.insert(&root.id) {
980 let Some(root) = self.packages.get(*index) else {
981 return Err(LockErrorKind::RootPackageMissingFromLock {
982 id: root.id.clone(),
983 }
984 .into());
985 };
986 concrete_roots.push(root);
987 }
988 }
989
990 LockedPackages {
991 lock: self,
992 install_path,
993 project_name,
994 }
995 .to_resolution_from_packages(
996 &concrete_roots,
997 None,
998 false,
999 DependencySelectionContext::None,
1000 marker_env,
1001 tags,
1002 extras,
1003 groups,
1004 build_options,
1005 install_options,
1006 )
1007 }
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012 use std::cell::Cell;
1013 use std::str::FromStr;
1014 use std::sync::LazyLock;
1015
1016 use petgraph::visit::EdgeRef;
1017 use uv_configuration::{DependencyGroups, ExtrasSpecification};
1018 use uv_distribution_types::Name;
1019 use uv_normalize::{DefaultExtras, DefaultGroups};
1020 use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
1021 use uv_platform_tags::{Arch, Os, Platform, TagsOptions};
1022 use uv_warnings::anstream;
1023
1024 use super::*;
1025
1026 static TAGS: LazyLock<Tags> = LazyLock::new(|| {
1027 Tags::from_env(
1028 Platform::new(
1029 Os::Macos {
1030 major: 14,
1031 minor: 0,
1032 },
1033 Arch::Aarch64,
1034 ),
1035 (3, 11),
1036 "cpython",
1037 (3, 11),
1038 TagsOptions::default(),
1039 )
1040 .expect("valid tags")
1041 });
1042
1043 static DARWIN_MARKERS: LazyLock<ResolverMarkerEnvironment> =
1044 LazyLock::new(|| ResolverMarkerEnvironment::from(marker_environment("darwin", "Darwin")));
1045
1046 static LINUX_MARKERS: LazyLock<ResolverMarkerEnvironment> =
1047 LazyLock::new(|| ResolverMarkerEnvironment::from(marker_environment("linux", "Linux")));
1048
1049 fn marker_environment(
1050 sys_platform: &'static str,
1051 platform_system: &'static str,
1052 ) -> MarkerEnvironment {
1053 MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
1054 implementation_name: "cpython",
1055 implementation_version: "3.11.5",
1056 os_name: "posix",
1057 platform_machine: "arm64",
1058 platform_python_implementation: "CPython",
1059 platform_release: "23.0.0",
1060 platform_system,
1061 platform_version: "test",
1062 python_full_version: "3.11.5",
1063 python_version: "3.11",
1064 sys_platform,
1065 })
1066 .expect("valid marker environment")
1067 }
1068
1069 fn lock() -> Lock {
1070 toml::from_str(
1071 r#"
1072version = 1
1073revision = 3
1074requires-python = ">=3.11"
1075resolution-markers = [
1076 "sys_platform == 'darwin'",
1077 "sys_platform != 'darwin'",
1078]
1079
1080[manifest]
1081requirements = [{ name = "unrelated" }]
1082
1083[[package]]
1084name = "dev-dependency"
1085version = "1.0.0"
1086source = { registry = "https://example.com/simple" }
1087sdist = { url = "https://example.com/dev_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
1088
1089[[package]]
1090name = "forked"
1091version = "1.0.0"
1092source = { registry = "https://example.com/simple" }
1093resolution-markers = ["sys_platform == 'darwin'"]
1094sdist = { url = "https://example.com/forked-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
1095
1096[[package]]
1097name = "forked"
1098version = "2.0.0"
1099source = { registry = "https://example.com/simple" }
1100resolution-markers = ["sys_platform != 'darwin'"]
1101sdist = { url = "https://example.com/forked-2.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
1102
1103[[package]]
1104name = "optional-dependency"
1105version = "1.0.0"
1106source = { registry = "https://example.com/simple" }
1107sdist = { url = "https://example.com/optional_dependency-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
1108
1109[[package]]
1110name = "root-a"
1111version = "1.0.0"
1112source = { registry = "https://example.com/simple" }
1113dependencies = [
1114 { name = "forked", version = "1.0.0", source = { registry = "https://example.com/simple" }, marker = "sys_platform == 'darwin'" },
1115 { name = "forked", version = "2.0.0", source = { registry = "https://example.com/simple" }, marker = "sys_platform != 'darwin'" },
1116 { name = "shared" },
1117]
1118sdist = { url = "https://example.com/root_a-1.0.0.tar.gz", hash = "sha256:5555555555555555555555555555555555555555555555555555555555555555" }
1119
1120[package.optional-dependencies]
1121feature = [{ name = "optional-dependency" }]
1122
1123[package.dependency-groups]
1124dev = [{ name = "dev-dependency" }]
1125
1126[package.metadata]
1127provides-extras = ["feature"]
1128
1129[[package]]
1130name = "root-b"
1131version = "1.0.0"
1132source = { registry = "https://example.com/simple" }
1133dependencies = [{ name = "shared" }]
1134sdist = { url = "https://example.com/root_b-1.0.0.tar.gz", hash = "sha256:6666666666666666666666666666666666666666666666666666666666666666" }
1135
1136[[package]]
1137name = "shared"
1138version = "1.0.0"
1139source = { registry = "https://example.com/simple" }
1140sdist = { url = "https://example.com/shared-1.0.0.tar.gz", hash = "sha256:7777777777777777777777777777777777777777777777777777777777777777" }
1141
1142[[package]]
1143name = "unrelated"
1144version = "1.0.0"
1145source = { registry = "https://example.com/simple" }
1146sdist = { url = "https://example.com/unrelated-1.0.0.tar.gz", hash = "sha256:8888888888888888888888888888888888888888888888888888888888888888" }
1147"#,
1148 )
1149 .expect("valid lock")
1150 }
1151
1152 fn conflict_lock() -> Lock {
1153 toml::from_str(
1154 r#"
1155version = 1
1156revision = 3
1157requires-python = ">=3.11"
1158conflicts = [
1159 [
1160 { package = "tool", extra = "cpu" },
1161 { package = "tool", extra = "gpu" },
1162 ],
1163 [
1164 { package = "project", extra = "foo" },
1165 { package = "project", extra = "bar" },
1166 ],
1167]
1168
1169[[package]]
1170name = "contextual-dependency"
1171version = "1.0.0"
1172source = { registry = "https://example.com/simple" }
1173sdist = { url = "https://example.com/contextual_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
1174
1175[[package]]
1176name = "contextual-tool"
1177version = "1.0.0"
1178source = { registry = "https://example.com/simple" }
1179dependencies = [
1180 { name = "contextual-dependency", marker = "sys_platform == 'linux' or (sys_platform == 'darwin' and extra == 'extra-7-project-foo')" },
1181]
1182sdist = { url = "https://example.com/contextual_tool-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
1183
1184[[package]]
1185name = "cpu-backend"
1186version = "1.0.0"
1187source = { registry = "https://example.com/simple" }
1188sdist = { url = "https://example.com/cpu_backend-1.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
1189
1190[[package]]
1191name = "gpu-backend"
1192version = "1.0.0"
1193source = { registry = "https://example.com/simple" }
1194sdist = { url = "https://example.com/gpu_backend-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
1195
1196[[package]]
1197name = "project"
1198version = "1.0.0"
1199source = { registry = "https://example.com/simple" }
1200sdist = { url = "https://example.com/project-1.0.0.tar.gz", hash = "sha256:5555555555555555555555555555555555555555555555555555555555555555" }
1201
1202[package.optional-dependencies]
1203foo = []
1204bar = []
1205
1206[package.metadata]
1207provides-extras = ["foo", "bar"]
1208
1209[[package]]
1210name = "runtime"
1211version = "1.0.0"
1212source = { registry = "https://example.com/simple" }
1213dependencies = [
1214 { name = "cpu-backend", marker = "extra == 'extra-4-tool-cpu'" },
1215 { name = "gpu-backend", marker = "extra == 'extra-4-tool-gpu'" },
1216]
1217sdist = { url = "https://example.com/runtime-1.0.0.tar.gz", hash = "sha256:6666666666666666666666666666666666666666666666666666666666666666" }
1218
1219[[package]]
1220name = "tool"
1221version = "1.0.0"
1222source = { registry = "https://example.com/simple" }
1223dependencies = [{ name = "runtime" }]
1224sdist = { url = "https://example.com/tool-1.0.0.tar.gz", hash = "sha256:7777777777777777777777777777777777777777777777777777777777777777" }
1225
1226[package.optional-dependencies]
1227cpu = []
1228gpu = []
1229
1230[package.metadata]
1231provides-extras = ["cpu", "gpu"]
1232"#,
1233 )
1234 .expect("valid lock")
1235 }
1236
1237 fn dependency_selection_lock() -> Lock {
1238 toml::from_str(
1239 r#"
1240version = 1
1241revision = 3
1242requires-python = ">=3.11"
1243conflicts = [[
1244 { package = "project", group = "dev" },
1245 { package = "project", group = "other" },
1246]]
1247
1248[[package]]
1249name = "contextual-dev-dependency"
1250version = "1.0.0"
1251source = { registry = "https://example.com/simple" }
1252sdist = { url = "https://example.com/contextual_dev_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
1253
1254[[package]]
1255name = "contextual-other-dependency"
1256version = "1.0.0"
1257source = { registry = "https://example.com/simple" }
1258sdist = { url = "https://example.com/contextual_other_dependency-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
1259
1260[[package]]
1261name = "optional-dependency"
1262version = "1.0.0"
1263source = { registry = "https://example.com/simple" }
1264sdist = { url = "https://example.com/optional_dependency-1.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
1265
1266[[package]]
1267name = "project"
1268version = "1.0.0"
1269source = { virtual = "." }
1270
1271[package.dependency-groups]
1272dev = [{ name = "tool", extra = ["cli"] }]
1273other = [{ name = "tool" }]
1274
1275[[package]]
1276name = "tool"
1277version = "1.0.0"
1278source = { registry = "https://example.com/simple" }
1279dependencies = [
1280 { name = "contextual-dev-dependency", marker = "extra == 'group-7-project-dev'" },
1281 { name = "contextual-other-dependency", marker = "extra == 'group-7-project-other'" },
1282]
1283sdist = { url = "https://example.com/tool-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
1284
1285[package.optional-dependencies]
1286cli = [{ name = "optional-dependency" }]
1287
1288[package.metadata]
1289provides-extras = ["cli"]
1290"#,
1291 )
1292 .expect("valid lock")
1293 }
1294
1295 fn package<'lock>(lock: &'lock Lock, name: &str, version: &str) -> &'lock Package {
1296 lock.packages()
1297 .iter()
1298 .find(|package| {
1299 package.name().as_ref() == name
1300 && package
1301 .version()
1302 .is_some_and(|package_version| package_version.to_string() == version)
1303 })
1304 .expect("locked package")
1305 }
1306
1307 fn materialize(
1308 lock: &Lock,
1309 roots: &[&Package],
1310 marker_env: &ResolverMarkerEnvironment,
1311 ) -> Resolution {
1312 let extras = ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default());
1313 let groups = DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default());
1314 lock.to_resolution(
1315 Path::new("."),
1316 roots.iter().copied(),
1317 None,
1318 marker_env,
1319 &TAGS,
1320 &extras,
1321 &groups,
1322 &BuildOptions::default(),
1323 &InstallOptions::default(),
1324 )
1325 .expect("valid resolution")
1326 }
1327
1328 fn materialize_with_extras(
1329 lock: &Lock,
1330 roots: &[&Package],
1331 marker_env: &ResolverMarkerEnvironment,
1332 extras: &ExtrasSpecification,
1333 ) -> Result<Resolution, LockError> {
1334 let extras = extras.with_defaults(DefaultExtras::default());
1335 let groups = DependencyGroupsWithDefaults::none();
1336 lock.to_resolution(
1337 Path::new("."),
1338 roots.iter().copied(),
1339 None,
1340 marker_env,
1341 &TAGS,
1342 &extras,
1343 &groups,
1344 &BuildOptions::default(),
1345 &InstallOptions::default(),
1346 )
1347 }
1348
1349 fn materialize_selected_dependency(lock: &Lock, group: &str) -> Resolution {
1350 let project_name = PackageName::from_str("project").expect("valid package name");
1351 let dependency_name = PackageName::from_str("tool").expect("valid package name");
1352 let group = GroupName::from_str(group).expect("valid group name");
1353 let selection = lock
1354 .dependency_selection(
1355 Some(&project_name),
1356 &dependency_name,
1357 DARWIN_MARKERS.markers(),
1358 )
1359 .expect("unique dependency selection");
1360 let dependency = selection.group(&group).expect("group dependency");
1361
1362 lock.to_resolution_from_dependency(
1363 Path::new("."),
1364 dependency,
1365 Some(&project_name),
1366 &DARWIN_MARKERS,
1367 &TAGS,
1368 &BuildOptions::default(),
1369 &InstallOptions::default(),
1370 )
1371 .expect("valid resolution")
1372 }
1373
1374 struct OverridingInstallable<'lock> {
1375 lock: &'lock Lock,
1376 root_name: &'lock PackageName,
1377 package_to_node_calls: Cell<usize>,
1378 }
1379
1380 impl<'lock> Installable<'lock> for OverridingInstallable<'lock> {
1381 fn install_path(&self) -> &'lock Path {
1382 Path::new(".")
1383 }
1384
1385 fn lock(&self) -> &'lock Lock {
1386 self.lock
1387 }
1388
1389 fn roots(&self) -> impl Iterator<Item = &PackageName> {
1390 std::iter::once(self.root_name)
1391 }
1392
1393 fn project_name(&self) -> Option<&PackageName> {
1394 None
1395 }
1396
1397 fn package_to_node(
1398 &self,
1399 _package: &Package,
1400 _tags: &Tags,
1401 _build_options: &BuildOptions,
1402 _install_options: &InstallOptions,
1403 _marker_env: &ResolverMarkerEnvironment,
1404 ) -> Result<Node, LockError> {
1405 self.package_to_node_calls
1406 .set(self.package_to_node_calls.get() + 1);
1407 Ok(Node::Root)
1408 }
1409 }
1410
1411 fn graph_snapshot(resolution: &Resolution) -> (Vec<String>, Vec<String>) {
1412 let graph = resolution.graph();
1413 let labels = graph
1414 .node_weights()
1415 .map(|node| match node {
1416 Node::Root => "root".to_string(),
1417 Node::Dist {
1418 dist,
1419 hashes,
1420 install,
1421 } => format!(
1422 "{}=={} (install: {install}, hashes: {})",
1423 dist.name(),
1424 dist.version()
1425 .map(ToString::to_string)
1426 .unwrap_or_else(|| "<dynamic>".to_string()),
1427 hashes.iter().map(ToString::to_string).join(", ")
1428 ),
1429 })
1430 .collect::<Vec<_>>();
1431 let mut nodes = labels.clone();
1432 nodes.sort_unstable();
1433 let mut edges = graph
1434 .edge_references()
1435 .map(|edge| {
1436 format!(
1437 "{} --{:?}--> {}",
1438 labels[edge.source().index()],
1439 edge.weight(),
1440 labels[edge.target().index()]
1441 )
1442 })
1443 .collect::<Vec<_>>();
1444 edges.sort_unstable();
1445 (nodes, edges)
1446 }
1447
1448 #[test]
1449 fn materializes_multiple_concrete_roots_with_shared_dependencies() {
1450 let lock = lock();
1451 let resolution = materialize(
1452 &lock,
1453 &[
1454 package(&lock, "root-a", "1.0.0"),
1455 package(&lock, "root-b", "1.0.0"),
1456 ],
1457 &DARWIN_MARKERS,
1458 );
1459
1460 insta::with_settings!({
1461 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1462 }, {
1463 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1464 (
1465 [
1466 "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1467 "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1468 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1469 "root",
1470 "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1471 "root-b==1.0.0 (install: true, hashes: sha256:[HASH])",
1472 "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1473 ],
1474 [
1475 "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1476 "root --Prod--> root-b==1.0.0 (install: true, hashes: sha256:[HASH])",
1477 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1478 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1479 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1480 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1481 "root-b==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1482 ],
1483 )
1484 "#);
1485 });
1486 }
1487
1488 #[test]
1489 fn materializes_group_root_referenced_by_production_dependency() {
1490 let lock = lock();
1491 let project = package(&lock, "root-a", "1.0.0");
1492 let group_root = package(&lock, "shared", "1.0.0");
1493 let extras = ExtrasSpecification::default().with_defaults(DefaultExtras::default());
1494 let groups = DependencyGroupsWithDefaults::none();
1495
1496 let resolution = LockedPackages {
1497 lock: &lock,
1498 install_path: Path::new("."),
1499 project_name: Some(project.name()),
1500 }
1501 .to_resolution_from_packages(
1502 &[project],
1503 Some(group_root),
1504 false,
1505 DependencySelectionContext::None,
1506 &DARWIN_MARKERS,
1507 &TAGS,
1508 &extras,
1509 &groups,
1510 &BuildOptions::default(),
1511 &InstallOptions::default(),
1512 )
1513 .expect("valid resolution");
1514
1515 assert!(
1516 resolution
1517 .distributions()
1518 .any(|distribution| distribution.name() == group_root.name())
1519 );
1520 }
1521
1522 #[test]
1523 fn materializes_the_selected_universal_lock_fork() {
1524 let lock = lock();
1525 let root = package(&lock, "root-a", "1.0.0");
1526 let darwin = materialize(&lock, &[root], &DARWIN_MARKERS);
1527 let linux = materialize(&lock, &[root], &LINUX_MARKERS);
1528 let concrete_fork =
1529 materialize(&lock, &[package(&lock, "forked", "1.0.0")], &DARWIN_MARKERS);
1530
1531 insta::with_settings!({
1532 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1533 }, {
1534 insta::assert_debug_snapshot!(graph_snapshot(&darwin), @r#"
1535 (
1536 [
1537 "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1538 "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1539 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1540 "root",
1541 "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1542 "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1543 ],
1544 [
1545 "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1546 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1547 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1548 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1549 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1550 ],
1551 )
1552 "#);
1553 insta::assert_debug_snapshot!(graph_snapshot(&linux), @r#"
1554 (
1555 [
1556 "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1557 "forked==2.0.0 (install: true, hashes: sha256:[HASH])",
1558 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1559 "root",
1560 "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1561 "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1562 ],
1563 [
1564 "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1565 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1566 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1567 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==2.0.0 (install: true, hashes: sha256:[HASH])",
1568 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1569 ],
1570 )
1571 "#);
1572 insta::assert_debug_snapshot!(graph_snapshot(&concrete_fork), @r#"
1573 (
1574 [
1575 "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1576 "root",
1577 ],
1578 [
1579 "root --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1580 ],
1581 )
1582 "#);
1583 });
1584 }
1585
1586 #[test]
1587 fn materializes_conflicting_extras_within_the_synthetic_root() {
1588 let lock = conflict_lock();
1589 let extras =
1590 ExtrasSpecification::from_extra(vec!["cpu".parse().expect("valid extra name")]);
1591 let resolution = materialize_with_extras(
1592 &lock,
1593 &[package(&lock, "tool", "1.0.0")],
1594 &DARWIN_MARKERS,
1595 &extras,
1596 )
1597 .expect("conflict markers are resolved within the subgraph");
1598
1599 insta::with_settings!({
1600 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1601 }, {
1602 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1603 (
1604 [
1605 "cpu-backend==1.0.0 (install: true, hashes: sha256:[HASH])",
1606 "root",
1607 "runtime==1.0.0 (install: true, hashes: sha256:[HASH])",
1608 "tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1609 ],
1610 [
1611 "root --Prod--> tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1612 "runtime==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> cpu-backend==1.0.0 (install: true, hashes: sha256:[HASH])",
1613 "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> runtime==1.0.0 (install: true, hashes: sha256:[HASH])",
1614 ],
1615 )
1616 "#);
1617 });
1618 }
1619
1620 #[test]
1621 fn materializes_selected_dependency_extras() {
1622 let resolution = materialize_selected_dependency(&dependency_selection_lock(), "dev");
1623
1624 insta::with_settings!({
1625 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1626 }, {
1627 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1628 (
1629 [
1630 "contextual-dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1631 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1632 "root",
1633 "tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1634 ],
1635 [
1636 "root --Prod--> tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1637 "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"cli\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1638 "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> contextual-dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1639 ],
1640 )
1641 "#);
1642 });
1643 }
1644
1645 #[test]
1646 fn materializes_selected_dependency_project_conflict_context() {
1647 let resolution = materialize_selected_dependency(&dependency_selection_lock(), "other");
1648
1649 insta::with_settings!({
1650 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1651 }, {
1652 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1653 (
1654 [
1655 "contextual-other-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1656 "root",
1657 "tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1658 ],
1659 [
1660 "root --Prod--> tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1661 "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> contextual-other-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1662 ],
1663 )
1664 "#);
1665 });
1666 }
1667
1668 #[test]
1669 fn rejects_conflicts_outside_the_synthetic_root() {
1670 let lock = conflict_lock();
1671 let root = package(&lock, "contextual-tool", "1.0.0");
1672 let extras = ExtrasSpecification::default();
1673
1674 let error = materialize_with_extras(&lock, &[root], &DARWIN_MARKERS, &extras)
1675 .expect_err("Darwin dependency depends on the project extra");
1676 let error = error.to_string();
1677 let error = anstream::adapter::strip_str(&error);
1678 insta::assert_snapshot!(error, @"Cannot materialize dependency `contextual-dependency==1.0.0 @ registry+https://example.com/simple` of `contextual-tool==1.0.0 @ registry+https://example.com/simple` because its conflict marker depends on a package outside the selected subgraph");
1679
1680 let linux = materialize_with_extras(&lock, &[root], &LINUX_MARKERS, &extras)
1681 .expect("the dependency is unconditional on Linux");
1682 insta::with_settings!({
1683 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1684 }, {
1685 insta::assert_debug_snapshot!(graph_snapshot(&linux), @r#"
1686 (
1687 [
1688 "contextual-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1689 "contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1690 "root",
1691 ],
1692 [
1693 "contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> contextual-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1694 "root --Prod--> contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1695 ],
1696 )
1697 "#);
1698 });
1699 }
1700
1701 #[test]
1702 fn installable_to_resolution_preserves_node_overrides() {
1703 let mut lock = lock();
1704 lock.manifest.requirements.clear();
1705 let target = OverridingInstallable {
1706 root_name: package(&lock, "root-a", "1.0.0").name(),
1707 lock: &lock,
1708 package_to_node_calls: Cell::new(0),
1709 };
1710 let extras = ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default());
1711 let groups = DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default());
1712
1713 target
1714 .to_resolution(
1715 &DARWIN_MARKERS,
1716 &TAGS,
1717 &extras,
1718 &groups,
1719 &BuildOptions::default(),
1720 &InstallOptions::default(),
1721 )
1722 .expect("valid resolution");
1723
1724 assert!(target.package_to_node_calls.get() > 0);
1725 }
1726}