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::ExtrasSpecificationWithDefaults;
13use uv_configuration::{BuildOptions, DependencyGroupsWithDefaults, InstallOptions};
14use uv_distribution_types::{Edge, Node, Resolution, ResolvedDist};
15use uv_normalize::{ExtraName, GroupName, PackageName};
16use uv_platform_tags::Tags;
17use uv_pypi_types::{ConflictKind, ConflictSet, ResolverMarkerEnvironment};
18
19use crate::lock::{Dependency, HashedDist, LockErrorKind, Package, TagPolicy};
20use crate::{Lock, LockError};
21
22fn newly_activated_extras<'lock>(
23 dep: &'lock Dependency,
24 activated_extras: &[(&'lock PackageName, &'lock ExtraName)],
25) -> Vec<(&'lock PackageName, &'lock ExtraName)> {
26 dep.extra
27 .iter()
28 .filter_map(|extra| {
29 let key = (&dep.package_id.name, extra);
30 (!activated_extras.contains(&key)).then_some(key)
31 })
32 .collect()
33}
34
35pub trait Installable<'lock> {
36 fn install_path(&self) -> &'lock Path;
38
39 fn lock(&self) -> &'lock Lock;
41
42 fn roots(&self) -> impl Iterator<Item = &PackageName>;
44
45 fn project_name(&self) -> Option<&PackageName>;
47
48 fn to_resolution(
50 &self,
51 marker_env: &ResolverMarkerEnvironment,
52 tags: &Tags,
53 extras: &ExtrasSpecificationWithDefaults,
54 groups: &DependencyGroupsWithDefaults,
55 build_options: &BuildOptions,
56 install_options: &InstallOptions,
57 ) -> Result<Resolution, LockError> {
58 let roots = self
59 .roots()
60 .map(|root_name| {
61 self.lock()
62 .find_by_name(root_name)
63 .map_err(|_| LockErrorKind::MultipleRootPackages {
64 name: root_name.clone(),
65 })?
66 .ok_or_else(|| {
67 LockError::from(LockErrorKind::MissingRootPackage {
68 name: root_name.clone(),
69 })
70 })
71 })
72 .collect::<Result<Vec<_>, LockError>>()?;
73
74 InstallableExt::to_resolution_from_packages(
75 self,
76 &roots,
77 true,
78 marker_env,
79 tags,
80 extras,
81 groups,
82 build_options,
83 install_options,
84 )
85 }
86
87 fn installable_node(
89 &self,
90 package: &Package,
91 tags: &Tags,
92 marker_env: &ResolverMarkerEnvironment,
93 build_options: &BuildOptions,
94 ) -> Result<Node, LockError> {
95 let tag_policy = TagPolicy::Required(tags);
96 let HashedDist { dist, hashes } =
97 package.to_dist(self.install_path(), tag_policy, build_options, marker_env)?;
98 let version = package.version().cloned();
99 let dist = ResolvedDist::Installable {
100 dist: Arc::new(dist),
101 version,
102 };
103 Ok(Node::Dist {
104 dist,
105 hashes,
106 install: true,
107 })
108 }
109
110 fn non_installable_node(
112 &self,
113 package: &Package,
114 tags: &Tags,
115 marker_env: &ResolverMarkerEnvironment,
116 ) -> Result<Node, LockError> {
117 let HashedDist { dist, .. } = package.to_dist(
118 self.install_path(),
119 TagPolicy::Preferred(tags),
120 &BuildOptions::default(),
121 marker_env,
122 )?;
123 let version = package.version().cloned();
124 let dist = ResolvedDist::Installable {
125 dist: Arc::new(dist),
126 version,
127 };
128 let hashes = package.hashes();
129 Ok(Node::Dist {
130 dist,
131 hashes,
132 install: false,
133 })
134 }
135
136 fn package_to_node(
138 &self,
139 package: &Package,
140 tags: &Tags,
141 build_options: &BuildOptions,
142 install_options: &InstallOptions,
143 marker_env: &ResolverMarkerEnvironment,
144 ) -> Result<Node, LockError> {
145 if install_options.include_package(
146 package.as_install_target(),
147 self.project_name(),
148 self.lock().members(),
149 ) {
150 self.installable_node(package, tags, marker_env, build_options)
151 } else {
152 self.non_installable_node(package, tags, marker_env)
153 }
154 }
155}
156
157trait InstallableExt<'lock>: Installable<'lock> {
159 fn to_resolution_from_packages(
164 &self,
165 roots: &[&Package],
166 include_manifest: bool,
167 marker_env: &ResolverMarkerEnvironment,
168 tags: &Tags,
169 extras: &ExtrasSpecificationWithDefaults,
170 groups: &DependencyGroupsWithDefaults,
171 build_options: &BuildOptions,
172 install_options: &InstallOptions,
173 ) -> Result<Resolution, LockError> {
174 let size_guess = self.lock().packages.len();
175 let mut petgraph = Graph::with_capacity(size_guess, size_guess);
176 let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher);
177
178 let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
179 let mut seen = FxHashSet::default();
180 let mut activated_projects: Vec<&PackageName> = vec![];
181 let mut activated_extras: Vec<(&PackageName, &ExtraName)> = vec![];
182 let mut activated_groups: Vec<(&PackageName, &GroupName)> = vec![];
183 let validate_conflicts = !include_manifest && !self.lock().conflicts().is_empty();
184 let mut dependencies_for_conflict_validation = vec![];
185
186 let root = petgraph.add_node(Node::Root);
187
188 if !self.lock().conflicts().is_empty() {
196 for dist in roots.iter().copied() {
197 if groups.prod() {
199 activated_projects.push(&dist.id.name);
200 for extra in extras.extra_names(dist.optional_dependencies.keys()) {
201 activated_extras.push((&dist.id.name, extra));
202 }
203 }
204
205 for group in dist
207 .dependency_groups
208 .keys()
209 .filter(|group| groups.contains(group))
210 {
211 activated_groups.push((&dist.id.name, group));
212 }
213 }
214 }
215
216 let mut initialized_roots = vec![];
218 for dist in roots.iter().copied() {
219 let index = petgraph.add_node(if groups.prod() {
221 self.package_to_node(dist, tags, build_options, install_options, marker_env)?
222 } else {
223 self.non_installable_node(dist, tags, marker_env)?
224 });
225 inverse.insert(&dist.id, index);
226
227 petgraph.add_edge(root, index, Edge::Prod);
229
230 initialized_roots.push((dist, index));
232 }
233
234 for (dist, index) in initialized_roots {
236 if groups.prod() {
237 queue.push_back((dist, None));
239 for extra in extras.extra_names(dist.optional_dependencies.keys()) {
240 queue.push_back((dist, Some(extra)));
241 }
242 }
243
244 for (group, dep) in dist
246 .dependency_groups
247 .iter()
248 .filter_map(|(group, deps)| {
249 if groups.contains(group) {
250 Some(deps.iter().map(move |dep| (group, dep)))
251 } else {
252 None
253 }
254 })
255 .flatten()
256 {
257 if validate_conflicts && dep.complexified_marker.has_conflict_marker() {
258 dependencies_for_conflict_validation.push((dist, dep));
259 }
260 let additional_activated_extras = newly_activated_extras(dep, &activated_extras);
261 if !dep.complexified_marker.evaluate(
262 marker_env,
263 activated_projects.iter().copied(),
264 activated_extras
265 .iter()
266 .chain(additional_activated_extras.iter())
267 .copied(),
268 activated_groups.iter().copied(),
269 ) {
270 continue;
271 }
272
273 let dep_dist = self.lock().find_by_id(&dep.package_id);
274
275 let dep_index = match inverse.entry(&dep.package_id) {
277 Entry::Vacant(entry) => {
278 let index = petgraph.add_node(self.package_to_node(
279 dep_dist,
280 tags,
281 build_options,
282 install_options,
283 marker_env,
284 )?);
285 entry.insert(index);
286 index
287 }
288 Entry::Occupied(entry) => {
289 let index = *entry.get();
293 let node = &mut petgraph[index];
294 if !groups.prod() {
295 *node = self.package_to_node(
296 dep_dist,
297 tags,
298 build_options,
299 install_options,
300 marker_env,
301 )?;
302 }
303 index
304 }
305 };
306
307 petgraph.add_edge(
308 index,
309 dep_index,
310 Edge::Dev(group.clone()),
315 );
316
317 if seen.insert((&dep.package_id, None)) {
319 queue.push_back((dep_dist, None));
320 }
321 for extra in &dep.extra {
322 if seen.insert((&dep.package_id, Some(extra))) {
323 queue.push_back((dep_dist, Some(extra)));
324 }
325 }
326 }
327 }
328
329 if include_manifest {
330 for dependency in self.lock().requirements() {
333 if !dependency.marker.evaluate(marker_env, &[]) {
334 continue;
335 }
336
337 let root_name = &dependency.name;
338 let dist = self
339 .lock()
340 .find_by_markers(root_name, marker_env)
341 .map_err(|_| LockErrorKind::MultipleRootPackages {
342 name: root_name.clone(),
343 })?
344 .ok_or_else(|| LockErrorKind::MissingRootPackage {
345 name: root_name.clone(),
346 })?;
347
348 let index = petgraph.add_node(if groups.prod() {
350 self.package_to_node(dist, tags, build_options, install_options, marker_env)?
351 } else {
352 self.non_installable_node(dist, tags, marker_env)?
353 });
354 inverse.insert(&dist.id, index);
355
356 petgraph.add_edge(root, index, Edge::Prod);
358
359 if seen.insert((&dist.id, None)) {
361 queue.push_back((dist, None));
362 }
363 for extra in &dependency.extras {
364 if seen.insert((&dist.id, Some(extra))) {
365 queue.push_back((dist, Some(extra)));
366 }
367 }
368 }
369
370 for (group, dependency) in self
373 .lock()
374 .dependency_groups()
375 .iter()
376 .filter_map(|(group, deps)| {
377 if groups.contains(group) {
378 Some(deps.iter().map(move |dep| (group, dep)))
379 } else {
380 None
381 }
382 })
383 .flatten()
384 {
385 if !dependency.marker.evaluate(marker_env, &[]) {
386 continue;
387 }
388
389 let root_name = &dependency.name;
390 let dist = self
391 .lock()
392 .find_by_markers(root_name, marker_env)
393 .map_err(|_| LockErrorKind::MultipleRootPackages {
394 name: root_name.clone(),
395 })?
396 .ok_or_else(|| LockErrorKind::MissingRootPackage {
397 name: root_name.clone(),
398 })?;
399
400 let index = match inverse.entry(&dist.id) {
402 Entry::Vacant(entry) => {
403 let index = petgraph.add_node(self.package_to_node(
404 dist,
405 tags,
406 build_options,
407 install_options,
408 marker_env,
409 )?);
410 entry.insert(index);
411 index
412 }
413 Entry::Occupied(entry) => {
414 let index = *entry.get();
418 let node = &mut petgraph[index];
419 if !groups.prod() {
420 *node = self.package_to_node(
421 dist,
422 tags,
423 build_options,
424 install_options,
425 marker_env,
426 )?;
427 }
428 index
429 }
430 };
431
432 petgraph.add_edge(root, index, Edge::Dev(group.clone()));
434
435 if seen.insert((&dist.id, None)) {
437 queue.push_back((dist, None));
438 }
439 for extra in &dependency.extras {
440 if seen.insert((&dist.id, Some(extra))) {
441 queue.push_back((dist, Some(extra)));
442 }
443 }
444 }
445 }
446
447 if !self.lock().conflicts().is_empty() {
476 let mut activated_extras_set: BTreeSet<(&PackageName, &ExtraName)> =
477 activated_extras.iter().copied().collect();
478 let mut queue = queue.clone();
479 let mut seen = seen.clone();
480 while let Some((package, extra)) = queue.pop_front() {
481 let deps = if let Some(extra) = extra {
482 Either::Left(
483 package
484 .optional_dependencies
485 .get(extra)
486 .into_iter()
487 .flatten(),
488 )
489 } else {
490 Either::Right(package.dependencies.iter())
491 };
492 for dep in deps {
493 let additional_activated_extras =
494 newly_activated_extras(dep, &activated_extras);
495 if !dep.complexified_marker.evaluate(
496 marker_env,
497 activated_projects.iter().copied(),
498 activated_extras
499 .iter()
500 .chain(additional_activated_extras.iter())
501 .copied(),
502 activated_groups.iter().copied(),
503 ) {
504 continue;
505 }
506 for key in additional_activated_extras {
527 activated_extras_set.insert(key);
528 activated_extras.push(key);
529 }
530 let dep_dist = self.lock().find_by_id(&dep.package_id);
531 if seen.insert((&dep.package_id, None)) {
533 queue.push_back((dep_dist, None));
534 }
535 for extra in &dep.extra {
536 if seen.insert((&dep.package_id, Some(extra))) {
537 queue.push_back((dep_dist, Some(extra)));
538 }
539 }
540 }
541 }
542 for set in self.lock().conflicts().iter() {
554 for ((pkg1, extra1), (pkg2, extra2)) in
555 activated_extras_set.iter().tuple_combinations()
556 {
557 if set.contains(pkg1, *extra1) && set.contains(pkg2, *extra2) {
558 return Err(LockErrorKind::ConflictingExtra {
559 package1: (*pkg1).clone(),
560 extra1: (*extra1).clone(),
561 package2: (*pkg2).clone(),
562 extra2: (*extra2).clone(),
563 }
564 .into());
565 }
566 }
567 }
568 }
569
570 while let Some((package, extra)) = queue.pop_front() {
571 let deps = if let Some(extra) = extra {
572 Either::Left(
573 package
574 .optional_dependencies
575 .get(extra)
576 .into_iter()
577 .flatten(),
578 )
579 } else {
580 Either::Right(package.dependencies.iter())
581 };
582 for dep in deps {
583 if validate_conflicts && dep.complexified_marker.has_conflict_marker() {
584 dependencies_for_conflict_validation.push((package, dep));
585 }
586 if !dep.complexified_marker.evaluate(
587 marker_env,
588 activated_projects.iter().copied(),
589 activated_extras.iter().copied(),
590 activated_groups.iter().copied(),
591 ) {
592 continue;
593 }
594
595 let dep_dist = self.lock().find_by_id(&dep.package_id);
596
597 let dep_index = match inverse.entry(&dep.package_id) {
599 Entry::Vacant(entry) => {
600 let index = petgraph.add_node(self.package_to_node(
601 dep_dist,
602 tags,
603 build_options,
604 install_options,
605 marker_env,
606 )?);
607 entry.insert(index);
608 index
609 }
610 Entry::Occupied(entry) => *entry.get(),
611 };
612
613 let index = inverse[&package.id];
615 petgraph.add_edge(
616 index,
617 dep_index,
618 if let Some(extra) = extra {
619 Edge::Optional(extra.clone())
620 } else {
621 Edge::Prod
622 },
623 );
624
625 if seen.insert((&dep.package_id, None)) {
627 queue.push_back((dep_dist, None));
628 }
629 for extra in &dep.extra {
630 if seen.insert((&dep.package_id, Some(extra))) {
631 queue.push_back((dep_dist, Some(extra)));
632 }
633 }
634 }
635 }
636
637 if !dependencies_for_conflict_validation.is_empty() {
640 let subgraph_packages = inverse
641 .keys()
642 .map(|package_id| &package_id.name)
643 .collect::<FxHashSet<_>>();
644
645 let mut validated_markers = FxHashSet::default();
648 for (package, dependency) in dependencies_for_conflict_validation {
649 if !validated_markers.insert(dependency.complexified_marker) {
650 continue;
651 }
652 let mut marker = dependency.complexified_marker;
653 for item in self.lock().conflicts().iter().flat_map(ConflictSet::iter) {
654 if !subgraph_packages.contains(item.package()) {
655 continue;
656 }
657
658 let active = match item.kind() {
659 ConflictKind::Project => activated_projects.contains(&item.package()),
660 ConflictKind::Extra(extra) => {
661 activated_extras.contains(&(item.package(), extra))
662 }
663 ConflictKind::Group(group) => {
664 activated_groups.contains(&(item.package(), group))
665 }
666 };
667 if active {
668 marker.assume_conflict_item(item);
669 } else {
670 marker.assume_not_conflict_item(item);
671 }
672 }
673
674 let conflict = marker.conflict_for_environment(marker_env);
675 if !conflict.is_constant() {
678 return Err(LockErrorKind::DependencyConflictOutsideSubgraph {
679 package: package.id.clone(),
680 dependency: dependency.package_id.clone(),
681 }
682 .into());
683 }
684 }
685 }
686
687 Ok(Resolution::new(petgraph))
688 }
689}
690
691impl<'lock, T> InstallableExt<'lock> for T where T: Installable<'lock> + ?Sized {}
692
693struct LockedPackages<'lock> {
695 lock: &'lock Lock,
696 install_path: &'lock Path,
697 project_name: Option<&'lock PackageName>,
698}
699
700impl<'lock> Installable<'lock> for LockedPackages<'lock> {
701 fn install_path(&self) -> &'lock Path {
702 self.install_path
703 }
704
705 fn lock(&self) -> &'lock Lock {
706 self.lock
707 }
708
709 fn roots(&self) -> impl Iterator<Item = &PackageName> {
710 std::iter::empty()
711 }
712
713 fn project_name(&self) -> Option<&PackageName> {
714 self.project_name
715 }
716}
717
718impl Lock {
719 pub fn to_resolution<'lock>(
734 &'lock self,
735 install_path: &'lock Path,
736 roots: impl IntoIterator<Item = &'lock Package>,
737 project_name: Option<&'lock PackageName>,
738 marker_env: &ResolverMarkerEnvironment,
739 tags: &Tags,
740 extras: &ExtrasSpecificationWithDefaults,
741 groups: &DependencyGroupsWithDefaults,
742 build_options: &BuildOptions,
743 install_options: &InstallOptions,
744 ) -> Result<Resolution, LockError> {
745 let mut seen = FxHashSet::default();
746 let mut concrete_roots = Vec::new();
747 for root in roots {
748 let Some(index) = self.by_id.get(&root.id) else {
749 return Err(LockErrorKind::RootPackageMissingFromLock {
750 id: root.id.clone(),
751 }
752 .into());
753 };
754 if seen.insert(&root.id) {
755 let Some(root) = self.packages.get(*index) else {
756 return Err(LockErrorKind::RootPackageMissingFromLock {
757 id: root.id.clone(),
758 }
759 .into());
760 };
761 concrete_roots.push(root);
762 }
763 }
764
765 LockedPackages {
766 lock: self,
767 install_path,
768 project_name,
769 }
770 .to_resolution_from_packages(
771 &concrete_roots,
772 false,
773 marker_env,
774 tags,
775 extras,
776 groups,
777 build_options,
778 install_options,
779 )
780 }
781}
782
783#[cfg(test)]
784mod tests {
785 use std::cell::Cell;
786 use std::sync::LazyLock;
787
788 use petgraph::visit::EdgeRef;
789 use uv_configuration::{DependencyGroups, ExtrasSpecification};
790 use uv_distribution_types::Name;
791 use uv_normalize::{DefaultExtras, DefaultGroups};
792 use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
793 use uv_platform_tags::{Arch, Os, Platform, TagsOptions};
794 use uv_warnings::anstream;
795
796 use super::*;
797
798 static TAGS: LazyLock<Tags> = LazyLock::new(|| {
799 Tags::from_env(
800 &Platform::new(
801 Os::Macos {
802 major: 14,
803 minor: 0,
804 },
805 Arch::Aarch64,
806 ),
807 (3, 11),
808 "cpython",
809 (3, 11),
810 TagsOptions::default(),
811 )
812 .expect("valid tags")
813 });
814
815 static DARWIN_MARKERS: LazyLock<ResolverMarkerEnvironment> =
816 LazyLock::new(|| ResolverMarkerEnvironment::from(marker_environment("darwin", "Darwin")));
817
818 static LINUX_MARKERS: LazyLock<ResolverMarkerEnvironment> =
819 LazyLock::new(|| ResolverMarkerEnvironment::from(marker_environment("linux", "Linux")));
820
821 fn marker_environment(
822 sys_platform: &'static str,
823 platform_system: &'static str,
824 ) -> MarkerEnvironment {
825 MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
826 implementation_name: "cpython",
827 implementation_version: "3.11.5",
828 os_name: "posix",
829 platform_machine: "arm64",
830 platform_python_implementation: "CPython",
831 platform_release: "23.0.0",
832 platform_system,
833 platform_version: "test",
834 python_full_version: "3.11.5",
835 python_version: "3.11",
836 sys_platform,
837 })
838 .expect("valid marker environment")
839 }
840
841 fn lock() -> Lock {
842 toml::from_str(
843 r#"
844version = 1
845revision = 3
846requires-python = ">=3.11"
847resolution-markers = [
848 "sys_platform == 'darwin'",
849 "sys_platform != 'darwin'",
850]
851
852[manifest]
853requirements = [{ name = "unrelated" }]
854
855[[package]]
856name = "dev-dependency"
857version = "1.0.0"
858source = { registry = "https://example.com/simple" }
859sdist = { url = "https://example.com/dev_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
860
861[[package]]
862name = "forked"
863version = "1.0.0"
864source = { registry = "https://example.com/simple" }
865resolution-markers = ["sys_platform == 'darwin'"]
866sdist = { url = "https://example.com/forked-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
867
868[[package]]
869name = "forked"
870version = "2.0.0"
871source = { registry = "https://example.com/simple" }
872resolution-markers = ["sys_platform != 'darwin'"]
873sdist = { url = "https://example.com/forked-2.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
874
875[[package]]
876name = "optional-dependency"
877version = "1.0.0"
878source = { registry = "https://example.com/simple" }
879sdist = { url = "https://example.com/optional_dependency-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
880
881[[package]]
882name = "root-a"
883version = "1.0.0"
884source = { registry = "https://example.com/simple" }
885dependencies = [
886 { name = "forked", version = "1.0.0", source = { registry = "https://example.com/simple" }, marker = "sys_platform == 'darwin'" },
887 { name = "forked", version = "2.0.0", source = { registry = "https://example.com/simple" }, marker = "sys_platform != 'darwin'" },
888 { name = "shared" },
889]
890sdist = { url = "https://example.com/root_a-1.0.0.tar.gz", hash = "sha256:5555555555555555555555555555555555555555555555555555555555555555" }
891
892[package.optional-dependencies]
893feature = [{ name = "optional-dependency" }]
894
895[package.dependency-groups]
896dev = [{ name = "dev-dependency" }]
897
898[package.metadata]
899provides-extras = ["feature"]
900
901[[package]]
902name = "root-b"
903version = "1.0.0"
904source = { registry = "https://example.com/simple" }
905dependencies = [{ name = "shared" }]
906sdist = { url = "https://example.com/root_b-1.0.0.tar.gz", hash = "sha256:6666666666666666666666666666666666666666666666666666666666666666" }
907
908[[package]]
909name = "shared"
910version = "1.0.0"
911source = { registry = "https://example.com/simple" }
912sdist = { url = "https://example.com/shared-1.0.0.tar.gz", hash = "sha256:7777777777777777777777777777777777777777777777777777777777777777" }
913
914[[package]]
915name = "unrelated"
916version = "1.0.0"
917source = { registry = "https://example.com/simple" }
918sdist = { url = "https://example.com/unrelated-1.0.0.tar.gz", hash = "sha256:8888888888888888888888888888888888888888888888888888888888888888" }
919"#,
920 )
921 .expect("valid lock")
922 }
923
924 fn conflict_lock() -> Lock {
925 toml::from_str(
926 r#"
927version = 1
928revision = 3
929requires-python = ">=3.11"
930conflicts = [
931 [
932 { package = "tool", extra = "cpu" },
933 { package = "tool", extra = "gpu" },
934 ],
935 [
936 { package = "project", extra = "foo" },
937 { package = "project", extra = "bar" },
938 ],
939]
940
941[[package]]
942name = "contextual-dependency"
943version = "1.0.0"
944source = { registry = "https://example.com/simple" }
945sdist = { url = "https://example.com/contextual_dependency-1.0.0.tar.gz", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111" }
946
947[[package]]
948name = "contextual-tool"
949version = "1.0.0"
950source = { registry = "https://example.com/simple" }
951dependencies = [
952 { name = "contextual-dependency", marker = "sys_platform == 'linux' or (sys_platform == 'darwin' and extra == 'extra-7-project-foo')" },
953]
954sdist = { url = "https://example.com/contextual_tool-1.0.0.tar.gz", hash = "sha256:2222222222222222222222222222222222222222222222222222222222222222" }
955
956[[package]]
957name = "cpu-backend"
958version = "1.0.0"
959source = { registry = "https://example.com/simple" }
960sdist = { url = "https://example.com/cpu_backend-1.0.0.tar.gz", hash = "sha256:3333333333333333333333333333333333333333333333333333333333333333" }
961
962[[package]]
963name = "gpu-backend"
964version = "1.0.0"
965source = { registry = "https://example.com/simple" }
966sdist = { url = "https://example.com/gpu_backend-1.0.0.tar.gz", hash = "sha256:4444444444444444444444444444444444444444444444444444444444444444" }
967
968[[package]]
969name = "project"
970version = "1.0.0"
971source = { registry = "https://example.com/simple" }
972sdist = { url = "https://example.com/project-1.0.0.tar.gz", hash = "sha256:5555555555555555555555555555555555555555555555555555555555555555" }
973
974[package.optional-dependencies]
975foo = []
976bar = []
977
978[package.metadata]
979provides-extras = ["foo", "bar"]
980
981[[package]]
982name = "runtime"
983version = "1.0.0"
984source = { registry = "https://example.com/simple" }
985dependencies = [
986 { name = "cpu-backend", marker = "extra == 'extra-4-tool-cpu'" },
987 { name = "gpu-backend", marker = "extra == 'extra-4-tool-gpu'" },
988]
989sdist = { url = "https://example.com/runtime-1.0.0.tar.gz", hash = "sha256:6666666666666666666666666666666666666666666666666666666666666666" }
990
991[[package]]
992name = "tool"
993version = "1.0.0"
994source = { registry = "https://example.com/simple" }
995dependencies = [{ name = "runtime" }]
996sdist = { url = "https://example.com/tool-1.0.0.tar.gz", hash = "sha256:7777777777777777777777777777777777777777777777777777777777777777" }
997
998[package.optional-dependencies]
999cpu = []
1000gpu = []
1001
1002[package.metadata]
1003provides-extras = ["cpu", "gpu"]
1004"#,
1005 )
1006 .expect("valid lock")
1007 }
1008
1009 fn package<'lock>(lock: &'lock Lock, name: &str, version: &str) -> &'lock Package {
1010 lock.packages()
1011 .iter()
1012 .find(|package| {
1013 package.name().as_ref() == name
1014 && package
1015 .version()
1016 .is_some_and(|package_version| package_version.to_string() == version)
1017 })
1018 .expect("locked package")
1019 }
1020
1021 fn materialize(
1022 lock: &Lock,
1023 roots: &[&Package],
1024 marker_env: &ResolverMarkerEnvironment,
1025 ) -> Resolution {
1026 let extras = ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default());
1027 let groups = DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default());
1028 lock.to_resolution(
1029 Path::new("."),
1030 roots.iter().copied(),
1031 None,
1032 marker_env,
1033 &TAGS,
1034 &extras,
1035 &groups,
1036 &BuildOptions::default(),
1037 &InstallOptions::default(),
1038 )
1039 .expect("valid resolution")
1040 }
1041
1042 fn materialize_with_extras(
1043 lock: &Lock,
1044 roots: &[&Package],
1045 marker_env: &ResolverMarkerEnvironment,
1046 extras: &ExtrasSpecification,
1047 ) -> Result<Resolution, LockError> {
1048 let extras = extras.with_defaults(DefaultExtras::default());
1049 let groups = DependencyGroupsWithDefaults::none();
1050 lock.to_resolution(
1051 Path::new("."),
1052 roots.iter().copied(),
1053 None,
1054 marker_env,
1055 &TAGS,
1056 &extras,
1057 &groups,
1058 &BuildOptions::default(),
1059 &InstallOptions::default(),
1060 )
1061 }
1062
1063 struct OverridingInstallable<'lock> {
1064 lock: &'lock Lock,
1065 root_name: &'lock PackageName,
1066 package_to_node_calls: Cell<usize>,
1067 }
1068
1069 impl<'lock> Installable<'lock> for OverridingInstallable<'lock> {
1070 fn install_path(&self) -> &'lock Path {
1071 Path::new(".")
1072 }
1073
1074 fn lock(&self) -> &'lock Lock {
1075 self.lock
1076 }
1077
1078 fn roots(&self) -> impl Iterator<Item = &PackageName> {
1079 std::iter::once(self.root_name)
1080 }
1081
1082 fn project_name(&self) -> Option<&PackageName> {
1083 None
1084 }
1085
1086 fn package_to_node(
1087 &self,
1088 _package: &Package,
1089 _tags: &Tags,
1090 _build_options: &BuildOptions,
1091 _install_options: &InstallOptions,
1092 _marker_env: &ResolverMarkerEnvironment,
1093 ) -> Result<Node, LockError> {
1094 self.package_to_node_calls
1095 .set(self.package_to_node_calls.get() + 1);
1096 Ok(Node::Root)
1097 }
1098 }
1099
1100 fn graph_snapshot(resolution: &Resolution) -> (Vec<String>, Vec<String>) {
1101 let graph = resolution.graph();
1102 let labels = graph
1103 .node_weights()
1104 .map(|node| match node {
1105 Node::Root => "root".to_string(),
1106 Node::Dist {
1107 dist,
1108 hashes,
1109 install,
1110 } => format!(
1111 "{}=={} (install: {install}, hashes: {})",
1112 dist.name(),
1113 dist.version()
1114 .map(ToString::to_string)
1115 .unwrap_or_else(|| "<dynamic>".to_string()),
1116 hashes.iter().map(ToString::to_string).join(", ")
1117 ),
1118 })
1119 .collect::<Vec<_>>();
1120 let mut nodes = labels.clone();
1121 nodes.sort_unstable();
1122 let mut edges = graph
1123 .edge_references()
1124 .map(|edge| {
1125 format!(
1126 "{} --{:?}--> {}",
1127 labels[edge.source().index()],
1128 edge.weight(),
1129 labels[edge.target().index()]
1130 )
1131 })
1132 .collect::<Vec<_>>();
1133 edges.sort_unstable();
1134 (nodes, edges)
1135 }
1136
1137 #[test]
1138 fn materializes_multiple_concrete_roots_with_shared_dependencies() {
1139 let lock = lock();
1140 let resolution = materialize(
1141 &lock,
1142 &[
1143 package(&lock, "root-a", "1.0.0"),
1144 package(&lock, "root-b", "1.0.0"),
1145 ],
1146 &DARWIN_MARKERS,
1147 );
1148
1149 insta::with_settings!({
1150 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1151 }, {
1152 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1153 (
1154 [
1155 "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1156 "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1157 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1158 "root",
1159 "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1160 "root-b==1.0.0 (install: true, hashes: sha256:[HASH])",
1161 "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1162 ],
1163 [
1164 "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1165 "root --Prod--> root-b==1.0.0 (install: true, hashes: sha256:[HASH])",
1166 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1167 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1168 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1169 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1170 "root-b==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1171 ],
1172 )
1173 "#);
1174 });
1175 }
1176
1177 #[test]
1178 fn materializes_the_selected_universal_lock_fork() {
1179 let lock = lock();
1180 let root = package(&lock, "root-a", "1.0.0");
1181 let darwin = materialize(&lock, &[root], &DARWIN_MARKERS);
1182 let linux = materialize(&lock, &[root], &LINUX_MARKERS);
1183 let concrete_fork =
1184 materialize(&lock, &[package(&lock, "forked", "1.0.0")], &DARWIN_MARKERS);
1185
1186 insta::with_settings!({
1187 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1188 }, {
1189 insta::assert_debug_snapshot!(graph_snapshot(&darwin), @r#"
1190 (
1191 [
1192 "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1193 "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1194 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1195 "root",
1196 "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1197 "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1198 ],
1199 [
1200 "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1201 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1202 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1203 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1204 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1205 ],
1206 )
1207 "#);
1208 insta::assert_debug_snapshot!(graph_snapshot(&linux), @r#"
1209 (
1210 [
1211 "dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1212 "forked==2.0.0 (install: true, hashes: sha256:[HASH])",
1213 "optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1214 "root",
1215 "root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1216 "shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1217 ],
1218 [
1219 "root --Prod--> root-a==1.0.0 (install: true, hashes: sha256:[HASH])",
1220 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Dev(GroupName(\"dev\"))--> dev-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1221 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Optional(ExtraName(\"feature\"))--> optional-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1222 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> forked==2.0.0 (install: true, hashes: sha256:[HASH])",
1223 "root-a==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> shared==1.0.0 (install: true, hashes: sha256:[HASH])",
1224 ],
1225 )
1226 "#);
1227 insta::assert_debug_snapshot!(graph_snapshot(&concrete_fork), @r#"
1228 (
1229 [
1230 "forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1231 "root",
1232 ],
1233 [
1234 "root --Prod--> forked==1.0.0 (install: true, hashes: sha256:[HASH])",
1235 ],
1236 )
1237 "#);
1238 });
1239 }
1240
1241 #[test]
1242 fn materializes_conflicting_extras_within_the_synthetic_root() {
1243 let lock = conflict_lock();
1244 let extras =
1245 ExtrasSpecification::from_extra(vec!["cpu".parse().expect("valid extra name")]);
1246 let resolution = materialize_with_extras(
1247 &lock,
1248 &[package(&lock, "tool", "1.0.0")],
1249 &DARWIN_MARKERS,
1250 &extras,
1251 )
1252 .expect("conflict markers are resolved within the subgraph");
1253
1254 insta::with_settings!({
1255 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1256 }, {
1257 insta::assert_debug_snapshot!(graph_snapshot(&resolution), @r#"
1258 (
1259 [
1260 "cpu-backend==1.0.0 (install: true, hashes: sha256:[HASH])",
1261 "root",
1262 "runtime==1.0.0 (install: true, hashes: sha256:[HASH])",
1263 "tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1264 ],
1265 [
1266 "root --Prod--> tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1267 "runtime==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> cpu-backend==1.0.0 (install: true, hashes: sha256:[HASH])",
1268 "tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> runtime==1.0.0 (install: true, hashes: sha256:[HASH])",
1269 ],
1270 )
1271 "#);
1272 });
1273 }
1274
1275 #[test]
1276 fn rejects_conflicts_outside_the_synthetic_root() {
1277 let lock = conflict_lock();
1278 let root = package(&lock, "contextual-tool", "1.0.0");
1279 let extras = ExtrasSpecification::default();
1280
1281 let error = materialize_with_extras(&lock, &[root], &DARWIN_MARKERS, &extras)
1282 .expect_err("Darwin dependency depends on the project extra");
1283 let error = error.to_string();
1284 let error = anstream::adapter::strip_str(&error);
1285 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");
1286
1287 let linux = materialize_with_extras(&lock, &[root], &LINUX_MARKERS, &extras)
1288 .expect("the dependency is unconditional on Linux");
1289 insta::with_settings!({
1290 filters => [(r"sha256:[0-9a-f]{64}", "sha256:[HASH]")],
1291 }, {
1292 insta::assert_debug_snapshot!(graph_snapshot(&linux), @r#"
1293 (
1294 [
1295 "contextual-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1296 "contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1297 "root",
1298 ],
1299 [
1300 "contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH]) --Prod--> contextual-dependency==1.0.0 (install: true, hashes: sha256:[HASH])",
1301 "root --Prod--> contextual-tool==1.0.0 (install: true, hashes: sha256:[HASH])",
1302 ],
1303 )
1304 "#);
1305 });
1306 }
1307
1308 #[test]
1309 fn installable_to_resolution_preserves_node_overrides() {
1310 let mut lock = lock();
1311 lock.manifest.requirements.clear();
1312 let target = OverridingInstallable {
1313 root_name: package(&lock, "root-a", "1.0.0").name(),
1314 lock: &lock,
1315 package_to_node_calls: Cell::new(0),
1316 };
1317 let extras = ExtrasSpecification::from_all_extras().with_defaults(DefaultExtras::default());
1318 let groups = DependencyGroups::from_all_groups().with_defaults(DefaultGroups::default());
1319
1320 target
1321 .to_resolution(
1322 &DARWIN_MARKERS,
1323 &TAGS,
1324 &extras,
1325 &groups,
1326 &BuildOptions::default(),
1327 &InstallOptions::default(),
1328 )
1329 .expect("valid resolution");
1330
1331 assert!(target.package_to_node_calls.get() > 0);
1332 }
1333}