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