1use std::borrow::Cow;
2use std::collections::{BTreeMap, BTreeSet, VecDeque};
3use std::error::Error;
4use std::fmt::{Debug, Display, Formatter};
5use std::io;
6use std::path::{Path, PathBuf};
7use std::str::FromStr;
8use std::sync::{Arc, LazyLock};
9
10use itertools::Itertools;
11use jiff::Timestamp;
12use owo_colors::OwoColorize;
13use petgraph::graph::NodeIndex;
14use petgraph::visit::EdgeRef;
15use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
16use serde::Serializer;
17use toml_edit::{Array, ArrayOfTables, InlineTable, Item, Table, Value, value};
18use tracing::{debug, instrument, trace};
19use url::Url;
20
21use uv_cache_key::RepositoryUrl;
22use uv_configuration::{
23 BuildOptions, Constraints, DependencyGroupsWithDefaults, ExtrasSpecificationWithDefaults,
24 InstallTarget,
25};
26use uv_distribution::{DistributionDatabase, FlatRequiresDist};
27use uv_distribution_filename::{
28 BuildTag, DistExtension, ExtensionError, SourceDistExtension, WheelFilename,
29};
30use uv_distribution_types::{
31 BuiltDist, DependencyMetadata, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist,
32 Dist, FileLocation, GitSourceDist, Identifier, IndexLocations, IndexMetadata, IndexUrl, Name,
33 PathBuiltDist, PathSourceDist, RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist,
34 RemoteSource, Requirement, RequirementSource, RequiresPython, ResolvedDist,
35 SimplifiedMarkerTree, StaticMetadata, ToUrlError, UrlString,
36};
37use uv_fs::{PortablePath, PortablePathBuf, Simplified, normalize_path, try_relative_to_if};
38use uv_git::{RepositoryReference, ResolvedRepositoryReference};
39use uv_git_types::{GitLfs, GitOid, GitReference, GitUrl, GitUrlParseError};
40use uv_normalize::{ExtraName, GroupName, PackageName};
41use uv_pep440::Version;
42use uv_pep508::{
43 MarkerEnvironment, MarkerTree, Scheme, VerbatimUrl, VerbatimUrlError, split_scheme,
44};
45use uv_platform_tags::{
46 AbiTag, IncompatibleTag, LanguageTag, PlatformTag, TagCompatibility, TagPriority, Tags,
47};
48use uv_pypi_types::{
49 ConflictKind, Conflicts, HashAlgorithm, HashDigest, HashDigests, Hashes, ParsedArchiveUrl,
50 ParsedGitUrl, PyProjectToml,
51};
52use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};
53use uv_small_str::SmallString;
54use uv_types::{BuildContext, HashStrategy};
55use uv_workspace::{Editability, WorkspaceMember};
56
57use crate::fork_strategy::ForkStrategy;
58pub(crate) use crate::lock::export::PylockTomlPackage;
59pub use crate::lock::export::RequirementsTxtExport;
60pub use crate::lock::export::{Metadata, PylockToml, PylockTomlErrorKind, cyclonedx_json};
61pub use crate::lock::installable::Installable;
62pub use crate::lock::map::PackageMap;
63pub use crate::lock::tree::TreeDisplay;
64use crate::resolution::{AnnotatedDist, ResolutionGraphNode};
65use crate::universal_marker::{ConflictMarker, UniversalMarker};
66use crate::{
67 ExcludeNewer, ExcludeNewerOverride, ExcludeNewerPackage, ExcludeNewerSpan, ExcludeNewerValue,
68 InMemoryIndex, MetadataResponse, PrereleaseMode, ResolutionMode, ResolverOutput,
69};
70
71mod export;
72mod installable;
73mod map;
74mod tree;
75
76pub const VERSION: u32 = 1;
78
79const REVISION: u32 = 3;
81
82static LINUX_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
83 let pep508 = MarkerTree::from_str("os_name == 'posix' and sys_platform == 'linux'").unwrap();
84 UniversalMarker::new(pep508, ConflictMarker::TRUE)
85});
86static WINDOWS_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
87 let pep508 = MarkerTree::from_str("os_name == 'nt' and sys_platform == 'win32'").unwrap();
88 UniversalMarker::new(pep508, ConflictMarker::TRUE)
89});
90static MAC_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
91 let pep508 = MarkerTree::from_str("os_name == 'posix' and sys_platform == 'darwin'").unwrap();
92 UniversalMarker::new(pep508, ConflictMarker::TRUE)
93});
94static ANDROID_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
95 let pep508 = MarkerTree::from_str("sys_platform == 'android'").unwrap();
96 UniversalMarker::new(pep508, ConflictMarker::TRUE)
97});
98static ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
99 let pep508 =
100 MarkerTree::from_str("platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ARM64'")
101 .unwrap();
102 UniversalMarker::new(pep508, ConflictMarker::TRUE)
103});
104static X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
105 let pep508 =
106 MarkerTree::from_str("platform_machine == 'x86_64' or platform_machine == 'amd64' or platform_machine == 'AMD64'")
107 .unwrap();
108 UniversalMarker::new(pep508, ConflictMarker::TRUE)
109});
110static X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
111 let pep508 = MarkerTree::from_str(
112 "platform_machine == 'i686' or platform_machine == 'i386' or platform_machine == 'win32' or platform_machine == 'x86'",
113 )
114 .unwrap();
115 UniversalMarker::new(pep508, ConflictMarker::TRUE)
116});
117static PPC64LE_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
118 let pep508 = MarkerTree::from_str("platform_machine == 'ppc64le'").unwrap();
119 UniversalMarker::new(pep508, ConflictMarker::TRUE)
120});
121static PPC64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
122 let pep508 = MarkerTree::from_str("platform_machine == 'ppc64'").unwrap();
123 UniversalMarker::new(pep508, ConflictMarker::TRUE)
124});
125static S390X_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
126 let pep508 = MarkerTree::from_str("platform_machine == 's390x'").unwrap();
127 UniversalMarker::new(pep508, ConflictMarker::TRUE)
128});
129static RISCV64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
130 let pep508 = MarkerTree::from_str("platform_machine == 'riscv64'").unwrap();
131 UniversalMarker::new(pep508, ConflictMarker::TRUE)
132});
133static LOONGARCH64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
134 let pep508 = MarkerTree::from_str("platform_machine == 'loongarch64'").unwrap();
135 UniversalMarker::new(pep508, ConflictMarker::TRUE)
136});
137static ARMV7L_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
138 let pep508 =
139 MarkerTree::from_str("platform_machine == 'armv7l' or platform_machine == 'armv8l'")
140 .unwrap();
141 UniversalMarker::new(pep508, ConflictMarker::TRUE)
142});
143static ARMV6L_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
144 let pep508 = MarkerTree::from_str("platform_machine == 'armv6l'").unwrap();
145 UniversalMarker::new(pep508, ConflictMarker::TRUE)
146});
147static LINUX_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
148 let mut marker = *LINUX_MARKERS;
149 marker.and(*ARM_MARKERS);
150 marker
151});
152static LINUX_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
153 let mut marker = *LINUX_MARKERS;
154 marker.and(*X86_64_MARKERS);
155 marker
156});
157static LINUX_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
158 let mut marker = *LINUX_MARKERS;
159 marker.and(*X86_MARKERS);
160 marker
161});
162static LINUX_PPC64LE_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
163 let mut marker = *LINUX_MARKERS;
164 marker.and(*PPC64LE_MARKERS);
165 marker
166});
167static LINUX_PPC64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
168 let mut marker = *LINUX_MARKERS;
169 marker.and(*PPC64_MARKERS);
170 marker
171});
172static LINUX_S390X_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
173 let mut marker = *LINUX_MARKERS;
174 marker.and(*S390X_MARKERS);
175 marker
176});
177static LINUX_RISCV64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
178 let mut marker = *LINUX_MARKERS;
179 marker.and(*RISCV64_MARKERS);
180 marker
181});
182static LINUX_LOONGARCH64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
183 let mut marker = *LINUX_MARKERS;
184 marker.and(*LOONGARCH64_MARKERS);
185 marker
186});
187static LINUX_ARMV7L_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
188 let mut marker = *LINUX_MARKERS;
189 marker.and(*ARMV7L_MARKERS);
190 marker
191});
192static LINUX_ARMV6L_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
193 let mut marker = *LINUX_MARKERS;
194 marker.and(*ARMV6L_MARKERS);
195 marker
196});
197static WINDOWS_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
198 let mut marker = *WINDOWS_MARKERS;
199 marker.and(*ARM_MARKERS);
200 marker
201});
202static WINDOWS_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
203 let mut marker = *WINDOWS_MARKERS;
204 marker.and(*X86_64_MARKERS);
205 marker
206});
207static WINDOWS_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
208 let mut marker = *WINDOWS_MARKERS;
209 marker.and(*X86_MARKERS);
210 marker
211});
212static MAC_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
213 let mut marker = *MAC_MARKERS;
214 marker.and(*ARM_MARKERS);
215 marker
216});
217static MAC_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
218 let mut marker = *MAC_MARKERS;
219 marker.and(*X86_64_MARKERS);
220 marker
221});
222static MAC_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
223 let mut marker = *MAC_MARKERS;
224 marker.and(*X86_MARKERS);
225 marker
226});
227static ANDROID_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
228 let mut marker = *ANDROID_MARKERS;
229 marker.and(*ARM_MARKERS);
230 marker
231});
232static ANDROID_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
233 let mut marker = *ANDROID_MARKERS;
234 marker.and(*X86_64_MARKERS);
235 marker
236});
237static ANDROID_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
238 let mut marker = *ANDROID_MARKERS;
239 marker.and(*X86_MARKERS);
240 marker
241});
242
243pub(crate) struct HashedDist {
248 pub(crate) dist: Dist,
249 pub(crate) hashes: HashDigests,
250}
251
252#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
253#[serde(try_from = "LockWire")]
254pub struct Lock {
255 version: u32,
264 revision: u32,
270 fork_markers: Vec<UniversalMarker>,
273 conflicts: Conflicts,
275 supported_environments: Vec<MarkerTree>,
277 required_environments: Vec<MarkerTree>,
279 requires_python: RequiresPython,
281 options: ResolverOptions,
283 packages: Vec<Package>,
285 by_id: FxHashMap<PackageId, usize>,
297 manifest: ResolverManifest,
299}
300
301impl Lock {
302 pub fn from_resolution(
304 resolution: &ResolverOutput,
305 root: &Path,
306 supported_environments: Vec<MarkerTree>,
307 ) -> Result<Self, LockError> {
308 let mut packages = BTreeMap::new();
309 let requires_python = resolution.requires_python.clone();
310 let supported_environments = supported_environments
311 .into_iter()
312 .map(|marker| requires_python.complexify_markers(marker))
313 .collect::<Vec<_>>();
314 let supported_environments_marker = if supported_environments.is_empty() {
315 None
316 } else {
317 let mut combined = MarkerTree::FALSE;
318 for marker in &supported_environments {
319 combined.or(*marker);
320 }
321 Some(UniversalMarker::new(combined, ConflictMarker::TRUE))
322 };
323
324 let mut seen = FxHashSet::default();
326 let mut duplicates = FxHashSet::default();
327 for node_index in resolution.graph.node_indices() {
328 let ResolutionGraphNode::Dist(dist) = &resolution.graph[node_index] else {
329 continue;
330 };
331 if !dist.is_base() {
332 continue;
333 }
334 if !seen.insert(dist.name()) {
335 duplicates.insert(dist.name());
336 }
337 }
338
339 for node_index in resolution.graph.node_indices() {
341 let ResolutionGraphNode::Dist(dist) = &resolution.graph[node_index] else {
342 continue;
343 };
344 if !dist.is_base() {
345 continue;
346 }
347
348 let fork_markers = if duplicates.contains(dist.name()) {
354 let fork_markers = resolution
355 .fork_markers
356 .iter()
357 .filter(|fork_markers| !fork_markers.is_disjoint(dist.marker))
358 .copied()
359 .collect::<Vec<_>>();
360 canonicalize_universal_markers(&fork_markers, &requires_python)
361 } else {
362 vec![]
363 };
364
365 let mut package = Package::from_annotated_dist(dist, fork_markers, root)?;
366 let mut wheel_marker = dist.marker;
367 if let Some(supported_environments_marker) = supported_environments_marker {
368 wheel_marker.and(supported_environments_marker);
369 }
370 let wheels = &mut package.wheels;
371 wheels.retain(|wheel| {
372 !is_wheel_unreachable_for_marker(
373 &wheel.filename,
374 &requires_python,
375 &wheel_marker,
376 None,
377 )
378 });
379
380 for edge in resolution.graph.edges(node_index) {
382 let ResolutionGraphNode::Dist(dependency_dist) = &resolution.graph[edge.target()]
383 else {
384 continue;
385 };
386 let marker = *edge.weight();
387 package.add_dependency(&requires_python, dependency_dist, marker, root)?;
388 }
389
390 let id = package.id.clone();
391 if let Some(locked_dist) = packages.insert(id, package) {
392 return Err(LockErrorKind::DuplicatePackage {
393 id: locked_dist.id.clone(),
394 }
395 .into());
396 }
397 }
398
399 for node_index in resolution.graph.node_indices() {
401 let ResolutionGraphNode::Dist(dist) = &resolution.graph[node_index] else {
402 continue;
403 };
404 if let Some(extra) = dist.extra.as_ref() {
405 let id = PackageId::from_annotated_dist(dist, root)?;
406 let Some(package) = packages.get_mut(&id) else {
407 return Err(LockErrorKind::MissingExtraBase {
408 id,
409 extra: extra.clone(),
410 }
411 .into());
412 };
413 for edge in resolution.graph.edges(node_index) {
414 let ResolutionGraphNode::Dist(dependency_dist) =
415 &resolution.graph[edge.target()]
416 else {
417 continue;
418 };
419 let marker = *edge.weight();
420 package.add_optional_dependency(
421 &requires_python,
422 extra.clone(),
423 dependency_dist,
424 marker,
425 root,
426 )?;
427 }
428 }
429 if let Some(group) = dist.group.as_ref() {
430 let id = PackageId::from_annotated_dist(dist, root)?;
431 let Some(package) = packages.get_mut(&id) else {
432 return Err(LockErrorKind::MissingDevBase {
433 id,
434 group: group.clone(),
435 }
436 .into());
437 };
438 for edge in resolution.graph.edges(node_index) {
439 let ResolutionGraphNode::Dist(dependency_dist) =
440 &resolution.graph[edge.target()]
441 else {
442 continue;
443 };
444 let marker = *edge.weight();
445 package.add_group_dependency(
446 &requires_python,
447 group.clone(),
448 dependency_dist,
449 marker,
450 root,
451 )?;
452 }
453 }
454 }
455
456 let packages = packages.into_values().collect();
457
458 let options = ResolverOptions {
459 resolution_mode: resolution.options.resolution_mode,
460 prerelease_mode: resolution.options.prerelease_mode,
461 fork_strategy: resolution.options.fork_strategy,
462 exclude_newer: resolution.options.exclude_newer.clone().into(),
463 };
464 let fork_markers =
469 canonicalize_universal_markers(&resolution.fork_markers, &requires_python);
470 let lock = Self::new(
471 VERSION,
472 REVISION,
473 packages,
474 requires_python,
475 options,
476 ResolverManifest::default(),
477 Conflicts::empty(),
478 supported_environments,
479 vec![],
480 fork_markers,
481 )?;
482 Ok(lock)
483 }
484
485 fn new(
487 version: u32,
488 revision: u32,
489 mut packages: Vec<Package>,
490 requires_python: RequiresPython,
491 options: ResolverOptions,
492 manifest: ResolverManifest,
493 conflicts: Conflicts,
494 supported_environments: Vec<MarkerTree>,
495 required_environments: Vec<MarkerTree>,
496 fork_markers: Vec<UniversalMarker>,
497 ) -> Result<Self, LockError> {
498 for package in &mut packages {
501 package.dependencies.sort();
502 for windows in package.dependencies.windows(2) {
503 let (dep1, dep2) = (&windows[0], &windows[1]);
504 if dep1 == dep2 {
505 return Err(LockErrorKind::DuplicateDependency {
506 id: package.id.clone(),
507 dependency: dep1.clone(),
508 }
509 .into());
510 }
511 }
512
513 for (extra, dependencies) in &mut package.optional_dependencies {
515 dependencies.sort();
516 for windows in dependencies.windows(2) {
517 let (dep1, dep2) = (&windows[0], &windows[1]);
518 if dep1 == dep2 {
519 return Err(LockErrorKind::DuplicateOptionalDependency {
520 id: package.id.clone(),
521 extra: extra.clone(),
522 dependency: dep1.clone(),
523 }
524 .into());
525 }
526 }
527 }
528
529 for (group, dependencies) in &mut package.dependency_groups {
531 dependencies.sort();
532 for windows in dependencies.windows(2) {
533 let (dep1, dep2) = (&windows[0], &windows[1]);
534 if dep1 == dep2 {
535 return Err(LockErrorKind::DuplicateDevDependency {
536 id: package.id.clone(),
537 group: group.clone(),
538 dependency: dep1.clone(),
539 }
540 .into());
541 }
542 }
543 }
544 }
545 packages.sort_by(|dist1, dist2| dist1.id.cmp(&dist2.id));
546
547 let mut by_id = FxHashMap::default();
550 for (i, dist) in packages.iter().enumerate() {
551 if by_id.insert(dist.id.clone(), i).is_some() {
552 return Err(LockErrorKind::DuplicatePackage {
553 id: dist.id.clone(),
554 }
555 .into());
556 }
557 }
558
559 let mut extras_by_id = FxHashMap::default();
561 for dist in &packages {
562 for extra in dist.optional_dependencies.keys() {
563 extras_by_id
564 .entry(dist.id.clone())
565 .or_insert_with(FxHashSet::default)
566 .insert(extra.clone());
567 }
568 }
569
570 for dist in &mut packages {
572 for dep in dist
573 .dependencies
574 .iter_mut()
575 .chain(dist.optional_dependencies.values_mut().flatten())
576 .chain(dist.dependency_groups.values_mut().flatten())
577 {
578 dep.extra.retain(|extra| {
579 extras_by_id
580 .get(&dep.package_id)
581 .is_some_and(|extras| extras.contains(extra))
582 });
583 }
584 }
585
586 for dist in &packages {
590 for dep in &dist.dependencies {
591 if !by_id.contains_key(&dep.package_id) {
592 return Err(LockErrorKind::UnrecognizedDependency {
593 id: dist.id.clone(),
594 dependency: dep.clone(),
595 }
596 .into());
597 }
598 }
599
600 for dependencies in dist.optional_dependencies.values() {
602 for dep in dependencies {
603 if !by_id.contains_key(&dep.package_id) {
604 return Err(LockErrorKind::UnrecognizedDependency {
605 id: dist.id.clone(),
606 dependency: dep.clone(),
607 }
608 .into());
609 }
610 }
611 }
612
613 for dependencies in dist.dependency_groups.values() {
615 for dep in dependencies {
616 if !by_id.contains_key(&dep.package_id) {
617 return Err(LockErrorKind::UnrecognizedDependency {
618 id: dist.id.clone(),
619 dependency: dep.clone(),
620 }
621 .into());
622 }
623 }
624 }
625
626 if let Some(requires_hash) = dist.id.source.requires_hash() {
629 for wheel in &dist.wheels {
630 if requires_hash != wheel.hash.is_some() {
631 return Err(LockErrorKind::Hash {
632 id: dist.id.clone(),
633 artifact_type: "wheel",
634 expected: requires_hash,
635 }
636 .into());
637 }
638 }
639 }
640 }
641 let lock = Self {
642 version,
643 revision,
644 fork_markers,
645 conflicts,
646 supported_environments,
647 required_environments,
648 requires_python,
649 options,
650 packages,
651 by_id,
652 manifest,
653 };
654 Ok(lock)
655 }
656
657 #[must_use]
659 pub fn with_manifest(mut self, manifest: ResolverManifest) -> Self {
660 self.manifest = manifest;
661 self
662 }
663
664 #[must_use]
666 pub fn with_conflicts(mut self, conflicts: Conflicts) -> Self {
667 self.conflicts = conflicts;
668 self
669 }
670
671 #[must_use]
673 pub fn with_supported_environments(mut self, supported_environments: Vec<MarkerTree>) -> Self {
674 self.supported_environments = supported_environments
684 .into_iter()
685 .map(|marker| self.requires_python.complexify_markers(marker))
686 .collect();
687 self
688 }
689
690 #[must_use]
692 pub fn with_required_environments(mut self, required_environments: Vec<MarkerTree>) -> Self {
693 self.required_environments = required_environments
694 .into_iter()
695 .map(|marker| self.requires_python.complexify_markers(marker))
696 .collect();
697 self
698 }
699
700 pub fn supports_provides_extra(&self) -> bool {
702 (self.version(), self.revision()) >= (1, 1)
704 }
705
706 pub fn includes_empty_groups(&self) -> bool {
708 (self.version(), self.revision()) >= (1, 1)
711 }
712
713 pub fn version(&self) -> u32 {
715 self.version
716 }
717
718 pub fn revision(&self) -> u32 {
720 self.revision
721 }
722
723 pub fn len(&self) -> usize {
725 self.packages.len()
726 }
727
728 pub fn is_empty(&self) -> bool {
730 self.packages.is_empty()
731 }
732
733 pub fn packages(&self) -> &[Package] {
735 &self.packages
736 }
737
738 pub fn requires_python(&self) -> &RequiresPython {
740 &self.requires_python
741 }
742
743 pub fn resolution_mode(&self) -> ResolutionMode {
745 self.options.resolution_mode
746 }
747
748 pub fn prerelease_mode(&self) -> PrereleaseMode {
750 self.options.prerelease_mode
751 }
752
753 pub fn fork_strategy(&self) -> ForkStrategy {
755 self.options.fork_strategy
756 }
757
758 pub fn exclude_newer(&self) -> ExcludeNewer {
760 self.options.exclude_newer.clone().into()
763 }
764
765 pub fn conflicts(&self) -> &Conflicts {
767 &self.conflicts
768 }
769
770 pub fn supported_environments(&self) -> &[MarkerTree] {
772 &self.supported_environments
773 }
774
775 pub fn required_environments(&self) -> &[MarkerTree] {
777 &self.required_environments
778 }
779
780 pub fn members(&self) -> &BTreeSet<PackageName> {
782 &self.manifest.members
783 }
784
785 pub fn requirements(&self) -> &BTreeSet<Requirement> {
787 &self.manifest.requirements
788 }
789
790 pub fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
792 &self.manifest.dependency_groups
793 }
794
795 pub fn build_constraints(&self, root: &Path) -> Constraints {
797 Constraints::from_requirements(
798 self.manifest
799 .build_constraints
800 .iter()
801 .cloned()
802 .map(|requirement| requirement.to_absolute(root)),
803 )
804 }
805
806 pub fn packages_for_audit<'lock>(
813 &'lock self,
814 extras: &'lock ExtrasSpecificationWithDefaults,
815 groups: &'lock DependencyGroupsWithDefaults,
816 ) -> Vec<(&'lock PackageName, &'lock Version)> {
817 fn enqueue_dep<'lock>(
819 lock: &'lock Lock,
820 seen: &mut FxHashSet<(&'lock PackageId, Option<&'lock ExtraName>)>,
821 queue: &mut VecDeque<(&'lock Package, Option<&'lock ExtraName>)>,
822 dep: &'lock Dependency,
823 ) {
824 let dep_pkg = lock.find_by_id(&dep.package_id);
825 for maybe_extra in std::iter::once(None).chain(dep.extra.iter().map(Some)) {
826 if seen.insert((&dep.package_id, maybe_extra)) {
827 queue.push_back((dep_pkg, maybe_extra));
828 }
829 }
830 }
831
832 let workspace_member_ids: FxHashSet<&PackageId> = if self.members().is_empty() {
834 self.root().into_iter().map(|package| &package.id).collect()
835 } else {
836 self.packages
837 .iter()
838 .filter(|package| self.members().contains(&package.id.name))
839 .map(|package| &package.id)
840 .collect()
841 };
842
843 let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
845 let mut seen: FxHashSet<(&PackageId, Option<&ExtraName>)> = FxHashSet::default();
846
847 for package in self
850 .packages
851 .iter()
852 .filter(|p| workspace_member_ids.contains(&p.id))
853 {
854 if seen.insert((&package.id, None)) {
855 queue.push_back((package, None));
856 }
857 if groups.prod() {
858 for extra in extras.extra_names(package.optional_dependencies.keys()) {
859 if seen.insert((&package.id, Some(extra))) {
860 queue.push_back((package, Some(extra)));
861 }
862 }
863 }
864 }
865
866 for requirement in self.requirements() {
868 for package in self
869 .packages
870 .iter()
871 .filter(|p| p.id.name == requirement.name)
872 {
873 if seen.insert((&package.id, None)) {
874 queue.push_back((package, None));
875 }
876 for extra in &*requirement.extras {
877 if seen.insert((&package.id, Some(extra))) {
878 queue.push_back((package, Some(extra)));
879 }
880 }
881 }
882 }
883
884 for (group, requirements) in self.dependency_groups() {
887 if !groups.contains(group) {
888 continue;
889 }
890 for requirement in requirements {
891 for package in self
892 .packages
893 .iter()
894 .filter(|p| p.id.name == requirement.name)
895 {
896 if seen.insert((&package.id, None)) {
897 queue.push_back((package, None));
898 }
899 for extra in &*requirement.extras {
900 if seen.insert((&package.id, Some(extra))) {
901 queue.push_back((package, Some(extra)));
902 }
903 }
904 }
905 }
906 }
907
908 let mut auditable: BTreeSet<(&PackageName, &Version)> = BTreeSet::default();
909
910 while let Some((package, extra)) = queue.pop_front() {
911 let is_member = workspace_member_ids.contains(&package.id);
912
913 if !is_member {
915 if let Some(version) = package.version() {
916 auditable.insert((package.name(), version));
917 } else {
918 trace!(
919 "Skipping audit for `{}` because it has no version information",
920 package.name()
921 );
922 }
923 }
924
925 if is_member && extra.is_none() {
927 for dep in package
928 .dependency_groups
929 .iter()
930 .filter(|(group, _)| groups.contains(group))
931 .flat_map(|(_, deps)| deps)
932 {
933 enqueue_dep(self, &mut seen, &mut queue, dep);
934 }
935 }
936
937 let dependencies: &[Dependency] = match extra {
940 Some(extra) => package
941 .optional_dependencies
942 .get(extra)
943 .map(Vec::as_slice)
944 .unwrap_or_default(),
945 None if is_member && !groups.prod() => &[],
946 None => &package.dependencies,
947 };
948
949 for dep in dependencies {
950 enqueue_dep(self, &mut seen, &mut queue, dep);
951 }
952 }
953
954 auditable.into_iter().collect()
955 }
956
957 pub fn root(&self) -> Option<&Package> {
959 self.packages.iter().find(|package| {
960 let (Source::Editable(path) | Source::Virtual(path)) = &package.id.source else {
961 return false;
962 };
963 path.as_ref() == Path::new("")
964 })
965 }
966
967 pub fn simplified_supported_environments(&self) -> Vec<MarkerTree> {
977 self.supported_environments()
978 .iter()
979 .copied()
980 .map(|marker| self.simplify_environment(marker))
981 .collect()
982 }
983
984 pub fn simplified_required_environments(&self) -> Vec<MarkerTree> {
987 self.required_environments()
988 .iter()
989 .copied()
990 .map(|marker| self.simplify_environment(marker))
991 .collect()
992 }
993
994 pub fn simplify_environment(&self, marker: MarkerTree) -> MarkerTree {
997 self.requires_python.simplify_markers(marker)
998 }
999
1000 pub fn fork_markers(&self) -> &[UniversalMarker] {
1003 self.fork_markers.as_slice()
1004 }
1005
1006 pub fn check_marker_coverage(&self) -> Result<(), (MarkerTree, MarkerTree)> {
1010 let fork_markers_union = if self.fork_markers().is_empty() {
1011 self.requires_python.to_marker_tree()
1012 } else {
1013 let mut fork_markers_union = MarkerTree::FALSE;
1014 for fork_marker in self.fork_markers() {
1015 fork_markers_union.or(fork_marker.pep508());
1016 }
1017 fork_markers_union
1018 };
1019 let mut environments_union = if !self.supported_environments.is_empty() {
1020 let mut environments_union = MarkerTree::FALSE;
1021 for fork_marker in &self.supported_environments {
1022 environments_union.or(*fork_marker);
1023 }
1024 environments_union
1025 } else {
1026 MarkerTree::TRUE
1027 };
1028 environments_union.and(self.requires_python.to_marker_tree());
1030 if fork_markers_union.negate().is_disjoint(environments_union) {
1031 Ok(())
1032 } else {
1033 Err((fork_markers_union, environments_union))
1034 }
1035 }
1036
1037 pub fn requires_python_coverage(
1047 &self,
1048 new_requires_python: &RequiresPython,
1049 ) -> Result<(), (MarkerTree, MarkerTree)> {
1050 let fork_markers_union = if self.fork_markers().is_empty() {
1051 self.requires_python.to_marker_tree()
1052 } else {
1053 let mut fork_markers_union = MarkerTree::FALSE;
1054 for fork_marker in self.fork_markers() {
1055 fork_markers_union.or(fork_marker.pep508());
1056 }
1057 fork_markers_union
1058 };
1059 let new_requires_python = new_requires_python.to_marker_tree();
1060 if fork_markers_union.is_disjoint(new_requires_python) {
1061 Err((fork_markers_union, new_requires_python))
1062 } else {
1063 Ok(())
1064 }
1065 }
1066
1067 pub fn to_toml(&self) -> Result<String, toml_edit::ser::Error> {
1069 debug_assert!(self.check_marker_coverage().is_ok());
1072
1073 let mut doc = toml_edit::DocumentMut::new();
1076 doc.insert("version", value(i64::from(self.version)));
1077
1078 if self.revision > 0 {
1079 doc.insert("revision", value(i64::from(self.revision)));
1080 }
1081
1082 doc.insert("requires-python", value(self.requires_python.to_string()));
1083
1084 if !self.fork_markers.is_empty() {
1085 let fork_markers = each_element_on_its_line_array(
1086 simplified_universal_markers(&self.fork_markers, &self.requires_python).into_iter(),
1087 );
1088 if !fork_markers.is_empty() {
1089 doc.insert("resolution-markers", value(fork_markers));
1090 }
1091 }
1092
1093 if !self.supported_environments.is_empty() {
1094 let supported_environments = each_element_on_its_line_array(
1095 self.supported_environments
1096 .iter()
1097 .copied()
1098 .map(|marker| SimplifiedMarkerTree::new(&self.requires_python, marker))
1099 .filter_map(SimplifiedMarkerTree::try_to_string),
1100 );
1101 doc.insert("supported-markers", value(supported_environments));
1102 }
1103
1104 if !self.required_environments.is_empty() {
1105 let required_environments = each_element_on_its_line_array(
1106 self.required_environments
1107 .iter()
1108 .copied()
1109 .map(|marker| SimplifiedMarkerTree::new(&self.requires_python, marker))
1110 .filter_map(SimplifiedMarkerTree::try_to_string),
1111 );
1112 doc.insert("required-markers", value(required_environments));
1113 }
1114
1115 if !self.conflicts.is_empty() {
1116 let mut list = Array::new();
1117 for set in self.conflicts.iter() {
1118 list.push(each_element_on_its_line_array(set.iter().map(|item| {
1119 let mut table = InlineTable::new();
1120 table.insert("package", Value::from(item.package().to_string()));
1121 match item.kind() {
1122 ConflictKind::Project => {}
1123 ConflictKind::Extra(extra) => {
1124 table.insert("extra", Value::from(extra.to_string()));
1125 }
1126 ConflictKind::Group(group) => {
1127 table.insert("group", Value::from(group.to_string()));
1128 }
1129 }
1130 table
1131 })));
1132 }
1133 doc.insert("conflicts", value(list));
1134 }
1135
1136 {
1140 let mut options_table = Table::new();
1141
1142 if self.options.resolution_mode != ResolutionMode::default() {
1143 options_table.insert(
1144 "resolution-mode",
1145 value(self.options.resolution_mode.to_string()),
1146 );
1147 }
1148 if self.options.prerelease_mode != PrereleaseMode::default() {
1149 options_table.insert(
1150 "prerelease-mode",
1151 value(self.options.prerelease_mode.to_string()),
1152 );
1153 }
1154 if self.options.fork_strategy != ForkStrategy::default() {
1155 options_table.insert(
1156 "fork-strategy",
1157 value(self.options.fork_strategy.to_string()),
1158 );
1159 }
1160 let exclude_newer = ExcludeNewer::from(self.options.exclude_newer.clone());
1161 if !exclude_newer.is_empty() {
1162 if let Some(global) = &exclude_newer.global {
1164 if let Some(span) = global.span() {
1165 let mut noop = value(ExcludeNewerValue::PLACEHOLDER);
1169 if let Item::Value(ref mut v) = noop {
1170 v.decor_mut().set_suffix(" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.");
1171 }
1172 options_table.insert("exclude-newer", noop);
1173 options_table.insert("exclude-newer-span", value(span.to_string()));
1174 } else {
1175 options_table.insert("exclude-newer", value(global.to_string()));
1176 }
1177 }
1178
1179 if !exclude_newer.package.is_empty() {
1181 let mut package_table = toml_edit::Table::new();
1182 for (name, setting) in &exclude_newer.package {
1183 match setting {
1184 ExcludeNewerOverride::Enabled(exclude_newer_value) => {
1185 if let Some(span) = exclude_newer_value.span() {
1186 let mut inline = toml_edit::InlineTable::new();
1190 inline
1191 .insert("timestamp", ExcludeNewerValue::PLACEHOLDER.into());
1192 inline.insert("span", span.to_string().into());
1193 package_table.insert(name.as_ref(), Item::Value(inline.into()));
1194 } else {
1195 package_table.insert(
1197 name.as_ref(),
1198 value(exclude_newer_value.to_string()),
1199 );
1200 }
1201 }
1202 ExcludeNewerOverride::Disabled => {
1203 package_table.insert(name.as_ref(), value(false));
1204 }
1205 }
1206 }
1207 options_table.insert("exclude-newer-package", Item::Table(package_table));
1208 }
1209 }
1210
1211 if !options_table.is_empty() {
1212 doc.insert("options", Item::Table(options_table));
1213 }
1214 }
1215
1216 {
1218 let mut manifest_table = Table::new();
1219
1220 if !self.manifest.members.is_empty() {
1221 manifest_table.insert(
1222 "members",
1223 value(each_element_on_its_line_array(
1224 self.manifest
1225 .members
1226 .iter()
1227 .map(std::string::ToString::to_string),
1228 )),
1229 );
1230 }
1231
1232 if !self.manifest.requirements.is_empty() {
1233 let requirements = self
1234 .manifest
1235 .requirements
1236 .iter()
1237 .map(|requirement| {
1238 serde::Serialize::serialize(
1239 &requirement,
1240 toml_edit::ser::ValueSerializer::new(),
1241 )
1242 })
1243 .collect::<Result<Vec<_>, _>>()?;
1244 let requirements = match requirements.as_slice() {
1245 [] => Array::new(),
1246 [requirement] => Array::from_iter([requirement]),
1247 requirements => each_element_on_its_line_array(requirements.iter()),
1248 };
1249 manifest_table.insert("requirements", value(requirements));
1250 }
1251
1252 if !self.manifest.constraints.is_empty() {
1253 let constraints = self
1254 .manifest
1255 .constraints
1256 .iter()
1257 .map(|requirement| {
1258 serde::Serialize::serialize(
1259 &requirement,
1260 toml_edit::ser::ValueSerializer::new(),
1261 )
1262 })
1263 .collect::<Result<Vec<_>, _>>()?;
1264 let constraints = match constraints.as_slice() {
1265 [] => Array::new(),
1266 [requirement] => Array::from_iter([requirement]),
1267 constraints => each_element_on_its_line_array(constraints.iter()),
1268 };
1269 manifest_table.insert("constraints", value(constraints));
1270 }
1271
1272 if !self.manifest.overrides.is_empty() {
1273 let overrides = self
1274 .manifest
1275 .overrides
1276 .iter()
1277 .map(|requirement| {
1278 serde::Serialize::serialize(
1279 &requirement,
1280 toml_edit::ser::ValueSerializer::new(),
1281 )
1282 })
1283 .collect::<Result<Vec<_>, _>>()?;
1284 let overrides = match overrides.as_slice() {
1285 [] => Array::new(),
1286 [requirement] => Array::from_iter([requirement]),
1287 overrides => each_element_on_its_line_array(overrides.iter()),
1288 };
1289 manifest_table.insert("overrides", value(overrides));
1290 }
1291
1292 if !self.manifest.excludes.is_empty() {
1293 let excludes = self
1294 .manifest
1295 .excludes
1296 .iter()
1297 .map(|name| {
1298 serde::Serialize::serialize(&name, toml_edit::ser::ValueSerializer::new())
1299 })
1300 .collect::<Result<Vec<_>, _>>()?;
1301 let excludes = match excludes.as_slice() {
1302 [] => Array::new(),
1303 [name] => Array::from_iter([name]),
1304 excludes => each_element_on_its_line_array(excludes.iter()),
1305 };
1306 manifest_table.insert("excludes", value(excludes));
1307 }
1308
1309 if !self.manifest.build_constraints.is_empty() {
1310 let build_constraints = self
1311 .manifest
1312 .build_constraints
1313 .iter()
1314 .map(|requirement| {
1315 serde::Serialize::serialize(
1316 &requirement,
1317 toml_edit::ser::ValueSerializer::new(),
1318 )
1319 })
1320 .collect::<Result<Vec<_>, _>>()?;
1321 let build_constraints = match build_constraints.as_slice() {
1322 [] => Array::new(),
1323 [requirement] => Array::from_iter([requirement]),
1324 build_constraints => each_element_on_its_line_array(build_constraints.iter()),
1325 };
1326 manifest_table.insert("build-constraints", value(build_constraints));
1327 }
1328
1329 if !self.manifest.dependency_groups.is_empty() {
1330 let mut dependency_groups = Table::new();
1331 for (extra, requirements) in &self.manifest.dependency_groups {
1332 let requirements = requirements
1333 .iter()
1334 .map(|requirement| {
1335 serde::Serialize::serialize(
1336 &requirement,
1337 toml_edit::ser::ValueSerializer::new(),
1338 )
1339 })
1340 .collect::<Result<Vec<_>, _>>()?;
1341 let requirements = match requirements.as_slice() {
1342 [] => Array::new(),
1343 [requirement] => Array::from_iter([requirement]),
1344 requirements => each_element_on_its_line_array(requirements.iter()),
1345 };
1346 if !requirements.is_empty() {
1347 dependency_groups.insert(extra.as_ref(), value(requirements));
1348 }
1349 }
1350 if !dependency_groups.is_empty() {
1351 manifest_table.insert("dependency-groups", Item::Table(dependency_groups));
1352 }
1353 }
1354
1355 if !self.manifest.dependency_metadata.is_empty() {
1356 let mut tables = ArrayOfTables::new();
1357 for metadata in &self.manifest.dependency_metadata {
1358 let mut table = Table::new();
1359 table.insert("name", value(metadata.name.to_string()));
1360 if let Some(version) = metadata.version.as_ref() {
1361 table.insert("version", value(version.to_string()));
1362 }
1363 if !metadata.requires_dist.is_empty() {
1364 table.insert(
1365 "requires-dist",
1366 value(serde::Serialize::serialize(
1367 &metadata.requires_dist,
1368 toml_edit::ser::ValueSerializer::new(),
1369 )?),
1370 );
1371 }
1372 if let Some(requires_python) = metadata.requires_python.as_ref() {
1373 table.insert("requires-python", value(requires_python.to_string()));
1374 }
1375 if !metadata.provides_extra.is_empty() {
1376 table.insert(
1377 "provides-extras",
1378 value(serde::Serialize::serialize(
1379 &metadata.provides_extra,
1380 toml_edit::ser::ValueSerializer::new(),
1381 )?),
1382 );
1383 }
1384 tables.push(table);
1385 }
1386 manifest_table.insert("dependency-metadata", Item::ArrayOfTables(tables));
1387 }
1388
1389 if !manifest_table.is_empty() {
1390 doc.insert("manifest", Item::Table(manifest_table));
1391 }
1392 }
1393
1394 let mut dist_count_by_name: FxHashMap<PackageName, u64> = FxHashMap::default();
1399 for dist in &self.packages {
1400 *dist_count_by_name.entry(dist.id.name.clone()).or_default() += 1;
1401 }
1402
1403 let mut packages = ArrayOfTables::new();
1404 for dist in &self.packages {
1405 packages.push(dist.to_toml(&self.requires_python, &dist_count_by_name)?);
1406 }
1407
1408 doc.insert("package", Item::ArrayOfTables(packages));
1409 Ok(doc.to_string())
1410 }
1411
1412 pub fn find_by_name(&self, name: &PackageName) -> Result<Option<&Package>, String> {
1416 let mut found_dist = None;
1417 for dist in &self.packages {
1418 if &dist.id.name == name {
1419 if found_dist.is_some() {
1420 return Err(format!("found multiple packages matching `{name}`"));
1421 }
1422 found_dist = Some(dist);
1423 }
1424 }
1425 Ok(found_dist)
1426 }
1427
1428 fn find_by_markers(
1438 &self,
1439 name: &PackageName,
1440 marker_env: &MarkerEnvironment,
1441 ) -> Result<Option<&Package>, String> {
1442 let mut found_dist = None;
1443 for dist in &self.packages {
1444 if &dist.id.name == name {
1445 if dist.fork_markers.is_empty()
1446 || dist
1447 .fork_markers
1448 .iter()
1449 .any(|marker| marker.evaluate_no_extras(marker_env))
1450 {
1451 if found_dist.is_some() {
1452 return Err(format!("found multiple packages matching `{name}`"));
1453 }
1454 found_dist = Some(dist);
1455 }
1456 }
1457 }
1458 Ok(found_dist)
1459 }
1460
1461 fn find_by_id(&self, id: &PackageId) -> &Package {
1462 let index = *self.by_id.get(id).expect("locked package for ID");
1463
1464 (self.packages.get(index).expect("valid index for package")) as _
1465 }
1466
1467 fn satisfies_provides_extra<'lock>(
1469 &self,
1470 provides_extra: Box<[ExtraName]>,
1471 package: &'lock Package,
1472 ) -> SatisfiesResult<'lock> {
1473 if !self.supports_provides_extra() {
1474 return SatisfiesResult::Satisfied;
1475 }
1476
1477 let expected: BTreeSet<_> = provides_extra.iter().collect();
1478 let actual: BTreeSet<_> = package.metadata.provides_extra.iter().collect();
1479
1480 if expected != actual {
1481 let expected = Box::into_iter(provides_extra).collect();
1482 return SatisfiesResult::MismatchedPackageProvidesExtra(
1483 &package.id.name,
1484 package.id.version.as_ref(),
1485 expected,
1486 actual,
1487 );
1488 }
1489
1490 SatisfiesResult::Satisfied
1491 }
1492
1493 fn satisfies_requires_dist<'lock>(
1495 &self,
1496 requires_dist: Box<[Requirement]>,
1497 dependency_groups: BTreeMap<GroupName, Box<[Requirement]>>,
1498 package: &'lock Package,
1499 root: &Path,
1500 ) -> Result<SatisfiesResult<'lock>, LockError> {
1501 let flattened = if package.is_dynamic() {
1503 Some(
1504 FlatRequiresDist::from_requirements(requires_dist.clone(), &package.id.name)
1505 .into_iter()
1506 .map(|requirement| {
1507 normalize_requirement(requirement, root, &self.requires_python)
1508 })
1509 .collect::<Result<BTreeSet<_>, _>>()?,
1510 )
1511 } else {
1512 None
1513 };
1514
1515 let expected: BTreeSet<_> = Box::into_iter(requires_dist)
1517 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1518 .collect::<Result<_, _>>()?;
1519 let actual: BTreeSet<_> = package
1520 .metadata
1521 .requires_dist
1522 .iter()
1523 .cloned()
1524 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1525 .collect::<Result<_, _>>()?;
1526
1527 if expected != actual && flattened.is_none_or(|expected| expected != actual) {
1528 return Ok(SatisfiesResult::MismatchedPackageRequirements(
1529 &package.id.name,
1530 package.id.version.as_ref(),
1531 expected,
1532 actual,
1533 ));
1534 }
1535
1536 let expected: BTreeMap<GroupName, BTreeSet<Requirement>> = dependency_groups
1538 .into_iter()
1539 .filter(|(_, requirements)| self.includes_empty_groups() || !requirements.is_empty())
1540 .map(|(group, requirements)| {
1541 Ok::<_, LockError>((
1542 group,
1543 Box::into_iter(requirements)
1544 .map(|requirement| {
1545 normalize_requirement(requirement, root, &self.requires_python)
1546 })
1547 .collect::<Result<_, _>>()?,
1548 ))
1549 })
1550 .collect::<Result<_, _>>()?;
1551 let actual: BTreeMap<GroupName, BTreeSet<Requirement>> = package
1552 .metadata
1553 .dependency_groups
1554 .iter()
1555 .filter(|(_, requirements)| self.includes_empty_groups() || !requirements.is_empty())
1556 .map(|(group, requirements)| {
1557 Ok::<_, LockError>((
1558 group.clone(),
1559 requirements
1560 .iter()
1561 .cloned()
1562 .map(|requirement| {
1563 normalize_requirement(requirement, root, &self.requires_python)
1564 })
1565 .collect::<Result<_, _>>()?,
1566 ))
1567 })
1568 .collect::<Result<_, _>>()?;
1569
1570 if expected != actual {
1571 return Ok(SatisfiesResult::MismatchedPackageDependencyGroups(
1572 &package.id.name,
1573 package.id.version.as_ref(),
1574 expected,
1575 actual,
1576 ));
1577 }
1578
1579 Ok(SatisfiesResult::Satisfied)
1580 }
1581
1582 #[instrument(skip_all)]
1584 pub async fn satisfies<Context: BuildContext>(
1585 &self,
1586 root: &Path,
1587 packages: &BTreeMap<PackageName, WorkspaceMember>,
1588 members: &[PackageName],
1589 required_members: &BTreeMap<PackageName, Editability>,
1590 requirements: &[Requirement],
1591 constraints: &[Requirement],
1592 overrides: &[Requirement],
1593 excludes: &[PackageName],
1594 build_constraints: &[Requirement],
1595 dependency_groups: &BTreeMap<GroupName, Vec<Requirement>>,
1596 dependency_metadata: &DependencyMetadata,
1597 indexes: Option<&IndexLocations>,
1598 tags: &Tags,
1599 markers: &MarkerEnvironment,
1600 hasher: &HashStrategy,
1601 index: &InMemoryIndex,
1602 database: &DistributionDatabase<'_, Context>,
1603 ) -> Result<SatisfiesResult<'_>, LockError> {
1604 let mut queue: VecDeque<&Package> = VecDeque::new();
1605 let mut seen = FxHashSet::default();
1606
1607 {
1609 let expected = members.iter().cloned().collect::<BTreeSet<_>>();
1610 let actual = &self.manifest.members;
1611 if expected != *actual {
1612 return Ok(SatisfiesResult::MismatchedMembers(expected, actual));
1613 }
1614 }
1615
1616 for (name, member) in packages {
1619 let source = self.find_by_name(name).ok().flatten();
1620
1621 let value = required_members.get(name);
1623 let is_required_member = value.is_some();
1624 let editability = value.copied().flatten();
1625
1626 let expected_virtual = !member.pyproject_toml().is_package(!is_required_member);
1628 let actual_virtual =
1629 source.map(|package| matches!(package.id.source, Source::Virtual(..)));
1630 if actual_virtual != Some(expected_virtual) {
1631 return Ok(SatisfiesResult::MismatchedVirtual(
1632 name.clone(),
1633 expected_virtual,
1634 ));
1635 }
1636
1637 let expected_editable = if expected_virtual {
1639 false
1640 } else {
1641 editability.unwrap_or(true)
1642 };
1643 let actual_editable =
1644 source.map(|package| matches!(package.id.source, Source::Editable(..)));
1645 if actual_editable != Some(expected_editable) {
1646 return Ok(SatisfiesResult::MismatchedEditable(
1647 name.clone(),
1648 expected_editable,
1649 ));
1650 }
1651 }
1652
1653 {
1655 let expected: BTreeSet<_> = requirements
1656 .iter()
1657 .cloned()
1658 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1659 .collect::<Result<_, _>>()?;
1660 let actual: BTreeSet<_> = self
1661 .manifest
1662 .requirements
1663 .iter()
1664 .cloned()
1665 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1666 .collect::<Result<_, _>>()?;
1667 if expected != actual {
1668 return Ok(SatisfiesResult::MismatchedRequirements(expected, actual));
1669 }
1670 }
1671
1672 {
1674 let expected: BTreeSet<_> = constraints
1675 .iter()
1676 .cloned()
1677 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1678 .collect::<Result<_, _>>()?;
1679 let actual: BTreeSet<_> = self
1680 .manifest
1681 .constraints
1682 .iter()
1683 .cloned()
1684 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1685 .collect::<Result<_, _>>()?;
1686 if expected != actual {
1687 return Ok(SatisfiesResult::MismatchedConstraints(expected, actual));
1688 }
1689 }
1690
1691 {
1693 let expected: BTreeSet<_> = overrides
1694 .iter()
1695 .cloned()
1696 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1697 .collect::<Result<_, _>>()?;
1698 let actual: BTreeSet<_> = self
1699 .manifest
1700 .overrides
1701 .iter()
1702 .cloned()
1703 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1704 .collect::<Result<_, _>>()?;
1705 if expected != actual {
1706 return Ok(SatisfiesResult::MismatchedOverrides(expected, actual));
1707 }
1708 }
1709
1710 {
1712 let expected: BTreeSet<_> = excludes.iter().cloned().collect();
1713 let actual: BTreeSet<_> = self.manifest.excludes.iter().cloned().collect();
1714 if expected != actual {
1715 return Ok(SatisfiesResult::MismatchedExcludes(expected, actual));
1716 }
1717 }
1718
1719 {
1721 let expected: BTreeSet<_> = build_constraints
1722 .iter()
1723 .cloned()
1724 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1725 .collect::<Result<_, _>>()?;
1726 let actual: BTreeSet<_> = self
1727 .manifest
1728 .build_constraints
1729 .iter()
1730 .cloned()
1731 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1732 .collect::<Result<_, _>>()?;
1733 if expected != actual {
1734 return Ok(SatisfiesResult::MismatchedBuildConstraints(
1735 expected, actual,
1736 ));
1737 }
1738 }
1739
1740 {
1742 let expected: BTreeMap<GroupName, BTreeSet<Requirement>> = dependency_groups
1743 .iter()
1744 .filter(|(_, requirements)| !requirements.is_empty())
1745 .map(|(group, requirements)| {
1746 Ok::<_, LockError>((
1747 group.clone(),
1748 requirements
1749 .iter()
1750 .cloned()
1751 .map(|requirement| {
1752 normalize_requirement(requirement, root, &self.requires_python)
1753 })
1754 .collect::<Result<_, _>>()?,
1755 ))
1756 })
1757 .collect::<Result<_, _>>()?;
1758 let actual: BTreeMap<GroupName, BTreeSet<Requirement>> = self
1759 .manifest
1760 .dependency_groups
1761 .iter()
1762 .filter(|(_, requirements)| !requirements.is_empty())
1763 .map(|(group, requirements)| {
1764 Ok::<_, LockError>((
1765 group.clone(),
1766 requirements
1767 .iter()
1768 .cloned()
1769 .map(|requirement| {
1770 normalize_requirement(requirement, root, &self.requires_python)
1771 })
1772 .collect::<Result<_, _>>()?,
1773 ))
1774 })
1775 .collect::<Result<_, _>>()?;
1776 if expected != actual {
1777 return Ok(SatisfiesResult::MismatchedDependencyGroups(
1778 expected, actual,
1779 ));
1780 }
1781 }
1782
1783 {
1785 let expected = dependency_metadata
1786 .values()
1787 .cloned()
1788 .collect::<BTreeSet<_>>();
1789 let actual = &self.manifest.dependency_metadata;
1790 if expected != *actual {
1791 return Ok(SatisfiesResult::MismatchedStaticMetadata(expected, actual));
1792 }
1793 }
1794
1795 let mut remotes = indexes.map(|locations| {
1797 locations
1798 .allowed_indexes()
1799 .into_iter()
1800 .filter_map(|index| match index.url() {
1801 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
1802 Some(UrlString::from(index.url().without_credentials().as_ref()))
1803 }
1804 IndexUrl::Path(_) => None,
1805 })
1806 .collect::<BTreeSet<_>>()
1807 });
1808
1809 let mut locals = indexes.map(|locations| {
1810 locations
1811 .allowed_indexes()
1812 .into_iter()
1813 .filter_map(|index| match index.url() {
1814 IndexUrl::Pypi(_) | IndexUrl::Url(_) => None,
1815 IndexUrl::Path(url) => {
1816 let path = url.to_file_path().ok()?;
1817 let path = try_relative_to_if(&path, root, !url.was_given_absolute())
1818 .ok()?
1819 .into_boxed_path();
1820 Some(path)
1821 }
1822 })
1823 .collect::<BTreeSet<_>>()
1824 });
1825
1826 for root_name in packages.keys() {
1828 let root = self
1829 .find_by_name(root_name)
1830 .expect("found too many packages matching root");
1831
1832 let Some(root) = root else {
1833 return Ok(SatisfiesResult::MissingRoot(root_name.clone()));
1835 };
1836
1837 if seen.insert(&root.id) {
1838 queue.push_back(root);
1839 }
1840 }
1841
1842 let root_requirements = requirements
1845 .iter()
1846 .chain(dependency_groups.values().flatten())
1847 .collect::<Vec<_>>();
1848
1849 for requirement in &root_requirements {
1850 if let RequirementSource::Registry {
1851 index: Some(index), ..
1852 } = &requirement.source
1853 {
1854 match &index.url {
1855 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
1856 if let Some(remotes) = remotes.as_mut() {
1857 remotes.insert(UrlString::from(
1858 index.url().without_credentials().as_ref(),
1859 ));
1860 }
1861 }
1862 IndexUrl::Path(url) => {
1863 if let Some(locals) = locals.as_mut() {
1864 if let Some(path) = url.to_file_path().ok().and_then(|path| {
1865 try_relative_to_if(&path, root, !url.was_given_absolute()).ok()
1866 }) {
1867 locals.insert(path.into_boxed_path());
1868 }
1869 }
1870 }
1871 }
1872 }
1873 }
1874
1875 if !root_requirements.is_empty() {
1876 let names = root_requirements
1877 .iter()
1878 .map(|requirement| &requirement.name)
1879 .collect::<FxHashSet<_>>();
1880
1881 let by_name: FxHashMap<_, Vec<_>> = self.packages.iter().fold(
1882 FxHashMap::with_capacity_and_hasher(self.packages.len(), FxBuildHasher),
1883 |mut by_name, package| {
1884 if names.contains(&package.id.name) {
1885 by_name.entry(&package.id.name).or_default().push(package);
1886 }
1887 by_name
1888 },
1889 );
1890
1891 for requirement in root_requirements {
1892 for package in by_name.get(&requirement.name).into_iter().flatten() {
1893 if !package.id.source.is_source_tree() {
1894 continue;
1895 }
1896
1897 let marker = if package.fork_markers.is_empty() {
1898 requirement.marker
1899 } else {
1900 let mut combined = MarkerTree::FALSE;
1901 for fork_marker in &package.fork_markers {
1902 combined.or(fork_marker.pep508());
1903 }
1904 combined.and(requirement.marker);
1905 combined
1906 };
1907 if marker.is_false() {
1908 continue;
1909 }
1910 if !marker.evaluate(markers, &[]) {
1911 continue;
1912 }
1913
1914 if seen.insert(&package.id) {
1915 queue.push_back(package);
1916 }
1917 }
1918 }
1919 }
1920
1921 while let Some(package) = queue.pop_front() {
1922 if let Source::Registry(index) = &package.id.source {
1924 match index {
1925 RegistrySource::Url(url) => {
1926 if remotes
1927 .as_ref()
1928 .is_some_and(|remotes| !remotes.contains(url))
1929 {
1930 let name = &package.id.name;
1931 let version = &package
1932 .id
1933 .version
1934 .as_ref()
1935 .expect("version for registry source");
1936 return Ok(SatisfiesResult::MissingRemoteIndex(name, version, url));
1937 }
1938 }
1939 RegistrySource::Path(path) => {
1940 if locals.as_ref().is_some_and(|locals| !locals.contains(path)) {
1941 let name = &package.id.name;
1942 let version = &package
1943 .id
1944 .version
1945 .as_ref()
1946 .expect("version for registry source");
1947 return Ok(SatisfiesResult::MissingLocalIndex(name, version, path));
1948 }
1949 }
1950 }
1951 }
1952
1953 if package.id.source.is_immutable() {
1955 continue;
1956 }
1957
1958 if let Some(version) = package.id.version.as_ref() {
1959 let HashedDist { dist, .. } = package.to_dist(
1961 root,
1962 TagPolicy::Preferred(tags),
1963 &BuildOptions::default(),
1964 markers,
1965 )?;
1966
1967 let metadata = {
1968 let id = dist.distribution_id();
1969 if let Some(archive) =
1970 index
1971 .distributions()
1972 .get(&id)
1973 .as_deref()
1974 .and_then(|response| {
1975 if let MetadataResponse::Found(archive, ..) = response {
1976 Some(archive)
1977 } else {
1978 None
1979 }
1980 })
1981 {
1982 archive.metadata.clone()
1984 } else {
1985 let archive = database
1987 .get_or_build_wheel_metadata(&dist, hasher.get(&dist))
1988 .await
1989 .map_err(|err| LockErrorKind::Resolution {
1990 id: package.id.clone(),
1991 err,
1992 })?;
1993
1994 let metadata = archive.metadata.clone();
1995
1996 index
1998 .distributions()
1999 .done(id, Arc::new(MetadataResponse::Found(archive)));
2000
2001 metadata
2002 }
2003 };
2004
2005 if package.id.source.is_source_tree() {
2008 if metadata.dynamic {
2009 return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, false));
2010 }
2011 }
2012
2013 if metadata.version != *version {
2015 return Ok(SatisfiesResult::MismatchedVersion(
2016 &package.id.name,
2017 version.clone(),
2018 Some(metadata.version.clone()),
2019 ));
2020 }
2021
2022 match self.satisfies_provides_extra(metadata.provides_extra, package) {
2024 SatisfiesResult::Satisfied => {}
2025 result => return Ok(result),
2026 }
2027
2028 match self.satisfies_requires_dist(
2030 metadata.requires_dist,
2031 metadata.dependency_groups,
2032 package,
2033 root,
2034 )? {
2035 SatisfiesResult::Satisfied => {}
2036 result => return Ok(result),
2037 }
2038 } else if let Some(source_tree) = package.id.source.as_source_tree() {
2039 let parent = root.join(source_tree);
2049 let path = parent.join("pyproject.toml");
2050 let metadata = match fs_err::tokio::read_to_string(&path).await {
2051 Ok(contents) => {
2052 let pyproject_toml =
2053 PyProjectToml::from_toml(&contents, path.user_display()).map_err(
2054 |err| LockErrorKind::InvalidPyprojectToml {
2055 path: path.clone(),
2056 err,
2057 },
2058 )?;
2059 database
2060 .requires_dist(&parent, &pyproject_toml)
2061 .await
2062 .map_err(|err| LockErrorKind::Resolution {
2063 id: package.id.clone(),
2064 err,
2065 })?
2066 }
2067 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
2068 Err(err) => {
2069 return Err(LockErrorKind::UnreadablePyprojectToml { path, err }.into());
2070 }
2071 };
2072
2073 let satisfied = metadata.is_some_and(|metadata| {
2074 if !metadata.dynamic {
2076 debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
2077 return false;
2078 }
2079
2080 if let SatisfiesResult::Satisfied = self.satisfies_provides_extra(metadata.provides_extra, package, ) {
2082 debug!("Static `provides-extra` for `{}` is up-to-date", package.id);
2083 } else {
2084 debug!("Static `provides-extra` for `{}` is out-of-date; falling back to distribution database", package.id);
2085 return false;
2086 }
2087
2088 match self.satisfies_requires_dist(metadata.requires_dist, metadata.dependency_groups, package, root) {
2090 Ok(SatisfiesResult::Satisfied) => {
2091 debug!("Static `requires-dist` for `{}` is up-to-date", package.id);
2092 },
2093 Ok(..) => {
2094 debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
2095 return false;
2096 },
2097 Err(..) => {
2098 debug!("Static `requires-dist` for `{}` is invalid; falling back to distribution database", package.id);
2099 return false;
2100 },
2101 }
2102
2103 true
2104 });
2105
2106 if !satisfied {
2112 let HashedDist { dist, .. } = package.to_dist(
2113 root,
2114 TagPolicy::Preferred(tags),
2115 &BuildOptions::default(),
2116 markers,
2117 )?;
2118
2119 let metadata = {
2120 let id = dist.distribution_id();
2121 if let Some(archive) =
2122 index
2123 .distributions()
2124 .get(&id)
2125 .as_deref()
2126 .and_then(|response| {
2127 if let MetadataResponse::Found(archive, ..) = response {
2128 Some(archive)
2129 } else {
2130 None
2131 }
2132 })
2133 {
2134 archive.metadata.clone()
2136 } else {
2137 let archive = database
2139 .get_or_build_wheel_metadata(&dist, hasher.get(&dist))
2140 .await
2141 .map_err(|err| LockErrorKind::Resolution {
2142 id: package.id.clone(),
2143 err,
2144 })?;
2145
2146 let metadata = archive.metadata.clone();
2147
2148 index
2150 .distributions()
2151 .done(id, Arc::new(MetadataResponse::Found(archive)));
2152
2153 metadata
2154 }
2155 };
2156
2157 if !metadata.dynamic {
2159 return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, true));
2160 }
2161
2162 match self.satisfies_provides_extra(metadata.provides_extra, package) {
2164 SatisfiesResult::Satisfied => {}
2165 result => return Ok(result),
2166 }
2167
2168 match self.satisfies_requires_dist(
2170 metadata.requires_dist,
2171 metadata.dependency_groups,
2172 package,
2173 root,
2174 )? {
2175 SatisfiesResult::Satisfied => {}
2176 result => return Ok(result),
2177 }
2178 }
2179 } else {
2180 return Ok(SatisfiesResult::MissingVersion(&package.id.name));
2181 }
2182
2183 for requirement in package
2188 .metadata
2189 .requires_dist
2190 .iter()
2191 .chain(package.metadata.dependency_groups.values().flatten())
2192 {
2193 if let RequirementSource::Registry {
2194 index: Some(index), ..
2195 } = &requirement.source
2196 {
2197 match &index.url {
2198 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
2199 if let Some(remotes) = remotes.as_mut() {
2200 remotes.insert(UrlString::from(
2201 index.url().without_credentials().as_ref(),
2202 ));
2203 }
2204 }
2205 IndexUrl::Path(url) => {
2206 if let Some(locals) = locals.as_mut() {
2207 if let Some(path) = url.to_file_path().ok().and_then(|path| {
2208 try_relative_to_if(&path, root, !url.was_given_absolute()).ok()
2209 }) {
2210 locals.insert(path.into_boxed_path());
2211 }
2212 }
2213 }
2214 }
2215 }
2216 }
2217
2218 for dep in &package.dependencies {
2220 if seen.insert(&dep.package_id) {
2221 let dep_dist = self.find_by_id(&dep.package_id);
2222 queue.push_back(dep_dist);
2223 }
2224 }
2225
2226 for dependencies in package.optional_dependencies.values() {
2227 for dep in dependencies {
2228 if seen.insert(&dep.package_id) {
2229 let dep_dist = self.find_by_id(&dep.package_id);
2230 queue.push_back(dep_dist);
2231 }
2232 }
2233 }
2234
2235 for dependencies in package.dependency_groups.values() {
2236 for dep in dependencies {
2237 if seen.insert(&dep.package_id) {
2238 let dep_dist = self.find_by_id(&dep.package_id);
2239 queue.push_back(dep_dist);
2240 }
2241 }
2242 }
2243 }
2244
2245 Ok(SatisfiesResult::Satisfied)
2246 }
2247}
2248
2249#[derive(Debug, Copy, Clone)]
2250enum TagPolicy<'tags> {
2251 Required(&'tags Tags),
2253 Preferred(&'tags Tags),
2256}
2257
2258impl<'tags> TagPolicy<'tags> {
2259 fn tags(&self) -> &'tags Tags {
2261 match self {
2262 Self::Required(tags) | Self::Preferred(tags) => tags,
2263 }
2264 }
2265}
2266
2267#[derive(Debug)]
2269pub enum SatisfiesResult<'lock> {
2270 Satisfied,
2272 MismatchedMembers(BTreeSet<PackageName>, &'lock BTreeSet<PackageName>),
2274 MismatchedVirtual(PackageName, bool),
2276 MismatchedEditable(PackageName, bool),
2278 MismatchedDynamic(&'lock PackageName, bool),
2280 MismatchedVersion(&'lock PackageName, Version, Option<Version>),
2282 MismatchedRequirements(BTreeSet<Requirement>, BTreeSet<Requirement>),
2284 MismatchedConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
2286 MismatchedOverrides(BTreeSet<Requirement>, BTreeSet<Requirement>),
2288 MismatchedExcludes(BTreeSet<PackageName>, BTreeSet<PackageName>),
2290 MismatchedBuildConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
2292 MismatchedDependencyGroups(
2294 BTreeMap<GroupName, BTreeSet<Requirement>>,
2295 BTreeMap<GroupName, BTreeSet<Requirement>>,
2296 ),
2297 MismatchedStaticMetadata(BTreeSet<StaticMetadata>, &'lock BTreeSet<StaticMetadata>),
2299 MissingRoot(PackageName),
2301 MissingRemoteIndex(&'lock PackageName, &'lock Version, &'lock UrlString),
2303 MissingLocalIndex(&'lock PackageName, &'lock Version, &'lock Path),
2305 MismatchedPackageRequirements(
2307 &'lock PackageName,
2308 Option<&'lock Version>,
2309 BTreeSet<Requirement>,
2310 BTreeSet<Requirement>,
2311 ),
2312 MismatchedPackageProvidesExtra(
2314 &'lock PackageName,
2315 Option<&'lock Version>,
2316 BTreeSet<ExtraName>,
2317 BTreeSet<&'lock ExtraName>,
2318 ),
2319 MismatchedPackageDependencyGroups(
2321 &'lock PackageName,
2322 Option<&'lock Version>,
2323 BTreeMap<GroupName, BTreeSet<Requirement>>,
2324 BTreeMap<GroupName, BTreeSet<Requirement>>,
2325 ),
2326 MissingVersion(&'lock PackageName),
2328}
2329
2330#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2332#[serde(rename_all = "kebab-case")]
2333struct ResolverOptions {
2334 #[serde(default)]
2336 resolution_mode: ResolutionMode,
2337 #[serde(default)]
2339 prerelease_mode: PrereleaseMode,
2340 #[serde(default)]
2342 fork_strategy: ForkStrategy,
2343 #[serde(flatten)]
2345 exclude_newer: ExcludeNewerWire,
2346}
2347
2348#[expect(clippy::struct_field_names)]
2349#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2350#[serde(rename_all = "kebab-case")]
2351struct ExcludeNewerWire {
2352 exclude_newer: Option<Timestamp>,
2353 exclude_newer_span: Option<ExcludeNewerSpan>,
2354 #[serde(default, skip_serializing_if = "ExcludeNewerPackage::is_empty")]
2355 exclude_newer_package: ExcludeNewerPackage,
2356}
2357
2358impl From<ExcludeNewerWire> for ExcludeNewer {
2359 fn from(wire: ExcludeNewerWire) -> Self {
2360 let global = match (wire.exclude_newer, wire.exclude_newer_span) {
2361 (Some(timestamp), None) => Some(ExcludeNewerValue::absolute(timestamp)),
2362 (Some(_), Some(span)) => Some(ExcludeNewerValue::relative(span)),
2365 (None, Some(span)) => Some(ExcludeNewerValue::relative(span)),
2368 (None, None) => None,
2369 };
2370 Self {
2371 global,
2372 package: wire.exclude_newer_package,
2373 }
2374 }
2375}
2376
2377impl From<ExcludeNewer> for ExcludeNewerWire {
2378 fn from(exclude_newer: ExcludeNewer) -> Self {
2379 let (timestamp, span) = match exclude_newer.global {
2380 Some(ExcludeNewerValue::Absolute(timestamp)) => (Some(timestamp), None),
2381 Some(ExcludeNewerValue::Relative(span)) => (None, Some(span)),
2382 None => (None, None),
2383 };
2384 Self {
2385 exclude_newer: timestamp,
2386 exclude_newer_span: span,
2387 exclude_newer_package: exclude_newer.package,
2388 }
2389 }
2390}
2391
2392#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2393#[serde(rename_all = "kebab-case")]
2394pub struct ResolverManifest {
2395 #[serde(default)]
2397 members: BTreeSet<PackageName>,
2398 #[serde(default)]
2403 requirements: BTreeSet<Requirement>,
2404 #[serde(default)]
2410 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
2411 #[serde(default)]
2413 constraints: BTreeSet<Requirement>,
2414 #[serde(default)]
2416 overrides: BTreeSet<Requirement>,
2417 #[serde(default)]
2419 excludes: BTreeSet<PackageName>,
2420 #[serde(default)]
2422 build_constraints: BTreeSet<Requirement>,
2423 #[serde(default)]
2425 dependency_metadata: BTreeSet<StaticMetadata>,
2426}
2427
2428impl ResolverManifest {
2429 pub fn new(
2432 members: impl IntoIterator<Item = PackageName>,
2433 requirements: impl IntoIterator<Item = Requirement>,
2434 constraints: impl IntoIterator<Item = Requirement>,
2435 overrides: impl IntoIterator<Item = Requirement>,
2436 excludes: impl IntoIterator<Item = PackageName>,
2437 build_constraints: impl IntoIterator<Item = Requirement>,
2438 dependency_groups: impl IntoIterator<Item = (GroupName, Vec<Requirement>)>,
2439 dependency_metadata: impl IntoIterator<Item = StaticMetadata>,
2440 ) -> Self {
2441 Self {
2442 members: members.into_iter().collect(),
2443 requirements: requirements.into_iter().collect(),
2444 constraints: constraints.into_iter().collect(),
2445 overrides: overrides.into_iter().collect(),
2446 excludes: excludes.into_iter().collect(),
2447 build_constraints: build_constraints.into_iter().collect(),
2448 dependency_groups: dependency_groups
2449 .into_iter()
2450 .map(|(group, requirements)| (group, requirements.into_iter().collect()))
2451 .collect(),
2452 dependency_metadata: dependency_metadata.into_iter().collect(),
2453 }
2454 }
2455
2456 pub fn relative_to(self, root: &Path) -> Result<Self, io::Error> {
2458 Ok(Self {
2459 members: self.members,
2460 requirements: self
2461 .requirements
2462 .into_iter()
2463 .map(|requirement| requirement.relative_to(root))
2464 .collect::<Result<BTreeSet<_>, _>>()?,
2465 constraints: self
2466 .constraints
2467 .into_iter()
2468 .map(|requirement| requirement.relative_to(root))
2469 .collect::<Result<BTreeSet<_>, _>>()?,
2470 overrides: self
2471 .overrides
2472 .into_iter()
2473 .map(|requirement| requirement.relative_to(root))
2474 .collect::<Result<BTreeSet<_>, _>>()?,
2475 excludes: self.excludes,
2476 build_constraints: self
2477 .build_constraints
2478 .into_iter()
2479 .map(|requirement| requirement.relative_to(root))
2480 .collect::<Result<BTreeSet<_>, _>>()?,
2481 dependency_groups: self
2482 .dependency_groups
2483 .into_iter()
2484 .map(|(group, requirements)| {
2485 Ok::<_, io::Error>((
2486 group,
2487 requirements
2488 .into_iter()
2489 .map(|requirement| requirement.relative_to(root))
2490 .collect::<Result<BTreeSet<_>, _>>()?,
2491 ))
2492 })
2493 .collect::<Result<BTreeMap<_, _>, _>>()?,
2494 dependency_metadata: self.dependency_metadata,
2495 })
2496 }
2497}
2498
2499#[derive(Clone, Debug, serde::Deserialize)]
2500#[serde(rename_all = "kebab-case")]
2501struct LockWire {
2502 version: u32,
2503 revision: Option<u32>,
2504 requires_python: RequiresPython,
2505 #[serde(rename = "resolution-markers", default)]
2508 fork_markers: Vec<SimplifiedMarkerTree>,
2509 #[serde(rename = "supported-markers", default)]
2510 supported_environments: Vec<SimplifiedMarkerTree>,
2511 #[serde(rename = "required-markers", default)]
2512 required_environments: Vec<SimplifiedMarkerTree>,
2513 #[serde(rename = "conflicts", default)]
2514 conflicts: Option<Conflicts>,
2515 #[serde(default)]
2517 options: ResolverOptions,
2518 #[serde(default)]
2519 manifest: ResolverManifest,
2520 #[serde(rename = "package", alias = "distribution", default)]
2521 packages: Vec<PackageWire>,
2522}
2523
2524impl TryFrom<LockWire> for Lock {
2525 type Error = LockError;
2526
2527 fn try_from(wire: LockWire) -> Result<Self, LockError> {
2528 let mut unambiguous_package_ids: FxHashMap<PackageName, PackageId> = FxHashMap::default();
2533 let mut ambiguous = FxHashSet::default();
2534 for dist in &wire.packages {
2535 if ambiguous.contains(&dist.id.name) {
2536 continue;
2537 }
2538 if let Some(id) = unambiguous_package_ids.remove(&dist.id.name) {
2539 ambiguous.insert(id.name);
2540 continue;
2541 }
2542 unambiguous_package_ids.insert(dist.id.name.clone(), dist.id.clone());
2543 }
2544
2545 let packages = wire
2546 .packages
2547 .into_iter()
2548 .map(|dist| dist.unwire(&wire.requires_python, &unambiguous_package_ids))
2549 .collect::<Result<Vec<_>, _>>()?;
2550 let supported_environments = wire
2551 .supported_environments
2552 .into_iter()
2553 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2554 .collect();
2555 let required_environments = wire
2556 .required_environments
2557 .into_iter()
2558 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2559 .collect();
2560 let fork_markers = wire
2561 .fork_markers
2562 .into_iter()
2563 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2564 .map(UniversalMarker::from_combined)
2565 .collect();
2566 let mut options = wire.options;
2567 if options.exclude_newer.exclude_newer_span.is_some() {
2568 options.exclude_newer.exclude_newer = None;
2569 }
2570 let lock = Self::new(
2571 wire.version,
2572 wire.revision.unwrap_or(0),
2573 packages,
2574 wire.requires_python,
2575 options,
2576 wire.manifest,
2577 wire.conflicts.unwrap_or_else(Conflicts::empty),
2578 supported_environments,
2579 required_environments,
2580 fork_markers,
2581 )?;
2582
2583 Ok(lock)
2584 }
2585}
2586
2587#[derive(Clone, Debug, serde::Deserialize)]
2591#[serde(rename_all = "kebab-case")]
2592pub struct LockVersion {
2593 version: u32,
2594}
2595
2596impl LockVersion {
2597 pub fn version(&self) -> u32 {
2599 self.version
2600 }
2601}
2602
2603#[derive(Clone, Debug, PartialEq, Eq)]
2604pub struct Package {
2605 pub(crate) id: PackageId,
2606 sdist: Option<SourceDist>,
2607 wheels: Vec<Wheel>,
2608 fork_markers: Vec<UniversalMarker>,
2614 dependencies: Vec<Dependency>,
2616 optional_dependencies: BTreeMap<ExtraName, Vec<Dependency>>,
2618 dependency_groups: BTreeMap<GroupName, Vec<Dependency>>,
2620 metadata: PackageMetadata,
2622}
2623
2624impl Package {
2625 fn from_annotated_dist(
2626 annotated_dist: &AnnotatedDist,
2627 fork_markers: Vec<UniversalMarker>,
2628 root: &Path,
2629 ) -> Result<Self, LockError> {
2630 let id = PackageId::from_annotated_dist(annotated_dist, root)?;
2631 let sdist = SourceDist::from_annotated_dist(&id, annotated_dist)?;
2632 let wheels = Wheel::from_annotated_dist(annotated_dist)?;
2633 let requires_dist = if id.source.is_immutable() {
2634 BTreeSet::default()
2635 } else {
2636 annotated_dist
2637 .metadata
2638 .as_ref()
2639 .expect("metadata is present")
2640 .requires_dist
2641 .iter()
2642 .cloned()
2643 .map(|requirement| requirement.relative_to(root))
2644 .collect::<Result<_, _>>()
2645 .map_err(LockErrorKind::RequirementRelativePath)?
2646 };
2647 let provides_extra = if id.source.is_immutable() {
2648 Box::default()
2649 } else {
2650 annotated_dist
2651 .metadata
2652 .as_ref()
2653 .expect("metadata is present")
2654 .provides_extra
2655 .clone()
2656 };
2657 let dependency_groups = if id.source.is_immutable() {
2658 BTreeMap::default()
2659 } else {
2660 annotated_dist
2661 .metadata
2662 .as_ref()
2663 .expect("metadata is present")
2664 .dependency_groups
2665 .iter()
2666 .map(|(group, requirements)| {
2667 let requirements = requirements
2668 .iter()
2669 .cloned()
2670 .map(|requirement| requirement.relative_to(root))
2671 .collect::<Result<_, _>>()
2672 .map_err(LockErrorKind::RequirementRelativePath)?;
2673 Ok::<_, LockError>((group.clone(), requirements))
2674 })
2675 .collect::<Result<_, _>>()?
2676 };
2677 Ok(Self {
2678 id,
2679 sdist,
2680 wheels,
2681 fork_markers,
2682 dependencies: vec![],
2683 optional_dependencies: BTreeMap::default(),
2684 dependency_groups: BTreeMap::default(),
2685 metadata: PackageMetadata {
2686 requires_dist,
2687 provides_extra,
2688 dependency_groups,
2689 },
2690 })
2691 }
2692
2693 fn add_dependency(
2695 &mut self,
2696 requires_python: &RequiresPython,
2697 annotated_dist: &AnnotatedDist,
2698 marker: UniversalMarker,
2699 root: &Path,
2700 ) -> Result<(), LockError> {
2701 let new_dep =
2702 Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2703 for existing_dep in &mut self.dependencies {
2704 if existing_dep.package_id == new_dep.package_id
2705 && existing_dep.simplified_marker == new_dep.simplified_marker
2728 {
2729 existing_dep.extra.extend(new_dep.extra);
2730 return Ok(());
2731 }
2732 }
2733
2734 self.dependencies.push(new_dep);
2735 Ok(())
2736 }
2737
2738 fn add_optional_dependency(
2740 &mut self,
2741 requires_python: &RequiresPython,
2742 extra: ExtraName,
2743 annotated_dist: &AnnotatedDist,
2744 marker: UniversalMarker,
2745 root: &Path,
2746 ) -> Result<(), LockError> {
2747 let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2748 let optional_deps = self.optional_dependencies.entry(extra).or_default();
2749 for existing_dep in &mut *optional_deps {
2750 if existing_dep.package_id == dep.package_id
2751 && existing_dep.simplified_marker == dep.simplified_marker
2754 {
2755 existing_dep.extra.extend(dep.extra);
2756 return Ok(());
2757 }
2758 }
2759
2760 optional_deps.push(dep);
2761 Ok(())
2762 }
2763
2764 fn add_group_dependency(
2766 &mut self,
2767 requires_python: &RequiresPython,
2768 group: GroupName,
2769 annotated_dist: &AnnotatedDist,
2770 marker: UniversalMarker,
2771 root: &Path,
2772 ) -> Result<(), LockError> {
2773 let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2774 let deps = self.dependency_groups.entry(group).or_default();
2775 for existing_dep in &mut *deps {
2776 if existing_dep.package_id == dep.package_id
2777 && existing_dep.simplified_marker == dep.simplified_marker
2780 {
2781 existing_dep.extra.extend(dep.extra);
2782 return Ok(());
2783 }
2784 }
2785
2786 deps.push(dep);
2787 Ok(())
2788 }
2789
2790 fn to_dist(
2792 &self,
2793 workspace_root: &Path,
2794 tag_policy: TagPolicy<'_>,
2795 build_options: &BuildOptions,
2796 markers: &MarkerEnvironment,
2797 ) -> Result<HashedDist, LockError> {
2798 let no_binary = build_options.no_binary_package(&self.id.name);
2799 let no_build = build_options.no_build_package(&self.id.name);
2800
2801 if !no_binary {
2802 if let Some(best_wheel_index) = self.find_best_wheel(tag_policy) {
2803 let hashes = {
2804 let wheel = &self.wheels[best_wheel_index];
2805 HashDigests::from(
2806 wheel
2807 .hash
2808 .iter()
2809 .chain(wheel.zstd.iter().flat_map(|z| z.hash.iter()))
2810 .map(|h| h.0.clone())
2811 .collect::<Vec<_>>(),
2812 )
2813 };
2814
2815 let dist = match &self.id.source {
2816 Source::Registry(source) => {
2817 let wheels = self
2818 .wheels
2819 .iter()
2820 .map(|wheel| wheel.to_registry_wheel(source, workspace_root))
2821 .collect::<Result<_, LockError>>()?;
2822 let reg_built_dist = RegistryBuiltDist {
2823 wheels,
2824 best_wheel_index,
2825 sdist: None,
2826 };
2827 Dist::Built(BuiltDist::Registry(reg_built_dist))
2828 }
2829 Source::Path(path) => {
2830 let filename: WheelFilename =
2831 self.wheels[best_wheel_index].filename.clone();
2832 let install_path = absolute_path(workspace_root, path)?;
2833 let path_dist = PathBuiltDist {
2834 filename,
2835 url: verbatim_url(&install_path, &self.id)?,
2836 install_path: absolute_path(workspace_root, path)?.into_boxed_path(),
2837 };
2838 let built_dist = BuiltDist::Path(path_dist);
2839 Dist::Built(built_dist)
2840 }
2841 Source::Direct(url, direct) => {
2842 let filename: WheelFilename =
2843 self.wheels[best_wheel_index].filename.clone();
2844 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
2845 url: url.to_url().map_err(LockErrorKind::InvalidUrl)?,
2846 subdirectory: direct.subdirectory.clone(),
2847 ext: DistExtension::Wheel,
2848 });
2849 let direct_dist = DirectUrlBuiltDist {
2850 filename,
2851 location: Box::new(url.clone()),
2852 url: VerbatimUrl::from_url(url),
2853 };
2854 let built_dist = BuiltDist::DirectUrl(direct_dist);
2855 Dist::Built(built_dist)
2856 }
2857 Source::Git(_, _) => {
2858 return Err(LockErrorKind::InvalidWheelSource {
2859 id: self.id.clone(),
2860 source_type: "Git",
2861 }
2862 .into());
2863 }
2864 Source::Directory(_) => {
2865 return Err(LockErrorKind::InvalidWheelSource {
2866 id: self.id.clone(),
2867 source_type: "directory",
2868 }
2869 .into());
2870 }
2871 Source::Editable(_) => {
2872 return Err(LockErrorKind::InvalidWheelSource {
2873 id: self.id.clone(),
2874 source_type: "editable",
2875 }
2876 .into());
2877 }
2878 Source::Virtual(_) => {
2879 return Err(LockErrorKind::InvalidWheelSource {
2880 id: self.id.clone(),
2881 source_type: "virtual",
2882 }
2883 .into());
2884 }
2885 };
2886
2887 return Ok(HashedDist { dist, hashes });
2888 }
2889 }
2890
2891 if let Some(sdist) = self.to_source_dist(workspace_root)? {
2892 if !no_build || sdist.is_virtual() {
2896 let hashes = self
2897 .sdist
2898 .as_ref()
2899 .and_then(|s| s.hash())
2900 .map(|hash| HashDigests::from(vec![hash.0.clone()]))
2901 .unwrap_or_else(|| HashDigests::from(vec![]));
2902 return Ok(HashedDist {
2903 dist: Dist::Source(sdist),
2904 hashes,
2905 });
2906 }
2907 }
2908
2909 match (no_binary, no_build) {
2910 (true, true) => Err(LockErrorKind::NoBinaryNoBuild {
2911 id: self.id.clone(),
2912 }
2913 .into()),
2914 (true, false) if self.id.source.is_wheel() => Err(LockErrorKind::NoBinaryWheelOnly {
2915 id: self.id.clone(),
2916 }
2917 .into()),
2918 (true, false) => Err(LockErrorKind::NoBinary {
2919 id: self.id.clone(),
2920 }
2921 .into()),
2922 (false, true) => Err(LockErrorKind::NoBuild {
2923 id: self.id.clone(),
2924 }
2925 .into()),
2926 (false, false) if self.id.source.is_wheel() => Err(LockError {
2927 kind: Box::new(LockErrorKind::IncompatibleWheelOnly {
2928 id: self.id.clone(),
2929 }),
2930 hint: self.tag_hint(tag_policy, markers),
2931 }),
2932 (false, false) => Err(LockError {
2933 kind: Box::new(LockErrorKind::NeitherSourceDistNorWheel {
2934 id: self.id.clone(),
2935 }),
2936 hint: self.tag_hint(tag_policy, markers),
2937 }),
2938 }
2939 }
2940
2941 fn tag_hint(
2943 &self,
2944 tag_policy: TagPolicy<'_>,
2945 markers: &MarkerEnvironment,
2946 ) -> Option<WheelTagHint> {
2947 let filenames = self
2948 .wheels
2949 .iter()
2950 .map(|wheel| &wheel.filename)
2951 .collect::<Vec<_>>();
2952 WheelTagHint::from_wheels(
2953 &self.id.name,
2954 self.id.version.as_ref(),
2955 &filenames,
2956 tag_policy.tags(),
2957 markers,
2958 )
2959 }
2960
2961 fn to_source_dist(
2966 &self,
2967 workspace_root: &Path,
2968 ) -> Result<Option<uv_distribution_types::SourceDist>, LockError> {
2969 let sdist = match &self.id.source {
2970 Source::Path(path) => {
2971 let DistExtension::Source(ext) = DistExtension::from_path(path).map_err(|err| {
2973 LockErrorKind::MissingExtension {
2974 id: self.id.clone(),
2975 err,
2976 }
2977 })?
2978 else {
2979 return Ok(None);
2980 };
2981 let install_path = absolute_path(workspace_root, path)?;
2982 let given = path.to_str().expect("lock file paths must be UTF-8");
2983 let path_dist = PathSourceDist {
2984 name: self.id.name.clone(),
2985 version: self.id.version.clone(),
2986 url: verbatim_url(&install_path, &self.id)?.with_given(given),
2987 install_path: install_path.into_boxed_path(),
2988 ext,
2989 };
2990 uv_distribution_types::SourceDist::Path(path_dist)
2991 }
2992 Source::Directory(path) => {
2993 let install_path = absolute_path(workspace_root, path)?;
2994 let given = path.to_str().expect("lock file paths must be UTF-8");
2995 let dir_dist = DirectorySourceDist {
2996 name: self.id.name.clone(),
2997 url: verbatim_url(&install_path, &self.id)?.with_given(given),
2998 install_path: install_path.into_boxed_path(),
2999 editable: Some(false),
3000 r#virtual: Some(false),
3001 };
3002 uv_distribution_types::SourceDist::Directory(dir_dist)
3003 }
3004 Source::Editable(path) => {
3005 let install_path = absolute_path(workspace_root, path)?;
3006 let given = path.to_str().expect("lock file paths must be UTF-8");
3007 let dir_dist = DirectorySourceDist {
3008 name: self.id.name.clone(),
3009 url: verbatim_url(&install_path, &self.id)?.with_given(given),
3010 install_path: install_path.into_boxed_path(),
3011 editable: Some(true),
3012 r#virtual: Some(false),
3013 };
3014 uv_distribution_types::SourceDist::Directory(dir_dist)
3015 }
3016 Source::Virtual(path) => {
3017 let install_path = absolute_path(workspace_root, path)?;
3018 let given = path.to_str().expect("lock file paths must be UTF-8");
3019 let dir_dist = DirectorySourceDist {
3020 name: self.id.name.clone(),
3021 url: verbatim_url(&install_path, &self.id)?.with_given(given),
3022 install_path: install_path.into_boxed_path(),
3023 editable: Some(false),
3024 r#virtual: Some(true),
3025 };
3026 uv_distribution_types::SourceDist::Directory(dir_dist)
3027 }
3028 Source::Git(url, git) => {
3029 let mut url = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
3032 url.set_fragment(None);
3033 url.set_query(None);
3034
3035 let git_url = GitUrl::from_commit(
3037 url,
3038 GitReference::from(git.kind.clone()),
3039 git.precise,
3040 git.lfs,
3041 )?;
3042
3043 let url = DisplaySafeUrl::from(ParsedGitUrl {
3045 url: git_url.clone(),
3046 subdirectory: git.subdirectory.clone(),
3047 });
3048
3049 let git_dist = GitSourceDist {
3050 name: self.id.name.clone(),
3051 url: VerbatimUrl::from_url(url),
3052 git: Box::new(git_url),
3053 subdirectory: git.subdirectory.clone(),
3054 };
3055 uv_distribution_types::SourceDist::Git(git_dist)
3056 }
3057 Source::Direct(url, direct) => {
3058 let DistExtension::Source(ext) =
3060 DistExtension::from_path(url.base_str()).map_err(|err| {
3061 LockErrorKind::MissingExtension {
3062 id: self.id.clone(),
3063 err,
3064 }
3065 })?
3066 else {
3067 return Ok(None);
3068 };
3069 let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
3070 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
3071 url: location.clone(),
3072 subdirectory: direct.subdirectory.clone(),
3073 ext: DistExtension::Source(ext),
3074 });
3075 let direct_dist = DirectUrlSourceDist {
3076 name: self.id.name.clone(),
3077 location: Box::new(location),
3078 subdirectory: direct.subdirectory.clone(),
3079 ext,
3080 url: VerbatimUrl::from_url(url),
3081 };
3082 uv_distribution_types::SourceDist::DirectUrl(direct_dist)
3083 }
3084 Source::Registry(RegistrySource::Url(url)) => {
3085 let Some(ref sdist) = self.sdist else {
3086 return Ok(None);
3087 };
3088
3089 let name = &self.id.name;
3090 let version = self
3091 .id
3092 .version
3093 .as_ref()
3094 .expect("version for registry source");
3095
3096 let file_url = sdist.url().ok_or_else(|| LockErrorKind::MissingUrl {
3097 name: name.clone(),
3098 version: version.clone(),
3099 })?;
3100 let filename = sdist
3101 .filename()
3102 .ok_or_else(|| LockErrorKind::MissingFilename {
3103 id: self.id.clone(),
3104 })?;
3105 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
3106 LockErrorKind::MissingExtension {
3107 id: self.id.clone(),
3108 err,
3109 }
3110 })?;
3111 let file = Box::new(uv_distribution_types::File {
3112 dist_info_metadata: false,
3113 filename: SmallString::from(filename),
3114 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
3115 HashDigests::from(hash.0.clone())
3116 }),
3117 requires_python: None,
3118 size: sdist.size(),
3119 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
3120 url: FileLocation::AbsoluteUrl(file_url.clone()),
3121 yanked: None,
3122 zstd: None,
3123 });
3124
3125 let index = IndexUrl::from(VerbatimUrl::from_url(
3126 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
3127 ));
3128
3129 let reg_dist = RegistrySourceDist {
3130 name: name.clone(),
3131 version: version.clone(),
3132 file,
3133 ext,
3134 index,
3135 wheels: vec![],
3136 };
3137 uv_distribution_types::SourceDist::Registry(reg_dist)
3138 }
3139 Source::Registry(RegistrySource::Path(path)) => {
3140 let Some(ref sdist) = self.sdist else {
3141 return Ok(None);
3142 };
3143
3144 let name = &self.id.name;
3145 let version = self
3146 .id
3147 .version
3148 .as_ref()
3149 .expect("version for registry source");
3150
3151 let file_url = match sdist {
3152 SourceDist::Url { url: file_url, .. } => {
3153 FileLocation::AbsoluteUrl(file_url.clone())
3154 }
3155 SourceDist::Path {
3156 path: file_path, ..
3157 } => {
3158 let file_path = workspace_root.join(path).join(file_path);
3159 let file_url =
3160 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
3161 LockErrorKind::PathToUrl {
3162 path: file_path.into_boxed_path(),
3163 }
3164 })?;
3165 FileLocation::AbsoluteUrl(UrlString::from(file_url))
3166 }
3167 SourceDist::Metadata { .. } => {
3168 return Err(LockErrorKind::MissingPath {
3169 name: name.clone(),
3170 version: version.clone(),
3171 }
3172 .into());
3173 }
3174 };
3175 let filename = sdist
3176 .filename()
3177 .ok_or_else(|| LockErrorKind::MissingFilename {
3178 id: self.id.clone(),
3179 })?;
3180 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
3181 LockErrorKind::MissingExtension {
3182 id: self.id.clone(),
3183 err,
3184 }
3185 })?;
3186 let file = Box::new(uv_distribution_types::File {
3187 dist_info_metadata: false,
3188 filename: SmallString::from(filename),
3189 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
3190 HashDigests::from(hash.0.clone())
3191 }),
3192 requires_python: None,
3193 size: sdist.size(),
3194 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
3195 url: file_url,
3196 yanked: None,
3197 zstd: None,
3198 });
3199
3200 let index = IndexUrl::from(
3201 VerbatimUrl::from_absolute_path(workspace_root.join(path))
3202 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
3203 );
3204
3205 let reg_dist = RegistrySourceDist {
3206 name: name.clone(),
3207 version: version.clone(),
3208 file,
3209 ext,
3210 index,
3211 wheels: vec![],
3212 };
3213 uv_distribution_types::SourceDist::Registry(reg_dist)
3214 }
3215 };
3216
3217 Ok(Some(sdist))
3218 }
3219
3220 fn to_toml(
3221 &self,
3222 requires_python: &RequiresPython,
3223 dist_count_by_name: &FxHashMap<PackageName, u64>,
3224 ) -> Result<Table, toml_edit::ser::Error> {
3225 let mut table = Table::new();
3226
3227 self.id.to_toml(None, &mut table);
3228
3229 if !self.fork_markers.is_empty() {
3230 let fork_markers = each_element_on_its_line_array(
3231 simplified_universal_markers(&self.fork_markers, requires_python).into_iter(),
3232 );
3233 if !fork_markers.is_empty() {
3234 table.insert("resolution-markers", value(fork_markers));
3235 }
3236 }
3237
3238 if !self.dependencies.is_empty() {
3239 let deps = each_element_on_its_line_array(self.dependencies.iter().map(|dep| {
3240 dep.to_toml(requires_python, dist_count_by_name)
3241 .into_inline_table()
3242 }));
3243 table.insert("dependencies", value(deps));
3244 }
3245
3246 if !self.optional_dependencies.is_empty() {
3247 let mut optional_deps = Table::new();
3248 for (extra, deps) in &self.optional_dependencies {
3249 let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3250 dep.to_toml(requires_python, dist_count_by_name)
3251 .into_inline_table()
3252 }));
3253 if !deps.is_empty() {
3254 optional_deps.insert(extra.as_ref(), value(deps));
3255 }
3256 }
3257 if !optional_deps.is_empty() {
3258 table.insert("optional-dependencies", Item::Table(optional_deps));
3259 }
3260 }
3261
3262 if !self.dependency_groups.is_empty() {
3263 let mut dependency_groups = Table::new();
3264 for (extra, deps) in &self.dependency_groups {
3265 let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3266 dep.to_toml(requires_python, dist_count_by_name)
3267 .into_inline_table()
3268 }));
3269 if !deps.is_empty() {
3270 dependency_groups.insert(extra.as_ref(), value(deps));
3271 }
3272 }
3273 if !dependency_groups.is_empty() {
3274 table.insert("dev-dependencies", Item::Table(dependency_groups));
3275 }
3276 }
3277
3278 if let Some(ref sdist) = self.sdist {
3279 table.insert("sdist", value(sdist.to_toml()?));
3280 }
3281
3282 if !self.wheels.is_empty() {
3283 let wheels = each_element_on_its_line_array(
3284 self.wheels
3285 .iter()
3286 .map(Wheel::to_toml)
3287 .collect::<Result<Vec<_>, _>>()?
3288 .into_iter(),
3289 );
3290 table.insert("wheels", value(wheels));
3291 }
3292
3293 {
3295 let mut metadata_table = Table::new();
3296
3297 if !self.metadata.requires_dist.is_empty() {
3298 let requires_dist = self
3299 .metadata
3300 .requires_dist
3301 .iter()
3302 .map(|requirement| {
3303 serde::Serialize::serialize(
3304 &requirement,
3305 toml_edit::ser::ValueSerializer::new(),
3306 )
3307 })
3308 .collect::<Result<Vec<_>, _>>()?;
3309 let requires_dist = match requires_dist.as_slice() {
3310 [] => Array::new(),
3311 [requirement] => Array::from_iter([requirement]),
3312 requires_dist => each_element_on_its_line_array(requires_dist.iter()),
3313 };
3314 metadata_table.insert("requires-dist", value(requires_dist));
3315 }
3316
3317 if !self.metadata.dependency_groups.is_empty() {
3318 let mut dependency_groups = Table::new();
3319 for (extra, deps) in &self.metadata.dependency_groups {
3320 let deps = deps
3321 .iter()
3322 .map(|requirement| {
3323 serde::Serialize::serialize(
3324 &requirement,
3325 toml_edit::ser::ValueSerializer::new(),
3326 )
3327 })
3328 .collect::<Result<Vec<_>, _>>()?;
3329 let deps = match deps.as_slice() {
3330 [] => Array::new(),
3331 [requirement] => Array::from_iter([requirement]),
3332 deps => each_element_on_its_line_array(deps.iter()),
3333 };
3334 dependency_groups.insert(extra.as_ref(), value(deps));
3335 }
3336 if !dependency_groups.is_empty() {
3337 metadata_table.insert("requires-dev", Item::Table(dependency_groups));
3338 }
3339 }
3340
3341 if !self.metadata.provides_extra.is_empty() {
3342 let provides_extras = self
3343 .metadata
3344 .provides_extra
3345 .iter()
3346 .map(|extra| {
3347 serde::Serialize::serialize(&extra, toml_edit::ser::ValueSerializer::new())
3348 })
3349 .collect::<Result<Vec<_>, _>>()?;
3350 let provides_extras = Array::from_iter(provides_extras);
3352 metadata_table.insert("provides-extras", value(provides_extras));
3353 }
3354
3355 if !metadata_table.is_empty() {
3356 table.insert("metadata", Item::Table(metadata_table));
3357 }
3358 }
3359
3360 Ok(table)
3361 }
3362
3363 fn find_best_wheel(&self, tag_policy: TagPolicy<'_>) -> Option<usize> {
3364 type WheelPriority<'lock> = (TagPriority, Option<&'lock BuildTag>);
3365
3366 let mut best: Option<(WheelPriority, usize)> = None;
3367 for (i, wheel) in self.wheels.iter().enumerate() {
3368 let TagCompatibility::Compatible(tag_priority) =
3369 wheel.filename.compatibility(tag_policy.tags())
3370 else {
3371 continue;
3372 };
3373 let build_tag = wheel.filename.build_tag();
3374 let wheel_priority = (tag_priority, build_tag);
3375 match best {
3376 None => {
3377 best = Some((wheel_priority, i));
3378 }
3379 Some((best_priority, _)) => {
3380 if wheel_priority > best_priority {
3381 best = Some((wheel_priority, i));
3382 }
3383 }
3384 }
3385 }
3386
3387 let best = best.map(|(_, i)| i);
3388 match tag_policy {
3389 TagPolicy::Required(_) => best,
3390 TagPolicy::Preferred(_) => best.or_else(|| self.wheels.first().map(|_| 0)),
3391 }
3392 }
3393
3394 pub fn name(&self) -> &PackageName {
3396 &self.id.name
3397 }
3398
3399 pub fn version(&self) -> Option<&Version> {
3401 self.id.version.as_ref()
3402 }
3403
3404 pub fn git_sha(&self) -> Option<&GitOid> {
3406 match &self.id.source {
3407 Source::Git(_, git) => Some(&git.precise),
3408 _ => None,
3409 }
3410 }
3411
3412 pub fn fork_markers(&self) -> &[UniversalMarker] {
3414 self.fork_markers.as_slice()
3415 }
3416
3417 pub fn index(&self, root: &Path) -> Result<Option<IndexUrl>, LockError> {
3419 match &self.id.source {
3420 Source::Registry(RegistrySource::Url(url)) => {
3421 let index = IndexUrl::from(VerbatimUrl::from_url(
3422 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
3423 ));
3424 Ok(Some(index))
3425 }
3426 Source::Registry(RegistrySource::Path(path)) => {
3427 let index = IndexUrl::from(
3428 VerbatimUrl::from_absolute_path(root.join(path))
3429 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
3430 );
3431 Ok(Some(index))
3432 }
3433 _ => Ok(None),
3434 }
3435 }
3436
3437 fn hashes(&self) -> HashDigests {
3439 let mut hashes = Vec::with_capacity(
3440 usize::from(self.sdist.as_ref().and_then(|sdist| sdist.hash()).is_some())
3441 + self
3442 .wheels
3443 .iter()
3444 .map(|wheel| usize::from(wheel.hash.is_some()))
3445 .sum::<usize>(),
3446 );
3447 if let Some(ref sdist) = self.sdist {
3448 if let Some(hash) = sdist.hash() {
3449 hashes.push(hash.0.clone());
3450 }
3451 }
3452 for wheel in &self.wheels {
3453 hashes.extend(wheel.hash.as_ref().map(|h| h.0.clone()));
3454 if let Some(zstd) = wheel.zstd.as_ref() {
3455 hashes.extend(zstd.hash.as_ref().map(|h| h.0.clone()));
3456 }
3457 }
3458 HashDigests::from(hashes)
3459 }
3460
3461 pub fn as_git_ref(&self) -> Result<Option<ResolvedRepositoryReference>, LockError> {
3463 match &self.id.source {
3464 Source::Git(url, git) => Ok(Some(ResolvedRepositoryReference {
3465 reference: RepositoryReference {
3466 url: RepositoryUrl::new(&url.to_url().map_err(LockErrorKind::InvalidUrl)?),
3467 reference: GitReference::from(git.kind.clone()),
3468 },
3469 sha: git.precise,
3470 })),
3471 _ => Ok(None),
3472 }
3473 }
3474
3475 fn is_dynamic(&self) -> bool {
3477 self.id.version.is_none()
3478 }
3479
3480 pub fn provides_extras(&self) -> &[ExtraName] {
3482 &self.metadata.provides_extra
3483 }
3484
3485 pub fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
3487 &self.metadata.dependency_groups
3488 }
3489
3490 pub fn dependencies(&self) -> &[Dependency] {
3492 &self.dependencies
3493 }
3494
3495 pub fn optional_dependencies(&self) -> &BTreeMap<ExtraName, Vec<Dependency>> {
3497 &self.optional_dependencies
3498 }
3499
3500 pub fn resolved_dependency_groups(&self) -> &BTreeMap<GroupName, Vec<Dependency>> {
3502 &self.dependency_groups
3503 }
3504
3505 pub fn as_install_target(&self) -> InstallTarget<'_> {
3507 InstallTarget {
3508 name: self.name(),
3509 is_local: self.id.source.is_local(),
3510 }
3511 }
3512}
3513
3514fn verbatim_url(path: &Path, id: &PackageId) -> Result<VerbatimUrl, LockError> {
3516 let url =
3517 VerbatimUrl::from_normalized_path(path).map_err(|err| LockErrorKind::VerbatimUrl {
3518 id: id.clone(),
3519 err,
3520 })?;
3521 Ok(url)
3522}
3523
3524fn absolute_path(workspace_root: &Path, path: &Path) -> Result<PathBuf, LockError> {
3526 let path = uv_fs::normalize_absolute_path(&workspace_root.join(path))
3527 .map_err(LockErrorKind::AbsolutePath)?;
3528 Ok(path)
3529}
3530
3531#[derive(Clone, Debug, serde::Deserialize)]
3532#[serde(rename_all = "kebab-case")]
3533struct PackageWire {
3534 #[serde(flatten)]
3535 id: PackageId,
3536 #[serde(default)]
3537 metadata: PackageMetadata,
3538 #[serde(default)]
3539 sdist: Option<SourceDist>,
3540 #[serde(default)]
3541 wheels: Vec<Wheel>,
3542 #[serde(default, rename = "resolution-markers")]
3543 fork_markers: Vec<SimplifiedMarkerTree>,
3544 #[serde(default)]
3545 dependencies: Vec<DependencyWire>,
3546 #[serde(default)]
3547 optional_dependencies: BTreeMap<ExtraName, Vec<DependencyWire>>,
3548 #[serde(default, rename = "dev-dependencies", alias = "dependency-groups")]
3549 dependency_groups: BTreeMap<GroupName, Vec<DependencyWire>>,
3550}
3551
3552#[derive(Clone, Default, Debug, Eq, PartialEq, serde::Deserialize)]
3553#[serde(rename_all = "kebab-case")]
3554struct PackageMetadata {
3555 #[serde(default)]
3556 requires_dist: BTreeSet<Requirement>,
3557 #[serde(default, rename = "provides-extras")]
3558 provides_extra: Box<[ExtraName]>,
3559 #[serde(default, rename = "requires-dev", alias = "dependency-groups")]
3560 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
3561}
3562
3563impl PackageWire {
3564 fn unwire(
3565 self,
3566 requires_python: &RequiresPython,
3567 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3568 ) -> Result<Package, LockError> {
3569 if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) {
3571 if let Some(version) = &self.id.version {
3572 for wheel in &self.wheels {
3573 if *version != wheel.filename.version
3574 && *version != wheel.filename.version.clone().without_local()
3575 {
3576 return Err(LockError::from(LockErrorKind::InconsistentVersions {
3577 name: self.id.name,
3578 version: version.clone(),
3579 wheel: wheel.clone(),
3580 }));
3581 }
3582 }
3583 }
3586 }
3587
3588 let unwire_deps = |deps: Vec<DependencyWire>| -> Result<Vec<Dependency>, LockError> {
3589 deps.into_iter()
3590 .map(|dep| dep.unwire(requires_python, unambiguous_package_ids))
3591 .collect()
3592 };
3593
3594 Ok(Package {
3595 id: self.id,
3596 metadata: self.metadata,
3597 sdist: self.sdist,
3598 wheels: self.wheels,
3599 fork_markers: self
3600 .fork_markers
3601 .into_iter()
3602 .map(|simplified_marker| simplified_marker.into_marker(requires_python))
3603 .map(UniversalMarker::from_combined)
3604 .collect(),
3605 dependencies: unwire_deps(self.dependencies)?,
3606 optional_dependencies: self
3607 .optional_dependencies
3608 .into_iter()
3609 .map(|(extra, deps)| Ok((extra, unwire_deps(deps)?)))
3610 .collect::<Result<_, LockError>>()?,
3611 dependency_groups: self
3612 .dependency_groups
3613 .into_iter()
3614 .map(|(group, deps)| Ok((group, unwire_deps(deps)?)))
3615 .collect::<Result<_, LockError>>()?,
3616 })
3617 }
3618}
3619
3620#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3623#[serde(rename_all = "kebab-case")]
3624pub(crate) struct PackageId {
3625 pub(crate) name: PackageName,
3626 pub(crate) version: Option<Version>,
3627 source: Source,
3628}
3629
3630impl PackageId {
3631 fn from_annotated_dist(annotated_dist: &AnnotatedDist, root: &Path) -> Result<Self, LockError> {
3632 let source = Source::from_resolved_dist(&annotated_dist.dist, root)?;
3634 let version = if source.is_source_tree()
3636 && annotated_dist
3637 .metadata
3638 .as_ref()
3639 .is_some_and(|metadata| metadata.dynamic)
3640 {
3641 None
3642 } else {
3643 Some(annotated_dist.version.clone())
3644 };
3645 let name = annotated_dist.name.clone();
3646 Ok(Self {
3647 name,
3648 version,
3649 source,
3650 })
3651 }
3652
3653 fn to_toml(&self, dist_count_by_name: Option<&FxHashMap<PackageName, u64>>, table: &mut Table) {
3660 let count = dist_count_by_name.and_then(|map| map.get(&self.name).copied());
3661 table.insert("name", value(self.name.to_string()));
3662 if count.map(|count| count > 1).unwrap_or(true) {
3663 if let Some(version) = &self.version {
3664 table.insert("version", value(version.to_string()));
3665 }
3666 self.source.to_toml(table);
3667 }
3668 }
3669}
3670
3671impl Display for PackageId {
3672 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3673 if let Some(version) = &self.version {
3674 write!(f, "{}=={} @ {}", self.name, version, self.source)
3675 } else {
3676 write!(f, "{} @ {}", self.name, self.source)
3677 }
3678 }
3679}
3680
3681#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3682#[serde(rename_all = "kebab-case")]
3683struct PackageIdForDependency {
3684 name: PackageName,
3685 version: Option<Version>,
3686 source: Option<Source>,
3687}
3688
3689impl PackageIdForDependency {
3690 fn unwire(
3691 self,
3692 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3693 ) -> Result<PackageId, LockError> {
3694 let unambiguous_package_id = unambiguous_package_ids.get(&self.name);
3695 let source = self.source.map(Ok::<_, LockError>).unwrap_or_else(|| {
3696 let Some(package_id) = unambiguous_package_id else {
3697 return Err(LockErrorKind::MissingDependencySource {
3698 name: self.name.clone(),
3699 }
3700 .into());
3701 };
3702 Ok(package_id.source.clone())
3703 })?;
3704 let version = if let Some(version) = self.version {
3705 Some(version)
3706 } else {
3707 if let Some(package_id) = unambiguous_package_id {
3708 package_id.version.clone()
3709 } else {
3710 if source.is_source_tree() {
3713 None
3714 } else {
3715 return Err(LockErrorKind::MissingDependencyVersion {
3716 name: self.name.clone(),
3717 }
3718 .into());
3719 }
3720 }
3721 };
3722 Ok(PackageId {
3723 name: self.name,
3724 version,
3725 source,
3726 })
3727 }
3728}
3729
3730impl From<PackageId> for PackageIdForDependency {
3731 fn from(id: PackageId) -> Self {
3732 Self {
3733 name: id.name,
3734 version: id.version,
3735 source: Some(id.source),
3736 }
3737 }
3738}
3739
3740#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3748#[serde(try_from = "SourceWire")]
3749enum Source {
3750 Registry(RegistrySource),
3752 Git(UrlString, GitSource),
3754 Direct(UrlString, DirectSource),
3756 Path(Box<Path>),
3758 Directory(Box<Path>),
3760 Editable(Box<Path>),
3762 Virtual(Box<Path>),
3764}
3765
3766impl Source {
3767 fn from_resolved_dist(resolved_dist: &ResolvedDist, root: &Path) -> Result<Self, LockError> {
3768 match *resolved_dist {
3769 ResolvedDist::Installed { .. } => unreachable!(),
3771 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(dist, root),
3772 }
3773 }
3774
3775 fn from_dist(dist: &Dist, root: &Path) -> Result<Self, LockError> {
3776 match *dist {
3777 Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, root),
3778 Dist::Source(ref source_dist) => Self::from_source_dist(source_dist, root),
3779 }
3780 }
3781
3782 fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result<Self, LockError> {
3783 match *built_dist {
3784 BuiltDist::Registry(ref reg_dist) => Self::from_registry_built_dist(reg_dist, root),
3785 BuiltDist::DirectUrl(ref direct_dist) => Ok(Self::from_direct_built_dist(direct_dist)),
3786 BuiltDist::Path(ref path_dist) => Self::from_path_built_dist(path_dist, root),
3787 }
3788 }
3789
3790 fn from_source_dist(
3791 source_dist: &uv_distribution_types::SourceDist,
3792 root: &Path,
3793 ) -> Result<Self, LockError> {
3794 match *source_dist {
3795 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
3796 Self::from_registry_source_dist(reg_dist, root)
3797 }
3798 uv_distribution_types::SourceDist::DirectUrl(ref direct_dist) => {
3799 Ok(Self::from_direct_source_dist(direct_dist))
3800 }
3801 uv_distribution_types::SourceDist::Git(ref git_dist) => {
3802 Ok(Self::from_git_dist(git_dist))
3803 }
3804 uv_distribution_types::SourceDist::Path(ref path_dist) => {
3805 Self::from_path_source_dist(path_dist, root)
3806 }
3807 uv_distribution_types::SourceDist::Directory(ref directory) => {
3808 Self::from_directory_source_dist(directory, root)
3809 }
3810 }
3811 }
3812
3813 fn from_registry_built_dist(
3814 reg_dist: &RegistryBuiltDist,
3815 root: &Path,
3816 ) -> Result<Self, LockError> {
3817 Self::from_index_url(®_dist.best_wheel().index, root)
3818 }
3819
3820 fn from_registry_source_dist(
3821 reg_dist: &RegistrySourceDist,
3822 root: &Path,
3823 ) -> Result<Self, LockError> {
3824 Self::from_index_url(®_dist.index, root)
3825 }
3826
3827 fn from_direct_built_dist(direct_dist: &DirectUrlBuiltDist) -> Self {
3828 Self::Direct(
3829 normalize_url(direct_dist.url.to_url()),
3830 DirectSource { subdirectory: None },
3831 )
3832 }
3833
3834 fn from_direct_source_dist(direct_dist: &DirectUrlSourceDist) -> Self {
3835 Self::Direct(
3836 normalize_url(direct_dist.url.to_url()),
3837 DirectSource {
3838 subdirectory: direct_dist.subdirectory.clone(),
3839 },
3840 )
3841 }
3842
3843 fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result<Self, LockError> {
3844 let path = try_relative_to_if(
3845 &path_dist.install_path,
3846 root,
3847 !path_dist.url.was_given_absolute(),
3848 )
3849 .map_err(LockErrorKind::DistributionRelativePath)?;
3850 Ok(Self::Path(path.into_boxed_path()))
3851 }
3852
3853 fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Self, LockError> {
3854 let path = try_relative_to_if(
3855 &path_dist.install_path,
3856 root,
3857 !path_dist.url.was_given_absolute(),
3858 )
3859 .map_err(LockErrorKind::DistributionRelativePath)?;
3860 Ok(Self::Path(path.into_boxed_path()))
3861 }
3862
3863 fn from_directory_source_dist(
3864 directory_dist: &DirectorySourceDist,
3865 root: &Path,
3866 ) -> Result<Self, LockError> {
3867 let path = try_relative_to_if(
3868 &directory_dist.install_path,
3869 root,
3870 !directory_dist.url.was_given_absolute(),
3871 )
3872 .map_err(LockErrorKind::DistributionRelativePath)?;
3873 if directory_dist.editable.unwrap_or(false) {
3874 Ok(Self::Editable(path.into_boxed_path()))
3875 } else if directory_dist.r#virtual.unwrap_or(false) {
3876 Ok(Self::Virtual(path.into_boxed_path()))
3877 } else {
3878 Ok(Self::Directory(path.into_boxed_path()))
3879 }
3880 }
3881
3882 fn from_index_url(index_url: &IndexUrl, root: &Path) -> Result<Self, LockError> {
3883 match index_url {
3884 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
3885 let redacted = index_url.without_credentials();
3887 let source = RegistrySource::Url(UrlString::from(redacted.as_ref()));
3888 Ok(Self::Registry(source))
3889 }
3890 IndexUrl::Path(url) => {
3891 let path = url
3892 .to_file_path()
3893 .map_err(|()| LockErrorKind::UrlToPath { url: url.to_url() })?;
3894 let path = try_relative_to_if(&path, root, !url.was_given_absolute())
3895 .map_err(LockErrorKind::IndexRelativePath)?;
3896 let source = RegistrySource::Path(path.into_boxed_path());
3897 Ok(Self::Registry(source))
3898 }
3899 }
3900 }
3901
3902 fn from_git_dist(git_dist: &GitSourceDist) -> Self {
3903 Self::Git(
3904 UrlString::from(locked_git_url(git_dist)),
3905 GitSource {
3906 kind: GitSourceKind::from(git_dist.git.reference().clone()),
3907 precise: git_dist.git.precise().unwrap_or_else(|| {
3908 panic!("Git distribution is missing a precise hash: {git_dist}")
3909 }),
3910 subdirectory: git_dist.subdirectory.clone(),
3911 lfs: git_dist.git.lfs(),
3912 },
3913 )
3914 }
3915
3916 fn is_immutable(&self) -> bool {
3923 matches!(self, Self::Registry(..) | Self::Git(_, _))
3924 }
3925
3926 fn is_wheel(&self) -> bool {
3928 match self {
3929 Self::Path(path) => {
3930 matches!(
3931 DistExtension::from_path(path).ok(),
3932 Some(DistExtension::Wheel)
3933 )
3934 }
3935 Self::Direct(url, _) => {
3936 matches!(
3937 DistExtension::from_path(url.as_ref()).ok(),
3938 Some(DistExtension::Wheel)
3939 )
3940 }
3941 Self::Directory(..) => false,
3942 Self::Editable(..) => false,
3943 Self::Virtual(..) => false,
3944 Self::Git(..) => false,
3945 Self::Registry(..) => false,
3946 }
3947 }
3948
3949 fn is_source_tree(&self) -> bool {
3951 match self {
3952 Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => true,
3953 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => false,
3954 }
3955 }
3956
3957 fn as_source_tree(&self) -> Option<&Path> {
3959 match self {
3960 Self::Directory(path) | Self::Editable(path) | Self::Virtual(path) => Some(path),
3961 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => None,
3962 }
3963 }
3964
3965 fn to_toml(&self, table: &mut Table) {
3966 let mut source_table = InlineTable::new();
3967 match self {
3968 Self::Registry(source) => match source {
3969 RegistrySource::Url(url) => {
3970 source_table.insert("registry", Value::from(url.as_ref()));
3971 }
3972 RegistrySource::Path(path) => {
3973 source_table.insert(
3974 "registry",
3975 Value::from(PortablePath::from(path).to_string()),
3976 );
3977 }
3978 },
3979 Self::Git(url, _) => {
3980 source_table.insert("git", Value::from(url.as_ref()));
3981 }
3982 Self::Direct(url, DirectSource { subdirectory }) => {
3983 source_table.insert("url", Value::from(url.as_ref()));
3984 if let Some(ref subdirectory) = *subdirectory {
3985 source_table.insert(
3986 "subdirectory",
3987 Value::from(PortablePath::from(subdirectory).to_string()),
3988 );
3989 }
3990 }
3991 Self::Path(path) => {
3992 source_table.insert("path", Value::from(PortablePath::from(path).to_string()));
3993 }
3994 Self::Directory(path) => {
3995 source_table.insert(
3996 "directory",
3997 Value::from(PortablePath::from(path).to_string()),
3998 );
3999 }
4000 Self::Editable(path) => {
4001 source_table.insert(
4002 "editable",
4003 Value::from(PortablePath::from(path).to_string()),
4004 );
4005 }
4006 Self::Virtual(path) => {
4007 source_table.insert("virtual", Value::from(PortablePath::from(path).to_string()));
4008 }
4009 }
4010 table.insert("source", value(source_table));
4011 }
4012
4013 pub(crate) fn is_local(&self) -> bool {
4015 matches!(
4016 self,
4017 Self::Path(_) | Self::Directory(_) | Self::Editable(_) | Self::Virtual(_)
4018 )
4019 }
4020}
4021
4022impl Display for Source {
4023 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
4024 match self {
4025 Self::Registry(RegistrySource::Url(url)) | Self::Git(url, _) | Self::Direct(url, _) => {
4026 write!(f, "{}+{}", self.name(), url)
4027 }
4028 Self::Registry(RegistrySource::Path(path))
4029 | Self::Path(path)
4030 | Self::Directory(path)
4031 | Self::Editable(path)
4032 | Self::Virtual(path) => {
4033 write!(f, "{}+{}", self.name(), PortablePath::from(path))
4034 }
4035 }
4036 }
4037}
4038
4039impl Source {
4040 fn name(&self) -> &str {
4041 match self {
4042 Self::Registry(..) => "registry",
4043 Self::Git(..) => "git",
4044 Self::Direct(..) => "direct",
4045 Self::Path(..) => "path",
4046 Self::Directory(..) => "directory",
4047 Self::Editable(..) => "editable",
4048 Self::Virtual(..) => "virtual",
4049 }
4050 }
4051
4052 fn requires_hash(&self) -> Option<bool> {
4060 match self {
4061 Self::Registry(..) => None,
4062 Self::Direct(..) | Self::Path(..) => Some(true),
4063 Self::Git(..) | Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => {
4064 Some(false)
4065 }
4066 }
4067 }
4068}
4069
4070#[derive(Clone, Debug, serde::Deserialize)]
4071#[serde(untagged, rename_all = "kebab-case")]
4072enum SourceWire {
4073 Registry {
4074 registry: RegistrySourceWire,
4075 },
4076 Git {
4077 git: String,
4078 },
4079 Direct {
4080 url: UrlString,
4081 subdirectory: Option<PortablePathBuf>,
4082 },
4083 Path {
4084 path: PortablePathBuf,
4085 },
4086 Directory {
4087 directory: PortablePathBuf,
4088 },
4089 Editable {
4090 editable: PortablePathBuf,
4091 },
4092 Virtual {
4093 r#virtual: PortablePathBuf,
4094 },
4095}
4096
4097impl TryFrom<SourceWire> for Source {
4098 type Error = LockError;
4099
4100 fn try_from(wire: SourceWire) -> Result<Self, LockError> {
4101 use self::SourceWire::{Direct, Directory, Editable, Git, Path, Registry, Virtual};
4102
4103 match wire {
4104 Registry { registry } => Ok(Self::Registry(registry.into())),
4105 Git { git } => {
4106 let url = DisplaySafeUrl::parse(&git)
4107 .map_err(|err| SourceParseError::InvalidUrl {
4108 given: git.clone(),
4109 err,
4110 })
4111 .map_err(LockErrorKind::InvalidGitSourceUrl)?;
4112
4113 let git_source = GitSource::from_url(&url)
4114 .map_err(|err| match err {
4115 GitSourceError::InvalidSha => SourceParseError::InvalidSha { given: git },
4116 GitSourceError::MissingSha => SourceParseError::MissingSha { given: git },
4117 })
4118 .map_err(LockErrorKind::InvalidGitSourceUrl)?;
4119
4120 Ok(Self::Git(UrlString::from(url), git_source))
4121 }
4122 Direct { url, subdirectory } => Ok(Self::Direct(
4123 url,
4124 DirectSource {
4125 subdirectory: subdirectory.map(Box::<std::path::Path>::from),
4126 },
4127 )),
4128 Path { path } => Ok(Self::Path(path.into())),
4129 Directory { directory } => Ok(Self::Directory(directory.into())),
4130 Editable { editable } => Ok(Self::Editable(editable.into())),
4131 Virtual { r#virtual } => Ok(Self::Virtual(r#virtual.into())),
4132 }
4133 }
4134}
4135
4136#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
4138enum RegistrySource {
4139 Url(UrlString),
4141 Path(Box<Path>),
4143}
4144
4145impl Display for RegistrySource {
4146 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
4147 match self {
4148 Self::Url(url) => write!(f, "{url}"),
4149 Self::Path(path) => write!(f, "{}", path.display()),
4150 }
4151 }
4152}
4153
4154#[derive(Clone, Debug)]
4155enum RegistrySourceWire {
4156 Url(UrlString),
4158 Path(PortablePathBuf),
4160}
4161
4162impl<'de> serde::de::Deserialize<'de> for RegistrySourceWire {
4163 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4164 where
4165 D: serde::de::Deserializer<'de>,
4166 {
4167 struct Visitor;
4168
4169 impl serde::de::Visitor<'_> for Visitor {
4170 type Value = RegistrySourceWire;
4171
4172 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
4173 formatter.write_str("a valid URL or a file path")
4174 }
4175
4176 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
4177 where
4178 E: serde::de::Error,
4179 {
4180 if split_scheme(value).is_some_and(|(scheme, _)| Scheme::parse(scheme).is_some()) {
4181 Ok(
4182 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
4183 value,
4184 ))
4185 .map(RegistrySourceWire::Url)?,
4186 )
4187 } else {
4188 Ok(
4189 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
4190 value,
4191 ))
4192 .map(RegistrySourceWire::Path)?,
4193 )
4194 }
4195 }
4196 }
4197
4198 deserializer.deserialize_str(Visitor)
4199 }
4200}
4201
4202impl From<RegistrySourceWire> for RegistrySource {
4203 fn from(wire: RegistrySourceWire) -> Self {
4204 match wire {
4205 RegistrySourceWire::Url(url) => Self::Url(url),
4206 RegistrySourceWire::Path(path) => Self::Path(path.into()),
4207 }
4208 }
4209}
4210
4211#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4212#[serde(rename_all = "kebab-case")]
4213struct DirectSource {
4214 subdirectory: Option<Box<Path>>,
4215}
4216
4217#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
4222struct GitSource {
4223 precise: GitOid,
4224 subdirectory: Option<Box<Path>>,
4225 kind: GitSourceKind,
4226 lfs: GitLfs,
4227}
4228
4229#[derive(Clone, Debug, Eq, PartialEq)]
4231enum GitSourceError {
4232 InvalidSha,
4233 MissingSha,
4234}
4235
4236impl GitSource {
4237 fn from_url(url: &Url) -> Result<Self, GitSourceError> {
4240 let mut kind = GitSourceKind::DefaultBranch;
4241 let mut subdirectory = None;
4242 let mut lfs = GitLfs::Disabled;
4243 for (key, val) in url.query_pairs() {
4244 match &*key {
4245 "tag" => kind = GitSourceKind::Tag(val.into_owned()),
4246 "branch" => kind = GitSourceKind::Branch(val.into_owned()),
4247 "rev" => kind = GitSourceKind::Rev(val.into_owned()),
4248 "subdirectory" => subdirectory = Some(PortablePathBuf::from(val.as_ref()).into()),
4249 "lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
4250 _ => {}
4251 }
4252 }
4253
4254 let precise = GitOid::from_str(url.fragment().ok_or(GitSourceError::MissingSha)?)
4255 .map_err(|_| GitSourceError::InvalidSha)?;
4256
4257 Ok(Self {
4258 precise,
4259 subdirectory,
4260 kind,
4261 lfs,
4262 })
4263 }
4264}
4265
4266#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4267#[serde(rename_all = "kebab-case")]
4268enum GitSourceKind {
4269 Tag(String),
4270 Branch(String),
4271 Rev(String),
4272 DefaultBranch,
4273}
4274
4275#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4277#[serde(rename_all = "kebab-case")]
4278struct SourceDistMetadata {
4279 hash: Option<Hash>,
4281 size: Option<u64>,
4285 #[serde(alias = "upload_time")]
4287 upload_time: Option<Timestamp>,
4288}
4289
4290#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4295#[serde(from = "SourceDistWire")]
4296enum SourceDist {
4297 Url {
4298 url: UrlString,
4299 #[serde(flatten)]
4300 metadata: SourceDistMetadata,
4301 },
4302 Path {
4303 path: Box<Path>,
4304 #[serde(flatten)]
4305 metadata: SourceDistMetadata,
4306 },
4307 Metadata {
4308 #[serde(flatten)]
4309 metadata: SourceDistMetadata,
4310 },
4311}
4312
4313impl SourceDist {
4314 fn filename(&self) -> Option<Cow<'_, str>> {
4315 match self {
4316 Self::Metadata { .. } => None,
4317 Self::Url { url, .. } => url.filename().ok(),
4318 Self::Path { path, .. } => path.file_name().map(|filename| filename.to_string_lossy()),
4319 }
4320 }
4321
4322 fn url(&self) -> Option<&UrlString> {
4323 match self {
4324 Self::Metadata { .. } => None,
4325 Self::Url { url, .. } => Some(url),
4326 Self::Path { .. } => None,
4327 }
4328 }
4329
4330 pub(crate) fn hash(&self) -> Option<&Hash> {
4331 match self {
4332 Self::Metadata { metadata } => metadata.hash.as_ref(),
4333 Self::Url { metadata, .. } => metadata.hash.as_ref(),
4334 Self::Path { metadata, .. } => metadata.hash.as_ref(),
4335 }
4336 }
4337
4338 pub(crate) fn size(&self) -> Option<u64> {
4339 match self {
4340 Self::Metadata { metadata } => metadata.size,
4341 Self::Url { metadata, .. } => metadata.size,
4342 Self::Path { metadata, .. } => metadata.size,
4343 }
4344 }
4345
4346 pub(crate) fn upload_time(&self) -> Option<Timestamp> {
4347 match self {
4348 Self::Metadata { metadata } => metadata.upload_time,
4349 Self::Url { metadata, .. } => metadata.upload_time,
4350 Self::Path { metadata, .. } => metadata.upload_time,
4351 }
4352 }
4353}
4354
4355impl SourceDist {
4356 fn from_annotated_dist(
4357 id: &PackageId,
4358 annotated_dist: &AnnotatedDist,
4359 ) -> Result<Option<Self>, LockError> {
4360 match annotated_dist.dist {
4361 ResolvedDist::Installed { .. } => unreachable!(),
4363 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4364 id,
4365 dist,
4366 annotated_dist.hashes.as_slice(),
4367 annotated_dist.index(),
4368 ),
4369 }
4370 }
4371
4372 fn from_dist(
4373 id: &PackageId,
4374 dist: &Dist,
4375 hashes: &[HashDigest],
4376 index: Option<&IndexUrl>,
4377 ) -> Result<Option<Self>, LockError> {
4378 match *dist {
4379 Dist::Built(BuiltDist::Registry(ref built_dist)) => {
4380 let Some(sdist) = built_dist.sdist.as_ref() else {
4381 return Ok(None);
4382 };
4383 Self::from_registry_dist(sdist, index)
4384 }
4385 Dist::Built(_) => Ok(None),
4386 Dist::Source(ref source_dist) => Self::from_source_dist(id, source_dist, hashes, index),
4387 }
4388 }
4389
4390 fn from_source_dist(
4391 id: &PackageId,
4392 source_dist: &uv_distribution_types::SourceDist,
4393 hashes: &[HashDigest],
4394 index: Option<&IndexUrl>,
4395 ) -> Result<Option<Self>, LockError> {
4396 match *source_dist {
4397 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
4398 Self::from_registry_dist(reg_dist, index)
4399 }
4400 uv_distribution_types::SourceDist::DirectUrl(_) => {
4401 Self::from_direct_dist(id, hashes).map(Some)
4402 }
4403 uv_distribution_types::SourceDist::Path(_) => {
4404 Self::from_path_dist(id, hashes).map(Some)
4405 }
4406 uv_distribution_types::SourceDist::Git(_)
4410 | uv_distribution_types::SourceDist::Directory(_) => Ok(None),
4411 }
4412 }
4413
4414 fn from_registry_dist(
4415 reg_dist: &RegistrySourceDist,
4416 index: Option<&IndexUrl>,
4417 ) -> Result<Option<Self>, LockError> {
4418 if index.is_none_or(|index| *index != reg_dist.index) {
4421 return Ok(None);
4422 }
4423
4424 match ®_dist.index {
4425 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4426 let url = normalize_file_location(®_dist.file.url)
4427 .map_err(LockErrorKind::InvalidUrl)
4428 .map_err(LockError::from)?;
4429 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4430 let size = reg_dist.file.size;
4431 let upload_time = reg_dist
4432 .file
4433 .upload_time_utc_ms
4434 .map(Timestamp::from_millisecond)
4435 .transpose()
4436 .map_err(LockErrorKind::InvalidTimestamp)?;
4437 Ok(Some(Self::Url {
4438 url,
4439 metadata: SourceDistMetadata {
4440 hash,
4441 size,
4442 upload_time,
4443 },
4444 }))
4445 }
4446 IndexUrl::Path(path) => {
4447 let index_path = path
4448 .to_file_path()
4449 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4450 let url = reg_dist
4451 .file
4452 .url
4453 .to_url()
4454 .map_err(LockErrorKind::InvalidUrl)?;
4455
4456 if url.scheme() == "file" {
4457 let reg_dist_path = url
4458 .to_file_path()
4459 .map_err(|()| LockErrorKind::UrlToPath { url })?;
4460 let path =
4461 try_relative_to_if(®_dist_path, index_path, !path.was_given_absolute())
4462 .map_err(LockErrorKind::DistributionRelativePath)?
4463 .into_boxed_path();
4464 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4465 let size = reg_dist.file.size;
4466 let upload_time = reg_dist
4467 .file
4468 .upload_time_utc_ms
4469 .map(Timestamp::from_millisecond)
4470 .transpose()
4471 .map_err(LockErrorKind::InvalidTimestamp)?;
4472 Ok(Some(Self::Path {
4473 path,
4474 metadata: SourceDistMetadata {
4475 hash,
4476 size,
4477 upload_time,
4478 },
4479 }))
4480 } else {
4481 let url = normalize_file_location(®_dist.file.url)
4482 .map_err(LockErrorKind::InvalidUrl)
4483 .map_err(LockError::from)?;
4484 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4485 let size = reg_dist.file.size;
4486 let upload_time = reg_dist
4487 .file
4488 .upload_time_utc_ms
4489 .map(Timestamp::from_millisecond)
4490 .transpose()
4491 .map_err(LockErrorKind::InvalidTimestamp)?;
4492 Ok(Some(Self::Url {
4493 url,
4494 metadata: SourceDistMetadata {
4495 hash,
4496 size,
4497 upload_time,
4498 },
4499 }))
4500 }
4501 }
4502 }
4503 }
4504
4505 fn from_direct_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4506 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4507 let kind = LockErrorKind::Hash {
4508 id: id.clone(),
4509 artifact_type: "direct URL source distribution",
4510 expected: true,
4511 };
4512 return Err(kind.into());
4513 };
4514 Ok(Self::Metadata {
4515 metadata: SourceDistMetadata {
4516 hash: Some(hash),
4517 size: None,
4518 upload_time: None,
4519 },
4520 })
4521 }
4522
4523 fn from_path_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4524 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4525 let kind = LockErrorKind::Hash {
4526 id: id.clone(),
4527 artifact_type: "path source distribution",
4528 expected: true,
4529 };
4530 return Err(kind.into());
4531 };
4532 Ok(Self::Metadata {
4533 metadata: SourceDistMetadata {
4534 hash: Some(hash),
4535 size: None,
4536 upload_time: None,
4537 },
4538 })
4539 }
4540}
4541
4542#[derive(Clone, Debug, serde::Deserialize)]
4543#[serde(untagged, rename_all = "kebab-case")]
4544enum SourceDistWire {
4545 Url {
4546 url: UrlString,
4547 #[serde(flatten)]
4548 metadata: SourceDistMetadata,
4549 },
4550 Path {
4551 path: PortablePathBuf,
4552 #[serde(flatten)]
4553 metadata: SourceDistMetadata,
4554 },
4555 Metadata {
4556 #[serde(flatten)]
4557 metadata: SourceDistMetadata,
4558 },
4559}
4560
4561impl SourceDist {
4562 fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
4564 let mut table = InlineTable::new();
4565 match self {
4566 Self::Metadata { .. } => {}
4567 Self::Url { url, .. } => {
4568 table.insert("url", Value::from(url.as_ref()));
4569 }
4570 Self::Path { path, .. } => {
4571 table.insert("path", Value::from(PortablePath::from(path).to_string()));
4572 }
4573 }
4574 if let Some(hash) = self.hash() {
4575 table.insert("hash", Value::from(hash.to_string()));
4576 }
4577 if let Some(size) = self.size() {
4578 table.insert(
4579 "size",
4580 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4581 );
4582 }
4583 if let Some(upload_time) = self.upload_time() {
4584 table.insert("upload-time", Value::from(upload_time.to_string()));
4585 }
4586 Ok(table)
4587 }
4588}
4589
4590impl From<SourceDistWire> for SourceDist {
4591 fn from(wire: SourceDistWire) -> Self {
4592 match wire {
4593 SourceDistWire::Url { url, metadata } => Self::Url { url, metadata },
4594 SourceDistWire::Path { path, metadata } => Self::Path {
4595 path: path.into(),
4596 metadata,
4597 },
4598 SourceDistWire::Metadata { metadata } => Self::Metadata { metadata },
4599 }
4600 }
4601}
4602
4603impl From<GitReference> for GitSourceKind {
4604 fn from(value: GitReference) -> Self {
4605 match value {
4606 GitReference::Branch(branch) => Self::Branch(branch),
4607 GitReference::Tag(tag) => Self::Tag(tag),
4608 GitReference::BranchOrTag(rev) => Self::Rev(rev),
4609 GitReference::BranchOrTagOrCommit(rev) => Self::Rev(rev),
4610 GitReference::NamedRef(rev) => Self::Rev(rev),
4611 GitReference::DefaultBranch => Self::DefaultBranch,
4612 }
4613 }
4614}
4615
4616impl From<GitSourceKind> for GitReference {
4617 fn from(value: GitSourceKind) -> Self {
4618 match value {
4619 GitSourceKind::Branch(branch) => Self::Branch(branch),
4620 GitSourceKind::Tag(tag) => Self::Tag(tag),
4621 GitSourceKind::Rev(rev) => Self::from_rev(rev),
4622 GitSourceKind::DefaultBranch => Self::DefaultBranch,
4623 }
4624 }
4625}
4626
4627fn locked_git_url(git_dist: &GitSourceDist) -> DisplaySafeUrl {
4629 let mut url = git_dist.git.url().clone();
4630
4631 url.remove_credentials();
4633
4634 url.set_fragment(None);
4636 url.set_query(None);
4637
4638 if let Some(subdirectory) = git_dist
4640 .subdirectory
4641 .as_deref()
4642 .map(PortablePath::from)
4643 .as_ref()
4644 .map(PortablePath::to_string)
4645 {
4646 url.query_pairs_mut()
4647 .append_pair("subdirectory", &subdirectory);
4648 }
4649
4650 if git_dist.git.lfs().enabled() {
4652 url.query_pairs_mut().append_pair("lfs", "true");
4653 }
4654
4655 match git_dist.git.reference() {
4657 GitReference::Branch(branch) => {
4658 url.query_pairs_mut().append_pair("branch", branch.as_str());
4659 }
4660 GitReference::Tag(tag) => {
4661 url.query_pairs_mut().append_pair("tag", tag.as_str());
4662 }
4663 GitReference::BranchOrTag(rev)
4664 | GitReference::BranchOrTagOrCommit(rev)
4665 | GitReference::NamedRef(rev) => {
4666 url.query_pairs_mut().append_pair("rev", rev.as_str());
4667 }
4668 GitReference::DefaultBranch => {}
4669 }
4670
4671 url.set_fragment(
4673 git_dist
4674 .git
4675 .precise()
4676 .as_ref()
4677 .map(GitOid::to_string)
4678 .as_deref(),
4679 );
4680
4681 url
4682}
4683
4684#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4685struct ZstdWheel {
4686 hash: Option<Hash>,
4687 size: Option<u64>,
4688}
4689
4690#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4692#[serde(try_from = "WheelWire")]
4693struct Wheel {
4694 url: WheelWireSource,
4699 hash: Option<Hash>,
4705 size: Option<u64>,
4709 upload_time: Option<Timestamp>,
4713 filename: WheelFilename,
4720 zstd: Option<ZstdWheel>,
4722}
4723
4724impl Wheel {
4725 fn from_annotated_dist(annotated_dist: &AnnotatedDist) -> Result<Vec<Self>, LockError> {
4726 match annotated_dist.dist {
4727 ResolvedDist::Installed { .. } => unreachable!(),
4729 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4730 dist,
4731 annotated_dist.hashes.as_slice(),
4732 annotated_dist.index(),
4733 ),
4734 }
4735 }
4736
4737 fn from_dist(
4738 dist: &Dist,
4739 hashes: &[HashDigest],
4740 index: Option<&IndexUrl>,
4741 ) -> Result<Vec<Self>, LockError> {
4742 match *dist {
4743 Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, hashes, index),
4744 Dist::Source(uv_distribution_types::SourceDist::Registry(ref source_dist)) => {
4745 source_dist
4746 .wheels
4747 .iter()
4748 .filter(|wheel| {
4749 index.is_some_and(|index| *index == wheel.index)
4752 })
4753 .map(Self::from_registry_wheel)
4754 .collect()
4755 }
4756 Dist::Source(_) => Ok(vec![]),
4757 }
4758 }
4759
4760 fn from_built_dist(
4761 built_dist: &BuiltDist,
4762 hashes: &[HashDigest],
4763 index: Option<&IndexUrl>,
4764 ) -> Result<Vec<Self>, LockError> {
4765 match *built_dist {
4766 BuiltDist::Registry(ref reg_dist) => Self::from_registry_dist(reg_dist, index),
4767 BuiltDist::DirectUrl(ref direct_dist) => {
4768 Ok(vec![Self::from_direct_dist(direct_dist, hashes)])
4769 }
4770 BuiltDist::Path(ref path_dist) => Ok(vec![Self::from_path_dist(path_dist, hashes)]),
4771 }
4772 }
4773
4774 fn from_registry_dist(
4775 reg_dist: &RegistryBuiltDist,
4776 index: Option<&IndexUrl>,
4777 ) -> Result<Vec<Self>, LockError> {
4778 reg_dist
4779 .wheels
4780 .iter()
4781 .filter(|wheel| {
4782 index.is_some_and(|index| *index == wheel.index)
4785 })
4786 .map(Self::from_registry_wheel)
4787 .collect()
4788 }
4789
4790 fn from_registry_wheel(wheel: &RegistryBuiltWheel) -> Result<Self, LockError> {
4791 let url = match &wheel.index {
4792 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4793 let url = normalize_file_location(&wheel.file.url)
4794 .map_err(LockErrorKind::InvalidUrl)
4795 .map_err(LockError::from)?;
4796 WheelWireSource::Url { url }
4797 }
4798 IndexUrl::Path(path) => {
4799 let index_path = path
4800 .to_file_path()
4801 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4802 let wheel_url = wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?;
4803
4804 if wheel_url.scheme() == "file" {
4805 let wheel_path = wheel_url
4806 .to_file_path()
4807 .map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?;
4808 let path =
4809 try_relative_to_if(&wheel_path, index_path, !path.was_given_absolute())
4810 .map_err(LockErrorKind::DistributionRelativePath)?
4811 .into_boxed_path();
4812 WheelWireSource::Path { path }
4813 } else {
4814 let url = normalize_file_location(&wheel.file.url)
4815 .map_err(LockErrorKind::InvalidUrl)
4816 .map_err(LockError::from)?;
4817 WheelWireSource::Url { url }
4818 }
4819 }
4820 };
4821 let filename = wheel.filename.clone();
4822 let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from);
4823 let size = wheel.file.size;
4824 let upload_time = wheel
4825 .file
4826 .upload_time_utc_ms
4827 .map(Timestamp::from_millisecond)
4828 .transpose()
4829 .map_err(LockErrorKind::InvalidTimestamp)?;
4830 let zstd = wheel.file.zstd.as_ref().map(|zstd| ZstdWheel {
4831 hash: zstd.hashes.iter().max().cloned().map(Hash::from),
4832 size: zstd.size,
4833 });
4834 Ok(Self {
4835 url,
4836 hash,
4837 size,
4838 upload_time,
4839 filename,
4840 zstd,
4841 })
4842 }
4843
4844 fn from_direct_dist(direct_dist: &DirectUrlBuiltDist, hashes: &[HashDigest]) -> Self {
4845 Self {
4846 url: WheelWireSource::Url {
4847 url: normalize_url(direct_dist.url.to_url()),
4848 },
4849 hash: hashes.iter().max().cloned().map(Hash::from),
4850 size: None,
4851 upload_time: None,
4852 filename: direct_dist.filename.clone(),
4853 zstd: None,
4854 }
4855 }
4856
4857 fn from_path_dist(path_dist: &PathBuiltDist, hashes: &[HashDigest]) -> Self {
4858 Self {
4859 url: WheelWireSource::Filename {
4860 filename: path_dist.filename.clone(),
4861 },
4862 hash: hashes.iter().max().cloned().map(Hash::from),
4863 size: None,
4864 upload_time: None,
4865 filename: path_dist.filename.clone(),
4866 zstd: None,
4867 }
4868 }
4869
4870 pub(crate) fn to_registry_wheel(
4871 &self,
4872 source: &RegistrySource,
4873 root: &Path,
4874 ) -> Result<RegistryBuiltWheel, LockError> {
4875 let filename: WheelFilename = self.filename.clone();
4876
4877 match source {
4878 RegistrySource::Url(url) => {
4879 let file_location = match &self.url {
4880 WheelWireSource::Url { url: file_url } => {
4881 FileLocation::AbsoluteUrl(file_url.clone())
4882 }
4883 WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => {
4884 return Err(LockErrorKind::MissingUrl {
4885 name: filename.name,
4886 version: filename.version,
4887 }
4888 .into());
4889 }
4890 };
4891 let file = Box::new(uv_distribution_types::File {
4892 dist_info_metadata: false,
4893 filename: SmallString::from(filename.to_string()),
4894 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
4895 requires_python: None,
4896 size: self.size,
4897 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
4898 url: file_location,
4899 yanked: None,
4900 zstd: self
4901 .zstd
4902 .as_ref()
4903 .map(|zstd| uv_distribution_types::Zstd {
4904 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
4905 size: zstd.size,
4906 })
4907 .map(Box::new),
4908 });
4909 let index = IndexUrl::from(VerbatimUrl::from_url(
4910 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
4911 ));
4912 Ok(RegistryBuiltWheel {
4913 filename,
4914 file,
4915 index,
4916 })
4917 }
4918 RegistrySource::Path(index_path) => {
4919 let file_location = match &self.url {
4920 WheelWireSource::Url { url: file_url } => {
4921 FileLocation::AbsoluteUrl(file_url.clone())
4922 }
4923 WheelWireSource::Path { path: file_path } => {
4924 let file_path = root.join(index_path).join(file_path);
4925 let file_url =
4926 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
4927 LockErrorKind::PathToUrl {
4928 path: file_path.into_boxed_path(),
4929 }
4930 })?;
4931 FileLocation::AbsoluteUrl(UrlString::from(file_url))
4932 }
4933 WheelWireSource::Filename { .. } => {
4934 return Err(LockErrorKind::MissingPath {
4935 name: filename.name,
4936 version: filename.version,
4937 }
4938 .into());
4939 }
4940 };
4941 let file = Box::new(uv_distribution_types::File {
4942 dist_info_metadata: false,
4943 filename: SmallString::from(filename.to_string()),
4944 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
4945 requires_python: None,
4946 size: self.size,
4947 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
4948 url: file_location,
4949 yanked: None,
4950 zstd: self
4951 .zstd
4952 .as_ref()
4953 .map(|zstd| uv_distribution_types::Zstd {
4954 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
4955 size: zstd.size,
4956 })
4957 .map(Box::new),
4958 });
4959 let index = IndexUrl::from(
4960 VerbatimUrl::from_absolute_path(root.join(index_path))
4961 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
4962 );
4963 Ok(RegistryBuiltWheel {
4964 filename,
4965 file,
4966 index,
4967 })
4968 }
4969 }
4970 }
4971}
4972
4973#[derive(Clone, Debug, serde::Deserialize)]
4974#[serde(rename_all = "kebab-case")]
4975struct WheelWire {
4976 #[serde(flatten)]
4977 url: WheelWireSource,
4978 hash: Option<Hash>,
4984 size: Option<u64>,
4988 #[serde(alias = "upload_time")]
4992 upload_time: Option<Timestamp>,
4993 #[serde(alias = "zstd")]
4995 zstd: Option<ZstdWheel>,
4996}
4997
4998#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4999#[serde(untagged, rename_all = "kebab-case")]
5000enum WheelWireSource {
5001 Url {
5003 url: UrlString,
5008 },
5009 Path {
5011 path: Box<Path>,
5013 },
5014 Filename {
5018 filename: WheelFilename,
5021 },
5022}
5023
5024impl Wheel {
5025 fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
5027 let mut table = InlineTable::new();
5028 match &self.url {
5029 WheelWireSource::Url { url } => {
5030 table.insert("url", Value::from(url.as_ref()));
5031 }
5032 WheelWireSource::Path { path } => {
5033 table.insert("path", Value::from(PortablePath::from(path).to_string()));
5034 }
5035 WheelWireSource::Filename { filename } => {
5036 table.insert("filename", Value::from(filename.to_string()));
5037 }
5038 }
5039 if let Some(ref hash) = self.hash {
5040 table.insert("hash", Value::from(hash.to_string()));
5041 }
5042 if let Some(size) = self.size {
5043 table.insert(
5044 "size",
5045 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
5046 );
5047 }
5048 if let Some(upload_time) = self.upload_time {
5049 table.insert("upload-time", Value::from(upload_time.to_string()));
5050 }
5051 if let Some(zstd) = &self.zstd {
5052 let mut inner = InlineTable::new();
5053 if let Some(ref hash) = zstd.hash {
5054 inner.insert("hash", Value::from(hash.to_string()));
5055 }
5056 if let Some(size) = zstd.size {
5057 inner.insert(
5058 "size",
5059 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
5060 );
5061 }
5062 table.insert("zstd", Value::from(inner));
5063 }
5064 Ok(table)
5065 }
5066}
5067
5068impl TryFrom<WheelWire> for Wheel {
5069 type Error = String;
5070
5071 fn try_from(wire: WheelWire) -> Result<Self, String> {
5072 let filename = match &wire.url {
5073 WheelWireSource::Url { url } => {
5074 let filename = url.filename().map_err(|err| err.to_string())?;
5075 filename.parse::<WheelFilename>().map_err(|err| {
5076 format!("failed to parse `{filename}` as wheel filename: {err}")
5077 })?
5078 }
5079 WheelWireSource::Path { path } => {
5080 let filename = path
5081 .file_name()
5082 .and_then(|file_name| file_name.to_str())
5083 .ok_or_else(|| {
5084 format!("path `{}` has no filename component", path.display())
5085 })?;
5086 filename.parse::<WheelFilename>().map_err(|err| {
5087 format!("failed to parse `{filename}` as wheel filename: {err}")
5088 })?
5089 }
5090 WheelWireSource::Filename { filename } => filename.clone(),
5091 };
5092
5093 Ok(Self {
5094 url: wire.url,
5095 hash: wire.hash,
5096 size: wire.size,
5097 upload_time: wire.upload_time,
5098 zstd: wire.zstd,
5099 filename,
5100 })
5101 }
5102}
5103
5104#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
5106pub struct Dependency {
5107 package_id: PackageId,
5108 extra: BTreeSet<ExtraName>,
5109 simplified_marker: SimplifiedMarkerTree,
5129 complexified_marker: UniversalMarker,
5133}
5134
5135impl Dependency {
5136 fn new(
5137 requires_python: &RequiresPython,
5138 package_id: PackageId,
5139 extra: BTreeSet<ExtraName>,
5140 complexified_marker: UniversalMarker,
5141 ) -> Self {
5142 let simplified_marker =
5143 SimplifiedMarkerTree::new(requires_python, complexified_marker.combined());
5144 let complexified_marker = simplified_marker.into_marker(requires_python);
5145 Self {
5146 package_id,
5147 extra,
5148 simplified_marker,
5149 complexified_marker: UniversalMarker::from_combined(complexified_marker),
5150 }
5151 }
5152
5153 fn from_annotated_dist(
5154 requires_python: &RequiresPython,
5155 annotated_dist: &AnnotatedDist,
5156 complexified_marker: UniversalMarker,
5157 root: &Path,
5158 ) -> Result<Self, LockError> {
5159 let package_id = PackageId::from_annotated_dist(annotated_dist, root)?;
5160 let extra = annotated_dist.extra.iter().cloned().collect();
5161 Ok(Self::new(
5162 requires_python,
5163 package_id,
5164 extra,
5165 complexified_marker,
5166 ))
5167 }
5168
5169 fn to_toml(
5171 &self,
5172 _requires_python: &RequiresPython,
5173 dist_count_by_name: &FxHashMap<PackageName, u64>,
5174 ) -> Table {
5175 let mut table = Table::new();
5176 self.package_id
5177 .to_toml(Some(dist_count_by_name), &mut table);
5178 if !self.extra.is_empty() {
5179 let extra_array = self
5180 .extra
5181 .iter()
5182 .map(ToString::to_string)
5183 .collect::<Array>();
5184 table.insert("extra", value(extra_array));
5185 }
5186 if let Some(marker) = self.simplified_marker.try_to_string() {
5187 table.insert("marker", value(marker));
5188 }
5189
5190 table
5191 }
5192
5193 pub fn package_name(&self) -> &PackageName {
5195 &self.package_id.name
5196 }
5197
5198 pub fn extra(&self) -> &BTreeSet<ExtraName> {
5200 &self.extra
5201 }
5202}
5203
5204impl Display for Dependency {
5205 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5206 match (self.extra.is_empty(), self.package_id.version.as_ref()) {
5207 (true, Some(version)) => write!(f, "{}=={}", self.package_id.name, version),
5208 (true, None) => write!(f, "{}", self.package_id.name),
5209 (false, Some(version)) => write!(
5210 f,
5211 "{}[{}]=={}",
5212 self.package_id.name,
5213 self.extra.iter().join(","),
5214 version
5215 ),
5216 (false, None) => write!(
5217 f,
5218 "{}[{}]",
5219 self.package_id.name,
5220 self.extra.iter().join(",")
5221 ),
5222 }
5223 }
5224}
5225
5226#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)]
5228#[serde(rename_all = "kebab-case")]
5229struct DependencyWire {
5230 #[serde(flatten)]
5231 package_id: PackageIdForDependency,
5232 #[serde(default)]
5233 extra: BTreeSet<ExtraName>,
5234 #[serde(default)]
5235 marker: SimplifiedMarkerTree,
5236}
5237
5238impl DependencyWire {
5239 fn unwire(
5240 self,
5241 requires_python: &RequiresPython,
5242 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
5243 ) -> Result<Dependency, LockError> {
5244 let complexified_marker = self.marker.into_marker(requires_python);
5245 Ok(Dependency {
5246 package_id: self.package_id.unwire(unambiguous_package_ids)?,
5247 extra: self.extra,
5248 simplified_marker: self.marker,
5249 complexified_marker: UniversalMarker::from_combined(complexified_marker),
5250 })
5251 }
5252}
5253
5254#[derive(Clone, Debug, PartialEq, Eq)]
5259struct Hash(HashDigest);
5260
5261impl From<HashDigest> for Hash {
5262 fn from(hd: HashDigest) -> Self {
5263 Self(hd)
5264 }
5265}
5266
5267impl FromStr for Hash {
5268 type Err = HashParseError;
5269
5270 fn from_str(s: &str) -> Result<Self, HashParseError> {
5271 let (algorithm, digest) = s.split_once(':').ok_or(HashParseError(
5272 "expected '{algorithm}:{digest}', but found no ':' in hash digest",
5273 ))?;
5274 let algorithm = algorithm
5275 .parse()
5276 .map_err(|_| HashParseError("unrecognized hash algorithm"))?;
5277 Ok(Self(HashDigest {
5278 algorithm,
5279 digest: digest.into(),
5280 }))
5281 }
5282}
5283
5284impl Display for Hash {
5285 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5286 write!(f, "{}:{}", self.0.algorithm, self.0.digest)
5287 }
5288}
5289
5290impl<'de> serde::Deserialize<'de> for Hash {
5291 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5292 where
5293 D: serde::de::Deserializer<'de>,
5294 {
5295 struct Visitor;
5296
5297 impl serde::de::Visitor<'_> for Visitor {
5298 type Value = Hash;
5299
5300 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
5301 f.write_str("a string")
5302 }
5303
5304 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
5305 Hash::from_str(v).map_err(serde::de::Error::custom)
5306 }
5307 }
5308
5309 deserializer.deserialize_str(Visitor)
5310 }
5311}
5312
5313impl From<Hash> for Hashes {
5314 fn from(value: Hash) -> Self {
5315 match value.0.algorithm {
5316 HashAlgorithm::Md5 => Self {
5317 md5: Some(value.0.digest),
5318 sha256: None,
5319 sha384: None,
5320 sha512: None,
5321 blake2b: None,
5322 },
5323 HashAlgorithm::Sha256 => Self {
5324 md5: None,
5325 sha256: Some(value.0.digest),
5326 sha384: None,
5327 sha512: None,
5328 blake2b: None,
5329 },
5330 HashAlgorithm::Sha384 => Self {
5331 md5: None,
5332 sha256: None,
5333 sha384: Some(value.0.digest),
5334 sha512: None,
5335 blake2b: None,
5336 },
5337 HashAlgorithm::Sha512 => Self {
5338 md5: None,
5339 sha256: None,
5340 sha384: None,
5341 sha512: Some(value.0.digest),
5342 blake2b: None,
5343 },
5344 HashAlgorithm::Blake2b => Self {
5345 md5: None,
5346 sha256: None,
5347 sha384: None,
5348 sha512: None,
5349 blake2b: Some(value.0.digest),
5350 },
5351 }
5352 }
5353}
5354
5355fn normalize_file_location(location: &FileLocation) -> Result<UrlString, ToUrlError> {
5357 match location {
5358 FileLocation::AbsoluteUrl(absolute) => Ok(absolute.without_fragment().into_owned()),
5359 FileLocation::RelativeUrl(_, _) => Ok(normalize_url(location.to_url()?)),
5360 }
5361}
5362
5363fn normalize_url(mut url: DisplaySafeUrl) -> UrlString {
5365 url.set_fragment(None);
5366 UrlString::from(url)
5367}
5368
5369fn normalize_requirement(
5379 mut requirement: Requirement,
5380 root: &Path,
5381 requires_python: &RequiresPython,
5382) -> Result<Requirement, LockError> {
5383 requirement.extras.sort();
5385 requirement.groups.sort();
5386
5387 match requirement.source {
5389 RequirementSource::Git {
5390 git,
5391 subdirectory,
5392 url: _,
5393 } => {
5394 let git = {
5396 let mut repository = git.url().clone();
5397
5398 repository.remove_credentials();
5400
5401 repository.set_fragment(None);
5403 repository.set_query(None);
5404
5405 GitUrl::from_fields(
5406 repository,
5407 git.reference().clone(),
5408 git.precise(),
5409 git.lfs(),
5410 )?
5411 };
5412
5413 let url = DisplaySafeUrl::from(ParsedGitUrl {
5415 url: git.clone(),
5416 subdirectory: subdirectory.clone(),
5417 });
5418
5419 Ok(Requirement {
5420 name: requirement.name,
5421 extras: requirement.extras,
5422 groups: requirement.groups,
5423 marker: requires_python.simplify_markers(requirement.marker),
5424 source: RequirementSource::Git {
5425 git,
5426 subdirectory,
5427 url: VerbatimUrl::from_url(url),
5428 },
5429 origin: None,
5430 })
5431 }
5432 RequirementSource::Path {
5433 install_path,
5434 ext,
5435 url: _,
5436 } => {
5437 let path = root.join(&install_path);
5438 let install_path = normalize_path(path).into_owned().into_boxed_path();
5439 let url = VerbatimUrl::from_normalized_path(&install_path)
5440 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5441
5442 Ok(Requirement {
5443 name: requirement.name,
5444 extras: requirement.extras,
5445 groups: requirement.groups,
5446 marker: requires_python.simplify_markers(requirement.marker),
5447 source: RequirementSource::Path {
5448 install_path,
5449 ext,
5450 url,
5451 },
5452 origin: None,
5453 })
5454 }
5455 RequirementSource::Directory {
5456 install_path,
5457 editable,
5458 r#virtual,
5459 url: _,
5460 } => {
5461 let path = root.join(&install_path);
5462 let install_path = normalize_path(path).into_owned().into_boxed_path();
5463 let url = VerbatimUrl::from_normalized_path(&install_path)
5464 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5465
5466 Ok(Requirement {
5467 name: requirement.name,
5468 extras: requirement.extras,
5469 groups: requirement.groups,
5470 marker: requires_python.simplify_markers(requirement.marker),
5471 source: RequirementSource::Directory {
5472 install_path,
5473 editable: Some(editable.unwrap_or(false)),
5474 r#virtual: Some(r#virtual.unwrap_or(false)),
5475 url,
5476 },
5477 origin: None,
5478 })
5479 }
5480 RequirementSource::Registry {
5481 specifier,
5482 index,
5483 conflict,
5484 } => {
5485 let index = index
5487 .map(|index| index.url.into_url())
5488 .map(|mut index| {
5489 index.remove_credentials();
5490 index
5491 })
5492 .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index))));
5493 Ok(Requirement {
5494 name: requirement.name,
5495 extras: requirement.extras,
5496 groups: requirement.groups,
5497 marker: requires_python.simplify_markers(requirement.marker),
5498 source: RequirementSource::Registry {
5499 specifier,
5500 index,
5501 conflict,
5502 },
5503 origin: None,
5504 })
5505 }
5506 RequirementSource::Url {
5507 mut location,
5508 subdirectory,
5509 ext,
5510 url: _,
5511 } => {
5512 location.remove_credentials();
5514
5515 location.set_fragment(None);
5517
5518 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
5520 url: location.clone(),
5521 subdirectory: subdirectory.clone(),
5522 ext,
5523 });
5524
5525 Ok(Requirement {
5526 name: requirement.name,
5527 extras: requirement.extras,
5528 groups: requirement.groups,
5529 marker: requires_python.simplify_markers(requirement.marker),
5530 source: RequirementSource::Url {
5531 location,
5532 subdirectory,
5533 ext,
5534 url: VerbatimUrl::from_url(url),
5535 },
5536 origin: None,
5537 })
5538 }
5539 }
5540}
5541
5542#[derive(Debug)]
5543pub struct LockError {
5544 kind: Box<LockErrorKind>,
5545 hint: Option<WheelTagHint>,
5546}
5547
5548impl std::error::Error for LockError {
5549 fn source(&self) -> Option<&(dyn Error + 'static)> {
5550 self.kind.source()
5551 }
5552}
5553
5554impl std::fmt::Display for LockError {
5555 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5556 write!(f, "{}", self.kind)?;
5557 if let Some(hint) = &self.hint {
5558 write!(f, "\n\n{hint}")?;
5559 }
5560 Ok(())
5561 }
5562}
5563
5564impl LockError {
5565 pub fn is_resolution(&self) -> bool {
5567 matches!(&*self.kind, LockErrorKind::Resolution { .. })
5568 }
5569}
5570
5571impl<E> From<E> for LockError
5572where
5573 LockErrorKind: From<E>,
5574{
5575 fn from(err: E) -> Self {
5576 Self {
5577 kind: Box::new(LockErrorKind::from(err)),
5578 hint: None,
5579 }
5580 }
5581}
5582
5583#[derive(Debug, Clone, PartialEq, Eq)]
5584#[expect(clippy::enum_variant_names)]
5585enum WheelTagHint {
5586 LanguageTags {
5589 package: PackageName,
5590 version: Option<Version>,
5591 tags: BTreeSet<LanguageTag>,
5592 best: Option<LanguageTag>,
5593 },
5594 AbiTags {
5597 package: PackageName,
5598 version: Option<Version>,
5599 tags: BTreeSet<AbiTag>,
5600 best: Option<AbiTag>,
5601 },
5602 PlatformTags {
5605 package: PackageName,
5606 version: Option<Version>,
5607 tags: BTreeSet<PlatformTag>,
5608 best: Option<PlatformTag>,
5609 markers: MarkerEnvironment,
5610 },
5611}
5612
5613impl WheelTagHint {
5614 fn from_wheels(
5616 name: &PackageName,
5617 version: Option<&Version>,
5618 filenames: &[&WheelFilename],
5619 tags: &Tags,
5620 markers: &MarkerEnvironment,
5621 ) -> Option<Self> {
5622 let incompatibility = filenames
5623 .iter()
5624 .map(|filename| {
5625 tags.compatibility(
5626 filename.python_tags(),
5627 filename.abi_tags(),
5628 filename.platform_tags(),
5629 )
5630 })
5631 .max()?;
5632 match incompatibility {
5633 TagCompatibility::Incompatible(IncompatibleTag::Python) => {
5634 let best = tags.python_tag();
5635 let tags = Self::python_tags(filenames.iter().copied()).collect::<BTreeSet<_>>();
5636 if tags.is_empty() {
5637 None
5638 } else {
5639 Some(Self::LanguageTags {
5640 package: name.clone(),
5641 version: version.cloned(),
5642 tags,
5643 best,
5644 })
5645 }
5646 }
5647 TagCompatibility::Incompatible(IncompatibleTag::Abi) => {
5648 let best = tags.abi_tag();
5649 let tags = Self::abi_tags(filenames.iter().copied())
5650 .filter(|tag| *tag != AbiTag::None)
5659 .collect::<BTreeSet<_>>();
5660 if tags.is_empty() {
5661 None
5662 } else {
5663 Some(Self::AbiTags {
5664 package: name.clone(),
5665 version: version.cloned(),
5666 tags,
5667 best,
5668 })
5669 }
5670 }
5671 TagCompatibility::Incompatible(IncompatibleTag::Platform) => {
5672 let best = tags.platform_tag().cloned();
5673 let incompatible_tags = Self::platform_tags(filenames.iter().copied(), tags)
5674 .cloned()
5675 .collect::<BTreeSet<_>>();
5676 if incompatible_tags.is_empty() {
5677 None
5678 } else {
5679 Some(Self::PlatformTags {
5680 package: name.clone(),
5681 version: version.cloned(),
5682 tags: incompatible_tags,
5683 best,
5684 markers: markers.clone(),
5685 })
5686 }
5687 }
5688 _ => None,
5689 }
5690 }
5691
5692 fn python_tags<'a>(
5694 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5695 ) -> impl Iterator<Item = LanguageTag> + 'a {
5696 filenames.flat_map(WheelFilename::python_tags).copied()
5697 }
5698
5699 fn abi_tags<'a>(
5701 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5702 ) -> impl Iterator<Item = AbiTag> + 'a {
5703 filenames.flat_map(WheelFilename::abi_tags).copied()
5704 }
5705
5706 fn platform_tags<'a>(
5709 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5710 tags: &'a Tags,
5711 ) -> impl Iterator<Item = &'a PlatformTag> + 'a {
5712 filenames.flat_map(move |filename| {
5713 if filename.python_tags().iter().any(|wheel_py| {
5714 filename
5715 .abi_tags()
5716 .iter()
5717 .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
5718 }) {
5719 filename.platform_tags().iter()
5720 } else {
5721 [].iter()
5722 }
5723 })
5724 }
5725
5726 fn suggest_environment_marker(markers: &MarkerEnvironment) -> String {
5727 let sys_platform = markers.sys_platform();
5728 let platform_machine = markers.platform_machine();
5729
5730 if platform_machine.is_empty() {
5732 format!("sys_platform == '{sys_platform}'")
5733 } else {
5734 format!("sys_platform == '{sys_platform}' and platform_machine == '{platform_machine}'")
5735 }
5736 }
5737}
5738
5739impl std::fmt::Display for WheelTagHint {
5740 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5741 match self {
5742 Self::LanguageTags {
5743 package,
5744 version,
5745 tags,
5746 best,
5747 } => {
5748 if let Some(best) = best {
5749 let s = if tags.len() == 1 { "" } else { "s" };
5750 let best = if let Some(pretty) = best.pretty() {
5751 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5752 } else {
5753 format!("{}", best.cyan())
5754 };
5755 if let Some(version) = version {
5756 write!(
5757 f,
5758 "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python implementation tag{s}: {}",
5759 "hint".bold().cyan(),
5760 ":".bold(),
5761 best,
5762 package.cyan(),
5763 format!("v{version}").cyan(),
5764 tags.iter()
5765 .map(|tag| format!("`{}`", tag.cyan()))
5766 .join(", "),
5767 )
5768 } else {
5769 write!(
5770 f,
5771 "{}{} You're using {}, but `{}` only has wheels with the following Python implementation tag{s}: {}",
5772 "hint".bold().cyan(),
5773 ":".bold(),
5774 best,
5775 package.cyan(),
5776 tags.iter()
5777 .map(|tag| format!("`{}`", tag.cyan()))
5778 .join(", "),
5779 )
5780 }
5781 } else {
5782 let s = if tags.len() == 1 { "" } else { "s" };
5783 if let Some(version) = version {
5784 write!(
5785 f,
5786 "{}{} Wheels are available for `{}` ({}) with the following Python implementation tag{s}: {}",
5787 "hint".bold().cyan(),
5788 ":".bold(),
5789 package.cyan(),
5790 format!("v{version}").cyan(),
5791 tags.iter()
5792 .map(|tag| format!("`{}`", tag.cyan()))
5793 .join(", "),
5794 )
5795 } else {
5796 write!(
5797 f,
5798 "{}{} Wheels are available for `{}` with the following Python implementation tag{s}: {}",
5799 "hint".bold().cyan(),
5800 ":".bold(),
5801 package.cyan(),
5802 tags.iter()
5803 .map(|tag| format!("`{}`", tag.cyan()))
5804 .join(", "),
5805 )
5806 }
5807 }
5808 }
5809 Self::AbiTags {
5810 package,
5811 version,
5812 tags,
5813 best,
5814 } => {
5815 if let Some(best) = best {
5816 let s = if tags.len() == 1 { "" } else { "s" };
5817 let best = if let Some(pretty) = best.pretty() {
5818 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5819 } else {
5820 format!("{}", best.cyan())
5821 };
5822 if let Some(version) = version {
5823 write!(
5824 f,
5825 "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python ABI tag{s}: {}",
5826 "hint".bold().cyan(),
5827 ":".bold(),
5828 best,
5829 package.cyan(),
5830 format!("v{version}").cyan(),
5831 tags.iter()
5832 .map(|tag| format!("`{}`", tag.cyan()))
5833 .join(", "),
5834 )
5835 } else {
5836 write!(
5837 f,
5838 "{}{} You're using {}, but `{}` only has wheels with the following Python ABI tag{s}: {}",
5839 "hint".bold().cyan(),
5840 ":".bold(),
5841 best,
5842 package.cyan(),
5843 tags.iter()
5844 .map(|tag| format!("`{}`", tag.cyan()))
5845 .join(", "),
5846 )
5847 }
5848 } else {
5849 let s = if tags.len() == 1 { "" } else { "s" };
5850 if let Some(version) = version {
5851 write!(
5852 f,
5853 "{}{} Wheels are available for `{}` ({}) with the following Python ABI tag{s}: {}",
5854 "hint".bold().cyan(),
5855 ":".bold(),
5856 package.cyan(),
5857 format!("v{version}").cyan(),
5858 tags.iter()
5859 .map(|tag| format!("`{}`", tag.cyan()))
5860 .join(", "),
5861 )
5862 } else {
5863 write!(
5864 f,
5865 "{}{} Wheels are available for `{}` with the following Python ABI tag{s}: {}",
5866 "hint".bold().cyan(),
5867 ":".bold(),
5868 package.cyan(),
5869 tags.iter()
5870 .map(|tag| format!("`{}`", tag.cyan()))
5871 .join(", "),
5872 )
5873 }
5874 }
5875 }
5876 Self::PlatformTags {
5877 package,
5878 version,
5879 tags,
5880 best,
5881 markers,
5882 } => {
5883 let s = if tags.len() == 1 { "" } else { "s" };
5884 if let Some(best) = best {
5885 let example_marker = Self::suggest_environment_marker(markers);
5886 let best = if let Some(pretty) = best.pretty() {
5887 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5888 } else {
5889 format!("`{}`", best.cyan())
5890 };
5891 let package_ref = if let Some(version) = version {
5892 format!("`{}` ({})", package.cyan(), format!("v{version}").cyan())
5893 } else {
5894 format!("`{}`", package.cyan())
5895 };
5896 write!(
5897 f,
5898 "{}{} You're on {}, but {} only has wheels for the following platform{s}: {}; consider adding {} to `{}` to ensure uv resolves to a version with compatible wheels",
5899 "hint".bold().cyan(),
5900 ":".bold(),
5901 best,
5902 package_ref,
5903 tags.iter()
5904 .map(|tag| format!("`{}`", tag.cyan()))
5905 .join(", "),
5906 format!("\"{example_marker}\"").cyan(),
5907 "tool.uv.required-environments".green()
5908 )
5909 } else {
5910 if let Some(version) = version {
5911 write!(
5912 f,
5913 "{}{} Wheels are available for `{}` ({}) on the following platform{s}: {}",
5914 "hint".bold().cyan(),
5915 ":".bold(),
5916 package.cyan(),
5917 format!("v{version}").cyan(),
5918 tags.iter()
5919 .map(|tag| format!("`{}`", tag.cyan()))
5920 .join(", "),
5921 )
5922 } else {
5923 write!(
5924 f,
5925 "{}{} Wheels are available for `{}` on the following platform{s}: {}",
5926 "hint".bold().cyan(),
5927 ":".bold(),
5928 package.cyan(),
5929 tags.iter()
5930 .map(|tag| format!("`{}`", tag.cyan()))
5931 .join(", "),
5932 )
5933 }
5934 }
5935 }
5936 }
5937 }
5938}
5939
5940#[derive(Debug, thiserror::Error)]
5947enum LockErrorKind {
5948 #[error("Found duplicate package `{id}`", id = id.cyan())]
5951 DuplicatePackage {
5952 id: PackageId,
5954 },
5955 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = id.cyan(), dependency = dependency.cyan())]
5958 DuplicateDependency {
5959 id: PackageId,
5962 dependency: Dependency,
5964 },
5965 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}[{extra}]").cyan(), dependency = dependency.cyan())]
5969 DuplicateOptionalDependency {
5970 id: PackageId,
5973 extra: ExtraName,
5975 dependency: Dependency,
5977 },
5978 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}:{group}").cyan(), dependency = dependency.cyan())]
5982 DuplicateDevDependency {
5983 id: PackageId,
5986 group: GroupName,
5988 dependency: Dependency,
5990 },
5991 #[error(transparent)]
5994 InvalidUrl(
5995 #[from]
5998 ToUrlError,
5999 ),
6000 #[error("Failed to parse file extension for `{id}`; expected one of: {err}", id = id.cyan())]
6003 MissingExtension {
6004 id: PackageId,
6006 err: ExtensionError,
6008 },
6009 #[error("Failed to parse Git URL")]
6011 InvalidGitSourceUrl(
6012 #[source]
6015 SourceParseError,
6016 ),
6017 #[error("Failed to parse timestamp")]
6018 InvalidTimestamp(
6019 #[source]
6022 jiff::Error,
6023 ),
6024 #[error("For package `{id}`, found dependency `{dependency}` with no locked package", id = id.cyan(), dependency = dependency.cyan())]
6028 UnrecognizedDependency {
6029 id: PackageId,
6031 dependency: Dependency,
6034 },
6035 #[error("Since the package `{id}` comes from a {source} dependency, a hash was {expected} but one was not found for {artifact_type}", id = id.cyan(), source = id.source.name(), expected = if *expected { "expected" } else { "not expected" })]
6038 Hash {
6039 id: PackageId,
6041 artifact_type: &'static str,
6044 expected: bool,
6046 },
6047 #[error("Found package `{id}` with extra `{extra}` but no base package", id = id.cyan(), extra = extra.cyan())]
6050 MissingExtraBase {
6051 id: PackageId,
6053 extra: ExtraName,
6055 },
6056 #[error("Found package `{id}` with development dependency group `{group}` but no base package", id = id.cyan())]
6060 MissingDevBase {
6061 id: PackageId,
6063 group: GroupName,
6065 },
6066 #[error("Wheels cannot come from {source_type} sources")]
6069 InvalidWheelSource {
6070 id: PackageId,
6072 source_type: &'static str,
6074 },
6075 #[error("Found registry distribution `{name}` ({version}) without a valid URL", name = name.cyan(), version = format!("v{version}").cyan())]
6078 MissingUrl {
6079 name: PackageName,
6081 version: Version,
6083 },
6084 #[error("Found registry distribution `{name}` ({version}) without a valid path", name = name.cyan(), version = format!("v{version}").cyan())]
6087 MissingPath {
6088 name: PackageName,
6090 version: Version,
6092 },
6093 #[error("Found registry distribution `{id}` without a valid filename", id = id.cyan())]
6096 MissingFilename {
6097 id: PackageId,
6099 },
6100 #[error("Distribution `{id}` can't be installed because it doesn't have a source distribution or wheel for the current platform", id = id.cyan())]
6103 NeitherSourceDistNorWheel {
6104 id: PackageId,
6106 },
6107 #[error("Distribution `{id}` can't be installed because it is marked as both `--no-binary` and `--no-build`", id = id.cyan())]
6109 NoBinaryNoBuild {
6110 id: PackageId,
6112 },
6113 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but has no source distribution", id = id.cyan())]
6116 NoBinary {
6117 id: PackageId,
6119 },
6120 #[error("Distribution `{id}` can't be installed because it is marked as `--no-build` but has no binary distribution", id = id.cyan())]
6123 NoBuild {
6124 id: PackageId,
6126 },
6127 #[error("Distribution `{id}` can't be installed because the binary distribution is incompatible with the current platform", id = id.cyan())]
6130 IncompatibleWheelOnly {
6131 id: PackageId,
6133 },
6134 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but is itself a binary distribution", id = id.cyan())]
6136 NoBinaryWheelOnly {
6137 id: PackageId,
6139 },
6140 #[error("Found dependency `{id}` with no locked distribution", id = id.cyan())]
6142 VerbatimUrl {
6143 id: PackageId,
6145 #[source]
6147 err: VerbatimUrlError,
6148 },
6149 #[error("Could not compute relative path between workspace and distribution")]
6151 DistributionRelativePath(
6152 #[source]
6154 io::Error,
6155 ),
6156 #[error("Could not compute relative path between workspace and index")]
6158 IndexRelativePath(
6159 #[source]
6161 io::Error,
6162 ),
6163 #[error("Could not compute absolute path from workspace root and lockfile path")]
6165 AbsolutePath(
6166 #[source]
6168 io::Error,
6169 ),
6170 #[error("Dependency `{name}` has missing `version` field but has more than one matching package", name = name.cyan())]
6173 MissingDependencyVersion {
6174 name: PackageName,
6176 },
6177 #[error("Dependency `{name}` has missing `source` field but has more than one matching package", name = name.cyan())]
6180 MissingDependencySource {
6181 name: PackageName,
6183 },
6184 #[error("Could not compute relative path between workspace and requirement")]
6186 RequirementRelativePath(
6187 #[source]
6189 io::Error,
6190 ),
6191 #[error("Could not convert between URL and path")]
6193 RequirementVerbatimUrl(
6194 #[source]
6196 VerbatimUrlError,
6197 ),
6198 #[error("Could not convert between URL and path")]
6200 RegistryVerbatimUrl(
6201 #[source]
6203 VerbatimUrlError,
6204 ),
6205 #[error("Failed to convert path to URL: {path}", path = path.display().cyan())]
6207 PathToUrl { path: Box<Path> },
6208 #[error("Failed to convert URL to path: {url}", url = url.cyan())]
6210 UrlToPath { url: DisplaySafeUrl },
6211 #[error("Found multiple packages matching `{name}`", name = name.cyan())]
6214 MultipleRootPackages {
6215 name: PackageName,
6217 },
6218 #[error("Could not find root package `{name}`", name = name.cyan())]
6220 MissingRootPackage {
6221 name: PackageName,
6223 },
6224 #[error("Failed to generate package metadata for `{id}`", id = id.cyan())]
6226 Resolution {
6227 id: PackageId,
6229 #[source]
6231 err: uv_distribution::Error,
6232 },
6233 #[error("The entry for package `{name}` ({version}) has wheel `{wheel_filename}` with inconsistent version ({wheel_version}), which indicates a malformed wheel. If this is intentional, set `{env_var}`.", name = name.cyan(), wheel_filename = wheel.filename, wheel_version = wheel.filename.version, env_var = "UV_SKIP_WHEEL_FILENAME_CHECK=1".green())]
6236 InconsistentVersions {
6237 name: PackageName,
6239 version: Version,
6241 wheel: Wheel,
6243 },
6244 #[error(
6245 "Found conflicting extras `{package1}[{extra1}]` \
6246 and `{package2}[{extra2}]` enabled simultaneously"
6247 )]
6248 ConflictingExtra {
6249 package1: PackageName,
6250 extra1: ExtraName,
6251 package2: PackageName,
6252 extra2: ExtraName,
6253 },
6254 #[error(transparent)]
6255 GitUrlParse(#[from] GitUrlParseError),
6256 #[error("Failed to read `{path}`")]
6257 UnreadablePyprojectToml {
6258 path: PathBuf,
6259 #[source]
6260 err: std::io::Error,
6261 },
6262 #[error("Failed to parse `{path}`")]
6263 InvalidPyprojectToml {
6264 path: PathBuf,
6265 #[source]
6266 err: uv_pypi_types::MetadataError,
6267 },
6268 #[error("Workspace member `{id}` has non-local source", id = id.cyan())]
6270 NonLocalWorkspaceMember {
6271 id: PackageId,
6273 },
6274}
6275
6276#[derive(Debug, thiserror::Error)]
6278enum SourceParseError {
6279 #[error("Invalid URL in source `{given}`")]
6281 InvalidUrl {
6282 given: String,
6284 #[source]
6286 err: DisplaySafeUrlError,
6287 },
6288 #[error("Missing SHA in source `{given}`")]
6290 MissingSha {
6291 given: String,
6293 },
6294 #[error("Invalid SHA in source `{given}`")]
6296 InvalidSha {
6297 given: String,
6299 },
6300}
6301
6302#[derive(Clone, Debug, Eq, PartialEq)]
6304struct HashParseError(&'static str);
6305
6306impl std::error::Error for HashParseError {}
6307
6308impl Display for HashParseError {
6309 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
6310 Display::fmt(self.0, f)
6311 }
6312}
6313
6314fn each_element_on_its_line_array(elements: impl Iterator<Item = impl Into<Value>>) -> Array {
6325 let mut array = elements
6326 .map(|item| {
6327 let mut value = item.into();
6328 value.decor_mut().set_prefix("\n ");
6330 value
6331 })
6332 .collect::<Array>();
6333 array.set_trailing_comma(true);
6336 array.set_trailing("\n");
6338 array
6339}
6340
6341fn simplified_universal_markers(
6346 markers: &[UniversalMarker],
6347 requires_python: &RequiresPython,
6348) -> Vec<String> {
6349 canonical_marker_trees(markers, requires_python)
6350 .into_iter()
6351 .filter_map(MarkerTree::try_to_string)
6352 .collect()
6353}
6354
6355fn canonicalize_universal_markers(
6362 markers: &[UniversalMarker],
6363 requires_python: &RequiresPython,
6364) -> Vec<UniversalMarker> {
6365 canonical_marker_trees(markers, requires_python)
6366 .into_iter()
6367 .map(|marker| {
6368 let simplified = SimplifiedMarkerTree::new(requires_python, marker);
6369 UniversalMarker::from_combined(simplified.into_marker(requires_python))
6370 })
6371 .collect()
6372}
6373
6374fn canonical_marker_trees(
6376 markers: &[UniversalMarker],
6377 requires_python: &RequiresPython,
6378) -> Vec<MarkerTree> {
6379 let mut pep508_only = vec![];
6380 let mut seen = FxHashSet::default();
6381 for marker in markers {
6382 let simplified =
6383 SimplifiedMarkerTree::new(requires_python, marker.pep508()).as_simplified_marker_tree();
6384 if seen.insert(simplified) {
6385 pep508_only.push(simplified);
6386 }
6387 }
6388 let any_overlap = pep508_only
6389 .iter()
6390 .tuple_combinations()
6391 .any(|(&marker1, &marker2)| !marker1.is_disjoint(marker2));
6392 let markers = if !any_overlap {
6393 pep508_only
6394 } else {
6395 markers
6396 .iter()
6397 .map(|marker| {
6398 SimplifiedMarkerTree::new(requires_python, marker.combined())
6399 .as_simplified_marker_tree()
6400 })
6401 .collect()
6402 };
6403 markers
6404 .into_iter()
6405 .filter(|marker| !marker.is_true())
6406 .collect()
6407}
6408
6409fn is_wheel_unreachable_for_marker(
6417 filename: &WheelFilename,
6418 requires_python: &RequiresPython,
6419 marker: &UniversalMarker,
6420 tags: Option<&Tags>,
6421) -> bool {
6422 if let Some(tags) = tags
6423 && !filename.compatibility(tags).is_compatible()
6424 {
6425 return true;
6426 }
6427 if !requires_python.matches_wheel_tag(filename) {
6429 return true;
6430 }
6431
6432 let platform_tags = filename.platform_tags();
6441
6442 if platform_tags.iter().all(PlatformTag::is_any) {
6443 return false;
6444 }
6445
6446 if platform_tags.iter().all(PlatformTag::is_linux) {
6447 if platform_tags.iter().all(PlatformTag::is_arm) {
6448 if marker.is_disjoint(*LINUX_ARM_MARKERS) {
6449 return true;
6450 }
6451 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6452 if marker.is_disjoint(*LINUX_X86_64_MARKERS) {
6453 return true;
6454 }
6455 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6456 if marker.is_disjoint(*LINUX_X86_MARKERS) {
6457 return true;
6458 }
6459 } else if platform_tags.iter().all(PlatformTag::is_ppc64le) {
6460 if marker.is_disjoint(*LINUX_PPC64LE_MARKERS) {
6461 return true;
6462 }
6463 } else if platform_tags.iter().all(PlatformTag::is_ppc64) {
6464 if marker.is_disjoint(*LINUX_PPC64_MARKERS) {
6465 return true;
6466 }
6467 } else if platform_tags.iter().all(PlatformTag::is_s390x) {
6468 if marker.is_disjoint(*LINUX_S390X_MARKERS) {
6469 return true;
6470 }
6471 } else if platform_tags.iter().all(PlatformTag::is_riscv64) {
6472 if marker.is_disjoint(*LINUX_RISCV64_MARKERS) {
6473 return true;
6474 }
6475 } else if platform_tags.iter().all(PlatformTag::is_loongarch64) {
6476 if marker.is_disjoint(*LINUX_LOONGARCH64_MARKERS) {
6477 return true;
6478 }
6479 } else if platform_tags.iter().all(PlatformTag::is_armv7l) {
6480 if marker.is_disjoint(*LINUX_ARMV7L_MARKERS) {
6481 return true;
6482 }
6483 } else if platform_tags.iter().all(PlatformTag::is_armv6l) {
6484 if marker.is_disjoint(*LINUX_ARMV6L_MARKERS) {
6485 return true;
6486 }
6487 } else if marker.is_disjoint(*LINUX_MARKERS) {
6488 return true;
6489 }
6490 }
6491
6492 if platform_tags.iter().all(PlatformTag::is_windows) {
6493 if platform_tags.iter().all(PlatformTag::is_arm) {
6494 if marker.is_disjoint(*WINDOWS_ARM_MARKERS) {
6495 return true;
6496 }
6497 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6498 if marker.is_disjoint(*WINDOWS_X86_64_MARKERS) {
6499 return true;
6500 }
6501 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6502 if marker.is_disjoint(*WINDOWS_X86_MARKERS) {
6503 return true;
6504 }
6505 } else if marker.is_disjoint(*WINDOWS_MARKERS) {
6506 return true;
6507 }
6508 }
6509
6510 if platform_tags.iter().all(PlatformTag::is_macos) {
6511 if platform_tags.iter().all(PlatformTag::is_arm) {
6512 if marker.is_disjoint(*MAC_ARM_MARKERS) {
6513 return true;
6514 }
6515 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6516 if marker.is_disjoint(*MAC_X86_64_MARKERS) {
6517 return true;
6518 }
6519 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6520 if marker.is_disjoint(*MAC_X86_MARKERS) {
6521 return true;
6522 }
6523 } else if marker.is_disjoint(*MAC_MARKERS) {
6524 return true;
6525 }
6526 }
6527
6528 if platform_tags.iter().all(PlatformTag::is_android) {
6529 if platform_tags.iter().all(PlatformTag::is_arm) {
6530 if marker.is_disjoint(*ANDROID_ARM_MARKERS) {
6531 return true;
6532 }
6533 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6534 if marker.is_disjoint(*ANDROID_X86_64_MARKERS) {
6535 return true;
6536 }
6537 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6538 if marker.is_disjoint(*ANDROID_X86_MARKERS) {
6539 return true;
6540 }
6541 } else if marker.is_disjoint(*ANDROID_MARKERS) {
6542 return true;
6543 }
6544 }
6545
6546 if platform_tags.iter().all(PlatformTag::is_arm) {
6547 if marker.is_disjoint(*ARM_MARKERS) {
6548 return true;
6549 }
6550 }
6551
6552 if platform_tags.iter().all(PlatformTag::is_x86_64) {
6553 if marker.is_disjoint(*X86_64_MARKERS) {
6554 return true;
6555 }
6556 }
6557
6558 if platform_tags.iter().all(PlatformTag::is_x86) {
6559 if marker.is_disjoint(*X86_MARKERS) {
6560 return true;
6561 }
6562 }
6563
6564 if platform_tags.iter().all(PlatformTag::is_ppc64le) {
6565 if marker.is_disjoint(*PPC64LE_MARKERS) {
6566 return true;
6567 }
6568 }
6569
6570 if platform_tags.iter().all(PlatformTag::is_ppc64) {
6571 if marker.is_disjoint(*PPC64_MARKERS) {
6572 return true;
6573 }
6574 }
6575
6576 if platform_tags.iter().all(PlatformTag::is_s390x) {
6577 if marker.is_disjoint(*S390X_MARKERS) {
6578 return true;
6579 }
6580 }
6581
6582 if platform_tags.iter().all(PlatformTag::is_riscv64) {
6583 if marker.is_disjoint(*RISCV64_MARKERS) {
6584 return true;
6585 }
6586 }
6587
6588 if platform_tags.iter().all(PlatformTag::is_loongarch64) {
6589 if marker.is_disjoint(*LOONGARCH64_MARKERS) {
6590 return true;
6591 }
6592 }
6593
6594 if platform_tags.iter().all(PlatformTag::is_armv7l) {
6595 if marker.is_disjoint(*ARMV7L_MARKERS) {
6596 return true;
6597 }
6598 }
6599
6600 if platform_tags.iter().all(PlatformTag::is_armv6l) {
6601 if marker.is_disjoint(*ARMV6L_MARKERS) {
6602 return true;
6603 }
6604 }
6605
6606 false
6607}
6608
6609pub(crate) fn is_wheel_unreachable(
6610 filename: &WheelFilename,
6611 graph: &ResolverOutput,
6612 requires_python: &RequiresPython,
6613 node_index: NodeIndex,
6614 tags: Option<&Tags>,
6615) -> bool {
6616 is_wheel_unreachable_for_marker(
6617 filename,
6618 requires_python,
6619 graph.graph[node_index].marker(),
6620 tags,
6621 )
6622}
6623
6624#[cfg(test)]
6625mod tests {
6626 use uv_warnings::anstream;
6627
6628 use super::*;
6629
6630 macro_rules! assert_stripped_snapshot {
6632 ($expr:expr, @$snapshot:literal) => {{
6633 let expr = format!("{}", $expr);
6634 let expr = format!("{}", anstream::adapter::strip_str(&expr));
6635 insta::assert_snapshot!(expr, @$snapshot);
6636 }};
6637 }
6638
6639 #[test]
6640 fn missing_dependency_source_unambiguous() {
6641 let data = r#"
6642version = 1
6643requires-python = ">=3.12"
6644
6645[[package]]
6646name = "a"
6647version = "0.1.0"
6648source = { registry = "https://pypi.org/simple" }
6649sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6650
6651[[package]]
6652name = "b"
6653version = "0.1.0"
6654source = { registry = "https://pypi.org/simple" }
6655sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6656
6657[[package.dependencies]]
6658name = "a"
6659version = "0.1.0"
6660"#;
6661 let result: Result<Lock, _> = toml::from_str(data);
6662 insta::assert_debug_snapshot!(result);
6663 }
6664
6665 #[test]
6666 fn missing_dependency_version_unambiguous() {
6667 let data = r#"
6668version = 1
6669requires-python = ">=3.12"
6670
6671[[package]]
6672name = "a"
6673version = "0.1.0"
6674source = { registry = "https://pypi.org/simple" }
6675sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6676
6677[[package]]
6678name = "b"
6679version = "0.1.0"
6680source = { registry = "https://pypi.org/simple" }
6681sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6682
6683[[package.dependencies]]
6684name = "a"
6685source = { registry = "https://pypi.org/simple" }
6686"#;
6687 let result: Result<Lock, _> = toml::from_str(data);
6688 insta::assert_debug_snapshot!(result);
6689 }
6690
6691 #[test]
6692 fn missing_dependency_source_version_unambiguous() {
6693 let data = r#"
6694version = 1
6695requires-python = ">=3.12"
6696
6697[[package]]
6698name = "a"
6699version = "0.1.0"
6700source = { registry = "https://pypi.org/simple" }
6701sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6702
6703[[package]]
6704name = "b"
6705version = "0.1.0"
6706source = { registry = "https://pypi.org/simple" }
6707sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6708
6709[[package.dependencies]]
6710name = "a"
6711"#;
6712 let result: Result<Lock, _> = toml::from_str(data);
6713 insta::assert_debug_snapshot!(result);
6714 }
6715
6716 #[test]
6717 fn missing_dependency_source_ambiguous() {
6718 let data = r#"
6719version = 1
6720requires-python = ">=3.12"
6721
6722[[package]]
6723name = "a"
6724version = "0.1.0"
6725source = { registry = "https://pypi.org/simple" }
6726sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6727
6728[[package]]
6729name = "a"
6730version = "0.1.1"
6731source = { registry = "https://pypi.org/simple" }
6732sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6733
6734[[package]]
6735name = "b"
6736version = "0.1.0"
6737source = { registry = "https://pypi.org/simple" }
6738sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6739
6740[[package.dependencies]]
6741name = "a"
6742version = "0.1.0"
6743"#;
6744 let result = toml::from_str::<Lock>(data).unwrap_err();
6745 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6746 }
6747
6748 #[test]
6749 fn missing_dependency_version_ambiguous() {
6750 let data = r#"
6751version = 1
6752requires-python = ">=3.12"
6753
6754[[package]]
6755name = "a"
6756version = "0.1.0"
6757source = { registry = "https://pypi.org/simple" }
6758sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6759
6760[[package]]
6761name = "a"
6762version = "0.1.1"
6763source = { registry = "https://pypi.org/simple" }
6764sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6765
6766[[package]]
6767name = "b"
6768version = "0.1.0"
6769source = { registry = "https://pypi.org/simple" }
6770sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6771
6772[[package.dependencies]]
6773name = "a"
6774source = { registry = "https://pypi.org/simple" }
6775"#;
6776 let result = toml::from_str::<Lock>(data).unwrap_err();
6777 assert_stripped_snapshot!(result, @"Dependency `a` has missing `version` field but has more than one matching package");
6778 }
6779
6780 #[test]
6781 fn missing_dependency_source_version_ambiguous() {
6782 let data = r#"
6783version = 1
6784requires-python = ">=3.12"
6785
6786[[package]]
6787name = "a"
6788version = "0.1.0"
6789source = { registry = "https://pypi.org/simple" }
6790sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6791
6792[[package]]
6793name = "a"
6794version = "0.1.1"
6795source = { registry = "https://pypi.org/simple" }
6796sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6797
6798[[package]]
6799name = "b"
6800version = "0.1.0"
6801source = { registry = "https://pypi.org/simple" }
6802sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6803
6804[[package.dependencies]]
6805name = "a"
6806"#;
6807 let result = toml::from_str::<Lock>(data).unwrap_err();
6808 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6809 }
6810
6811 #[test]
6812 fn missing_dependency_version_dynamic() {
6813 let data = r#"
6814version = 1
6815requires-python = ">=3.12"
6816
6817[[package]]
6818name = "a"
6819source = { editable = "path/to/a" }
6820
6821[[package]]
6822name = "a"
6823version = "0.1.1"
6824source = { registry = "https://pypi.org/simple" }
6825sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6826
6827[[package]]
6828name = "b"
6829version = "0.1.0"
6830source = { registry = "https://pypi.org/simple" }
6831sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6832
6833[[package.dependencies]]
6834name = "a"
6835source = { editable = "path/to/a" }
6836"#;
6837 let result = toml::from_str::<Lock>(data);
6838 insta::assert_debug_snapshot!(result);
6839 }
6840
6841 #[test]
6842 fn hash_optional_missing() {
6843 let data = r#"
6844version = 1
6845requires-python = ">=3.12"
6846
6847[[package]]
6848name = "anyio"
6849version = "4.3.0"
6850source = { registry = "https://pypi.org/simple" }
6851wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl" }]
6852"#;
6853 let result: Result<Lock, _> = toml::from_str(data);
6854 insta::assert_debug_snapshot!(result);
6855 }
6856
6857 #[test]
6858 fn hash_optional_present() {
6859 let data = r#"
6860version = 1
6861requires-python = ">=3.12"
6862
6863[[package]]
6864name = "anyio"
6865version = "4.3.0"
6866source = { registry = "https://pypi.org/simple" }
6867wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6868"#;
6869 let result: Result<Lock, _> = toml::from_str(data);
6870 insta::assert_debug_snapshot!(result);
6871 }
6872
6873 #[test]
6874 fn hash_required_present() {
6875 let data = r#"
6876version = 1
6877requires-python = ">=3.12"
6878
6879[[package]]
6880name = "anyio"
6881version = "4.3.0"
6882source = { path = "file:///foo/bar" }
6883wheels = [{ url = "file:///foo/bar/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6884"#;
6885 let result: Result<Lock, _> = toml::from_str(data);
6886 insta::assert_debug_snapshot!(result);
6887 }
6888
6889 #[test]
6890 fn source_direct_no_subdir() {
6891 let data = r#"
6892version = 1
6893requires-python = ">=3.12"
6894
6895[[package]]
6896name = "anyio"
6897version = "4.3.0"
6898source = { url = "https://burntsushi.net" }
6899"#;
6900 let result: Result<Lock, _> = toml::from_str(data);
6901 insta::assert_debug_snapshot!(result);
6902 }
6903
6904 #[test]
6905 fn source_direct_has_subdir() {
6906 let data = r#"
6907version = 1
6908requires-python = ">=3.12"
6909
6910[[package]]
6911name = "anyio"
6912version = "4.3.0"
6913source = { url = "https://burntsushi.net", subdirectory = "wat/foo/bar" }
6914"#;
6915 let result: Result<Lock, _> = toml::from_str(data);
6916 insta::assert_debug_snapshot!(result);
6917 }
6918
6919 #[test]
6920 fn source_directory() {
6921 let data = r#"
6922version = 1
6923requires-python = ">=3.12"
6924
6925[[package]]
6926name = "anyio"
6927version = "4.3.0"
6928source = { directory = "path/to/dir" }
6929"#;
6930 let result: Result<Lock, _> = toml::from_str(data);
6931 insta::assert_debug_snapshot!(result);
6932 }
6933
6934 #[test]
6935 fn source_editable() {
6936 let data = r#"
6937version = 1
6938requires-python = ">=3.12"
6939
6940[[package]]
6941name = "anyio"
6942version = "4.3.0"
6943source = { editable = "path/to/dir" }
6944"#;
6945 let result: Result<Lock, _> = toml::from_str(data);
6946 insta::assert_debug_snapshot!(result);
6947 }
6948
6949 #[test]
6952 fn registry_source_windows_drive_letter() {
6953 let data = r#"
6954version = 1
6955requires-python = ">=3.12"
6956
6957[[package]]
6958name = "tqdm"
6959version = "1000.0.0"
6960source = { registry = "C:/Users/user/links" }
6961wheels = [
6962 { path = "C:/Users/user/links/tqdm-1000.0.0-py3-none-any.whl" },
6963]
6964"#;
6965 let lock: Lock = toml::from_str(data).unwrap();
6966 assert_eq!(
6967 lock.packages[0].id.source,
6968 Source::Registry(RegistrySource::Path(
6969 Path::new("C:/Users/user/links").into()
6970 ))
6971 );
6972 }
6973}