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