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::{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, PackageIndex,
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<(PackageIndex, Option<&'lock ExtraName>), UniversalMarker>,
46 key: (PackageIndex, 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 = vec![None; size_guess];
269
270 let mut queue: VecDeque<(PackageIndex, 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 package_index = self.lock().by_id[&dist.id];
346 let index = petgraph.add_node(
347 if root_kind == InstallableRootKind::Production && groups.prod() {
348 self.package_to_node(dist, tags, build_options, install_options, marker_env)?
349 } else {
350 self.non_installable_node(dist, tags, marker_env)?
351 },
352 );
353 inverse[package_index.0] = Some(index);
354
355 petgraph.add_edge(root, index, Edge::Prod);
357
358 initialized_roots.push((dist, package_index, index, root_kind));
360 }
361
362 for (dist, package_index, index, root_kind) in initialized_roots {
364 if root_kind == InstallableRootKind::Production && groups.prod() {
365 queue.push_back((package_index, None));
367 add_reachability(
368 &mut conflict_reachability,
369 (package_index, None),
370 UniversalMarker::TRUE,
371 );
372 for extra in extras.extra_names(dist.optional_dependencies.keys()) {
373 queue.push_back((package_index, Some(extra)));
374 add_reachability(
375 &mut conflict_reachability,
376 (package_index, Some(extra)),
377 UniversalMarker::TRUE,
378 );
379 }
380 }
381
382 for (group, dep) in dist
384 .dependency_groups
385 .iter()
386 .filter_map(|(group, deps)| {
387 if self.includes_group(Some(&dist.id.name), group, groups) {
388 Some(deps.iter().map(move |dep| (group, dep)))
389 } else {
390 None
391 }
392 })
393 .flatten()
394 {
395 if validate_conflicts && dep.complexified_marker.has_conflict_marker() {
396 dependencies_for_conflict_validation.push((dist, dep));
397 }
398 let additional_activated_extras = newly_activated_extras(dep, &activated_extras);
399 if !dep.complexified_marker.evaluate(
400 marker_env,
401 activated_projects.iter().copied(),
402 activated_extras
403 .iter()
404 .chain(additional_activated_extras.iter())
405 .copied(),
406 activated_groups.iter().copied(),
407 ) {
408 continue;
409 }
410
411 let dep_dist = self.lock().package(dep.index);
412
413 let dep_index = match inverse[dep.index.0] {
415 None => {
416 let index = petgraph.add_node(self.package_to_node(
417 dep_dist,
418 tags,
419 build_options,
420 install_options,
421 marker_env,
422 )?);
423 inverse[dep.index.0] = Some(index);
424 index
425 }
426 Some(index) => {
427 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.index, None),
466 dep.complexified_marker,
467 );
468 if seen.insert((dep.index, None)) {
469 queue.push_back((dep.index, None));
470 }
471 for extra in &dep.extra {
472 add_reachability(
473 &mut conflict_reachability,
474 (dep.index, Some(extra)),
475 dep.complexified_marker,
476 );
477 if seen.insert((dep.index, Some(extra))) {
478 queue.push_back((dep.index, 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 package_index = self.lock().by_id[&dist.id];
505 let index = petgraph.add_node(if groups.prod() {
506 self.package_to_node(dist, tags, build_options, install_options, marker_env)?
507 } else {
508 self.non_installable_node(dist, tags, marker_env)?
509 });
510 inverse[package_index.0] = Some(index);
511
512 petgraph.add_edge(root, index, Edge::Prod);
514
515 add_reachability(
517 &mut conflict_reachability,
518 (package_index, None),
519 UniversalMarker::TRUE,
520 );
521 if seen.insert((package_index, None)) {
522 queue.push_back((package_index, None));
523 }
524 for extra in &dependency.extras {
525 add_reachability(
526 &mut conflict_reachability,
527 (package_index, Some(extra)),
528 UniversalMarker::TRUE,
529 );
530 if seen.insert((package_index, Some(extra))) {
531 queue.push_back((package_index, Some(extra)));
532 }
533 }
534 }
535
536 for (group, dependency) in self
539 .lock()
540 .dependency_groups()
541 .iter()
542 .filter_map(|(group, deps)| {
543 if self.includes_group(None, group, groups) {
544 Some(deps.iter().map(move |dep| (group, dep)))
545 } else {
546 None
547 }
548 })
549 .flatten()
550 {
551 if !dependency.marker.evaluate(marker_env, &[]) {
552 continue;
553 }
554
555 let root_name = &dependency.name;
556 let dist = self
557 .lock()
558 .find_by_markers(root_name, marker_env)
559 .map_err(|_| LockErrorKind::MultipleRootPackages {
560 name: root_name.clone(),
561 })?
562 .ok_or_else(|| LockErrorKind::MissingRootPackage {
563 name: root_name.clone(),
564 })?;
565
566 let package_index = self.lock().by_id[&dist.id];
568 let index = match inverse[package_index.0] {
569 None => {
570 let index = petgraph.add_node(self.package_to_node(
571 dist,
572 tags,
573 build_options,
574 install_options,
575 marker_env,
576 )?);
577 inverse[package_index.0] = Some(index);
578 index
579 }
580 Some(index) => {
581 let node = &mut petgraph[index];
585 if !groups.prod() {
586 *node = self.package_to_node(
587 dist,
588 tags,
589 build_options,
590 install_options,
591 marker_env,
592 )?;
593 }
594 index
595 }
596 };
597
598 petgraph.add_edge(root, index, Edge::Dev(group.clone()));
600
601 for extra in &dependency.extras {
606 let key = (&dist.id.name, extra);
607 if !activated_extras.contains(&key) {
608 activated_extras.push(key);
609 }
610 }
611
612 add_reachability(
614 &mut conflict_reachability,
615 (package_index, None),
616 UniversalMarker::TRUE,
617 );
618 if seen.insert((package_index, None)) {
619 queue.push_back((package_index, None));
620 }
621 for extra in &dependency.extras {
622 add_reachability(
623 &mut conflict_reachability,
624 (package_index, Some(extra)),
625 UniversalMarker::TRUE,
626 );
627 if seen.insert((package_index, Some(extra))) {
628 queue.push_back((package_index, Some(extra)));
629 }
630 }
631 }
632 }
633
634 if has_conflicts {
663 let mut activated_extras_set: BTreeSet<(&PackageName, &ExtraName)> =
664 activated_extras.iter().copied().collect();
665 let mut queue = queue.clone();
666 let mut reachability = conflict_reachability;
667 while let Some((package_index, extra)) = queue.pop_front() {
668 let package = self.lock().package(package_index);
669 let Some(parent_reachability) = reachability.get(&(package_index, extra)).copied()
670 else {
671 continue;
672 };
673 for dep in package_dependencies(package, extra) {
674 let mut dep_reachability = dep.complexified_marker;
675 dep_reachability.and(parent_reachability);
676 let additional_activated_extras =
677 newly_activated_extras(dep, &activated_extras);
678 if !dep_reachability.evaluate(
679 marker_env,
680 activated_projects.iter().copied(),
681 activated_extras
682 .iter()
683 .chain(additional_activated_extras.iter())
684 .copied(),
685 activated_groups.iter().copied(),
686 ) {
687 continue;
688 }
689 for key in additional_activated_extras {
698 activated_extras_set.insert(key);
699 activated_extras.push(key);
700 }
701 if add_reachability(&mut reachability, (dep.index, None), dep_reachability) {
703 queue.push_back((dep.index, None));
704 }
705 for extra in &dep.extra {
706 if add_reachability(
707 &mut reachability,
708 (dep.index, Some(extra)),
709 dep_reachability,
710 ) {
711 queue.push_back((dep.index, Some(extra)));
712 }
713 }
714 }
715 }
716 for set in self.lock().conflicts().iter() {
728 for ((pkg1, extra1), (pkg2, extra2)) in
729 activated_extras_set.iter().tuple_combinations()
730 {
731 if set.contains(pkg1, *extra1) && set.contains(pkg2, *extra2) {
732 return Err(LockErrorKind::ConflictingExtra {
733 package1: (*pkg1).clone(),
734 extra1: (*extra1).clone(),
735 package2: (*pkg2).clone(),
736 extra2: (*extra2).clone(),
737 }
738 .into());
739 }
740 }
741 }
742 }
743
744 let activated = ActivatedConflictItems::new(
747 activated_projects.iter().copied(),
748 activated_extras.iter().copied(),
749 activated_groups.iter().copied(),
750 );
751
752 while let Some((package_index, extra)) = queue.pop_front() {
753 let package = self.lock().package(package_index);
754 for dep in package_dependencies(package, extra) {
755 if validate_conflicts && dep.complexified_marker.has_conflict_marker() {
756 dependencies_for_conflict_validation.push((package, dep));
757 }
758 if !dep
759 .complexified_marker
760 .evaluate_activated(marker_env, &activated)
761 {
762 continue;
763 }
764
765 let dep_dist = self.lock().package(dep.index);
766
767 let dep_index = match inverse[dep.index.0] {
769 None => {
770 let index = petgraph.add_node(self.package_to_node(
771 dep_dist,
772 tags,
773 build_options,
774 install_options,
775 marker_env,
776 )?);
777 inverse[dep.index.0] = Some(index);
778 index
779 }
780 Some(index) => {
781 if matches!(&petgraph[index], Node::Dist { install: false, .. }) {
782 petgraph[index] = self.package_to_node(
783 dep_dist,
784 tags,
785 build_options,
786 install_options,
787 marker_env,
788 )?;
789 }
790 index
791 }
792 };
793
794 let index = inverse[package_index.0].expect("queued package has a graph node");
796 petgraph.add_edge(
797 index,
798 dep_index,
799 if let Some(extra) = extra {
800 Edge::Optional(extra.clone())
801 } else {
802 Edge::Prod
803 },
804 );
805
806 if seen.insert((dep.index, None)) {
808 queue.push_back((dep.index, None));
809 }
810 for extra in &dep.extra {
811 if seen.insert((dep.index, Some(extra))) {
812 queue.push_back((dep.index, Some(extra)));
813 }
814 }
815 }
816 }
817
818 if !dependencies_for_conflict_validation.is_empty() {
821 let subgraph_packages = inverse
822 .iter()
823 .enumerate()
824 .filter_map(|(package_index, index)| {
825 index.map(|_| &self.lock().packages[package_index].id.name)
826 })
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.0) 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.0) 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::cmp::Ordering;
1027 use std::str::FromStr;
1028 use std::sync::LazyLock;
1029
1030 use petgraph::visit::EdgeRef;
1031 use uv_configuration::{DependencyGroups, ExtrasSpecification};
1032 use uv_distribution_types::Name;
1033 use uv_normalize::{DefaultExtras, DefaultGroups};
1034 use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
1035 use uv_platform_tags::{Arch, Os, Platform, TagsOptions};
1036 use uv_warnings::anstream;
1037
1038 use super::*;
1039
1040 static TAGS: LazyLock<Tags> = LazyLock::new(|| {
1041 Tags::from_env(
1042 Platform::new(
1043 Os::Macos {
1044 major: 14,
1045 minor: 0,
1046 },
1047 Arch::Aarch64,
1048 ),
1049 (3, 11),
1050 "cpython",
1051 (3, 11),
1052 TagsOptions::default(),
1053 )
1054 .expect("valid tags")
1055 });
1056
1057 static DARWIN_MARKERS: LazyLock<ResolverMarkerEnvironment> =
1058 LazyLock::new(|| ResolverMarkerEnvironment::from(marker_environment("darwin", "Darwin")));
1059
1060 static LINUX_MARKERS: LazyLock<ResolverMarkerEnvironment> =
1061 LazyLock::new(|| ResolverMarkerEnvironment::from(marker_environment("linux", "Linux")));
1062
1063 fn marker_environment(
1064 sys_platform: &'static str,
1065 platform_system: &'static str,
1066 ) -> MarkerEnvironment {
1067 MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
1068 implementation_name: "cpython",
1069 implementation_version: "3.11.5",
1070 os_name: "posix",
1071 platform_machine: "arm64",
1072 platform_python_implementation: "CPython",
1073 platform_release: "23.0.0",
1074 platform_system,
1075 platform_version: "test",
1076 python_full_version: "3.11.5",
1077 python_version: "3.11",
1078 sys_platform,
1079 })
1080 .expect("valid marker environment")
1081 }
1082
1083 fn lock() -> Lock {
1084 toml::from_str(
1085 r#"
1086version = 1
1087revision = 3
1088requires-python = ">=3.11"
1089resolution-markers = [
1090 "sys_platform == 'darwin'",
1091 "sys_platform != 'darwin'",
1092]
1093
1094[manifest]
1095requirements = [{ name = "unrelated" }]
1096
1097[[package]]
1098name = "dev-dependency"
1099version = "1.0.0"
1100source = { registry = "https://example.com/simple" }
1101sdist = { url = "https://example.com/dev_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
1102
1103[[package]]
1104name = "forked"
1105version = "1.0.0"
1106source = { registry = "https://example.com/simple" }
1107resolution-markers = ["sys_platform == 'darwin'"]
1108sdist = { url = "https://example.com/forked-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
1109
1110[[package]]
1111name = "forked"
1112version = "2.0.0"
1113source = { registry = "https://example.com/simple" }
1114resolution-markers = ["sys_platform != 'darwin'"]
1115sdist = { url = "https://example.com/forked-2.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
1116
1117[[package]]
1118name = "optional-dependency"
1119version = "1.0.0"
1120source = { registry = "https://example.com/simple" }
1121sdist = { url = "https://example.com/optional_dependency-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
1122
1123[[package]]
1124name = "root-a"
1125version = "1.0.0"
1126source = { registry = "https://example.com/simple" }
1127dependencies = [
1128 { name = "forked", version = "1.0.0", source = { registry = "https://example.com/simple" }, marker = "sys_platform == 'darwin'" },
1129 { name = "forked", version = "2.0.0", source = { registry = "https://example.com/simple" }, marker = "sys_platform != 'darwin'" },
1130 { name = "shared" },
1131]
1132sdist = { url = "https://example.com/root_a-1.0.0.tar.gz", hash = "sha256:5555555555555555555555555555555555555555555555555555555555555555" }
1133
1134[package.optional-dependencies]
1135feature = [{ name = "optional-dependency" }]
1136
1137[package.dependency-groups]
1138dev = [{ name = "dev-dependency" }]
1139
1140[package.metadata]
1141provides-extras = ["feature"]
1142
1143[[package]]
1144name = "root-b"
1145version = "1.0.0"
1146source = { registry = "https://example.com/simple" }
1147dependencies = [{ name = "shared" }]
1148sdist = { url = "https://example.com/root_b-1.0.0.tar.gz", hash = "sha256:6666666666666666666666666666666666666666666666666666666666666666" }
1149
1150[[package]]
1151name = "shared"
1152version = "1.0.0"
1153source = { registry = "https://example.com/simple" }
1154sdist = { url = "https://example.com/shared-1.0.0.tar.gz", hash = "sha256:7777777777777777777777777777777777777777777777777777777777777777" }
1155
1156[[package]]
1157name = "unrelated"
1158version = "1.0.0"
1159source = { registry = "https://example.com/simple" }
1160sdist = { url = "https://example.com/unrelated-1.0.0.tar.gz", hash = "sha256:8888888888888888888888888888888888888888888888888888888888888888" }
1161"#,
1162 )
1163 .expect("valid lock")
1164 }
1165
1166 fn conflict_lock() -> Lock {
1167 toml::from_str(
1168 r#"
1169version = 1
1170revision = 3
1171requires-python = ">=3.11"
1172conflicts = [
1173 [
1174 { package = "tool", extra = "cpu" },
1175 { package = "tool", extra = "gpu" },
1176 ],
1177 [
1178 { package = "project", extra = "foo" },
1179 { package = "project", extra = "bar" },
1180 ],
1181]
1182
1183[[package]]
1184name = "contextual-dependency"
1185version = "1.0.0"
1186source = { registry = "https://example.com/simple" }
1187sdist = { url = "https://example.com/contextual_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
1188
1189[[package]]
1190name = "contextual-tool"
1191version = "1.0.0"
1192source = { registry = "https://example.com/simple" }
1193dependencies = [
1194 { name = "contextual-dependency", marker = "sys_platform == 'linux' or (sys_platform == 'darwin' and extra == 'extra-7-project-foo')" },
1195]
1196sdist = { url = "https://example.com/contextual_tool-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
1197
1198[[package]]
1199name = "cpu-backend"
1200version = "1.0.0"
1201source = { registry = "https://example.com/simple" }
1202sdist = { url = "https://example.com/cpu_backend-1.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
1203
1204[[package]]
1205name = "gpu-backend"
1206version = "1.0.0"
1207source = { registry = "https://example.com/simple" }
1208sdist = { url = "https://example.com/gpu_backend-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
1209
1210[[package]]
1211name = "project"
1212version = "1.0.0"
1213source = { registry = "https://example.com/simple" }
1214sdist = { url = "https://example.com/project-1.0.0.tar.gz", hash = "sha256:5555555555555555555555555555555555555555555555555555555555555555" }
1215
1216[package.optional-dependencies]
1217foo = []
1218bar = []
1219
1220[package.metadata]
1221provides-extras = ["foo", "bar"]
1222
1223[[package]]
1224name = "runtime"
1225version = "1.0.0"
1226source = { registry = "https://example.com/simple" }
1227dependencies = [
1228 { name = "cpu-backend", marker = "extra == 'extra-4-tool-cpu'" },
1229 { name = "gpu-backend", marker = "extra == 'extra-4-tool-gpu'" },
1230]
1231sdist = { url = "https://example.com/runtime-1.0.0.tar.gz", hash = "sha256:6666666666666666666666666666666666666666666666666666666666666666" }
1232
1233[[package]]
1234name = "tool"
1235version = "1.0.0"
1236source = { registry = "https://example.com/simple" }
1237dependencies = [{ name = "runtime" }]
1238sdist = { url = "https://example.com/tool-1.0.0.tar.gz", hash = "sha256:7777777777777777777777777777777777777777777777777777777777777777" }
1239
1240[package.optional-dependencies]
1241cpu = []
1242gpu = []
1243
1244[package.metadata]
1245provides-extras = ["cpu", "gpu"]
1246"#,
1247 )
1248 .expect("valid lock")
1249 }
1250
1251 fn dependency_selection_lock() -> Lock {
1252 toml::from_str(
1253 r#"
1254version = 1
1255revision = 3
1256requires-python = ">=3.11"
1257conflicts = [[
1258 { package = "project", group = "dev" },
1259 { package = "project", group = "other" },
1260]]
1261
1262[[package]]
1263name = "contextual-dev-dependency"
1264version = "1.0.0"
1265source = { registry = "https://example.com/simple" }
1266sdist = { url = "https://example.com/contextual_dev_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
1267
1268[[package]]
1269name = "contextual-other-dependency"
1270version = "1.0.0"
1271source = { registry = "https://example.com/simple" }
1272sdist = { url = "https://example.com/contextual_other_dependency-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
1273
1274[[package]]
1275name = "optional-dependency"
1276version = "1.0.0"
1277source = { registry = "https://example.com/simple" }
1278sdist = { url = "https://example.com/optional_dependency-1.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
1279
1280[[package]]
1281name = "project"
1282version = "1.0.0"
1283source = { virtual = "." }
1284
1285[package.dependency-groups]
1286dev = [{ name = "tool", extra = ["cli"] }]
1287other = [{ name = "tool" }]
1288
1289[[package]]
1290name = "tool"
1291version = "1.0.0"
1292source = { registry = "https://example.com/simple" }
1293dependencies = [
1294 { name = "contextual-dev-dependency", marker = "extra == 'group-7-project-dev'" },
1295 { name = "contextual-other-dependency", marker = "extra == 'group-7-project-other'" },
1296]
1297sdist = { url = "https://example.com/tool-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
1298
1299[package.optional-dependencies]
1300cli = [{ name = "optional-dependency" }]
1301
1302[package.metadata]
1303provides-extras = ["cli"]
1304"#,
1305 )
1306 .expect("valid lock")
1307 }
1308
1309 fn package<'lock>(lock: &'lock Lock, name: &str, version: &str) -> &'lock Package {
1310 lock.packages()
1311 .iter()
1312 .find(|package| {
1313 package.name().as_ref() == name
1314 && package
1315 .version()
1316 .is_some_and(|package_version| package_version.to_string() == version)
1317 })
1318 .expect("locked package")
1319 }
1320
1321 fn materialize(
1322 lock: &Lock,
1323 roots: &[&Package],
1324 marker_env: &ResolverMarkerEnvironment,
1325 ) -> Resolution {
1326 let extras = ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default());
1327 let groups = DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default());
1328 lock.to_resolution(
1329 Path::new("."),
1330 roots.iter().copied(),
1331 None,
1332 marker_env,
1333 &TAGS,
1334 &extras,
1335 &groups,
1336 &BuildOptions::default(),
1337 &InstallOptions::default(),
1338 )
1339 .expect("valid resolution")
1340 }
1341
1342 fn materialize_with_extras(
1343 lock: &Lock,
1344 roots: &[&Package],
1345 marker_env: &ResolverMarkerEnvironment,
1346 extras: &ExtrasSpecification,
1347 ) -> Result<Resolution, LockError> {
1348 let extras = extras.with_defaults(DefaultExtras::default());
1349 let groups = DependencyGroupsWithDefaults::none();
1350 lock.to_resolution(
1351 Path::new("."),
1352 roots.iter().copied(),
1353 None,
1354 marker_env,
1355 &TAGS,
1356 &extras,
1357 &groups,
1358 &BuildOptions::default(),
1359 &InstallOptions::default(),
1360 )
1361 }
1362
1363 fn materialize_selected_dependency(lock: &Lock, group: &str) -> Resolution {
1364 let project_name = PackageName::from_str("project").expect("valid package name");
1365 let dependency_name = PackageName::from_str("tool").expect("valid package name");
1366 let group = GroupName::from_str(group).expect("valid group name");
1367 let selection = lock
1368 .dependency_selection(
1369 Some(&project_name),
1370 &dependency_name,
1371 DARWIN_MARKERS.markers(),
1372 )
1373 .expect("unique dependency selection");
1374 let dependency = selection.group(&group).expect("group dependency");
1375
1376 lock.to_resolution_from_dependency(
1377 Path::new("."),
1378 dependency,
1379 Some(&project_name),
1380 &DARWIN_MARKERS,
1381 &TAGS,
1382 &BuildOptions::default(),
1383 &InstallOptions::default(),
1384 )
1385 .expect("valid resolution")
1386 }
1387
1388 #[test]
1389 fn unrelated_packages_do_not_change_dependency_identity() {
1390 let original = lock();
1391 let input = format!(
1392 "{}\n{}",
1393 original.to_toml().expect("valid lock TOML"),
1394 r#"
1395[[package]]
1396name = "aaa-unrelated"
1397version = "1.0.0"
1398source = { registry = "https://example.com/simple" }
1399"#
1400 );
1401 let extended = Lock::from_toml(&input).expect("valid extended lock");
1402 let original_root = package(&original, "root-a", "1.0.0");
1403 let extended_root = package(&extended, "root-a", "1.0.0");
1404
1405 assert_eq!(original_root, extended_root);
1406 assert_eq!(
1407 original_root.dependencies.cmp(&extended_root.dependencies),
1408 Ordering::Equal,
1409 );
1410 assert_eq!(
1411 graph_snapshot(&materialize(&original, &[original_root], &DARWIN_MARKERS)),
1412 graph_snapshot(&materialize(&extended, &[extended_root], &DARWIN_MARKERS)),
1413 );
1414 }
1415
1416 struct OverridingInstallable<'lock> {
1417 lock: &'lock Lock,
1418 root_name: &'lock PackageName,
1419 package_to_node_calls: Cell<usize>,
1420 }
1421
1422 impl<'lock> Installable<'lock> for OverridingInstallable<'lock> {
1423 fn install_path(&self) -> &'lock Path {
1424 Path::new(".")
1425 }
1426
1427 fn lock(&self) -> &'lock Lock {
1428 self.lock
1429 }
1430
1431 fn roots(&self) -> impl Iterator<Item = &PackageName> {
1432 std::iter::once(self.root_name)
1433 }
1434
1435 fn project_name(&self) -> Option<&PackageName> {
1436 None
1437 }
1438
1439 fn package_to_node(
1440 &self,
1441 _package: &Package,
1442 _tags: &Tags,
1443 _build_options: &BuildOptions,
1444 _install_options: &InstallOptions,
1445 _marker_env: &ResolverMarkerEnvironment,
1446 ) -> Result<Node, LockError> {
1447 self.package_to_node_calls
1448 .set(self.package_to_node_calls.get() + 1);
1449 Ok(Node::Root)
1450 }
1451 }
1452
1453 fn graph_snapshot(resolution: &Resolution) -> (Vec<String>, Vec<String>) {
1454 let graph = resolution.graph();
1455 let labels = graph
1456 .node_weights()
1457 .map(|node| match node {
1458 Node::Root => "root".to_string(),
1459 Node::Dist {
1460 dist,
1461 hashes,
1462 install,
1463 } => format!(
1464 "{}=={} (install: {install}, hashes: {})",
1465 dist.name(),
1466 dist.version()
1467 .map(ToString::to_string)
1468 .unwrap_or_else(|| "<dynamic>".to_string()),
1469 hashes.iter().map(ToString::to_string).join(", ")
1470 ),
1471 })
1472 .collect::<Vec<_>>();
1473 let mut nodes = labels.clone();
1474 nodes.sort_unstable();
1475 let mut edges = graph
1476 .edge_references()
1477 .map(|edge| {
1478 format!(
1479 "{} --{:?}--> {}",
1480 labels[edge.source().index()],
1481 edge.weight(),
1482 labels[edge.target().index()]
1483 )
1484 })
1485 .collect::<Vec<_>>();
1486 edges.sort_unstable();
1487 (nodes, edges)
1488 }
1489
1490 #[test]
1491 fn materializes_multiple_concrete_roots_with_shared_dependencies() {
1492 let lock = lock();
1493 let resolution = materialize(
1494 &lock,
1495 &[
1496 package(&lock, "root-a", "1.0.0"),
1497 package(&lock, "root-b", "1.0.0"),
1498 ],
1499 &DARWIN_MARKERS,
1500 );
1501
1502 insta::with_settings!({
1503 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1504 }, {
1505 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1506 (
1507 [
1508 "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1509 "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1510 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1511 "root",
1512 "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1513 "root-b==1.0.0 (install: true, hashes: sha256:[HASH])",
1514 "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1515 ],
1516 [
1517 "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1518 "root --Prod--> root-b==1.0.0 (install: true, hashes: sha256:[HASH])",
1519 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1520 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1521 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1522 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1523 "root-b==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1524 ],
1525 )
1526 "#);
1527 });
1528 }
1529
1530 #[test]
1531 fn materializes_group_root_referenced_by_production_dependency() {
1532 let lock = lock();
1533 let project = package(&lock, "root-a", "1.0.0");
1534 let group_root = package(&lock, "shared", "1.0.0");
1535 let extras = ExtrasSpecification::default().with_defaults(DefaultExtras::default());
1536 let groups = DependencyGroupsWithDefaults::none();
1537
1538 let resolution = LockedPackages {
1539 lock: &lock,
1540 install_path: Path::new("."),
1541 project_name: Some(project.name()),
1542 }
1543 .to_resolution_from_packages(
1544 &[project],
1545 Some(group_root),
1546 false,
1547 DependencySelectionContext::None,
1548 &DARWIN_MARKERS,
1549 &TAGS,
1550 &extras,
1551 &groups,
1552 &BuildOptions::default(),
1553 &InstallOptions::default(),
1554 )
1555 .expect("valid resolution");
1556
1557 assert!(
1558 resolution
1559 .distributions()
1560 .any(|distribution| distribution.name() == group_root.name())
1561 );
1562 }
1563
1564 #[test]
1565 fn materializes_the_selected_universal_lock_fork() {
1566 let lock = lock();
1567 let root = package(&lock, "root-a", "1.0.0");
1568 let darwin = materialize(&lock, &[root], &DARWIN_MARKERS);
1569 let linux = materialize(&lock, &[root], &LINUX_MARKERS);
1570 let concrete_fork =
1571 materialize(&lock, &[package(&lock, "forked", "1.0.0")], &DARWIN_MARKERS);
1572
1573 insta::with_settings!({
1574 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1575 }, {
1576 insta::assert_debug_snapshot!(graph_snapshot(&darwin), @r#"
1577 (
1578 [
1579 "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1580 "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1581 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1582 "root",
1583 "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1584 "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1585 ],
1586 [
1587 "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1588 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1589 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1590 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1591 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1592 ],
1593 )
1594 "#);
1595 insta::assert_debug_snapshot!(graph_snapshot(&linux), @r#"
1596 (
1597 [
1598 "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1599 "forked==2.0.0 (install: true, hashes: sha256:[HASH])",
1600 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1601 "root",
1602 "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1603 "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1604 ],
1605 [
1606 "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1607 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1608 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1609 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==2.0.0 (install: true, hashes: sha256:[HASH])",
1610 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1611 ],
1612 )
1613 "#);
1614 insta::assert_debug_snapshot!(graph_snapshot(&concrete_fork), @r#"
1615 (
1616 [
1617 "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1618 "root",
1619 ],
1620 [
1621 "root --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1622 ],
1623 )
1624 "#);
1625 });
1626 }
1627
1628 #[test]
1629 fn materializes_conflicting_extras_within_the_synthetic_root() {
1630 let lock = conflict_lock();
1631 let extras =
1632 ExtrasSpecification::from_extra(vec!["cpu".parse().expect("valid extra name")]);
1633 let resolution = materialize_with_extras(
1634 &lock,
1635 &[package(&lock, "tool", "1.0.0")],
1636 &DARWIN_MARKERS,
1637 &extras,
1638 )
1639 .expect("conflict markers are resolved within the subgraph");
1640
1641 insta::with_settings!({
1642 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1643 }, {
1644 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1645 (
1646 [
1647 "cpu-backend==1.0.0 (install: true, hashes: sha256:[HASH])",
1648 "root",
1649 "runtime==1.0.0 (install: true, hashes: sha256:[HASH])",
1650 "tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1651 ],
1652 [
1653 "root --Prod--> tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1654 "runtime==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> cpu-backend==1.0.0 (install: true, hashes: sha256:[HASH])",
1655 "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> runtime==1.0.0 (install: true, hashes: sha256:[HASH])",
1656 ],
1657 )
1658 "#);
1659 });
1660 }
1661
1662 #[test]
1663 fn materializes_selected_dependency_extras() {
1664 let resolution = materialize_selected_dependency(&dependency_selection_lock(), "dev");
1665
1666 insta::with_settings!({
1667 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1668 }, {
1669 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1670 (
1671 [
1672 "contextual-dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1673 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1674 "root",
1675 "tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1676 ],
1677 [
1678 "root --Prod--> tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1679 "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"cli\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1680 "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> contextual-dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1681 ],
1682 )
1683 "#);
1684 });
1685 }
1686
1687 #[test]
1688 fn materializes_selected_dependency_project_conflict_context() {
1689 let resolution = materialize_selected_dependency(&dependency_selection_lock(), "other");
1690
1691 insta::with_settings!({
1692 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1693 }, {
1694 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1695 (
1696 [
1697 "contextual-other-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1698 "root",
1699 "tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1700 ],
1701 [
1702 "root --Prod--> tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1703 "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> contextual-other-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1704 ],
1705 )
1706 "#);
1707 });
1708 }
1709
1710 #[test]
1711 fn rejects_conflicts_outside_the_synthetic_root() {
1712 let lock = conflict_lock();
1713 let root = package(&lock, "contextual-tool", "1.0.0");
1714 let extras = ExtrasSpecification::default();
1715
1716 let error = materialize_with_extras(&lock, &[root], &DARWIN_MARKERS, &extras)
1717 .expect_err("Darwin dependency depends on the project extra");
1718 let error = error.to_string();
1719 let error = anstream::adapter::strip_str(&error);
1720 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");
1721
1722 let linux = materialize_with_extras(&lock, &[root], &LINUX_MARKERS, &extras)
1723 .expect("the dependency is unconditional on Linux");
1724 insta::with_settings!({
1725 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1726 }, {
1727 insta::assert_debug_snapshot!(graph_snapshot(&linux), @r#"
1728 (
1729 [
1730 "contextual-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1731 "contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1732 "root",
1733 ],
1734 [
1735 "contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> contextual-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1736 "root --Prod--> contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1737 ],
1738 )
1739 "#);
1740 });
1741 }
1742
1743 #[test]
1744 fn installable_to_resolution_preserves_node_overrides() {
1745 let mut lock = lock();
1746 lock.manifest.requirements.clear();
1747 let target = OverridingInstallable {
1748 root_name: package(&lock, "root-a", "1.0.0").name(),
1749 lock: &lock,
1750 package_to_node_calls: Cell::new(0),
1751 };
1752 let extras = ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default());
1753 let groups = DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default());
1754
1755 target
1756 .to_resolution(
1757 &DARWIN_MARKERS,
1758 &TAGS,
1759 &extras,
1760 &groups,
1761 &BuildOptions::default(),
1762 &InstallOptions::default(),
1763 )
1764 .expect("valid resolution");
1765
1766 assert!(target.package_to_node_calls.get() > 0);
1767 }
1768}