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