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 {
241 for dist in roots.iter().copied() {
242 if groups.prod() {
244 activated_projects.push(&dist.id.name);
245 for extra in extras.extra_names(dist.optional_dependencies.keys()) {
246 activated_extras.push((&dist.id.name, extra));
247 }
248 }
249
250 for group in dist
252 .dependency_groups
253 .keys()
254 .filter(|group| groups.contains(group))
255 {
256 activated_groups.push((&dist.id.name, group));
257 }
258 }
259 }
260
261 let mut initialized_roots = vec![];
263 for dist in roots.iter().copied() {
264 let index = petgraph.add_node(if groups.prod() {
266 self.package_to_node(dist, tags, build_options, install_options, marker_env)?
267 } else {
268 self.non_installable_node(dist, tags, marker_env)?
269 });
270 inverse.insert(&dist.id, index);
271
272 petgraph.add_edge(root, index, Edge::Prod);
274
275 initialized_roots.push((dist, index));
277 }
278
279 for (dist, index) in initialized_roots {
281 if groups.prod() {
282 queue.push_back((dist, None));
284 add_reachability(
285 &mut conflict_reachability,
286 (&dist.id, None),
287 UniversalMarker::TRUE,
288 );
289 for extra in extras.extra_names(dist.optional_dependencies.keys()) {
290 queue.push_back((dist, Some(extra)));
291 add_reachability(
292 &mut conflict_reachability,
293 (&dist.id, Some(extra)),
294 UniversalMarker::TRUE,
295 );
296 }
297 }
298
299 for (group, dep) in dist
301 .dependency_groups
302 .iter()
303 .filter_map(|(group, deps)| {
304 if groups.contains(group) {
305 Some(deps.iter().map(move |dep| (group, dep)))
306 } else {
307 None
308 }
309 })
310 .flatten()
311 {
312 if validate_conflicts && dep.complexified_marker.has_conflict_marker() {
313 dependencies_for_conflict_validation.push((dist, dep));
314 }
315 let additional_activated_extras = newly_activated_extras(dep, &activated_extras);
316 if !dep.complexified_marker.evaluate(
317 marker_env,
318 activated_projects.iter().copied(),
319 activated_extras
320 .iter()
321 .chain(additional_activated_extras.iter())
322 .copied(),
323 activated_groups.iter().copied(),
324 ) {
325 continue;
326 }
327
328 let dep_dist = self.lock().find_by_id(&dep.package_id);
329
330 let dep_index = match inverse.entry(&dep.package_id) {
332 Entry::Vacant(entry) => {
333 let index = petgraph.add_node(self.package_to_node(
334 dep_dist,
335 tags,
336 build_options,
337 install_options,
338 marker_env,
339 )?);
340 entry.insert(index);
341 index
342 }
343 Entry::Occupied(entry) => {
344 let index = *entry.get();
348 let node = &mut petgraph[index];
349 if !groups.prod() {
350 *node = self.package_to_node(
351 dep_dist,
352 tags,
353 build_options,
354 install_options,
355 marker_env,
356 )?;
357 }
358 index
359 }
360 };
361
362 petgraph.add_edge(
363 index,
364 dep_index,
365 Edge::Dev(group.clone()),
370 );
371
372 add_reachability(
374 &mut conflict_reachability,
375 (&dep.package_id, None),
376 dep.complexified_marker,
377 );
378 if seen.insert((&dep.package_id, None)) {
379 queue.push_back((dep_dist, None));
380 }
381 for extra in &dep.extra {
382 add_reachability(
383 &mut conflict_reachability,
384 (&dep.package_id, Some(extra)),
385 dep.complexified_marker,
386 );
387 if seen.insert((&dep.package_id, Some(extra))) {
388 queue.push_back((dep_dist, Some(extra)));
389 }
390 }
391 }
392 }
393
394 if include_manifest {
395 for dependency in self.lock().requirements() {
398 if !dependency.marker.evaluate(marker_env, &[]) {
399 continue;
400 }
401
402 let root_name = &dependency.name;
403 let dist = self
404 .lock()
405 .find_by_markers(root_name, marker_env)
406 .map_err(|_| LockErrorKind::MultipleRootPackages {
407 name: root_name.clone(),
408 })?
409 .ok_or_else(|| LockErrorKind::MissingRootPackage {
410 name: root_name.clone(),
411 })?;
412
413 let index = petgraph.add_node(if groups.prod() {
415 self.package_to_node(dist, tags, build_options, install_options, marker_env)?
416 } else {
417 self.non_installable_node(dist, tags, marker_env)?
418 });
419 inverse.insert(&dist.id, index);
420
421 petgraph.add_edge(root, index, Edge::Prod);
423
424 add_reachability(
426 &mut conflict_reachability,
427 (&dist.id, None),
428 UniversalMarker::TRUE,
429 );
430 if seen.insert((&dist.id, None)) {
431 queue.push_back((dist, None));
432 }
433 for extra in &dependency.extras {
434 add_reachability(
435 &mut conflict_reachability,
436 (&dist.id, Some(extra)),
437 UniversalMarker::TRUE,
438 );
439 if seen.insert((&dist.id, Some(extra))) {
440 queue.push_back((dist, Some(extra)));
441 }
442 }
443 }
444
445 for (group, dependency) in self
448 .lock()
449 .dependency_groups()
450 .iter()
451 .filter_map(|(group, deps)| {
452 if groups.contains(group) {
453 Some(deps.iter().map(move |dep| (group, dep)))
454 } else {
455 None
456 }
457 })
458 .flatten()
459 {
460 if !dependency.marker.evaluate(marker_env, &[]) {
461 continue;
462 }
463
464 let root_name = &dependency.name;
465 let dist = self
466 .lock()
467 .find_by_markers(root_name, marker_env)
468 .map_err(|_| LockErrorKind::MultipleRootPackages {
469 name: root_name.clone(),
470 })?
471 .ok_or_else(|| LockErrorKind::MissingRootPackage {
472 name: root_name.clone(),
473 })?;
474
475 let index = match inverse.entry(&dist.id) {
477 Entry::Vacant(entry) => {
478 let index = petgraph.add_node(self.package_to_node(
479 dist,
480 tags,
481 build_options,
482 install_options,
483 marker_env,
484 )?);
485 entry.insert(index);
486 index
487 }
488 Entry::Occupied(entry) => {
489 let index = *entry.get();
493 let node = &mut petgraph[index];
494 if !groups.prod() {
495 *node = self.package_to_node(
496 dist,
497 tags,
498 build_options,
499 install_options,
500 marker_env,
501 )?;
502 }
503 index
504 }
505 };
506
507 petgraph.add_edge(root, index, Edge::Dev(group.clone()));
509
510 add_reachability(
512 &mut conflict_reachability,
513 (&dist.id, None),
514 UniversalMarker::TRUE,
515 );
516 if seen.insert((&dist.id, None)) {
517 queue.push_back((dist, None));
518 }
519 for extra in &dependency.extras {
520 add_reachability(
521 &mut conflict_reachability,
522 (&dist.id, Some(extra)),
523 UniversalMarker::TRUE,
524 );
525 if seen.insert((&dist.id, Some(extra))) {
526 queue.push_back((dist, Some(extra)));
527 }
528 }
529 }
530 }
531
532 if has_conflicts {
561 let mut activated_extras_set: BTreeSet<(&PackageName, &ExtraName)> =
562 activated_extras.iter().copied().collect();
563 let mut queue = queue.clone();
564 let mut reachability = conflict_reachability;
565 while let Some((package, extra)) = queue.pop_front() {
566 let Some(parent_reachability) = reachability.get(&(&package.id, extra)).copied()
567 else {
568 continue;
569 };
570 let deps = if let Some(extra) = extra {
571 Either::Left(
572 package
573 .optional_dependencies
574 .get(extra)
575 .into_iter()
576 .flatten(),
577 )
578 } else {
579 Either::Right(package.dependencies.iter())
580 };
581 for dep in deps {
582 let mut dep_reachability = dep.complexified_marker;
583 dep_reachability.and(parent_reachability);
584 let additional_activated_extras =
585 newly_activated_extras(dep, &activated_extras);
586 if !dep_reachability.evaluate(
587 marker_env,
588 activated_projects.iter().copied(),
589 activated_extras
590 .iter()
591 .chain(additional_activated_extras.iter())
592 .copied(),
593 activated_groups.iter().copied(),
594 ) {
595 continue;
596 }
597 for key in additional_activated_extras {
606 activated_extras_set.insert(key);
607 activated_extras.push(key);
608 }
609 let dep_dist = self.lock().find_by_id(&dep.package_id);
610 if add_reachability(
612 &mut reachability,
613 (&dep.package_id, None),
614 dep_reachability,
615 ) {
616 queue.push_back((dep_dist, None));
617 }
618 for extra in &dep.extra {
619 if add_reachability(
620 &mut reachability,
621 (&dep.package_id, Some(extra)),
622 dep_reachability,
623 ) {
624 queue.push_back((dep_dist, Some(extra)));
625 }
626 }
627 }
628 }
629 for set in self.lock().conflicts().iter() {
641 for ((pkg1, extra1), (pkg2, extra2)) in
642 activated_extras_set.iter().tuple_combinations()
643 {
644 if set.contains(pkg1, *extra1) && set.contains(pkg2, *extra2) {
645 return Err(LockErrorKind::ConflictingExtra {
646 package1: (*pkg1).clone(),
647 extra1: (*extra1).clone(),
648 package2: (*pkg2).clone(),
649 extra2: (*extra2).clone(),
650 }
651 .into());
652 }
653 }
654 }
655 }
656
657 while let Some((package, extra)) = queue.pop_front() {
658 let deps = if let Some(extra) = extra {
659 Either::Left(
660 package
661 .optional_dependencies
662 .get(extra)
663 .into_iter()
664 .flatten(),
665 )
666 } else {
667 Either::Right(package.dependencies.iter())
668 };
669 for dep in deps {
670 if validate_conflicts && dep.complexified_marker.has_conflict_marker() {
671 dependencies_for_conflict_validation.push((package, dep));
672 }
673 if !dep.complexified_marker.evaluate(
674 marker_env,
675 activated_projects.iter().copied(),
676 activated_extras.iter().copied(),
677 activated_groups.iter().copied(),
678 ) {
679 continue;
680 }
681
682 let dep_dist = self.lock().find_by_id(&dep.package_id);
683
684 let dep_index = match inverse.entry(&dep.package_id) {
686 Entry::Vacant(entry) => {
687 let index = petgraph.add_node(self.package_to_node(
688 dep_dist,
689 tags,
690 build_options,
691 install_options,
692 marker_env,
693 )?);
694 entry.insert(index);
695 index
696 }
697 Entry::Occupied(entry) => *entry.get(),
698 };
699
700 let index = inverse[&package.id];
702 petgraph.add_edge(
703 index,
704 dep_index,
705 if let Some(extra) = extra {
706 Edge::Optional(extra.clone())
707 } else {
708 Edge::Prod
709 },
710 );
711
712 if seen.insert((&dep.package_id, None)) {
714 queue.push_back((dep_dist, None));
715 }
716 for extra in &dep.extra {
717 if seen.insert((&dep.package_id, Some(extra))) {
718 queue.push_back((dep_dist, Some(extra)));
719 }
720 }
721 }
722 }
723
724 if !dependencies_for_conflict_validation.is_empty() {
727 let subgraph_packages = inverse
728 .keys()
729 .map(|package_id| &package_id.name)
730 .collect::<FxHashSet<_>>();
731 let selection_context_package = selection_context.package();
732
733 let mut validated_markers = FxHashSet::default();
736 for (package, dependency) in dependencies_for_conflict_validation {
737 if !validated_markers.insert(dependency.complexified_marker) {
738 continue;
739 }
740 let mut marker = dependency.complexified_marker;
741 for item in self.lock().conflicts().iter().flat_map(ConflictSet::iter) {
742 if selection_context_package != Some(item.package())
743 && !subgraph_packages.contains(item.package())
744 {
745 continue;
746 }
747
748 let active = match item.kind() {
749 ConflictKind::Project => activated_projects.contains(&item.package()),
750 ConflictKind::Extra(extra) => {
751 activated_extras.contains(&(item.package(), extra))
752 }
753 ConflictKind::Group(group) => {
754 activated_groups.contains(&(item.package(), group))
755 }
756 };
757 if active {
758 marker.assume_conflict_item(item);
759 } else {
760 marker.assume_not_conflict_item(item);
761 }
762 }
763
764 let conflict = marker.conflict_for_environment(marker_env);
765 if !conflict.is_constant() {
768 return Err(LockErrorKind::DependencyConflictOutsideSubgraph {
769 package: package.id.clone(),
770 dependency: dependency.package_id.clone(),
771 }
772 .into());
773 }
774 }
775 }
776
777 Ok(Resolution::new(petgraph))
778 }
779}
780
781impl<'lock, T> InstallableExt<'lock> for T where T: Installable<'lock> + ?Sized {}
782
783struct LockedPackages<'lock> {
785 lock: &'lock Lock,
786 install_path: &'lock Path,
787 project_name: Option<&'lock PackageName>,
788}
789
790impl<'lock> Installable<'lock> for LockedPackages<'lock> {
791 fn install_path(&self) -> &'lock Path {
792 self.install_path
793 }
794
795 fn lock(&self) -> &'lock Lock {
796 self.lock
797 }
798
799 fn roots(&self) -> impl Iterator<Item = &PackageName> {
800 std::iter::empty()
801 }
802
803 fn project_name(&self) -> Option<&PackageName> {
804 self.project_name
805 }
806}
807
808impl Lock {
809 pub fn to_resolution_from_dependency<'lock>(
815 &'lock self,
816 install_path: &'lock Path,
817 dependency: &SelectedDependency<'lock>,
818 project_name: Option<&'lock PackageName>,
819 marker_env: &ResolverMarkerEnvironment,
820 tags: &Tags,
821 build_options: &BuildOptions,
822 install_options: &InstallOptions,
823 ) -> Result<Resolution, LockError> {
824 let selected_package = dependency.package();
825 let Some(index) = self.by_id.get(&selected_package.id) else {
826 return Err(LockErrorKind::RootPackageMissingFromLock {
827 id: selected_package.id.clone(),
828 }
829 .into());
830 };
831 let Some(package) = self.packages.get(*index) else {
832 return Err(LockErrorKind::RootPackageMissingFromLock {
833 id: selected_package.id.clone(),
834 }
835 .into());
836 };
837 let extras = ExtrasSpecification::from_extra(dependency.extras().cloned().collect())
838 .with_defaults(DefaultExtras::default());
839 let groups = DependencyGroupsWithDefaults::none();
840
841 LockedPackages {
842 lock: self,
843 install_path,
844 project_name,
845 }
846 .to_resolution_from_packages(
847 &[package],
848 false,
849 dependency.context(),
850 marker_env,
851 tags,
852 &extras,
853 &groups,
854 build_options,
855 install_options,
856 )
857 }
858
859 pub fn to_resolution<'lock>(
874 &'lock self,
875 install_path: &'lock Path,
876 roots: impl IntoIterator<Item = &'lock Package>,
877 project_name: Option<&'lock PackageName>,
878 marker_env: &ResolverMarkerEnvironment,
879 tags: &Tags,
880 extras: &ExtrasSpecificationWithDefaults,
881 groups: &DependencyGroupsWithDefaults,
882 build_options: &BuildOptions,
883 install_options: &InstallOptions,
884 ) -> Result<Resolution, LockError> {
885 let mut seen = FxHashSet::default();
886 let mut concrete_roots = Vec::new();
887 for root in roots {
888 let Some(index) = self.by_id.get(&root.id) else {
889 return Err(LockErrorKind::RootPackageMissingFromLock {
890 id: root.id.clone(),
891 }
892 .into());
893 };
894 if seen.insert(&root.id) {
895 let Some(root) = self.packages.get(*index) else {
896 return Err(LockErrorKind::RootPackageMissingFromLock {
897 id: root.id.clone(),
898 }
899 .into());
900 };
901 concrete_roots.push(root);
902 }
903 }
904
905 LockedPackages {
906 lock: self,
907 install_path,
908 project_name,
909 }
910 .to_resolution_from_packages(
911 &concrete_roots,
912 false,
913 DependencySelectionContext::None,
914 marker_env,
915 tags,
916 extras,
917 groups,
918 build_options,
919 install_options,
920 )
921 }
922}
923
924#[cfg(test)]
925mod tests {
926 use std::cell::Cell;
927 use std::str::FromStr;
928 use std::sync::LazyLock;
929
930 use petgraph::visit::EdgeRef;
931 use uv_configuration::{DependencyGroups, ExtrasSpecification};
932 use uv_distribution_types::Name;
933 use uv_normalize::{DefaultExtras, DefaultGroups};
934 use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
935 use uv_platform_tags::{Arch, Os, Platform, TagsOptions};
936 use uv_warnings::anstream;
937
938 use super::*;
939
940 static TAGS: LazyLock<Tags> = LazyLock::new(|| {
941 Tags::from_env(
942 Platform::new(
943 Os::Macos {
944 major: 14,
945 minor: 0,
946 },
947 Arch::Aarch64,
948 ),
949 (3, 11),
950 "cpython",
951 (3, 11),
952 TagsOptions::default(),
953 )
954 .expect("valid tags")
955 });
956
957 static DARWIN_MARKERS: LazyLock<ResolverMarkerEnvironment> =
958 LazyLock::new(|| ResolverMarkerEnvironment::from(marker_environment("darwin", "Darwin")));
959
960 static LINUX_MARKERS: LazyLock<ResolverMarkerEnvironment> =
961 LazyLock::new(|| ResolverMarkerEnvironment::from(marker_environment("linux", "Linux")));
962
963 fn marker_environment(
964 sys_platform: &'static str,
965 platform_system: &'static str,
966 ) -> MarkerEnvironment {
967 MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
968 implementation_name: "cpython",
969 implementation_version: "3.11.5",
970 os_name: "posix",
971 platform_machine: "arm64",
972 platform_python_implementation: "CPython",
973 platform_release: "23.0.0",
974 platform_system,
975 platform_version: "test",
976 python_full_version: "3.11.5",
977 python_version: "3.11",
978 sys_platform,
979 })
980 .expect("valid marker environment")
981 }
982
983 fn lock() -> Lock {
984 toml::from_str(
985 r#"
986version = 1
987revision = 3
988requires-python = ">=3.11"
989resolution-markers = [
990 "sys_platform == 'darwin'",
991 "sys_platform != 'darwin'",
992]
993
994[manifest]
995requirements = [{ name = "unrelated" }]
996
997[[package]]
998name = "dev-dependency"
999version = "1.0.0"
1000source = { registry = "https://example.com/simple" }
1001sdist = { url = "https://example.com/dev_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
1002
1003[[package]]
1004name = "forked"
1005version = "1.0.0"
1006source = { registry = "https://example.com/simple" }
1007resolution-markers = ["sys_platform == 'darwin'"]
1008sdist = { url = "https://example.com/forked-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
1009
1010[[package]]
1011name = "forked"
1012version = "2.0.0"
1013source = { registry = "https://example.com/simple" }
1014resolution-markers = ["sys_platform != 'darwin'"]
1015sdist = { url = "https://example.com/forked-2.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
1016
1017[[package]]
1018name = "optional-dependency"
1019version = "1.0.0"
1020source = { registry = "https://example.com/simple" }
1021sdist = { url = "https://example.com/optional_dependency-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
1022
1023[[package]]
1024name = "root-a"
1025version = "1.0.0"
1026source = { registry = "https://example.com/simple" }
1027dependencies = [
1028 { name = "forked", version = "1.0.0", source = { registry = "https://example.com/simple" }, marker = "sys_platform == 'darwin'" },
1029 { name = "forked", version = "2.0.0", source = { registry = "https://example.com/simple" }, marker = "sys_platform != 'darwin'" },
1030 { name = "shared" },
1031]
1032sdist = { url = "https://example.com/root_a-1.0.0.tar.gz", hash = "sha256:5555555555555555555555555555555555555555555555555555555555555555" }
1033
1034[package.optional-dependencies]
1035feature = [{ name = "optional-dependency" }]
1036
1037[package.dependency-groups]
1038dev = [{ name = "dev-dependency" }]
1039
1040[package.metadata]
1041provides-extras = ["feature"]
1042
1043[[package]]
1044name = "root-b"
1045version = "1.0.0"
1046source = { registry = "https://example.com/simple" }
1047dependencies = [{ name = "shared" }]
1048sdist = { url = "https://example.com/root_b-1.0.0.tar.gz", hash = "sha256:6666666666666666666666666666666666666666666666666666666666666666" }
1049
1050[[package]]
1051name = "shared"
1052version = "1.0.0"
1053source = { registry = "https://example.com/simple" }
1054sdist = { url = "https://example.com/shared-1.0.0.tar.gz", hash = "sha256:7777777777777777777777777777777777777777777777777777777777777777" }
1055
1056[[package]]
1057name = "unrelated"
1058version = "1.0.0"
1059source = { registry = "https://example.com/simple" }
1060sdist = { url = "https://example.com/unrelated-1.0.0.tar.gz", hash = "sha256:8888888888888888888888888888888888888888888888888888888888888888" }
1061"#,
1062 )
1063 .expect("valid lock")
1064 }
1065
1066 fn conflict_lock() -> Lock {
1067 toml::from_str(
1068 r#"
1069version = 1
1070revision = 3
1071requires-python = ">=3.11"
1072conflicts = [
1073 [
1074 { package = "tool", extra = "cpu" },
1075 { package = "tool", extra = "gpu" },
1076 ],
1077 [
1078 { package = "project", extra = "foo" },
1079 { package = "project", extra = "bar" },
1080 ],
1081]
1082
1083[[package]]
1084name = "contextual-dependency"
1085version = "1.0.0"
1086source = { registry = "https://example.com/simple" }
1087sdist = { url = "https://example.com/contextual_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
1088
1089[[package]]
1090name = "contextual-tool"
1091version = "1.0.0"
1092source = { registry = "https://example.com/simple" }
1093dependencies = [
1094 { name = "contextual-dependency", marker = "sys_platform == 'linux' or (sys_platform == 'darwin' and extra == 'extra-7-project-foo')" },
1095]
1096sdist = { url = "https://example.com/contextual_tool-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
1097
1098[[package]]
1099name = "cpu-backend"
1100version = "1.0.0"
1101source = { registry = "https://example.com/simple" }
1102sdist = { url = "https://example.com/cpu_backend-1.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
1103
1104[[package]]
1105name = "gpu-backend"
1106version = "1.0.0"
1107source = { registry = "https://example.com/simple" }
1108sdist = { url = "https://example.com/gpu_backend-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
1109
1110[[package]]
1111name = "project"
1112version = "1.0.0"
1113source = { registry = "https://example.com/simple" }
1114sdist = { url = "https://example.com/project-1.0.0.tar.gz", hash = "sha256:5555555555555555555555555555555555555555555555555555555555555555" }
1115
1116[package.optional-dependencies]
1117foo = []
1118bar = []
1119
1120[package.metadata]
1121provides-extras = ["foo", "bar"]
1122
1123[[package]]
1124name = "runtime"
1125version = "1.0.0"
1126source = { registry = "https://example.com/simple" }
1127dependencies = [
1128 { name = "cpu-backend", marker = "extra == 'extra-4-tool-cpu'" },
1129 { name = "gpu-backend", marker = "extra == 'extra-4-tool-gpu'" },
1130]
1131sdist = { url = "https://example.com/runtime-1.0.0.tar.gz", hash = "sha256:6666666666666666666666666666666666666666666666666666666666666666" }
1132
1133[[package]]
1134name = "tool"
1135version = "1.0.0"
1136source = { registry = "https://example.com/simple" }
1137dependencies = [{ name = "runtime" }]
1138sdist = { url = "https://example.com/tool-1.0.0.tar.gz", hash = "sha256:7777777777777777777777777777777777777777777777777777777777777777" }
1139
1140[package.optional-dependencies]
1141cpu = []
1142gpu = []
1143
1144[package.metadata]
1145provides-extras = ["cpu", "gpu"]
1146"#,
1147 )
1148 .expect("valid lock")
1149 }
1150
1151 fn dependency_selection_lock() -> Lock {
1152 toml::from_str(
1153 r#"
1154version = 1
1155revision = 3
1156requires-python = ">=3.11"
1157conflicts = [[
1158 { package = "project", group = "dev" },
1159 { package = "project", group = "other" },
1160]]
1161
1162[[package]]
1163name = "contextual-dev-dependency"
1164version = "1.0.0"
1165source = { registry = "https://example.com/simple" }
1166sdist = { url = "https://example.com/contextual_dev_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
1167
1168[[package]]
1169name = "contextual-other-dependency"
1170version = "1.0.0"
1171source = { registry = "https://example.com/simple" }
1172sdist = { url = "https://example.com/contextual_other_dependency-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
1173
1174[[package]]
1175name = "optional-dependency"
1176version = "1.0.0"
1177source = { registry = "https://example.com/simple" }
1178sdist = { url = "https://example.com/optional_dependency-1.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
1179
1180[[package]]
1181name = "project"
1182version = "1.0.0"
1183source = { virtual = "." }
1184
1185[package.dependency-groups]
1186dev = [{ name = "tool", extra = ["cli"] }]
1187other = [{ name = "tool" }]
1188
1189[[package]]
1190name = "tool"
1191version = "1.0.0"
1192source = { registry = "https://example.com/simple" }
1193dependencies = [
1194 { name = "contextual-dev-dependency", marker = "extra == 'group-7-project-dev'" },
1195 { name = "contextual-other-dependency", marker = "extra == 'group-7-project-other'" },
1196]
1197sdist = { url = "https://example.com/tool-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
1198
1199[package.optional-dependencies]
1200cli = [{ name = "optional-dependency" }]
1201
1202[package.metadata]
1203provides-extras = ["cli"]
1204"#,
1205 )
1206 .expect("valid lock")
1207 }
1208
1209 fn package<'lock>(lock: &'lock Lock, name: &str, version: &str) -> &'lock Package {
1210 lock.packages()
1211 .iter()
1212 .find(|package| {
1213 package.name().as_ref() == name
1214 && package
1215 .version()
1216 .is_some_and(|package_version| package_version.to_string() == version)
1217 })
1218 .expect("locked package")
1219 }
1220
1221 fn materialize(
1222 lock: &Lock,
1223 roots: &[&Package],
1224 marker_env: &ResolverMarkerEnvironment,
1225 ) -> Resolution {
1226 let extras = ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default());
1227 let groups = DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default());
1228 lock.to_resolution(
1229 Path::new("."),
1230 roots.iter().copied(),
1231 None,
1232 marker_env,
1233 &TAGS,
1234 &extras,
1235 &groups,
1236 &BuildOptions::default(),
1237 &InstallOptions::default(),
1238 )
1239 .expect("valid resolution")
1240 }
1241
1242 fn materialize_with_extras(
1243 lock: &Lock,
1244 roots: &[&Package],
1245 marker_env: &ResolverMarkerEnvironment,
1246 extras: &ExtrasSpecification,
1247 ) -> Result<Resolution, LockError> {
1248 let extras = extras.with_defaults(DefaultExtras::default());
1249 let groups = DependencyGroupsWithDefaults::none();
1250 lock.to_resolution(
1251 Path::new("."),
1252 roots.iter().copied(),
1253 None,
1254 marker_env,
1255 &TAGS,
1256 &extras,
1257 &groups,
1258 &BuildOptions::default(),
1259 &InstallOptions::default(),
1260 )
1261 }
1262
1263 fn materialize_selected_dependency(lock: &Lock, group: &str) -> Resolution {
1264 let project_name = PackageName::from_str("project").expect("valid package name");
1265 let dependency_name = PackageName::from_str("tool").expect("valid package name");
1266 let group = GroupName::from_str(group).expect("valid group name");
1267 let selection = lock
1268 .dependency_selection(
1269 Some(&project_name),
1270 &dependency_name,
1271 DARWIN_MARKERS.markers(),
1272 )
1273 .expect("unique dependency selection");
1274 let dependency = selection.group(&group).expect("group dependency");
1275
1276 lock.to_resolution_from_dependency(
1277 Path::new("."),
1278 dependency,
1279 Some(&project_name),
1280 &DARWIN_MARKERS,
1281 &TAGS,
1282 &BuildOptions::default(),
1283 &InstallOptions::default(),
1284 )
1285 .expect("valid resolution")
1286 }
1287
1288 struct OverridingInstallable<'lock> {
1289 lock: &'lock Lock,
1290 root_name: &'lock PackageName,
1291 package_to_node_calls: Cell<usize>,
1292 }
1293
1294 impl<'lock> Installable<'lock> for OverridingInstallable<'lock> {
1295 fn install_path(&self) -> &'lock Path {
1296 Path::new(".")
1297 }
1298
1299 fn lock(&self) -> &'lock Lock {
1300 self.lock
1301 }
1302
1303 fn roots(&self) -> impl Iterator<Item = &PackageName> {
1304 std::iter::once(self.root_name)
1305 }
1306
1307 fn project_name(&self) -> Option<&PackageName> {
1308 None
1309 }
1310
1311 fn package_to_node(
1312 &self,
1313 _package: &Package,
1314 _tags: &Tags,
1315 _build_options: &BuildOptions,
1316 _install_options: &InstallOptions,
1317 _marker_env: &ResolverMarkerEnvironment,
1318 ) -> Result<Node, LockError> {
1319 self.package_to_node_calls
1320 .set(self.package_to_node_calls.get() + 1);
1321 Ok(Node::Root)
1322 }
1323 }
1324
1325 fn graph_snapshot(resolution: &Resolution) -> (Vec<String>, Vec<String>) {
1326 let graph = resolution.graph();
1327 let labels = graph
1328 .node_weights()
1329 .map(|node| match node {
1330 Node::Root => "root".to_string(),
1331 Node::Dist {
1332 dist,
1333 hashes,
1334 install,
1335 } => format!(
1336 "{}=={} (install: {install}, hashes: {})",
1337 dist.name(),
1338 dist.version()
1339 .map(ToString::to_string)
1340 .unwrap_or_else(|| "<dynamic>".to_string()),
1341 hashes.iter().map(ToString::to_string).join(", ")
1342 ),
1343 })
1344 .collect::<Vec<_>>();
1345 let mut nodes = labels.clone();
1346 nodes.sort_unstable();
1347 let mut edges = graph
1348 .edge_references()
1349 .map(|edge| {
1350 format!(
1351 "{} --{:?}--> {}",
1352 labels[edge.source().index()],
1353 edge.weight(),
1354 labels[edge.target().index()]
1355 )
1356 })
1357 .collect::<Vec<_>>();
1358 edges.sort_unstable();
1359 (nodes, edges)
1360 }
1361
1362 #[test]
1363 fn materializes_multiple_concrete_roots_with_shared_dependencies() {
1364 let lock = lock();
1365 let resolution = materialize(
1366 &lock,
1367 &[
1368 package(&lock, "root-a", "1.0.0"),
1369 package(&lock, "root-b", "1.0.0"),
1370 ],
1371 &DARWIN_MARKERS,
1372 );
1373
1374 insta::with_settings!({
1375 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1376 }, {
1377 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1378 (
1379 [
1380 "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1381 "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1382 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1383 "root",
1384 "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1385 "root-b==1.0.0 (install: true, hashes: sha256:[HASH])",
1386 "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1387 ],
1388 [
1389 "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1390 "root --Prod--> root-b==1.0.0 (install: true, hashes: sha256:[HASH])",
1391 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1392 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1393 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1394 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1395 "root-b==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1396 ],
1397 )
1398 "#);
1399 });
1400 }
1401
1402 #[test]
1403 fn materializes_the_selected_universal_lock_fork() {
1404 let lock = lock();
1405 let root = package(&lock, "root-a", "1.0.0");
1406 let darwin = materialize(&lock, &[root], &DARWIN_MARKERS);
1407 let linux = materialize(&lock, &[root], &LINUX_MARKERS);
1408 let concrete_fork =
1409 materialize(&lock, &[package(&lock, "forked", "1.0.0")], &DARWIN_MARKERS);
1410
1411 insta::with_settings!({
1412 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1413 }, {
1414 insta::assert_debug_snapshot!(graph_snapshot(&darwin), @r#"
1415 (
1416 [
1417 "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1418 "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1419 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1420 "root",
1421 "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1422 "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1423 ],
1424 [
1425 "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1426 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1427 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1428 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1429 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1430 ],
1431 )
1432 "#);
1433 insta::assert_debug_snapshot!(graph_snapshot(&linux), @r#"
1434 (
1435 [
1436 "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1437 "forked==2.0.0 (install: true, hashes: sha256:[HASH])",
1438 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1439 "root",
1440 "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1441 "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1442 ],
1443 [
1444 "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1445 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1446 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1447 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==2.0.0 (install: true, hashes: sha256:[HASH])",
1448 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1449 ],
1450 )
1451 "#);
1452 insta::assert_debug_snapshot!(graph_snapshot(&concrete_fork), @r#"
1453 (
1454 [
1455 "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1456 "root",
1457 ],
1458 [
1459 "root --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1460 ],
1461 )
1462 "#);
1463 });
1464 }
1465
1466 #[test]
1467 fn materializes_conflicting_extras_within_the_synthetic_root() {
1468 let lock = conflict_lock();
1469 let extras =
1470 ExtrasSpecification::from_extra(vec!["cpu".parse().expect("valid extra name")]);
1471 let resolution = materialize_with_extras(
1472 &lock,
1473 &[package(&lock, "tool", "1.0.0")],
1474 &DARWIN_MARKERS,
1475 &extras,
1476 )
1477 .expect("conflict markers are resolved within the subgraph");
1478
1479 insta::with_settings!({
1480 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1481 }, {
1482 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1483 (
1484 [
1485 "cpu-backend==1.0.0 (install: true, hashes: sha256:[HASH])",
1486 "root",
1487 "runtime==1.0.0 (install: true, hashes: sha256:[HASH])",
1488 "tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1489 ],
1490 [
1491 "root --Prod--> tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1492 "runtime==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> cpu-backend==1.0.0 (install: true, hashes: sha256:[HASH])",
1493 "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> runtime==1.0.0 (install: true, hashes: sha256:[HASH])",
1494 ],
1495 )
1496 "#);
1497 });
1498 }
1499
1500 #[test]
1501 fn materializes_selected_dependency_extras() {
1502 let resolution = materialize_selected_dependency(&dependency_selection_lock(), "dev");
1503
1504 insta::with_settings!({
1505 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1506 }, {
1507 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1508 (
1509 [
1510 "contextual-dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1511 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1512 "root",
1513 "tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1514 ],
1515 [
1516 "root --Prod--> tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1517 "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"cli\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1518 "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> contextual-dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1519 ],
1520 )
1521 "#);
1522 });
1523 }
1524
1525 #[test]
1526 fn materializes_selected_dependency_project_conflict_context() {
1527 let resolution = materialize_selected_dependency(&dependency_selection_lock(), "other");
1528
1529 insta::with_settings!({
1530 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1531 }, {
1532 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1533 (
1534 [
1535 "contextual-other-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1536 "root",
1537 "tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1538 ],
1539 [
1540 "root --Prod--> tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1541 "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> contextual-other-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1542 ],
1543 )
1544 "#);
1545 });
1546 }
1547
1548 #[test]
1549 fn rejects_conflicts_outside_the_synthetic_root() {
1550 let lock = conflict_lock();
1551 let root = package(&lock, "contextual-tool", "1.0.0");
1552 let extras = ExtrasSpecification::default();
1553
1554 let error = materialize_with_extras(&lock, &[root], &DARWIN_MARKERS, &extras)
1555 .expect_err("Darwin dependency depends on the project extra");
1556 let error = error.to_string();
1557 let error = anstream::adapter::strip_str(&error);
1558 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");
1559
1560 let linux = materialize_with_extras(&lock, &[root], &LINUX_MARKERS, &extras)
1561 .expect("the dependency is unconditional on Linux");
1562 insta::with_settings!({
1563 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1564 }, {
1565 insta::assert_debug_snapshot!(graph_snapshot(&linux), @r#"
1566 (
1567 [
1568 "contextual-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1569 "contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1570 "root",
1571 ],
1572 [
1573 "contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> contextual-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1574 "root --Prod--> contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1575 ],
1576 )
1577 "#);
1578 });
1579 }
1580
1581 #[test]
1582 fn installable_to_resolution_preserves_node_overrides() {
1583 let mut lock = lock();
1584 lock.manifest.requirements.clear();
1585 let target = OverridingInstallable {
1586 root_name: package(&lock, "root-a", "1.0.0").name(),
1587 lock: &lock,
1588 package_to_node_calls: Cell::new(0),
1589 };
1590 let extras = ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default());
1591 let groups = DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default());
1592
1593 target
1594 .to_resolution(
1595 &DARWIN_MARKERS,
1596 &TAGS,
1597 &extras,
1598 &groups,
1599 &BuildOptions::default(),
1600 &InstallOptions::default(),
1601 )
1602 .expect("valid resolution");
1603
1604 assert!(target.package_to_node_calls.get() > 0);
1605 }
1606}