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::{FxHashMap, FxHashSet};
16use serde::Serializer;
17use toml_edit::{Array, ArrayOfTables, InlineTable, Item, Table, Value, value};
18use tracing::debug;
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, 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 pub async fn satisfies<Context: BuildContext>(
1410 &self,
1411 root: &Path,
1412 packages: &BTreeMap<PackageName, WorkspaceMember>,
1413 members: &[PackageName],
1414 required_members: &BTreeMap<PackageName, Editability>,
1415 requirements: &[Requirement],
1416 constraints: &[Requirement],
1417 overrides: &[Requirement],
1418 excludes: &[PackageName],
1419 build_constraints: &[Requirement],
1420 dependency_groups: &BTreeMap<GroupName, Vec<Requirement>>,
1421 dependency_metadata: &DependencyMetadata,
1422 indexes: Option<&IndexLocations>,
1423 tags: &Tags,
1424 markers: &MarkerEnvironment,
1425 hasher: &HashStrategy,
1426 index: &InMemoryIndex,
1427 database: &DistributionDatabase<'_, Context>,
1428 ) -> Result<SatisfiesResult<'_>, LockError> {
1429 let mut queue: VecDeque<&Package> = VecDeque::new();
1430 let mut seen = FxHashSet::default();
1431
1432 {
1434 let expected = members.iter().cloned().collect::<BTreeSet<_>>();
1435 let actual = &self.manifest.members;
1436 if expected != *actual {
1437 return Ok(SatisfiesResult::MismatchedMembers(expected, actual));
1438 }
1439 }
1440
1441 for (name, member) in packages {
1444 let source = self.find_by_name(name).ok().flatten();
1445
1446 let value = required_members.get(name);
1448 let is_required_member = value.is_some();
1449 let editability = value.copied().flatten();
1450
1451 let expected_virtual = !member.pyproject_toml().is_package(!is_required_member);
1453 let actual_virtual =
1454 source.map(|package| matches!(package.id.source, Source::Virtual(..)));
1455 if actual_virtual != Some(expected_virtual) {
1456 return Ok(SatisfiesResult::MismatchedVirtual(
1457 name.clone(),
1458 expected_virtual,
1459 ));
1460 }
1461
1462 let expected_editable = if expected_virtual {
1464 false
1465 } else {
1466 editability.unwrap_or(true)
1467 };
1468 let actual_editable =
1469 source.map(|package| matches!(package.id.source, Source::Editable(..)));
1470 if actual_editable != Some(expected_editable) {
1471 return Ok(SatisfiesResult::MismatchedEditable(
1472 name.clone(),
1473 expected_editable,
1474 ));
1475 }
1476 }
1477
1478 {
1480 let expected: BTreeSet<_> = requirements
1481 .iter()
1482 .cloned()
1483 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1484 .collect::<Result<_, _>>()?;
1485 let actual: BTreeSet<_> = self
1486 .manifest
1487 .requirements
1488 .iter()
1489 .cloned()
1490 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1491 .collect::<Result<_, _>>()?;
1492 if expected != actual {
1493 return Ok(SatisfiesResult::MismatchedRequirements(expected, actual));
1494 }
1495 }
1496
1497 {
1499 let expected: BTreeSet<_> = constraints
1500 .iter()
1501 .cloned()
1502 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1503 .collect::<Result<_, _>>()?;
1504 let actual: BTreeSet<_> = self
1505 .manifest
1506 .constraints
1507 .iter()
1508 .cloned()
1509 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1510 .collect::<Result<_, _>>()?;
1511 if expected != actual {
1512 return Ok(SatisfiesResult::MismatchedConstraints(expected, actual));
1513 }
1514 }
1515
1516 {
1518 let expected: BTreeSet<_> = overrides
1519 .iter()
1520 .cloned()
1521 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1522 .collect::<Result<_, _>>()?;
1523 let actual: BTreeSet<_> = self
1524 .manifest
1525 .overrides
1526 .iter()
1527 .cloned()
1528 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1529 .collect::<Result<_, _>>()?;
1530 if expected != actual {
1531 return Ok(SatisfiesResult::MismatchedOverrides(expected, actual));
1532 }
1533 }
1534
1535 {
1537 let expected: BTreeSet<_> = excludes.iter().cloned().collect();
1538 let actual: BTreeSet<_> = self.manifest.excludes.iter().cloned().collect();
1539 if expected != actual {
1540 return Ok(SatisfiesResult::MismatchedExcludes(expected, actual));
1541 }
1542 }
1543
1544 {
1546 let expected: BTreeSet<_> = build_constraints
1547 .iter()
1548 .cloned()
1549 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1550 .collect::<Result<_, _>>()?;
1551 let actual: BTreeSet<_> = self
1552 .manifest
1553 .build_constraints
1554 .iter()
1555 .cloned()
1556 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1557 .collect::<Result<_, _>>()?;
1558 if expected != actual {
1559 return Ok(SatisfiesResult::MismatchedBuildConstraints(
1560 expected, actual,
1561 ));
1562 }
1563 }
1564
1565 {
1567 let expected: BTreeMap<GroupName, BTreeSet<Requirement>> = dependency_groups
1568 .iter()
1569 .filter(|(_, requirements)| !requirements.is_empty())
1570 .map(|(group, requirements)| {
1571 Ok::<_, LockError>((
1572 group.clone(),
1573 requirements
1574 .iter()
1575 .cloned()
1576 .map(|requirement| {
1577 normalize_requirement(requirement, root, &self.requires_python)
1578 })
1579 .collect::<Result<_, _>>()?,
1580 ))
1581 })
1582 .collect::<Result<_, _>>()?;
1583 let actual: BTreeMap<GroupName, BTreeSet<Requirement>> = self
1584 .manifest
1585 .dependency_groups
1586 .iter()
1587 .filter(|(_, requirements)| !requirements.is_empty())
1588 .map(|(group, requirements)| {
1589 Ok::<_, LockError>((
1590 group.clone(),
1591 requirements
1592 .iter()
1593 .cloned()
1594 .map(|requirement| {
1595 normalize_requirement(requirement, root, &self.requires_python)
1596 })
1597 .collect::<Result<_, _>>()?,
1598 ))
1599 })
1600 .collect::<Result<_, _>>()?;
1601 if expected != actual {
1602 return Ok(SatisfiesResult::MismatchedDependencyGroups(
1603 expected, actual,
1604 ));
1605 }
1606 }
1607
1608 {
1610 let expected = dependency_metadata
1611 .values()
1612 .cloned()
1613 .collect::<BTreeSet<_>>();
1614 let actual = &self.manifest.dependency_metadata;
1615 if expected != *actual {
1616 return Ok(SatisfiesResult::MismatchedStaticMetadata(expected, actual));
1617 }
1618 }
1619
1620 let mut remotes = indexes.map(|locations| {
1622 locations
1623 .allowed_indexes()
1624 .into_iter()
1625 .filter_map(|index| match index.url() {
1626 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
1627 Some(UrlString::from(index.url().without_credentials().as_ref()))
1628 }
1629 IndexUrl::Path(_) => None,
1630 })
1631 .collect::<BTreeSet<_>>()
1632 });
1633
1634 let mut locals = indexes.map(|locations| {
1635 locations
1636 .allowed_indexes()
1637 .into_iter()
1638 .filter_map(|index| match index.url() {
1639 IndexUrl::Pypi(_) | IndexUrl::Url(_) => None,
1640 IndexUrl::Path(url) => {
1641 let path = url.to_file_path().ok()?;
1642 let path = relative_to(&path, root)
1643 .or_else(|_| std::path::absolute(path))
1644 .ok()?
1645 .into_boxed_path();
1646 Some(path)
1647 }
1648 })
1649 .collect::<BTreeSet<_>>()
1650 });
1651
1652 for root_name in packages.keys() {
1654 let root = self
1655 .find_by_name(root_name)
1656 .expect("found too many packages matching root");
1657
1658 let Some(root) = root else {
1659 return Ok(SatisfiesResult::MissingRoot(root_name.clone()));
1661 };
1662
1663 queue.push_back(root);
1665 }
1666
1667 while let Some(package) = queue.pop_front() {
1668 if let Source::Registry(index) = &package.id.source {
1670 match index {
1671 RegistrySource::Url(url) => {
1672 if remotes
1673 .as_ref()
1674 .is_some_and(|remotes| !remotes.contains(url))
1675 {
1676 let name = &package.id.name;
1677 let version = &package
1678 .id
1679 .version
1680 .as_ref()
1681 .expect("version for registry source");
1682 return Ok(SatisfiesResult::MissingRemoteIndex(name, version, url));
1683 }
1684 }
1685 RegistrySource::Path(path) => {
1686 if locals.as_ref().is_some_and(|locals| !locals.contains(path)) {
1687 let name = &package.id.name;
1688 let version = &package
1689 .id
1690 .version
1691 .as_ref()
1692 .expect("version for registry source");
1693 return Ok(SatisfiesResult::MissingLocalIndex(name, version, path));
1694 }
1695 }
1696 }
1697 }
1698
1699 if package.id.source.is_immutable() {
1701 continue;
1702 }
1703
1704 if let Some(version) = package.id.version.as_ref() {
1705 let HashedDist { dist, .. } = package.to_dist(
1707 root,
1708 TagPolicy::Preferred(tags),
1709 &BuildOptions::default(),
1710 markers,
1711 )?;
1712
1713 let metadata = {
1714 let id = dist.version_id();
1715 if let Some(archive) =
1716 index
1717 .distributions()
1718 .get(&id)
1719 .as_deref()
1720 .and_then(|response| {
1721 if let MetadataResponse::Found(archive, ..) = response {
1722 Some(archive)
1723 } else {
1724 None
1725 }
1726 })
1727 {
1728 archive.metadata.clone()
1730 } else {
1731 let archive = database
1733 .get_or_build_wheel_metadata(&dist, hasher.get(&dist))
1734 .await
1735 .map_err(|err| LockErrorKind::Resolution {
1736 id: package.id.clone(),
1737 err,
1738 })?;
1739
1740 let metadata = archive.metadata.clone();
1741
1742 index
1744 .distributions()
1745 .done(id, Arc::new(MetadataResponse::Found(archive)));
1746
1747 metadata
1748 }
1749 };
1750
1751 if package.id.source.is_source_tree() {
1754 if metadata.dynamic {
1755 return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, false));
1756 }
1757 }
1758
1759 if metadata.version != *version {
1761 return Ok(SatisfiesResult::MismatchedVersion(
1762 &package.id.name,
1763 version.clone(),
1764 Some(metadata.version.clone()),
1765 ));
1766 }
1767
1768 match self.satisfies_provides_extra(metadata.provides_extra, package) {
1770 SatisfiesResult::Satisfied => {}
1771 result => return Ok(result),
1772 }
1773
1774 match self.satisfies_requires_dist(
1776 metadata.requires_dist,
1777 metadata.dependency_groups,
1778 package,
1779 root,
1780 )? {
1781 SatisfiesResult::Satisfied => {}
1782 result => return Ok(result),
1783 }
1784 } else if let Some(source_tree) = package.id.source.as_source_tree() {
1785 let parent = root.join(source_tree);
1795 let path = parent.join("pyproject.toml");
1796 let metadata =
1797 match fs_err::tokio::read_to_string(&path).await {
1798 Ok(contents) => {
1799 let pyproject_toml = toml::from_str::<PyProjectToml>(&contents)
1800 .map_err(|err| LockErrorKind::InvalidPyprojectToml {
1801 path: path.clone(),
1802 err,
1803 })?;
1804 database
1805 .requires_dist(&parent, &pyproject_toml)
1806 .await
1807 .map_err(|err| LockErrorKind::Resolution {
1808 id: package.id.clone(),
1809 err,
1810 })?
1811 }
1812 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
1813 Err(err) => {
1814 return Err(LockErrorKind::UnreadablePyprojectToml { path, err }.into());
1815 }
1816 };
1817
1818 let satisfied = metadata.is_some_and(|metadata| {
1819 if !metadata.dynamic {
1821 debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
1822 return false;
1823 }
1824
1825 if let SatisfiesResult::Satisfied = self.satisfies_provides_extra(metadata.provides_extra, package, ) {
1827 debug!("Static `provides-extra` for `{}` is up-to-date", package.id);
1828 } else {
1829 debug!("Static `provides-extra` for `{}` is out-of-date; falling back to distribution database", package.id);
1830 return false;
1831 }
1832
1833 match self.satisfies_requires_dist(metadata.requires_dist, metadata.dependency_groups, package, root) {
1835 Ok(SatisfiesResult::Satisfied) => {
1836 debug!("Static `requires-dist` for `{}` is up-to-date", package.id);
1837 },
1838 Ok(..) => {
1839 debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
1840 return false;
1841 },
1842 Err(..) => {
1843 debug!("Static `requires-dist` for `{}` is invalid; falling back to distribution database", package.id);
1844 return false;
1845 },
1846 }
1847
1848 true
1849 });
1850
1851 if !satisfied {
1857 let HashedDist { dist, .. } = package.to_dist(
1858 root,
1859 TagPolicy::Preferred(tags),
1860 &BuildOptions::default(),
1861 markers,
1862 )?;
1863
1864 let metadata = {
1865 let id = dist.version_id();
1866 if let Some(archive) =
1867 index
1868 .distributions()
1869 .get(&id)
1870 .as_deref()
1871 .and_then(|response| {
1872 if let MetadataResponse::Found(archive, ..) = response {
1873 Some(archive)
1874 } else {
1875 None
1876 }
1877 })
1878 {
1879 archive.metadata.clone()
1881 } else {
1882 let archive = database
1884 .get_or_build_wheel_metadata(&dist, hasher.get(&dist))
1885 .await
1886 .map_err(|err| LockErrorKind::Resolution {
1887 id: package.id.clone(),
1888 err,
1889 })?;
1890
1891 let metadata = archive.metadata.clone();
1892
1893 index
1895 .distributions()
1896 .done(id, Arc::new(MetadataResponse::Found(archive)));
1897
1898 metadata
1899 }
1900 };
1901
1902 if !metadata.dynamic {
1904 return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, true));
1905 }
1906
1907 match self.satisfies_provides_extra(metadata.provides_extra, package) {
1909 SatisfiesResult::Satisfied => {}
1910 result => return Ok(result),
1911 }
1912
1913 match self.satisfies_requires_dist(
1915 metadata.requires_dist,
1916 metadata.dependency_groups,
1917 package,
1918 root,
1919 )? {
1920 SatisfiesResult::Satisfied => {}
1921 result => return Ok(result),
1922 }
1923 }
1924 } else {
1925 return Ok(SatisfiesResult::MissingVersion(&package.id.name));
1926 }
1927
1928 for requirement in package
1933 .metadata
1934 .requires_dist
1935 .iter()
1936 .chain(package.metadata.dependency_groups.values().flatten())
1937 {
1938 if let RequirementSource::Registry {
1939 index: Some(index), ..
1940 } = &requirement.source
1941 {
1942 match &index.url {
1943 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
1944 if let Some(remotes) = remotes.as_mut() {
1945 remotes.insert(UrlString::from(
1946 index.url().without_credentials().as_ref(),
1947 ));
1948 }
1949 }
1950 IndexUrl::Path(url) => {
1951 if let Some(locals) = locals.as_mut() {
1952 if let Some(path) = url.to_file_path().ok().and_then(|path| {
1953 relative_to(&path, root)
1954 .or_else(|_| std::path::absolute(path))
1955 .ok()
1956 }) {
1957 locals.insert(path.into_boxed_path());
1958 }
1959 }
1960 }
1961 }
1962 }
1963 }
1964
1965 for dep in &package.dependencies {
1967 if seen.insert(&dep.package_id) {
1968 let dep_dist = self.find_by_id(&dep.package_id);
1969 queue.push_back(dep_dist);
1970 }
1971 }
1972
1973 for dependencies in package.optional_dependencies.values() {
1974 for dep in dependencies {
1975 if seen.insert(&dep.package_id) {
1976 let dep_dist = self.find_by_id(&dep.package_id);
1977 queue.push_back(dep_dist);
1978 }
1979 }
1980 }
1981
1982 for dependencies in package.dependency_groups.values() {
1983 for dep in dependencies {
1984 if seen.insert(&dep.package_id) {
1985 let dep_dist = self.find_by_id(&dep.package_id);
1986 queue.push_back(dep_dist);
1987 }
1988 }
1989 }
1990 }
1991
1992 Ok(SatisfiesResult::Satisfied)
1993 }
1994}
1995
1996#[derive(Debug, Copy, Clone)]
1997enum TagPolicy<'tags> {
1998 Required(&'tags Tags),
2000 Preferred(&'tags Tags),
2003}
2004
2005impl<'tags> TagPolicy<'tags> {
2006 fn tags(&self) -> &'tags Tags {
2008 match self {
2009 Self::Required(tags) | Self::Preferred(tags) => tags,
2010 }
2011 }
2012}
2013
2014#[derive(Debug)]
2016pub enum SatisfiesResult<'lock> {
2017 Satisfied,
2019 MismatchedMembers(BTreeSet<PackageName>, &'lock BTreeSet<PackageName>),
2021 MismatchedVirtual(PackageName, bool),
2023 MismatchedEditable(PackageName, bool),
2025 MismatchedDynamic(&'lock PackageName, bool),
2027 MismatchedVersion(&'lock PackageName, Version, Option<Version>),
2029 MismatchedRequirements(BTreeSet<Requirement>, BTreeSet<Requirement>),
2031 MismatchedConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
2033 MismatchedOverrides(BTreeSet<Requirement>, BTreeSet<Requirement>),
2035 MismatchedExcludes(BTreeSet<PackageName>, BTreeSet<PackageName>),
2037 MismatchedBuildConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
2039 MismatchedDependencyGroups(
2041 BTreeMap<GroupName, BTreeSet<Requirement>>,
2042 BTreeMap<GroupName, BTreeSet<Requirement>>,
2043 ),
2044 MismatchedStaticMetadata(BTreeSet<StaticMetadata>, &'lock BTreeSet<StaticMetadata>),
2046 MissingRoot(PackageName),
2048 MissingRemoteIndex(&'lock PackageName, &'lock Version, &'lock UrlString),
2050 MissingLocalIndex(&'lock PackageName, &'lock Version, &'lock Path),
2052 MismatchedPackageRequirements(
2054 &'lock PackageName,
2055 Option<&'lock Version>,
2056 BTreeSet<Requirement>,
2057 BTreeSet<Requirement>,
2058 ),
2059 MismatchedPackageProvidesExtra(
2061 &'lock PackageName,
2062 Option<&'lock Version>,
2063 BTreeSet<ExtraName>,
2064 BTreeSet<&'lock ExtraName>,
2065 ),
2066 MismatchedPackageDependencyGroups(
2068 &'lock PackageName,
2069 Option<&'lock Version>,
2070 BTreeMap<GroupName, BTreeSet<Requirement>>,
2071 BTreeMap<GroupName, BTreeSet<Requirement>>,
2072 ),
2073 MissingVersion(&'lock PackageName),
2075}
2076
2077#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2079#[serde(rename_all = "kebab-case")]
2080struct ResolverOptions {
2081 #[serde(default)]
2083 resolution_mode: ResolutionMode,
2084 #[serde(default)]
2086 prerelease_mode: PrereleaseMode,
2087 #[serde(default)]
2089 fork_strategy: ForkStrategy,
2090 #[serde(flatten)]
2092 exclude_newer: ExcludeNewerWire,
2093}
2094
2095#[expect(clippy::struct_field_names)]
2096#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2097#[serde(rename_all = "kebab-case")]
2098struct ExcludeNewerWire {
2099 exclude_newer: Option<Timestamp>,
2100 exclude_newer_span: Option<ExcludeNewerSpan>,
2101 #[serde(default, skip_serializing_if = "ExcludeNewerPackage::is_empty")]
2102 exclude_newer_package: ExcludeNewerPackage,
2103}
2104
2105impl From<ExcludeNewerWire> for ExcludeNewer {
2106 fn from(wire: ExcludeNewerWire) -> Self {
2107 Self {
2108 global: wire
2109 .exclude_newer
2110 .map(|timestamp| ExcludeNewerValue::new(timestamp, wire.exclude_newer_span)),
2111 package: wire.exclude_newer_package,
2112 }
2113 }
2114}
2115
2116impl From<ExcludeNewer> for ExcludeNewerWire {
2117 fn from(exclude_newer: ExcludeNewer) -> Self {
2118 let (timestamp, span) = exclude_newer
2119 .global
2120 .map(ExcludeNewerValue::into_parts)
2121 .map_or((None, None), |(t, s)| (Some(t), s));
2122 Self {
2123 exclude_newer: timestamp,
2124 exclude_newer_span: span,
2125 exclude_newer_package: exclude_newer.package,
2126 }
2127 }
2128}
2129
2130#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2131#[serde(rename_all = "kebab-case")]
2132pub struct ResolverManifest {
2133 #[serde(default)]
2135 members: BTreeSet<PackageName>,
2136 #[serde(default)]
2141 requirements: BTreeSet<Requirement>,
2142 #[serde(default)]
2148 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
2149 #[serde(default)]
2151 constraints: BTreeSet<Requirement>,
2152 #[serde(default)]
2154 overrides: BTreeSet<Requirement>,
2155 #[serde(default)]
2157 excludes: BTreeSet<PackageName>,
2158 #[serde(default)]
2160 build_constraints: BTreeSet<Requirement>,
2161 #[serde(default)]
2163 dependency_metadata: BTreeSet<StaticMetadata>,
2164}
2165
2166impl ResolverManifest {
2167 pub fn new(
2170 members: impl IntoIterator<Item = PackageName>,
2171 requirements: impl IntoIterator<Item = Requirement>,
2172 constraints: impl IntoIterator<Item = Requirement>,
2173 overrides: impl IntoIterator<Item = Requirement>,
2174 excludes: impl IntoIterator<Item = PackageName>,
2175 build_constraints: impl IntoIterator<Item = Requirement>,
2176 dependency_groups: impl IntoIterator<Item = (GroupName, Vec<Requirement>)>,
2177 dependency_metadata: impl IntoIterator<Item = StaticMetadata>,
2178 ) -> Self {
2179 Self {
2180 members: members.into_iter().collect(),
2181 requirements: requirements.into_iter().collect(),
2182 constraints: constraints.into_iter().collect(),
2183 overrides: overrides.into_iter().collect(),
2184 excludes: excludes.into_iter().collect(),
2185 build_constraints: build_constraints.into_iter().collect(),
2186 dependency_groups: dependency_groups
2187 .into_iter()
2188 .map(|(group, requirements)| (group, requirements.into_iter().collect()))
2189 .collect(),
2190 dependency_metadata: dependency_metadata.into_iter().collect(),
2191 }
2192 }
2193
2194 pub fn relative_to(self, root: &Path) -> Result<Self, io::Error> {
2196 Ok(Self {
2197 members: self.members,
2198 requirements: self
2199 .requirements
2200 .into_iter()
2201 .map(|requirement| requirement.relative_to(root))
2202 .collect::<Result<BTreeSet<_>, _>>()?,
2203 constraints: self
2204 .constraints
2205 .into_iter()
2206 .map(|requirement| requirement.relative_to(root))
2207 .collect::<Result<BTreeSet<_>, _>>()?,
2208 overrides: self
2209 .overrides
2210 .into_iter()
2211 .map(|requirement| requirement.relative_to(root))
2212 .collect::<Result<BTreeSet<_>, _>>()?,
2213 excludes: self.excludes,
2214 build_constraints: self
2215 .build_constraints
2216 .into_iter()
2217 .map(|requirement| requirement.relative_to(root))
2218 .collect::<Result<BTreeSet<_>, _>>()?,
2219 dependency_groups: self
2220 .dependency_groups
2221 .into_iter()
2222 .map(|(group, requirements)| {
2223 Ok::<_, io::Error>((
2224 group,
2225 requirements
2226 .into_iter()
2227 .map(|requirement| requirement.relative_to(root))
2228 .collect::<Result<BTreeSet<_>, _>>()?,
2229 ))
2230 })
2231 .collect::<Result<BTreeMap<_, _>, _>>()?,
2232 dependency_metadata: self.dependency_metadata,
2233 })
2234 }
2235}
2236
2237#[derive(Clone, Debug, serde::Deserialize)]
2238#[serde(rename_all = "kebab-case")]
2239struct LockWire {
2240 version: u32,
2241 revision: Option<u32>,
2242 requires_python: RequiresPython,
2243 #[serde(rename = "resolution-markers", default)]
2246 fork_markers: Vec<SimplifiedMarkerTree>,
2247 #[serde(rename = "supported-markers", default)]
2248 supported_environments: Vec<SimplifiedMarkerTree>,
2249 #[serde(rename = "required-markers", default)]
2250 required_environments: Vec<SimplifiedMarkerTree>,
2251 #[serde(rename = "conflicts", default)]
2252 conflicts: Option<Conflicts>,
2253 #[serde(default)]
2255 options: ResolverOptions,
2256 #[serde(default)]
2257 manifest: ResolverManifest,
2258 #[serde(rename = "package", alias = "distribution", default)]
2259 packages: Vec<PackageWire>,
2260}
2261
2262impl TryFrom<LockWire> for Lock {
2263 type Error = LockError;
2264
2265 fn try_from(wire: LockWire) -> Result<Self, LockError> {
2266 let mut unambiguous_package_ids: FxHashMap<PackageName, PackageId> = FxHashMap::default();
2271 let mut ambiguous = FxHashSet::default();
2272 for dist in &wire.packages {
2273 if ambiguous.contains(&dist.id.name) {
2274 continue;
2275 }
2276 if let Some(id) = unambiguous_package_ids.remove(&dist.id.name) {
2277 ambiguous.insert(id.name);
2278 continue;
2279 }
2280 unambiguous_package_ids.insert(dist.id.name.clone(), dist.id.clone());
2281 }
2282
2283 let packages = wire
2284 .packages
2285 .into_iter()
2286 .map(|dist| dist.unwire(&wire.requires_python, &unambiguous_package_ids))
2287 .collect::<Result<Vec<_>, _>>()?;
2288 let supported_environments = wire
2289 .supported_environments
2290 .into_iter()
2291 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2292 .collect();
2293 let required_environments = wire
2294 .required_environments
2295 .into_iter()
2296 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2297 .collect();
2298 let fork_markers = wire
2299 .fork_markers
2300 .into_iter()
2301 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2302 .map(UniversalMarker::from_combined)
2303 .collect();
2304 let lock = Self::new(
2305 wire.version,
2306 wire.revision.unwrap_or(0),
2307 packages,
2308 wire.requires_python,
2309 wire.options,
2310 wire.manifest,
2311 wire.conflicts.unwrap_or_else(Conflicts::empty),
2312 supported_environments,
2313 required_environments,
2314 fork_markers,
2315 )?;
2316
2317 Ok(lock)
2318 }
2319}
2320
2321#[derive(Clone, Debug, serde::Deserialize)]
2325#[serde(rename_all = "kebab-case")]
2326pub struct LockVersion {
2327 version: u32,
2328}
2329
2330impl LockVersion {
2331 pub fn version(&self) -> u32 {
2333 self.version
2334 }
2335}
2336
2337#[derive(Clone, Debug, PartialEq, Eq)]
2338pub struct Package {
2339 pub(crate) id: PackageId,
2340 sdist: Option<SourceDist>,
2341 wheels: Vec<Wheel>,
2342 fork_markers: Vec<UniversalMarker>,
2348 dependencies: Vec<Dependency>,
2350 optional_dependencies: BTreeMap<ExtraName, Vec<Dependency>>,
2352 dependency_groups: BTreeMap<GroupName, Vec<Dependency>>,
2354 metadata: PackageMetadata,
2356}
2357
2358impl Package {
2359 fn from_annotated_dist(
2360 annotated_dist: &AnnotatedDist,
2361 fork_markers: Vec<UniversalMarker>,
2362 root: &Path,
2363 ) -> Result<Self, LockError> {
2364 let id = PackageId::from_annotated_dist(annotated_dist, root)?;
2365 let sdist = SourceDist::from_annotated_dist(&id, annotated_dist)?;
2366 let wheels = Wheel::from_annotated_dist(annotated_dist)?;
2367 let requires_dist = if id.source.is_immutable() {
2368 BTreeSet::default()
2369 } else {
2370 annotated_dist
2371 .metadata
2372 .as_ref()
2373 .expect("metadata is present")
2374 .requires_dist
2375 .iter()
2376 .cloned()
2377 .map(|requirement| requirement.relative_to(root))
2378 .collect::<Result<_, _>>()
2379 .map_err(LockErrorKind::RequirementRelativePath)?
2380 };
2381 let provides_extra = if id.source.is_immutable() {
2382 Box::default()
2383 } else {
2384 annotated_dist
2385 .metadata
2386 .as_ref()
2387 .expect("metadata is present")
2388 .provides_extra
2389 .clone()
2390 };
2391 let dependency_groups = if id.source.is_immutable() {
2392 BTreeMap::default()
2393 } else {
2394 annotated_dist
2395 .metadata
2396 .as_ref()
2397 .expect("metadata is present")
2398 .dependency_groups
2399 .iter()
2400 .map(|(group, requirements)| {
2401 let requirements = requirements
2402 .iter()
2403 .cloned()
2404 .map(|requirement| requirement.relative_to(root))
2405 .collect::<Result<_, _>>()
2406 .map_err(LockErrorKind::RequirementRelativePath)?;
2407 Ok::<_, LockError>((group.clone(), requirements))
2408 })
2409 .collect::<Result<_, _>>()?
2410 };
2411 Ok(Self {
2412 id,
2413 sdist,
2414 wheels,
2415 fork_markers,
2416 dependencies: vec![],
2417 optional_dependencies: BTreeMap::default(),
2418 dependency_groups: BTreeMap::default(),
2419 metadata: PackageMetadata {
2420 requires_dist,
2421 provides_extra,
2422 dependency_groups,
2423 },
2424 })
2425 }
2426
2427 fn add_dependency(
2429 &mut self,
2430 requires_python: &RequiresPython,
2431 annotated_dist: &AnnotatedDist,
2432 marker: UniversalMarker,
2433 root: &Path,
2434 ) -> Result<(), LockError> {
2435 let new_dep =
2436 Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2437 for existing_dep in &mut self.dependencies {
2438 if existing_dep.package_id == new_dep.package_id
2439 && existing_dep.simplified_marker == new_dep.simplified_marker
2462 {
2463 existing_dep.extra.extend(new_dep.extra);
2464 return Ok(());
2465 }
2466 }
2467
2468 self.dependencies.push(new_dep);
2469 Ok(())
2470 }
2471
2472 fn add_optional_dependency(
2474 &mut self,
2475 requires_python: &RequiresPython,
2476 extra: ExtraName,
2477 annotated_dist: &AnnotatedDist,
2478 marker: UniversalMarker,
2479 root: &Path,
2480 ) -> Result<(), LockError> {
2481 let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2482 let optional_deps = self.optional_dependencies.entry(extra).or_default();
2483 for existing_dep in &mut *optional_deps {
2484 if existing_dep.package_id == dep.package_id
2485 && existing_dep.simplified_marker == dep.simplified_marker
2488 {
2489 existing_dep.extra.extend(dep.extra);
2490 return Ok(());
2491 }
2492 }
2493
2494 optional_deps.push(dep);
2495 Ok(())
2496 }
2497
2498 fn add_group_dependency(
2500 &mut self,
2501 requires_python: &RequiresPython,
2502 group: GroupName,
2503 annotated_dist: &AnnotatedDist,
2504 marker: UniversalMarker,
2505 root: &Path,
2506 ) -> Result<(), LockError> {
2507 let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2508 let deps = self.dependency_groups.entry(group).or_default();
2509 for existing_dep in &mut *deps {
2510 if existing_dep.package_id == dep.package_id
2511 && existing_dep.simplified_marker == dep.simplified_marker
2514 {
2515 existing_dep.extra.extend(dep.extra);
2516 return Ok(());
2517 }
2518 }
2519
2520 deps.push(dep);
2521 Ok(())
2522 }
2523
2524 fn to_dist(
2526 &self,
2527 workspace_root: &Path,
2528 tag_policy: TagPolicy<'_>,
2529 build_options: &BuildOptions,
2530 markers: &MarkerEnvironment,
2531 ) -> Result<HashedDist, LockError> {
2532 let no_binary = build_options.no_binary_package(&self.id.name);
2533 let no_build = build_options.no_build_package(&self.id.name);
2534
2535 if !no_binary {
2536 if let Some(best_wheel_index) = self.find_best_wheel(tag_policy) {
2537 let hashes = {
2538 let wheel = &self.wheels[best_wheel_index];
2539 HashDigests::from(
2540 wheel
2541 .hash
2542 .iter()
2543 .chain(wheel.zstd.iter().flat_map(|z| z.hash.iter()))
2544 .map(|h| h.0.clone())
2545 .collect::<Vec<_>>(),
2546 )
2547 };
2548
2549 let dist = match &self.id.source {
2550 Source::Registry(source) => {
2551 let wheels = self
2552 .wheels
2553 .iter()
2554 .map(|wheel| wheel.to_registry_wheel(source, workspace_root))
2555 .collect::<Result<_, LockError>>()?;
2556 let reg_built_dist = RegistryBuiltDist {
2557 wheels,
2558 best_wheel_index,
2559 sdist: None,
2560 };
2561 Dist::Built(BuiltDist::Registry(reg_built_dist))
2562 }
2563 Source::Path(path) => {
2564 let filename: WheelFilename =
2565 self.wheels[best_wheel_index].filename.clone();
2566 let install_path = absolute_path(workspace_root, path)?;
2567 let path_dist = PathBuiltDist {
2568 filename,
2569 url: verbatim_url(&install_path, &self.id)?,
2570 install_path: absolute_path(workspace_root, path)?.into_boxed_path(),
2571 };
2572 let built_dist = BuiltDist::Path(path_dist);
2573 Dist::Built(built_dist)
2574 }
2575 Source::Direct(url, direct) => {
2576 let filename: WheelFilename =
2577 self.wheels[best_wheel_index].filename.clone();
2578 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
2579 url: url.to_url().map_err(LockErrorKind::InvalidUrl)?,
2580 subdirectory: direct.subdirectory.clone(),
2581 ext: DistExtension::Wheel,
2582 });
2583 let direct_dist = DirectUrlBuiltDist {
2584 filename,
2585 location: Box::new(url.clone()),
2586 url: VerbatimUrl::from_url(url),
2587 };
2588 let built_dist = BuiltDist::DirectUrl(direct_dist);
2589 Dist::Built(built_dist)
2590 }
2591 Source::Git(_, _) => {
2592 return Err(LockErrorKind::InvalidWheelSource {
2593 id: self.id.clone(),
2594 source_type: "Git",
2595 }
2596 .into());
2597 }
2598 Source::Directory(_) => {
2599 return Err(LockErrorKind::InvalidWheelSource {
2600 id: self.id.clone(),
2601 source_type: "directory",
2602 }
2603 .into());
2604 }
2605 Source::Editable(_) => {
2606 return Err(LockErrorKind::InvalidWheelSource {
2607 id: self.id.clone(),
2608 source_type: "editable",
2609 }
2610 .into());
2611 }
2612 Source::Virtual(_) => {
2613 return Err(LockErrorKind::InvalidWheelSource {
2614 id: self.id.clone(),
2615 source_type: "virtual",
2616 }
2617 .into());
2618 }
2619 };
2620
2621 return Ok(HashedDist { dist, hashes });
2622 }
2623 }
2624
2625 if let Some(sdist) = self.to_source_dist(workspace_root)? {
2626 if !no_build || sdist.is_virtual() {
2630 let hashes = self
2631 .sdist
2632 .as_ref()
2633 .and_then(|s| s.hash())
2634 .map(|hash| HashDigests::from(vec![hash.0.clone()]))
2635 .unwrap_or_else(|| HashDigests::from(vec![]));
2636 return Ok(HashedDist {
2637 dist: Dist::Source(sdist),
2638 hashes,
2639 });
2640 }
2641 }
2642
2643 match (no_binary, no_build) {
2644 (true, true) => Err(LockErrorKind::NoBinaryNoBuild {
2645 id: self.id.clone(),
2646 }
2647 .into()),
2648 (true, false) if self.id.source.is_wheel() => Err(LockErrorKind::NoBinaryWheelOnly {
2649 id: self.id.clone(),
2650 }
2651 .into()),
2652 (true, false) => Err(LockErrorKind::NoBinary {
2653 id: self.id.clone(),
2654 }
2655 .into()),
2656 (false, true) => Err(LockErrorKind::NoBuild {
2657 id: self.id.clone(),
2658 }
2659 .into()),
2660 (false, false) if self.id.source.is_wheel() => Err(LockError {
2661 kind: Box::new(LockErrorKind::IncompatibleWheelOnly {
2662 id: self.id.clone(),
2663 }),
2664 hint: self.tag_hint(tag_policy, markers),
2665 }),
2666 (false, false) => Err(LockError {
2667 kind: Box::new(LockErrorKind::NeitherSourceDistNorWheel {
2668 id: self.id.clone(),
2669 }),
2670 hint: self.tag_hint(tag_policy, markers),
2671 }),
2672 }
2673 }
2674
2675 fn tag_hint(
2677 &self,
2678 tag_policy: TagPolicy<'_>,
2679 markers: &MarkerEnvironment,
2680 ) -> Option<WheelTagHint> {
2681 let filenames = self
2682 .wheels
2683 .iter()
2684 .map(|wheel| &wheel.filename)
2685 .collect::<Vec<_>>();
2686 WheelTagHint::from_wheels(
2687 &self.id.name,
2688 self.id.version.as_ref(),
2689 &filenames,
2690 tag_policy.tags(),
2691 markers,
2692 )
2693 }
2694
2695 fn to_source_dist(
2700 &self,
2701 workspace_root: &Path,
2702 ) -> Result<Option<uv_distribution_types::SourceDist>, LockError> {
2703 let sdist = match &self.id.source {
2704 Source::Path(path) => {
2705 let DistExtension::Source(ext) = DistExtension::from_path(path).map_err(|err| {
2707 LockErrorKind::MissingExtension {
2708 id: self.id.clone(),
2709 err,
2710 }
2711 })?
2712 else {
2713 return Ok(None);
2714 };
2715 let install_path = absolute_path(workspace_root, path)?;
2716 let path_dist = PathSourceDist {
2717 name: self.id.name.clone(),
2718 version: self.id.version.clone(),
2719 url: verbatim_url(&install_path, &self.id)?,
2720 install_path: install_path.into_boxed_path(),
2721 ext,
2722 };
2723 uv_distribution_types::SourceDist::Path(path_dist)
2724 }
2725 Source::Directory(path) => {
2726 let install_path = absolute_path(workspace_root, path)?;
2727 let dir_dist = DirectorySourceDist {
2728 name: self.id.name.clone(),
2729 url: verbatim_url(&install_path, &self.id)?,
2730 install_path: install_path.into_boxed_path(),
2731 editable: Some(false),
2732 r#virtual: Some(false),
2733 };
2734 uv_distribution_types::SourceDist::Directory(dir_dist)
2735 }
2736 Source::Editable(path) => {
2737 let install_path = absolute_path(workspace_root, path)?;
2738 let dir_dist = DirectorySourceDist {
2739 name: self.id.name.clone(),
2740 url: verbatim_url(&install_path, &self.id)?,
2741 install_path: install_path.into_boxed_path(),
2742 editable: Some(true),
2743 r#virtual: Some(false),
2744 };
2745 uv_distribution_types::SourceDist::Directory(dir_dist)
2746 }
2747 Source::Virtual(path) => {
2748 let install_path = absolute_path(workspace_root, path)?;
2749 let dir_dist = DirectorySourceDist {
2750 name: self.id.name.clone(),
2751 url: verbatim_url(&install_path, &self.id)?,
2752 install_path: install_path.into_boxed_path(),
2753 editable: Some(false),
2754 r#virtual: Some(true),
2755 };
2756 uv_distribution_types::SourceDist::Directory(dir_dist)
2757 }
2758 Source::Git(url, git) => {
2759 let mut url = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
2762 url.set_fragment(None);
2763 url.set_query(None);
2764
2765 let git_url = GitUrl::from_commit(
2767 url,
2768 GitReference::from(git.kind.clone()),
2769 git.precise,
2770 git.lfs,
2771 )?;
2772
2773 let url = DisplaySafeUrl::from(ParsedGitUrl {
2775 url: git_url.clone(),
2776 subdirectory: git.subdirectory.clone(),
2777 });
2778
2779 let git_dist = GitSourceDist {
2780 name: self.id.name.clone(),
2781 url: VerbatimUrl::from_url(url),
2782 git: Box::new(git_url),
2783 subdirectory: git.subdirectory.clone(),
2784 };
2785 uv_distribution_types::SourceDist::Git(git_dist)
2786 }
2787 Source::Direct(url, direct) => {
2788 let DistExtension::Source(ext) =
2790 DistExtension::from_path(url.base_str()).map_err(|err| {
2791 LockErrorKind::MissingExtension {
2792 id: self.id.clone(),
2793 err,
2794 }
2795 })?
2796 else {
2797 return Ok(None);
2798 };
2799 let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
2800 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
2801 url: location.clone(),
2802 subdirectory: direct.subdirectory.clone(),
2803 ext: DistExtension::Source(ext),
2804 });
2805 let direct_dist = DirectUrlSourceDist {
2806 name: self.id.name.clone(),
2807 location: Box::new(location),
2808 subdirectory: direct.subdirectory.clone(),
2809 ext,
2810 url: VerbatimUrl::from_url(url),
2811 };
2812 uv_distribution_types::SourceDist::DirectUrl(direct_dist)
2813 }
2814 Source::Registry(RegistrySource::Url(url)) => {
2815 let Some(ref sdist) = self.sdist else {
2816 return Ok(None);
2817 };
2818
2819 let name = &self.id.name;
2820 let version = self
2821 .id
2822 .version
2823 .as_ref()
2824 .expect("version for registry source");
2825
2826 let file_url = sdist.url().ok_or_else(|| LockErrorKind::MissingUrl {
2827 name: name.clone(),
2828 version: version.clone(),
2829 })?;
2830 let filename = sdist
2831 .filename()
2832 .ok_or_else(|| LockErrorKind::MissingFilename {
2833 id: self.id.clone(),
2834 })?;
2835 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
2836 LockErrorKind::MissingExtension {
2837 id: self.id.clone(),
2838 err,
2839 }
2840 })?;
2841 let file = Box::new(uv_distribution_types::File {
2842 dist_info_metadata: false,
2843 filename: SmallString::from(filename),
2844 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
2845 HashDigests::from(hash.0.clone())
2846 }),
2847 requires_python: None,
2848 size: sdist.size(),
2849 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
2850 url: FileLocation::AbsoluteUrl(file_url.clone()),
2851 yanked: None,
2852 zstd: None,
2853 });
2854
2855 let index = IndexUrl::from(VerbatimUrl::from_url(
2856 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
2857 ));
2858
2859 let reg_dist = RegistrySourceDist {
2860 name: name.clone(),
2861 version: version.clone(),
2862 file,
2863 ext,
2864 index,
2865 wheels: vec![],
2866 };
2867 uv_distribution_types::SourceDist::Registry(reg_dist)
2868 }
2869 Source::Registry(RegistrySource::Path(path)) => {
2870 let Some(ref sdist) = self.sdist else {
2871 return Ok(None);
2872 };
2873
2874 let name = &self.id.name;
2875 let version = self
2876 .id
2877 .version
2878 .as_ref()
2879 .expect("version for registry source");
2880
2881 let file_url = match sdist {
2882 SourceDist::Url { url: file_url, .. } => {
2883 FileLocation::AbsoluteUrl(file_url.clone())
2884 }
2885 SourceDist::Path {
2886 path: file_path, ..
2887 } => {
2888 let file_path = workspace_root.join(path).join(file_path);
2889 let file_url =
2890 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
2891 LockErrorKind::PathToUrl {
2892 path: file_path.into_boxed_path(),
2893 }
2894 })?;
2895 FileLocation::AbsoluteUrl(UrlString::from(file_url))
2896 }
2897 SourceDist::Metadata { .. } => {
2898 return Err(LockErrorKind::MissingPath {
2899 name: name.clone(),
2900 version: version.clone(),
2901 }
2902 .into());
2903 }
2904 };
2905 let filename = sdist
2906 .filename()
2907 .ok_or_else(|| LockErrorKind::MissingFilename {
2908 id: self.id.clone(),
2909 })?;
2910 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
2911 LockErrorKind::MissingExtension {
2912 id: self.id.clone(),
2913 err,
2914 }
2915 })?;
2916 let file = Box::new(uv_distribution_types::File {
2917 dist_info_metadata: false,
2918 filename: SmallString::from(filename),
2919 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
2920 HashDigests::from(hash.0.clone())
2921 }),
2922 requires_python: None,
2923 size: sdist.size(),
2924 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
2925 url: file_url,
2926 yanked: None,
2927 zstd: None,
2928 });
2929
2930 let index = IndexUrl::from(
2931 VerbatimUrl::from_absolute_path(workspace_root.join(path))
2932 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
2933 );
2934
2935 let reg_dist = RegistrySourceDist {
2936 name: name.clone(),
2937 version: version.clone(),
2938 file,
2939 ext,
2940 index,
2941 wheels: vec![],
2942 };
2943 uv_distribution_types::SourceDist::Registry(reg_dist)
2944 }
2945 };
2946
2947 Ok(Some(sdist))
2948 }
2949
2950 fn to_toml(
2951 &self,
2952 requires_python: &RequiresPython,
2953 dist_count_by_name: &FxHashMap<PackageName, u64>,
2954 ) -> Result<Table, toml_edit::ser::Error> {
2955 let mut table = Table::new();
2956
2957 self.id.to_toml(None, &mut table);
2958
2959 if !self.fork_markers.is_empty() {
2960 let fork_markers = each_element_on_its_line_array(
2961 simplified_universal_markers(&self.fork_markers, requires_python).into_iter(),
2962 );
2963 if !fork_markers.is_empty() {
2964 table.insert("resolution-markers", value(fork_markers));
2965 }
2966 }
2967
2968 if !self.dependencies.is_empty() {
2969 let deps = each_element_on_its_line_array(self.dependencies.iter().map(|dep| {
2970 dep.to_toml(requires_python, dist_count_by_name)
2971 .into_inline_table()
2972 }));
2973 table.insert("dependencies", value(deps));
2974 }
2975
2976 if !self.optional_dependencies.is_empty() {
2977 let mut optional_deps = Table::new();
2978 for (extra, deps) in &self.optional_dependencies {
2979 let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
2980 dep.to_toml(requires_python, dist_count_by_name)
2981 .into_inline_table()
2982 }));
2983 if !deps.is_empty() {
2984 optional_deps.insert(extra.as_ref(), value(deps));
2985 }
2986 }
2987 if !optional_deps.is_empty() {
2988 table.insert("optional-dependencies", Item::Table(optional_deps));
2989 }
2990 }
2991
2992 if !self.dependency_groups.is_empty() {
2993 let mut dependency_groups = Table::new();
2994 for (extra, deps) in &self.dependency_groups {
2995 let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
2996 dep.to_toml(requires_python, dist_count_by_name)
2997 .into_inline_table()
2998 }));
2999 if !deps.is_empty() {
3000 dependency_groups.insert(extra.as_ref(), value(deps));
3001 }
3002 }
3003 if !dependency_groups.is_empty() {
3004 table.insert("dev-dependencies", Item::Table(dependency_groups));
3005 }
3006 }
3007
3008 if let Some(ref sdist) = self.sdist {
3009 table.insert("sdist", value(sdist.to_toml()?));
3010 }
3011
3012 if !self.wheels.is_empty() {
3013 let wheels = each_element_on_its_line_array(
3014 self.wheels
3015 .iter()
3016 .map(Wheel::to_toml)
3017 .collect::<Result<Vec<_>, _>>()?
3018 .into_iter(),
3019 );
3020 table.insert("wheels", value(wheels));
3021 }
3022
3023 {
3025 let mut metadata_table = Table::new();
3026
3027 if !self.metadata.requires_dist.is_empty() {
3028 let requires_dist = self
3029 .metadata
3030 .requires_dist
3031 .iter()
3032 .map(|requirement| {
3033 serde::Serialize::serialize(
3034 &requirement,
3035 toml_edit::ser::ValueSerializer::new(),
3036 )
3037 })
3038 .collect::<Result<Vec<_>, _>>()?;
3039 let requires_dist = match requires_dist.as_slice() {
3040 [] => Array::new(),
3041 [requirement] => Array::from_iter([requirement]),
3042 requires_dist => each_element_on_its_line_array(requires_dist.iter()),
3043 };
3044 metadata_table.insert("requires-dist", value(requires_dist));
3045 }
3046
3047 if !self.metadata.dependency_groups.is_empty() {
3048 let mut dependency_groups = Table::new();
3049 for (extra, deps) in &self.metadata.dependency_groups {
3050 let deps = deps
3051 .iter()
3052 .map(|requirement| {
3053 serde::Serialize::serialize(
3054 &requirement,
3055 toml_edit::ser::ValueSerializer::new(),
3056 )
3057 })
3058 .collect::<Result<Vec<_>, _>>()?;
3059 let deps = match deps.as_slice() {
3060 [] => Array::new(),
3061 [requirement] => Array::from_iter([requirement]),
3062 deps => each_element_on_its_line_array(deps.iter()),
3063 };
3064 dependency_groups.insert(extra.as_ref(), value(deps));
3065 }
3066 if !dependency_groups.is_empty() {
3067 metadata_table.insert("requires-dev", Item::Table(dependency_groups));
3068 }
3069 }
3070
3071 if !self.metadata.provides_extra.is_empty() {
3072 let provides_extras = self
3073 .metadata
3074 .provides_extra
3075 .iter()
3076 .map(|extra| {
3077 serde::Serialize::serialize(&extra, toml_edit::ser::ValueSerializer::new())
3078 })
3079 .collect::<Result<Vec<_>, _>>()?;
3080 let provides_extras = Array::from_iter(provides_extras);
3082 metadata_table.insert("provides-extras", value(provides_extras));
3083 }
3084
3085 if !metadata_table.is_empty() {
3086 table.insert("metadata", Item::Table(metadata_table));
3087 }
3088 }
3089
3090 Ok(table)
3091 }
3092
3093 fn find_best_wheel(&self, tag_policy: TagPolicy<'_>) -> Option<usize> {
3094 type WheelPriority<'lock> = (TagPriority, Option<&'lock BuildTag>);
3095
3096 let mut best: Option<(WheelPriority, usize)> = None;
3097 for (i, wheel) in self.wheels.iter().enumerate() {
3098 let TagCompatibility::Compatible(tag_priority) =
3099 wheel.filename.compatibility(tag_policy.tags())
3100 else {
3101 continue;
3102 };
3103 let build_tag = wheel.filename.build_tag();
3104 let wheel_priority = (tag_priority, build_tag);
3105 match best {
3106 None => {
3107 best = Some((wheel_priority, i));
3108 }
3109 Some((best_priority, _)) => {
3110 if wheel_priority > best_priority {
3111 best = Some((wheel_priority, i));
3112 }
3113 }
3114 }
3115 }
3116
3117 let best = best.map(|(_, i)| i);
3118 match tag_policy {
3119 TagPolicy::Required(_) => best,
3120 TagPolicy::Preferred(_) => best.or_else(|| self.wheels.first().map(|_| 0)),
3121 }
3122 }
3123
3124 pub fn name(&self) -> &PackageName {
3126 &self.id.name
3127 }
3128
3129 pub fn version(&self) -> Option<&Version> {
3131 self.id.version.as_ref()
3132 }
3133
3134 pub fn git_sha(&self) -> Option<&GitOid> {
3136 match &self.id.source {
3137 Source::Git(_, git) => Some(&git.precise),
3138 _ => None,
3139 }
3140 }
3141
3142 pub fn fork_markers(&self) -> &[UniversalMarker] {
3144 self.fork_markers.as_slice()
3145 }
3146
3147 pub fn index(&self, root: &Path) -> Result<Option<IndexUrl>, LockError> {
3149 match &self.id.source {
3150 Source::Registry(RegistrySource::Url(url)) => {
3151 let index = IndexUrl::from(VerbatimUrl::from_url(
3152 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
3153 ));
3154 Ok(Some(index))
3155 }
3156 Source::Registry(RegistrySource::Path(path)) => {
3157 let index = IndexUrl::from(
3158 VerbatimUrl::from_absolute_path(root.join(path))
3159 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
3160 );
3161 Ok(Some(index))
3162 }
3163 _ => Ok(None),
3164 }
3165 }
3166
3167 fn hashes(&self) -> HashDigests {
3169 let mut hashes = Vec::with_capacity(
3170 usize::from(self.sdist.as_ref().and_then(|sdist| sdist.hash()).is_some())
3171 + self
3172 .wheels
3173 .iter()
3174 .map(|wheel| usize::from(wheel.hash.is_some()))
3175 .sum::<usize>(),
3176 );
3177 if let Some(ref sdist) = self.sdist {
3178 if let Some(hash) = sdist.hash() {
3179 hashes.push(hash.0.clone());
3180 }
3181 }
3182 for wheel in &self.wheels {
3183 hashes.extend(wheel.hash.as_ref().map(|h| h.0.clone()));
3184 if let Some(zstd) = wheel.zstd.as_ref() {
3185 hashes.extend(zstd.hash.as_ref().map(|h| h.0.clone()));
3186 }
3187 }
3188 HashDigests::from(hashes)
3189 }
3190
3191 pub fn as_git_ref(&self) -> Result<Option<ResolvedRepositoryReference>, LockError> {
3193 match &self.id.source {
3194 Source::Git(url, git) => Ok(Some(ResolvedRepositoryReference {
3195 reference: RepositoryReference {
3196 url: RepositoryUrl::new(&url.to_url().map_err(LockErrorKind::InvalidUrl)?),
3197 reference: GitReference::from(git.kind.clone()),
3198 },
3199 sha: git.precise,
3200 })),
3201 _ => Ok(None),
3202 }
3203 }
3204
3205 fn is_dynamic(&self) -> bool {
3207 self.id.version.is_none()
3208 }
3209
3210 pub fn provides_extras(&self) -> &[ExtraName] {
3212 &self.metadata.provides_extra
3213 }
3214
3215 pub fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
3217 &self.metadata.dependency_groups
3218 }
3219
3220 pub fn dependencies(&self) -> &[Dependency] {
3222 &self.dependencies
3223 }
3224
3225 pub fn optional_dependencies(&self) -> &BTreeMap<ExtraName, Vec<Dependency>> {
3227 &self.optional_dependencies
3228 }
3229
3230 pub fn resolved_dependency_groups(&self) -> &BTreeMap<GroupName, Vec<Dependency>> {
3232 &self.dependency_groups
3233 }
3234
3235 pub fn as_install_target(&self) -> InstallTarget<'_> {
3237 InstallTarget {
3238 name: self.name(),
3239 is_local: self.id.source.is_local(),
3240 }
3241 }
3242}
3243
3244fn verbatim_url(path: &Path, id: &PackageId) -> Result<VerbatimUrl, LockError> {
3246 let url =
3247 VerbatimUrl::from_normalized_path(path).map_err(|err| LockErrorKind::VerbatimUrl {
3248 id: id.clone(),
3249 err,
3250 })?;
3251 Ok(url)
3252}
3253
3254fn absolute_path(workspace_root: &Path, path: &Path) -> Result<PathBuf, LockError> {
3256 let path = uv_fs::normalize_absolute_path(&workspace_root.join(path))
3257 .map_err(LockErrorKind::AbsolutePath)?;
3258 Ok(path)
3259}
3260
3261#[derive(Clone, Debug, serde::Deserialize)]
3262#[serde(rename_all = "kebab-case")]
3263struct PackageWire {
3264 #[serde(flatten)]
3265 id: PackageId,
3266 #[serde(default)]
3267 metadata: PackageMetadata,
3268 #[serde(default)]
3269 sdist: Option<SourceDist>,
3270 #[serde(default)]
3271 wheels: Vec<Wheel>,
3272 #[serde(default, rename = "resolution-markers")]
3273 fork_markers: Vec<SimplifiedMarkerTree>,
3274 #[serde(default)]
3275 dependencies: Vec<DependencyWire>,
3276 #[serde(default)]
3277 optional_dependencies: BTreeMap<ExtraName, Vec<DependencyWire>>,
3278 #[serde(default, rename = "dev-dependencies", alias = "dependency-groups")]
3279 dependency_groups: BTreeMap<GroupName, Vec<DependencyWire>>,
3280}
3281
3282#[derive(Clone, Default, Debug, Eq, PartialEq, serde::Deserialize)]
3283#[serde(rename_all = "kebab-case")]
3284struct PackageMetadata {
3285 #[serde(default)]
3286 requires_dist: BTreeSet<Requirement>,
3287 #[serde(default, rename = "provides-extras")]
3288 provides_extra: Box<[ExtraName]>,
3289 #[serde(default, rename = "requires-dev", alias = "dependency-groups")]
3290 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
3291}
3292
3293impl PackageWire {
3294 fn unwire(
3295 self,
3296 requires_python: &RequiresPython,
3297 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3298 ) -> Result<Package, LockError> {
3299 if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) {
3301 if let Some(version) = &self.id.version {
3302 for wheel in &self.wheels {
3303 if *version != wheel.filename.version
3304 && *version != wheel.filename.version.clone().without_local()
3305 {
3306 return Err(LockError::from(LockErrorKind::InconsistentVersions {
3307 name: self.id.name,
3308 version: version.clone(),
3309 wheel: wheel.clone(),
3310 }));
3311 }
3312 }
3313 }
3316 }
3317
3318 let unwire_deps = |deps: Vec<DependencyWire>| -> Result<Vec<Dependency>, LockError> {
3319 deps.into_iter()
3320 .map(|dep| dep.unwire(requires_python, unambiguous_package_ids))
3321 .collect()
3322 };
3323
3324 Ok(Package {
3325 id: self.id,
3326 metadata: self.metadata,
3327 sdist: self.sdist,
3328 wheels: self.wheels,
3329 fork_markers: self
3330 .fork_markers
3331 .into_iter()
3332 .map(|simplified_marker| simplified_marker.into_marker(requires_python))
3333 .map(UniversalMarker::from_combined)
3334 .collect(),
3335 dependencies: unwire_deps(self.dependencies)?,
3336 optional_dependencies: self
3337 .optional_dependencies
3338 .into_iter()
3339 .map(|(extra, deps)| Ok((extra, unwire_deps(deps)?)))
3340 .collect::<Result<_, LockError>>()?,
3341 dependency_groups: self
3342 .dependency_groups
3343 .into_iter()
3344 .map(|(group, deps)| Ok((group, unwire_deps(deps)?)))
3345 .collect::<Result<_, LockError>>()?,
3346 })
3347 }
3348}
3349
3350#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3353#[serde(rename_all = "kebab-case")]
3354pub(crate) struct PackageId {
3355 pub(crate) name: PackageName,
3356 pub(crate) version: Option<Version>,
3357 source: Source,
3358}
3359
3360impl PackageId {
3361 fn from_annotated_dist(annotated_dist: &AnnotatedDist, root: &Path) -> Result<Self, LockError> {
3362 let source = Source::from_resolved_dist(&annotated_dist.dist, root)?;
3364 let version = if source.is_source_tree()
3366 && annotated_dist
3367 .metadata
3368 .as_ref()
3369 .is_some_and(|metadata| metadata.dynamic)
3370 {
3371 None
3372 } else {
3373 Some(annotated_dist.version.clone())
3374 };
3375 let name = annotated_dist.name.clone();
3376 Ok(Self {
3377 name,
3378 version,
3379 source,
3380 })
3381 }
3382
3383 fn to_toml(&self, dist_count_by_name: Option<&FxHashMap<PackageName, u64>>, table: &mut Table) {
3390 let count = dist_count_by_name.and_then(|map| map.get(&self.name).copied());
3391 table.insert("name", value(self.name.to_string()));
3392 if count.map(|count| count > 1).unwrap_or(true) {
3393 if let Some(version) = &self.version {
3394 table.insert("version", value(version.to_string()));
3395 }
3396 self.source.to_toml(table);
3397 }
3398 }
3399}
3400
3401impl Display for PackageId {
3402 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3403 if let Some(version) = &self.version {
3404 write!(f, "{}=={} @ {}", self.name, version, self.source)
3405 } else {
3406 write!(f, "{} @ {}", self.name, self.source)
3407 }
3408 }
3409}
3410
3411#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3412#[serde(rename_all = "kebab-case")]
3413struct PackageIdForDependency {
3414 name: PackageName,
3415 version: Option<Version>,
3416 source: Option<Source>,
3417}
3418
3419impl PackageIdForDependency {
3420 fn unwire(
3421 self,
3422 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3423 ) -> Result<PackageId, LockError> {
3424 let unambiguous_package_id = unambiguous_package_ids.get(&self.name);
3425 let source = self.source.map(Ok::<_, LockError>).unwrap_or_else(|| {
3426 let Some(package_id) = unambiguous_package_id else {
3427 return Err(LockErrorKind::MissingDependencySource {
3428 name: self.name.clone(),
3429 }
3430 .into());
3431 };
3432 Ok(package_id.source.clone())
3433 })?;
3434 let version = if let Some(version) = self.version {
3435 Some(version)
3436 } else {
3437 if let Some(package_id) = unambiguous_package_id {
3438 package_id.version.clone()
3439 } else {
3440 if source.is_source_tree() {
3443 None
3444 } else {
3445 return Err(LockErrorKind::MissingDependencyVersion {
3446 name: self.name.clone(),
3447 }
3448 .into());
3449 }
3450 }
3451 };
3452 Ok(PackageId {
3453 name: self.name,
3454 version,
3455 source,
3456 })
3457 }
3458}
3459
3460impl From<PackageId> for PackageIdForDependency {
3461 fn from(id: PackageId) -> Self {
3462 Self {
3463 name: id.name,
3464 version: id.version,
3465 source: Some(id.source),
3466 }
3467 }
3468}
3469
3470#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3478#[serde(try_from = "SourceWire")]
3479enum Source {
3480 Registry(RegistrySource),
3482 Git(UrlString, GitSource),
3484 Direct(UrlString, DirectSource),
3486 Path(Box<Path>),
3488 Directory(Box<Path>),
3490 Editable(Box<Path>),
3492 Virtual(Box<Path>),
3494}
3495
3496impl Source {
3497 fn from_resolved_dist(resolved_dist: &ResolvedDist, root: &Path) -> Result<Self, LockError> {
3498 match *resolved_dist {
3499 ResolvedDist::Installed { .. } => unreachable!(),
3501 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(dist, root),
3502 }
3503 }
3504
3505 fn from_dist(dist: &Dist, root: &Path) -> Result<Self, LockError> {
3506 match *dist {
3507 Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, root),
3508 Dist::Source(ref source_dist) => Self::from_source_dist(source_dist, root),
3509 }
3510 }
3511
3512 fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result<Self, LockError> {
3513 match *built_dist {
3514 BuiltDist::Registry(ref reg_dist) => Self::from_registry_built_dist(reg_dist, root),
3515 BuiltDist::DirectUrl(ref direct_dist) => Ok(Self::from_direct_built_dist(direct_dist)),
3516 BuiltDist::Path(ref path_dist) => Self::from_path_built_dist(path_dist, root),
3517 }
3518 }
3519
3520 fn from_source_dist(
3521 source_dist: &uv_distribution_types::SourceDist,
3522 root: &Path,
3523 ) -> Result<Self, LockError> {
3524 match *source_dist {
3525 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
3526 Self::from_registry_source_dist(reg_dist, root)
3527 }
3528 uv_distribution_types::SourceDist::DirectUrl(ref direct_dist) => {
3529 Ok(Self::from_direct_source_dist(direct_dist))
3530 }
3531 uv_distribution_types::SourceDist::Git(ref git_dist) => {
3532 Ok(Self::from_git_dist(git_dist))
3533 }
3534 uv_distribution_types::SourceDist::Path(ref path_dist) => {
3535 Self::from_path_source_dist(path_dist, root)
3536 }
3537 uv_distribution_types::SourceDist::Directory(ref directory) => {
3538 Self::from_directory_source_dist(directory, root)
3539 }
3540 }
3541 }
3542
3543 fn from_registry_built_dist(
3544 reg_dist: &RegistryBuiltDist,
3545 root: &Path,
3546 ) -> Result<Self, LockError> {
3547 Self::from_index_url(®_dist.best_wheel().index, root)
3548 }
3549
3550 fn from_registry_source_dist(
3551 reg_dist: &RegistrySourceDist,
3552 root: &Path,
3553 ) -> Result<Self, LockError> {
3554 Self::from_index_url(®_dist.index, root)
3555 }
3556
3557 fn from_direct_built_dist(direct_dist: &DirectUrlBuiltDist) -> Self {
3558 Self::Direct(
3559 normalize_url(direct_dist.url.to_url()),
3560 DirectSource { subdirectory: None },
3561 )
3562 }
3563
3564 fn from_direct_source_dist(direct_dist: &DirectUrlSourceDist) -> Self {
3565 Self::Direct(
3566 normalize_url(direct_dist.url.to_url()),
3567 DirectSource {
3568 subdirectory: direct_dist.subdirectory.clone(),
3569 },
3570 )
3571 }
3572
3573 fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result<Self, LockError> {
3574 let path = relative_to(&path_dist.install_path, root)
3575 .or_else(|_| std::path::absolute(&path_dist.install_path))
3576 .map_err(LockErrorKind::DistributionRelativePath)?;
3577 Ok(Self::Path(path.into_boxed_path()))
3578 }
3579
3580 fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Self, LockError> {
3581 let path = relative_to(&path_dist.install_path, root)
3582 .or_else(|_| std::path::absolute(&path_dist.install_path))
3583 .map_err(LockErrorKind::DistributionRelativePath)?;
3584 Ok(Self::Path(path.into_boxed_path()))
3585 }
3586
3587 fn from_directory_source_dist(
3588 directory_dist: &DirectorySourceDist,
3589 root: &Path,
3590 ) -> Result<Self, LockError> {
3591 let path = relative_to(&directory_dist.install_path, root)
3592 .or_else(|_| std::path::absolute(&directory_dist.install_path))
3593 .map_err(LockErrorKind::DistributionRelativePath)?;
3594 if directory_dist.editable.unwrap_or(false) {
3595 Ok(Self::Editable(path.into_boxed_path()))
3596 } else if directory_dist.r#virtual.unwrap_or(false) {
3597 Ok(Self::Virtual(path.into_boxed_path()))
3598 } else {
3599 Ok(Self::Directory(path.into_boxed_path()))
3600 }
3601 }
3602
3603 fn from_index_url(index_url: &IndexUrl, root: &Path) -> Result<Self, LockError> {
3604 match index_url {
3605 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
3606 let redacted = index_url.without_credentials();
3608 let source = RegistrySource::Url(UrlString::from(redacted.as_ref()));
3609 Ok(Self::Registry(source))
3610 }
3611 IndexUrl::Path(url) => {
3612 let path = url
3613 .to_file_path()
3614 .map_err(|()| LockErrorKind::UrlToPath { url: url.to_url() })?;
3615 let path = relative_to(&path, root)
3616 .or_else(|_| std::path::absolute(&path))
3617 .map_err(LockErrorKind::IndexRelativePath)?;
3618 let source = RegistrySource::Path(path.into_boxed_path());
3619 Ok(Self::Registry(source))
3620 }
3621 }
3622 }
3623
3624 fn from_git_dist(git_dist: &GitSourceDist) -> Self {
3625 Self::Git(
3626 UrlString::from(locked_git_url(git_dist)),
3627 GitSource {
3628 kind: GitSourceKind::from(git_dist.git.reference().clone()),
3629 precise: git_dist.git.precise().unwrap_or_else(|| {
3630 panic!("Git distribution is missing a precise hash: {git_dist}")
3631 }),
3632 subdirectory: git_dist.subdirectory.clone(),
3633 lfs: git_dist.git.lfs(),
3634 },
3635 )
3636 }
3637
3638 fn is_immutable(&self) -> bool {
3645 matches!(self, Self::Registry(..) | Self::Git(_, _))
3646 }
3647
3648 fn is_wheel(&self) -> bool {
3650 match self {
3651 Self::Path(path) => {
3652 matches!(
3653 DistExtension::from_path(path).ok(),
3654 Some(DistExtension::Wheel)
3655 )
3656 }
3657 Self::Direct(url, _) => {
3658 matches!(
3659 DistExtension::from_path(url.as_ref()).ok(),
3660 Some(DistExtension::Wheel)
3661 )
3662 }
3663 Self::Directory(..) => false,
3664 Self::Editable(..) => false,
3665 Self::Virtual(..) => false,
3666 Self::Git(..) => false,
3667 Self::Registry(..) => false,
3668 }
3669 }
3670
3671 fn is_source_tree(&self) -> bool {
3673 match self {
3674 Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => true,
3675 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => false,
3676 }
3677 }
3678
3679 fn as_source_tree(&self) -> Option<&Path> {
3681 match self {
3682 Self::Directory(path) | Self::Editable(path) | Self::Virtual(path) => Some(path),
3683 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => None,
3684 }
3685 }
3686
3687 fn to_toml(&self, table: &mut Table) {
3688 let mut source_table = InlineTable::new();
3689 match self {
3690 Self::Registry(source) => match source {
3691 RegistrySource::Url(url) => {
3692 source_table.insert("registry", Value::from(url.as_ref()));
3693 }
3694 RegistrySource::Path(path) => {
3695 source_table.insert(
3696 "registry",
3697 Value::from(PortablePath::from(path).to_string()),
3698 );
3699 }
3700 },
3701 Self::Git(url, _) => {
3702 source_table.insert("git", Value::from(url.as_ref()));
3703 }
3704 Self::Direct(url, DirectSource { subdirectory }) => {
3705 source_table.insert("url", Value::from(url.as_ref()));
3706 if let Some(ref subdirectory) = *subdirectory {
3707 source_table.insert(
3708 "subdirectory",
3709 Value::from(PortablePath::from(subdirectory).to_string()),
3710 );
3711 }
3712 }
3713 Self::Path(path) => {
3714 source_table.insert("path", Value::from(PortablePath::from(path).to_string()));
3715 }
3716 Self::Directory(path) => {
3717 source_table.insert(
3718 "directory",
3719 Value::from(PortablePath::from(path).to_string()),
3720 );
3721 }
3722 Self::Editable(path) => {
3723 source_table.insert(
3724 "editable",
3725 Value::from(PortablePath::from(path).to_string()),
3726 );
3727 }
3728 Self::Virtual(path) => {
3729 source_table.insert("virtual", Value::from(PortablePath::from(path).to_string()));
3730 }
3731 }
3732 table.insert("source", value(source_table));
3733 }
3734
3735 pub(crate) fn is_local(&self) -> bool {
3737 matches!(
3738 self,
3739 Self::Path(_) | Self::Directory(_) | Self::Editable(_) | Self::Virtual(_)
3740 )
3741 }
3742}
3743
3744impl Display for Source {
3745 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3746 match self {
3747 Self::Registry(RegistrySource::Url(url)) | Self::Git(url, _) | Self::Direct(url, _) => {
3748 write!(f, "{}+{}", self.name(), url)
3749 }
3750 Self::Registry(RegistrySource::Path(path))
3751 | Self::Path(path)
3752 | Self::Directory(path)
3753 | Self::Editable(path)
3754 | Self::Virtual(path) => {
3755 write!(f, "{}+{}", self.name(), PortablePath::from(path))
3756 }
3757 }
3758 }
3759}
3760
3761impl Source {
3762 fn name(&self) -> &str {
3763 match self {
3764 Self::Registry(..) => "registry",
3765 Self::Git(..) => "git",
3766 Self::Direct(..) => "direct",
3767 Self::Path(..) => "path",
3768 Self::Directory(..) => "directory",
3769 Self::Editable(..) => "editable",
3770 Self::Virtual(..) => "virtual",
3771 }
3772 }
3773
3774 fn requires_hash(&self) -> Option<bool> {
3782 match self {
3783 Self::Registry(..) => None,
3784 Self::Direct(..) | Self::Path(..) => Some(true),
3785 Self::Git(..) | Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => {
3786 Some(false)
3787 }
3788 }
3789 }
3790}
3791
3792#[derive(Clone, Debug, serde::Deserialize)]
3793#[serde(untagged, rename_all = "kebab-case")]
3794enum SourceWire {
3795 Registry {
3796 registry: RegistrySourceWire,
3797 },
3798 Git {
3799 git: String,
3800 },
3801 Direct {
3802 url: UrlString,
3803 subdirectory: Option<PortablePathBuf>,
3804 },
3805 Path {
3806 path: PortablePathBuf,
3807 },
3808 Directory {
3809 directory: PortablePathBuf,
3810 },
3811 Editable {
3812 editable: PortablePathBuf,
3813 },
3814 Virtual {
3815 r#virtual: PortablePathBuf,
3816 },
3817}
3818
3819impl TryFrom<SourceWire> for Source {
3820 type Error = LockError;
3821
3822 fn try_from(wire: SourceWire) -> Result<Self, LockError> {
3823 #[allow(clippy::enum_glob_use)]
3824 use self::SourceWire::*;
3825
3826 match wire {
3827 Registry { registry } => Ok(Self::Registry(registry.into())),
3828 Git { git } => {
3829 let url = DisplaySafeUrl::parse(&git)
3830 .map_err(|err| SourceParseError::InvalidUrl {
3831 given: git.clone(),
3832 err,
3833 })
3834 .map_err(LockErrorKind::InvalidGitSourceUrl)?;
3835
3836 let git_source = GitSource::from_url(&url)
3837 .map_err(|err| match err {
3838 GitSourceError::InvalidSha => SourceParseError::InvalidSha { given: git },
3839 GitSourceError::MissingSha => SourceParseError::MissingSha { given: git },
3840 })
3841 .map_err(LockErrorKind::InvalidGitSourceUrl)?;
3842
3843 Ok(Self::Git(UrlString::from(url), git_source))
3844 }
3845 Direct { url, subdirectory } => Ok(Self::Direct(
3846 url,
3847 DirectSource {
3848 subdirectory: subdirectory.map(Box::<std::path::Path>::from),
3849 },
3850 )),
3851 Path { path } => Ok(Self::Path(path.into())),
3852 Directory { directory } => Ok(Self::Directory(directory.into())),
3853 Editable { editable } => Ok(Self::Editable(editable.into())),
3854 Virtual { r#virtual } => Ok(Self::Virtual(r#virtual.into())),
3855 }
3856 }
3857}
3858
3859#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
3861enum RegistrySource {
3862 Url(UrlString),
3864 Path(Box<Path>),
3866}
3867
3868impl Display for RegistrySource {
3869 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3870 match self {
3871 Self::Url(url) => write!(f, "{url}"),
3872 Self::Path(path) => write!(f, "{}", path.display()),
3873 }
3874 }
3875}
3876
3877#[derive(Clone, Debug)]
3878enum RegistrySourceWire {
3879 Url(UrlString),
3881 Path(PortablePathBuf),
3883}
3884
3885impl<'de> serde::de::Deserialize<'de> for RegistrySourceWire {
3886 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3887 where
3888 D: serde::de::Deserializer<'de>,
3889 {
3890 struct Visitor;
3891
3892 impl serde::de::Visitor<'_> for Visitor {
3893 type Value = RegistrySourceWire;
3894
3895 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
3896 formatter.write_str("a valid URL or a file path")
3897 }
3898
3899 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
3900 where
3901 E: serde::de::Error,
3902 {
3903 if split_scheme(value).is_some() {
3904 Ok(
3905 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
3906 value,
3907 ))
3908 .map(RegistrySourceWire::Url)?,
3909 )
3910 } else {
3911 Ok(
3912 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
3913 value,
3914 ))
3915 .map(RegistrySourceWire::Path)?,
3916 )
3917 }
3918 }
3919 }
3920
3921 deserializer.deserialize_str(Visitor)
3922 }
3923}
3924
3925impl From<RegistrySourceWire> for RegistrySource {
3926 fn from(wire: RegistrySourceWire) -> Self {
3927 match wire {
3928 RegistrySourceWire::Url(url) => Self::Url(url),
3929 RegistrySourceWire::Path(path) => Self::Path(path.into()),
3930 }
3931 }
3932}
3933
3934#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3935#[serde(rename_all = "kebab-case")]
3936struct DirectSource {
3937 subdirectory: Option<Box<Path>>,
3938}
3939
3940#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
3945struct GitSource {
3946 precise: GitOid,
3947 subdirectory: Option<Box<Path>>,
3948 kind: GitSourceKind,
3949 lfs: GitLfs,
3950}
3951
3952#[derive(Clone, Debug, Eq, PartialEq)]
3954enum GitSourceError {
3955 InvalidSha,
3956 MissingSha,
3957}
3958
3959impl GitSource {
3960 fn from_url(url: &Url) -> Result<Self, GitSourceError> {
3963 let mut kind = GitSourceKind::DefaultBranch;
3964 let mut subdirectory = None;
3965 let mut lfs = GitLfs::Disabled;
3966 for (key, val) in url.query_pairs() {
3967 match &*key {
3968 "tag" => kind = GitSourceKind::Tag(val.into_owned()),
3969 "branch" => kind = GitSourceKind::Branch(val.into_owned()),
3970 "rev" => kind = GitSourceKind::Rev(val.into_owned()),
3971 "subdirectory" => subdirectory = Some(PortablePathBuf::from(val.as_ref()).into()),
3972 "lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
3973 _ => {}
3974 }
3975 }
3976
3977 let precise = GitOid::from_str(url.fragment().ok_or(GitSourceError::MissingSha)?)
3978 .map_err(|_| GitSourceError::InvalidSha)?;
3979
3980 Ok(Self {
3981 precise,
3982 subdirectory,
3983 kind,
3984 lfs,
3985 })
3986 }
3987}
3988
3989#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3990#[serde(rename_all = "kebab-case")]
3991enum GitSourceKind {
3992 Tag(String),
3993 Branch(String),
3994 Rev(String),
3995 DefaultBranch,
3996}
3997
3998#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4000#[serde(rename_all = "kebab-case")]
4001struct SourceDistMetadata {
4002 hash: Option<Hash>,
4004 size: Option<u64>,
4008 #[serde(alias = "upload_time")]
4010 upload_time: Option<Timestamp>,
4011}
4012
4013#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4018#[serde(from = "SourceDistWire")]
4019enum SourceDist {
4020 Url {
4021 url: UrlString,
4022 #[serde(flatten)]
4023 metadata: SourceDistMetadata,
4024 },
4025 Path {
4026 path: Box<Path>,
4027 #[serde(flatten)]
4028 metadata: SourceDistMetadata,
4029 },
4030 Metadata {
4031 #[serde(flatten)]
4032 metadata: SourceDistMetadata,
4033 },
4034}
4035
4036impl SourceDist {
4037 fn filename(&self) -> Option<Cow<'_, str>> {
4038 match self {
4039 Self::Metadata { .. } => None,
4040 Self::Url { url, .. } => url.filename().ok(),
4041 Self::Path { path, .. } => path.file_name().map(|filename| filename.to_string_lossy()),
4042 }
4043 }
4044
4045 fn url(&self) -> Option<&UrlString> {
4046 match self {
4047 Self::Metadata { .. } => None,
4048 Self::Url { url, .. } => Some(url),
4049 Self::Path { .. } => None,
4050 }
4051 }
4052
4053 pub(crate) fn hash(&self) -> Option<&Hash> {
4054 match self {
4055 Self::Metadata { metadata } => metadata.hash.as_ref(),
4056 Self::Url { metadata, .. } => metadata.hash.as_ref(),
4057 Self::Path { metadata, .. } => metadata.hash.as_ref(),
4058 }
4059 }
4060
4061 pub(crate) fn size(&self) -> Option<u64> {
4062 match self {
4063 Self::Metadata { metadata } => metadata.size,
4064 Self::Url { metadata, .. } => metadata.size,
4065 Self::Path { metadata, .. } => metadata.size,
4066 }
4067 }
4068
4069 pub(crate) fn upload_time(&self) -> Option<Timestamp> {
4070 match self {
4071 Self::Metadata { metadata } => metadata.upload_time,
4072 Self::Url { metadata, .. } => metadata.upload_time,
4073 Self::Path { metadata, .. } => metadata.upload_time,
4074 }
4075 }
4076}
4077
4078impl SourceDist {
4079 fn from_annotated_dist(
4080 id: &PackageId,
4081 annotated_dist: &AnnotatedDist,
4082 ) -> Result<Option<Self>, LockError> {
4083 match annotated_dist.dist {
4084 ResolvedDist::Installed { .. } => unreachable!(),
4086 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4087 id,
4088 dist,
4089 annotated_dist.hashes.as_slice(),
4090 annotated_dist.index(),
4091 ),
4092 }
4093 }
4094
4095 fn from_dist(
4096 id: &PackageId,
4097 dist: &Dist,
4098 hashes: &[HashDigest],
4099 index: Option<&IndexUrl>,
4100 ) -> Result<Option<Self>, LockError> {
4101 match *dist {
4102 Dist::Built(BuiltDist::Registry(ref built_dist)) => {
4103 let Some(sdist) = built_dist.sdist.as_ref() else {
4104 return Ok(None);
4105 };
4106 Self::from_registry_dist(sdist, index)
4107 }
4108 Dist::Built(_) => Ok(None),
4109 Dist::Source(ref source_dist) => Self::from_source_dist(id, source_dist, hashes, index),
4110 }
4111 }
4112
4113 fn from_source_dist(
4114 id: &PackageId,
4115 source_dist: &uv_distribution_types::SourceDist,
4116 hashes: &[HashDigest],
4117 index: Option<&IndexUrl>,
4118 ) -> Result<Option<Self>, LockError> {
4119 match *source_dist {
4120 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
4121 Self::from_registry_dist(reg_dist, index)
4122 }
4123 uv_distribution_types::SourceDist::DirectUrl(_) => {
4124 Self::from_direct_dist(id, hashes).map(Some)
4125 }
4126 uv_distribution_types::SourceDist::Path(_) => {
4127 Self::from_path_dist(id, hashes).map(Some)
4128 }
4129 uv_distribution_types::SourceDist::Git(_)
4133 | uv_distribution_types::SourceDist::Directory(_) => Ok(None),
4134 }
4135 }
4136
4137 fn from_registry_dist(
4138 reg_dist: &RegistrySourceDist,
4139 index: Option<&IndexUrl>,
4140 ) -> Result<Option<Self>, LockError> {
4141 if index.is_none_or(|index| *index != reg_dist.index) {
4144 return Ok(None);
4145 }
4146
4147 match ®_dist.index {
4148 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4149 let url = normalize_file_location(®_dist.file.url)
4150 .map_err(LockErrorKind::InvalidUrl)
4151 .map_err(LockError::from)?;
4152 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4153 let size = reg_dist.file.size;
4154 let upload_time = reg_dist
4155 .file
4156 .upload_time_utc_ms
4157 .map(Timestamp::from_millisecond)
4158 .transpose()
4159 .map_err(LockErrorKind::InvalidTimestamp)?;
4160 Ok(Some(Self::Url {
4161 url,
4162 metadata: SourceDistMetadata {
4163 hash,
4164 size,
4165 upload_time,
4166 },
4167 }))
4168 }
4169 IndexUrl::Path(path) => {
4170 let index_path = path
4171 .to_file_path()
4172 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4173 let url = reg_dist
4174 .file
4175 .url
4176 .to_url()
4177 .map_err(LockErrorKind::InvalidUrl)?;
4178
4179 if url.scheme() == "file" {
4180 let reg_dist_path = url
4181 .to_file_path()
4182 .map_err(|()| LockErrorKind::UrlToPath { url })?;
4183 let path = relative_to(®_dist_path, index_path)
4184 .or_else(|_| std::path::absolute(®_dist_path))
4185 .map_err(LockErrorKind::DistributionRelativePath)?
4186 .into_boxed_path();
4187 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4188 let size = reg_dist.file.size;
4189 let upload_time = reg_dist
4190 .file
4191 .upload_time_utc_ms
4192 .map(Timestamp::from_millisecond)
4193 .transpose()
4194 .map_err(LockErrorKind::InvalidTimestamp)?;
4195 Ok(Some(Self::Path {
4196 path,
4197 metadata: SourceDistMetadata {
4198 hash,
4199 size,
4200 upload_time,
4201 },
4202 }))
4203 } else {
4204 let url = normalize_file_location(®_dist.file.url)
4205 .map_err(LockErrorKind::InvalidUrl)
4206 .map_err(LockError::from)?;
4207 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4208 let size = reg_dist.file.size;
4209 let upload_time = reg_dist
4210 .file
4211 .upload_time_utc_ms
4212 .map(Timestamp::from_millisecond)
4213 .transpose()
4214 .map_err(LockErrorKind::InvalidTimestamp)?;
4215 Ok(Some(Self::Url {
4216 url,
4217 metadata: SourceDistMetadata {
4218 hash,
4219 size,
4220 upload_time,
4221 },
4222 }))
4223 }
4224 }
4225 }
4226 }
4227
4228 fn from_direct_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4229 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4230 let kind = LockErrorKind::Hash {
4231 id: id.clone(),
4232 artifact_type: "direct URL source distribution",
4233 expected: true,
4234 };
4235 return Err(kind.into());
4236 };
4237 Ok(Self::Metadata {
4238 metadata: SourceDistMetadata {
4239 hash: Some(hash),
4240 size: None,
4241 upload_time: None,
4242 },
4243 })
4244 }
4245
4246 fn from_path_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4247 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4248 let kind = LockErrorKind::Hash {
4249 id: id.clone(),
4250 artifact_type: "path source distribution",
4251 expected: true,
4252 };
4253 return Err(kind.into());
4254 };
4255 Ok(Self::Metadata {
4256 metadata: SourceDistMetadata {
4257 hash: Some(hash),
4258 size: None,
4259 upload_time: None,
4260 },
4261 })
4262 }
4263}
4264
4265#[derive(Clone, Debug, serde::Deserialize)]
4266#[serde(untagged, rename_all = "kebab-case")]
4267enum SourceDistWire {
4268 Url {
4269 url: UrlString,
4270 #[serde(flatten)]
4271 metadata: SourceDistMetadata,
4272 },
4273 Path {
4274 path: PortablePathBuf,
4275 #[serde(flatten)]
4276 metadata: SourceDistMetadata,
4277 },
4278 Metadata {
4279 #[serde(flatten)]
4280 metadata: SourceDistMetadata,
4281 },
4282}
4283
4284impl SourceDist {
4285 fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
4287 let mut table = InlineTable::new();
4288 match self {
4289 Self::Metadata { .. } => {}
4290 Self::Url { url, .. } => {
4291 table.insert("url", Value::from(url.as_ref()));
4292 }
4293 Self::Path { path, .. } => {
4294 table.insert("path", Value::from(PortablePath::from(path).to_string()));
4295 }
4296 }
4297 if let Some(hash) = self.hash() {
4298 table.insert("hash", Value::from(hash.to_string()));
4299 }
4300 if let Some(size) = self.size() {
4301 table.insert(
4302 "size",
4303 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4304 );
4305 }
4306 if let Some(upload_time) = self.upload_time() {
4307 table.insert("upload-time", Value::from(upload_time.to_string()));
4308 }
4309 Ok(table)
4310 }
4311}
4312
4313impl From<SourceDistWire> for SourceDist {
4314 fn from(wire: SourceDistWire) -> Self {
4315 match wire {
4316 SourceDistWire::Url { url, metadata } => Self::Url { url, metadata },
4317 SourceDistWire::Path { path, metadata } => Self::Path {
4318 path: path.into(),
4319 metadata,
4320 },
4321 SourceDistWire::Metadata { metadata } => Self::Metadata { metadata },
4322 }
4323 }
4324}
4325
4326impl From<GitReference> for GitSourceKind {
4327 fn from(value: GitReference) -> Self {
4328 match value {
4329 GitReference::Branch(branch) => Self::Branch(branch),
4330 GitReference::Tag(tag) => Self::Tag(tag),
4331 GitReference::BranchOrTag(rev) => Self::Rev(rev),
4332 GitReference::BranchOrTagOrCommit(rev) => Self::Rev(rev),
4333 GitReference::NamedRef(rev) => Self::Rev(rev),
4334 GitReference::DefaultBranch => Self::DefaultBranch,
4335 }
4336 }
4337}
4338
4339impl From<GitSourceKind> for GitReference {
4340 fn from(value: GitSourceKind) -> Self {
4341 match value {
4342 GitSourceKind::Branch(branch) => Self::Branch(branch),
4343 GitSourceKind::Tag(tag) => Self::Tag(tag),
4344 GitSourceKind::Rev(rev) => Self::from_rev(rev),
4345 GitSourceKind::DefaultBranch => Self::DefaultBranch,
4346 }
4347 }
4348}
4349
4350fn locked_git_url(git_dist: &GitSourceDist) -> DisplaySafeUrl {
4352 let mut url = git_dist.git.repository().clone();
4353
4354 url.remove_credentials();
4356
4357 url.set_fragment(None);
4359 url.set_query(None);
4360
4361 if let Some(subdirectory) = git_dist
4363 .subdirectory
4364 .as_deref()
4365 .map(PortablePath::from)
4366 .as_ref()
4367 .map(PortablePath::to_string)
4368 {
4369 url.query_pairs_mut()
4370 .append_pair("subdirectory", &subdirectory);
4371 }
4372
4373 if git_dist.git.lfs().enabled() {
4375 url.query_pairs_mut().append_pair("lfs", "true");
4376 }
4377
4378 match git_dist.git.reference() {
4380 GitReference::Branch(branch) => {
4381 url.query_pairs_mut().append_pair("branch", branch.as_str());
4382 }
4383 GitReference::Tag(tag) => {
4384 url.query_pairs_mut().append_pair("tag", tag.as_str());
4385 }
4386 GitReference::BranchOrTag(rev)
4387 | GitReference::BranchOrTagOrCommit(rev)
4388 | GitReference::NamedRef(rev) => {
4389 url.query_pairs_mut().append_pair("rev", rev.as_str());
4390 }
4391 GitReference::DefaultBranch => {}
4392 }
4393
4394 url.set_fragment(
4396 git_dist
4397 .git
4398 .precise()
4399 .as_ref()
4400 .map(GitOid::to_string)
4401 .as_deref(),
4402 );
4403
4404 url
4405}
4406
4407#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4408struct ZstdWheel {
4409 hash: Option<Hash>,
4410 size: Option<u64>,
4411}
4412
4413#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4415#[serde(try_from = "WheelWire")]
4416struct Wheel {
4417 url: WheelWireSource,
4422 hash: Option<Hash>,
4428 size: Option<u64>,
4432 upload_time: Option<Timestamp>,
4436 filename: WheelFilename,
4443 zstd: Option<ZstdWheel>,
4445}
4446
4447impl Wheel {
4448 fn from_annotated_dist(annotated_dist: &AnnotatedDist) -> Result<Vec<Self>, LockError> {
4449 match annotated_dist.dist {
4450 ResolvedDist::Installed { .. } => unreachable!(),
4452 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4453 dist,
4454 annotated_dist.hashes.as_slice(),
4455 annotated_dist.index(),
4456 ),
4457 }
4458 }
4459
4460 fn from_dist(
4461 dist: &Dist,
4462 hashes: &[HashDigest],
4463 index: Option<&IndexUrl>,
4464 ) -> Result<Vec<Self>, LockError> {
4465 match *dist {
4466 Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, hashes, index),
4467 Dist::Source(uv_distribution_types::SourceDist::Registry(ref source_dist)) => {
4468 source_dist
4469 .wheels
4470 .iter()
4471 .filter(|wheel| {
4472 index.is_some_and(|index| *index == wheel.index)
4475 })
4476 .map(Self::from_registry_wheel)
4477 .collect()
4478 }
4479 Dist::Source(_) => Ok(vec![]),
4480 }
4481 }
4482
4483 fn from_built_dist(
4484 built_dist: &BuiltDist,
4485 hashes: &[HashDigest],
4486 index: Option<&IndexUrl>,
4487 ) -> Result<Vec<Self>, LockError> {
4488 match *built_dist {
4489 BuiltDist::Registry(ref reg_dist) => Self::from_registry_dist(reg_dist, index),
4490 BuiltDist::DirectUrl(ref direct_dist) => {
4491 Ok(vec![Self::from_direct_dist(direct_dist, hashes)])
4492 }
4493 BuiltDist::Path(ref path_dist) => Ok(vec![Self::from_path_dist(path_dist, hashes)]),
4494 }
4495 }
4496
4497 fn from_registry_dist(
4498 reg_dist: &RegistryBuiltDist,
4499 index: Option<&IndexUrl>,
4500 ) -> Result<Vec<Self>, LockError> {
4501 reg_dist
4502 .wheels
4503 .iter()
4504 .filter(|wheel| {
4505 index.is_some_and(|index| *index == wheel.index)
4508 })
4509 .map(Self::from_registry_wheel)
4510 .collect()
4511 }
4512
4513 fn from_registry_wheel(wheel: &RegistryBuiltWheel) -> Result<Self, LockError> {
4514 let url = match &wheel.index {
4515 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4516 let url = normalize_file_location(&wheel.file.url)
4517 .map_err(LockErrorKind::InvalidUrl)
4518 .map_err(LockError::from)?;
4519 WheelWireSource::Url { url }
4520 }
4521 IndexUrl::Path(path) => {
4522 let index_path = path
4523 .to_file_path()
4524 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4525 let wheel_url = wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?;
4526
4527 if wheel_url.scheme() == "file" {
4528 let wheel_path = wheel_url
4529 .to_file_path()
4530 .map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?;
4531 let path = relative_to(&wheel_path, index_path)
4532 .or_else(|_| std::path::absolute(&wheel_path))
4533 .map_err(LockErrorKind::DistributionRelativePath)?
4534 .into_boxed_path();
4535 WheelWireSource::Path { path }
4536 } else {
4537 let url = normalize_file_location(&wheel.file.url)
4538 .map_err(LockErrorKind::InvalidUrl)
4539 .map_err(LockError::from)?;
4540 WheelWireSource::Url { url }
4541 }
4542 }
4543 };
4544 let filename = wheel.filename.clone();
4545 let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from);
4546 let size = wheel.file.size;
4547 let upload_time = wheel
4548 .file
4549 .upload_time_utc_ms
4550 .map(Timestamp::from_millisecond)
4551 .transpose()
4552 .map_err(LockErrorKind::InvalidTimestamp)?;
4553 let zstd = wheel.file.zstd.as_ref().map(|zstd| ZstdWheel {
4554 hash: zstd.hashes.iter().max().cloned().map(Hash::from),
4555 size: zstd.size,
4556 });
4557 Ok(Self {
4558 url,
4559 hash,
4560 size,
4561 upload_time,
4562 filename,
4563 zstd,
4564 })
4565 }
4566
4567 fn from_direct_dist(direct_dist: &DirectUrlBuiltDist, hashes: &[HashDigest]) -> Self {
4568 Self {
4569 url: WheelWireSource::Url {
4570 url: normalize_url(direct_dist.url.to_url()),
4571 },
4572 hash: hashes.iter().max().cloned().map(Hash::from),
4573 size: None,
4574 upload_time: None,
4575 filename: direct_dist.filename.clone(),
4576 zstd: None,
4577 }
4578 }
4579
4580 fn from_path_dist(path_dist: &PathBuiltDist, hashes: &[HashDigest]) -> Self {
4581 Self {
4582 url: WheelWireSource::Filename {
4583 filename: path_dist.filename.clone(),
4584 },
4585 hash: hashes.iter().max().cloned().map(Hash::from),
4586 size: None,
4587 upload_time: None,
4588 filename: path_dist.filename.clone(),
4589 zstd: None,
4590 }
4591 }
4592
4593 pub(crate) fn to_registry_wheel(
4594 &self,
4595 source: &RegistrySource,
4596 root: &Path,
4597 ) -> Result<RegistryBuiltWheel, LockError> {
4598 let filename: WheelFilename = self.filename.clone();
4599
4600 match source {
4601 RegistrySource::Url(url) => {
4602 let file_location = match &self.url {
4603 WheelWireSource::Url { url: file_url } => {
4604 FileLocation::AbsoluteUrl(file_url.clone())
4605 }
4606 WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => {
4607 return Err(LockErrorKind::MissingUrl {
4608 name: filename.name,
4609 version: filename.version,
4610 }
4611 .into());
4612 }
4613 };
4614 let file = Box::new(uv_distribution_types::File {
4615 dist_info_metadata: false,
4616 filename: SmallString::from(filename.to_string()),
4617 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
4618 requires_python: None,
4619 size: self.size,
4620 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
4621 url: file_location,
4622 yanked: None,
4623 zstd: self
4624 .zstd
4625 .as_ref()
4626 .map(|zstd| uv_distribution_types::Zstd {
4627 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
4628 size: zstd.size,
4629 })
4630 .map(Box::new),
4631 });
4632 let index = IndexUrl::from(VerbatimUrl::from_url(
4633 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
4634 ));
4635 Ok(RegistryBuiltWheel {
4636 filename,
4637 file,
4638 index,
4639 })
4640 }
4641 RegistrySource::Path(index_path) => {
4642 let file_location = match &self.url {
4643 WheelWireSource::Url { url: file_url } => {
4644 FileLocation::AbsoluteUrl(file_url.clone())
4645 }
4646 WheelWireSource::Path { path: file_path } => {
4647 let file_path = root.join(index_path).join(file_path);
4648 let file_url =
4649 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
4650 LockErrorKind::PathToUrl {
4651 path: file_path.into_boxed_path(),
4652 }
4653 })?;
4654 FileLocation::AbsoluteUrl(UrlString::from(file_url))
4655 }
4656 WheelWireSource::Filename { .. } => {
4657 return Err(LockErrorKind::MissingPath {
4658 name: filename.name,
4659 version: filename.version,
4660 }
4661 .into());
4662 }
4663 };
4664 let file = Box::new(uv_distribution_types::File {
4665 dist_info_metadata: false,
4666 filename: SmallString::from(filename.to_string()),
4667 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
4668 requires_python: None,
4669 size: self.size,
4670 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
4671 url: file_location,
4672 yanked: None,
4673 zstd: self
4674 .zstd
4675 .as_ref()
4676 .map(|zstd| uv_distribution_types::Zstd {
4677 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
4678 size: zstd.size,
4679 })
4680 .map(Box::new),
4681 });
4682 let index = IndexUrl::from(
4683 VerbatimUrl::from_absolute_path(root.join(index_path))
4684 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
4685 );
4686 Ok(RegistryBuiltWheel {
4687 filename,
4688 file,
4689 index,
4690 })
4691 }
4692 }
4693 }
4694}
4695
4696#[derive(Clone, Debug, serde::Deserialize)]
4697#[serde(rename_all = "kebab-case")]
4698struct WheelWire {
4699 #[serde(flatten)]
4700 url: WheelWireSource,
4701 hash: Option<Hash>,
4707 size: Option<u64>,
4711 #[serde(alias = "upload_time")]
4715 upload_time: Option<Timestamp>,
4716 #[serde(alias = "zstd")]
4718 zstd: Option<ZstdWheel>,
4719}
4720
4721#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4722#[serde(untagged, rename_all = "kebab-case")]
4723enum WheelWireSource {
4724 Url {
4726 url: UrlString,
4731 },
4732 Path {
4734 path: Box<Path>,
4736 },
4737 Filename {
4741 filename: WheelFilename,
4744 },
4745}
4746
4747impl Wheel {
4748 fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
4750 let mut table = InlineTable::new();
4751 match &self.url {
4752 WheelWireSource::Url { url } => {
4753 table.insert("url", Value::from(url.as_ref()));
4754 }
4755 WheelWireSource::Path { path } => {
4756 table.insert("path", Value::from(PortablePath::from(path).to_string()));
4757 }
4758 WheelWireSource::Filename { filename } => {
4759 table.insert("filename", Value::from(filename.to_string()));
4760 }
4761 }
4762 if let Some(ref hash) = self.hash {
4763 table.insert("hash", Value::from(hash.to_string()));
4764 }
4765 if let Some(size) = self.size {
4766 table.insert(
4767 "size",
4768 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4769 );
4770 }
4771 if let Some(upload_time) = self.upload_time {
4772 table.insert("upload-time", Value::from(upload_time.to_string()));
4773 }
4774 if let Some(zstd) = &self.zstd {
4775 let mut inner = InlineTable::new();
4776 if let Some(ref hash) = zstd.hash {
4777 inner.insert("hash", Value::from(hash.to_string()));
4778 }
4779 if let Some(size) = zstd.size {
4780 inner.insert(
4781 "size",
4782 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4783 );
4784 }
4785 table.insert("zstd", Value::from(inner));
4786 }
4787 Ok(table)
4788 }
4789}
4790
4791impl TryFrom<WheelWire> for Wheel {
4792 type Error = String;
4793
4794 fn try_from(wire: WheelWire) -> Result<Self, String> {
4795 let filename = match &wire.url {
4796 WheelWireSource::Url { url } => {
4797 let filename = url.filename().map_err(|err| err.to_string())?;
4798 filename.parse::<WheelFilename>().map_err(|err| {
4799 format!("failed to parse `{filename}` as wheel filename: {err}")
4800 })?
4801 }
4802 WheelWireSource::Path { path } => {
4803 let filename = path
4804 .file_name()
4805 .and_then(|file_name| file_name.to_str())
4806 .ok_or_else(|| {
4807 format!("path `{}` has no filename component", path.display())
4808 })?;
4809 filename.parse::<WheelFilename>().map_err(|err| {
4810 format!("failed to parse `{filename}` as wheel filename: {err}")
4811 })?
4812 }
4813 WheelWireSource::Filename { filename } => filename.clone(),
4814 };
4815
4816 Ok(Self {
4817 url: wire.url,
4818 hash: wire.hash,
4819 size: wire.size,
4820 upload_time: wire.upload_time,
4821 zstd: wire.zstd,
4822 filename,
4823 })
4824 }
4825}
4826
4827#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
4829pub struct Dependency {
4830 package_id: PackageId,
4831 extra: BTreeSet<ExtraName>,
4832 simplified_marker: SimplifiedMarkerTree,
4852 complexified_marker: UniversalMarker,
4856}
4857
4858impl Dependency {
4859 fn new(
4860 requires_python: &RequiresPython,
4861 package_id: PackageId,
4862 extra: BTreeSet<ExtraName>,
4863 complexified_marker: UniversalMarker,
4864 ) -> Self {
4865 let simplified_marker =
4866 SimplifiedMarkerTree::new(requires_python, complexified_marker.combined());
4867 let complexified_marker = simplified_marker.into_marker(requires_python);
4868 Self {
4869 package_id,
4870 extra,
4871 simplified_marker,
4872 complexified_marker: UniversalMarker::from_combined(complexified_marker),
4873 }
4874 }
4875
4876 fn from_annotated_dist(
4877 requires_python: &RequiresPython,
4878 annotated_dist: &AnnotatedDist,
4879 complexified_marker: UniversalMarker,
4880 root: &Path,
4881 ) -> Result<Self, LockError> {
4882 let package_id = PackageId::from_annotated_dist(annotated_dist, root)?;
4883 let extra = annotated_dist.extra.iter().cloned().collect();
4884 Ok(Self::new(
4885 requires_python,
4886 package_id,
4887 extra,
4888 complexified_marker,
4889 ))
4890 }
4891
4892 fn to_toml(
4894 &self,
4895 _requires_python: &RequiresPython,
4896 dist_count_by_name: &FxHashMap<PackageName, u64>,
4897 ) -> Table {
4898 let mut table = Table::new();
4899 self.package_id
4900 .to_toml(Some(dist_count_by_name), &mut table);
4901 if !self.extra.is_empty() {
4902 let extra_array = self
4903 .extra
4904 .iter()
4905 .map(ToString::to_string)
4906 .collect::<Array>();
4907 table.insert("extra", value(extra_array));
4908 }
4909 if let Some(marker) = self.simplified_marker.try_to_string() {
4910 table.insert("marker", value(marker));
4911 }
4912
4913 table
4914 }
4915
4916 pub fn package_name(&self) -> &PackageName {
4918 &self.package_id.name
4919 }
4920
4921 pub fn extra(&self) -> &BTreeSet<ExtraName> {
4923 &self.extra
4924 }
4925}
4926
4927impl Display for Dependency {
4928 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
4929 match (self.extra.is_empty(), self.package_id.version.as_ref()) {
4930 (true, Some(version)) => write!(f, "{}=={}", self.package_id.name, version),
4931 (true, None) => write!(f, "{}", self.package_id.name),
4932 (false, Some(version)) => write!(
4933 f,
4934 "{}[{}]=={}",
4935 self.package_id.name,
4936 self.extra.iter().join(","),
4937 version
4938 ),
4939 (false, None) => write!(
4940 f,
4941 "{}[{}]",
4942 self.package_id.name,
4943 self.extra.iter().join(",")
4944 ),
4945 }
4946 }
4947}
4948
4949#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4951#[serde(rename_all = "kebab-case")]
4952struct DependencyWire {
4953 #[serde(flatten)]
4954 package_id: PackageIdForDependency,
4955 #[serde(default)]
4956 extra: BTreeSet<ExtraName>,
4957 #[serde(default)]
4958 marker: SimplifiedMarkerTree,
4959}
4960
4961impl DependencyWire {
4962 fn unwire(
4963 self,
4964 requires_python: &RequiresPython,
4965 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
4966 ) -> Result<Dependency, LockError> {
4967 let complexified_marker = self.marker.into_marker(requires_python);
4968 Ok(Dependency {
4969 package_id: self.package_id.unwire(unambiguous_package_ids)?,
4970 extra: self.extra,
4971 simplified_marker: self.marker,
4972 complexified_marker: UniversalMarker::from_combined(complexified_marker),
4973 })
4974 }
4975}
4976
4977#[derive(Clone, Debug, PartialEq, Eq)]
4982struct Hash(HashDigest);
4983
4984impl From<HashDigest> for Hash {
4985 fn from(hd: HashDigest) -> Self {
4986 Self(hd)
4987 }
4988}
4989
4990impl FromStr for Hash {
4991 type Err = HashParseError;
4992
4993 fn from_str(s: &str) -> Result<Self, HashParseError> {
4994 let (algorithm, digest) = s.split_once(':').ok_or(HashParseError(
4995 "expected '{algorithm}:{digest}', but found no ':' in hash digest",
4996 ))?;
4997 let algorithm = algorithm
4998 .parse()
4999 .map_err(|_| HashParseError("unrecognized hash algorithm"))?;
5000 Ok(Self(HashDigest {
5001 algorithm,
5002 digest: digest.into(),
5003 }))
5004 }
5005}
5006
5007impl Display for Hash {
5008 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5009 write!(f, "{}:{}", self.0.algorithm, self.0.digest)
5010 }
5011}
5012
5013impl<'de> serde::Deserialize<'de> for Hash {
5014 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5015 where
5016 D: serde::de::Deserializer<'de>,
5017 {
5018 struct Visitor;
5019
5020 impl serde::de::Visitor<'_> for Visitor {
5021 type Value = Hash;
5022
5023 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
5024 f.write_str("a string")
5025 }
5026
5027 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
5028 Hash::from_str(v).map_err(serde::de::Error::custom)
5029 }
5030 }
5031
5032 deserializer.deserialize_str(Visitor)
5033 }
5034}
5035
5036impl From<Hash> for Hashes {
5037 fn from(value: Hash) -> Self {
5038 match value.0.algorithm {
5039 HashAlgorithm::Md5 => Self {
5040 md5: Some(value.0.digest),
5041 sha256: None,
5042 sha384: None,
5043 sha512: None,
5044 blake2b: None,
5045 },
5046 HashAlgorithm::Sha256 => Self {
5047 md5: None,
5048 sha256: Some(value.0.digest),
5049 sha384: None,
5050 sha512: None,
5051 blake2b: None,
5052 },
5053 HashAlgorithm::Sha384 => Self {
5054 md5: None,
5055 sha256: None,
5056 sha384: Some(value.0.digest),
5057 sha512: None,
5058 blake2b: None,
5059 },
5060 HashAlgorithm::Sha512 => Self {
5061 md5: None,
5062 sha256: None,
5063 sha384: None,
5064 sha512: Some(value.0.digest),
5065 blake2b: None,
5066 },
5067 HashAlgorithm::Blake2b => Self {
5068 md5: None,
5069 sha256: None,
5070 sha384: None,
5071 sha512: None,
5072 blake2b: Some(value.0.digest),
5073 },
5074 }
5075 }
5076}
5077
5078fn normalize_file_location(location: &FileLocation) -> Result<UrlString, ToUrlError> {
5080 match location {
5081 FileLocation::AbsoluteUrl(absolute) => Ok(absolute.without_fragment().into_owned()),
5082 FileLocation::RelativeUrl(_, _) => Ok(normalize_url(location.to_url()?)),
5083 }
5084}
5085
5086fn normalize_url(mut url: DisplaySafeUrl) -> UrlString {
5088 url.set_fragment(None);
5089 UrlString::from(url)
5090}
5091
5092fn normalize_requirement(
5102 mut requirement: Requirement,
5103 root: &Path,
5104 requires_python: &RequiresPython,
5105) -> Result<Requirement, LockError> {
5106 requirement.extras.sort();
5108 requirement.groups.sort();
5109
5110 match requirement.source {
5112 RequirementSource::Git {
5113 git,
5114 subdirectory,
5115 url: _,
5116 } => {
5117 let git = {
5119 let mut repository = git.repository().clone();
5120
5121 repository.remove_credentials();
5123
5124 repository.set_fragment(None);
5126 repository.set_query(None);
5127
5128 GitUrl::from_fields(
5129 repository,
5130 git.reference().clone(),
5131 git.precise(),
5132 git.lfs(),
5133 )?
5134 };
5135
5136 let url = DisplaySafeUrl::from(ParsedGitUrl {
5138 url: git.clone(),
5139 subdirectory: subdirectory.clone(),
5140 });
5141
5142 Ok(Requirement {
5143 name: requirement.name,
5144 extras: requirement.extras,
5145 groups: requirement.groups,
5146 marker: requires_python.simplify_markers(requirement.marker),
5147 source: RequirementSource::Git {
5148 git,
5149 subdirectory,
5150 url: VerbatimUrl::from_url(url),
5151 },
5152 origin: None,
5153 })
5154 }
5155 RequirementSource::Path {
5156 install_path,
5157 ext,
5158 url: _,
5159 } => {
5160 let install_path =
5161 uv_fs::normalize_path_buf(root.join(&install_path)).into_boxed_path();
5162 let url = VerbatimUrl::from_normalized_path(&install_path)
5163 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5164
5165 Ok(Requirement {
5166 name: requirement.name,
5167 extras: requirement.extras,
5168 groups: requirement.groups,
5169 marker: requires_python.simplify_markers(requirement.marker),
5170 source: RequirementSource::Path {
5171 install_path,
5172 ext,
5173 url,
5174 },
5175 origin: None,
5176 })
5177 }
5178 RequirementSource::Directory {
5179 install_path,
5180 editable,
5181 r#virtual,
5182 url: _,
5183 } => {
5184 let install_path =
5185 uv_fs::normalize_path_buf(root.join(&install_path)).into_boxed_path();
5186 let url = VerbatimUrl::from_normalized_path(&install_path)
5187 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5188
5189 Ok(Requirement {
5190 name: requirement.name,
5191 extras: requirement.extras,
5192 groups: requirement.groups,
5193 marker: requires_python.simplify_markers(requirement.marker),
5194 source: RequirementSource::Directory {
5195 install_path,
5196 editable: Some(editable.unwrap_or(false)),
5197 r#virtual: Some(r#virtual.unwrap_or(false)),
5198 url,
5199 },
5200 origin: None,
5201 })
5202 }
5203 RequirementSource::Registry {
5204 specifier,
5205 index,
5206 conflict,
5207 } => {
5208 let index = index
5210 .map(|index| index.url.into_url())
5211 .map(|mut index| {
5212 index.remove_credentials();
5213 index
5214 })
5215 .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index))));
5216 Ok(Requirement {
5217 name: requirement.name,
5218 extras: requirement.extras,
5219 groups: requirement.groups,
5220 marker: requires_python.simplify_markers(requirement.marker),
5221 source: RequirementSource::Registry {
5222 specifier,
5223 index,
5224 conflict,
5225 },
5226 origin: None,
5227 })
5228 }
5229 RequirementSource::Url {
5230 mut location,
5231 subdirectory,
5232 ext,
5233 url: _,
5234 } => {
5235 location.remove_credentials();
5237
5238 location.set_fragment(None);
5240
5241 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
5243 url: location.clone(),
5244 subdirectory: subdirectory.clone(),
5245 ext,
5246 });
5247
5248 Ok(Requirement {
5249 name: requirement.name,
5250 extras: requirement.extras,
5251 groups: requirement.groups,
5252 marker: requires_python.simplify_markers(requirement.marker),
5253 source: RequirementSource::Url {
5254 location,
5255 subdirectory,
5256 ext,
5257 url: VerbatimUrl::from_url(url),
5258 },
5259 origin: None,
5260 })
5261 }
5262 }
5263}
5264
5265#[derive(Debug)]
5266pub struct LockError {
5267 kind: Box<LockErrorKind>,
5268 hint: Option<WheelTagHint>,
5269}
5270
5271impl std::error::Error for LockError {
5272 fn source(&self) -> Option<&(dyn Error + 'static)> {
5273 self.kind.source()
5274 }
5275}
5276
5277impl std::fmt::Display for LockError {
5278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5279 write!(f, "{}", self.kind)?;
5280 if let Some(hint) = &self.hint {
5281 write!(f, "\n\n{hint}")?;
5282 }
5283 Ok(())
5284 }
5285}
5286
5287impl LockError {
5288 pub fn is_resolution(&self) -> bool {
5290 matches!(&*self.kind, LockErrorKind::Resolution { .. })
5291 }
5292}
5293
5294impl<E> From<E> for LockError
5295where
5296 LockErrorKind: From<E>,
5297{
5298 fn from(err: E) -> Self {
5299 Self {
5300 kind: Box::new(LockErrorKind::from(err)),
5301 hint: None,
5302 }
5303 }
5304}
5305
5306#[derive(Debug, Clone, PartialEq, Eq)]
5307#[expect(clippy::enum_variant_names)]
5308enum WheelTagHint {
5309 LanguageTags {
5312 package: PackageName,
5313 version: Option<Version>,
5314 tags: BTreeSet<LanguageTag>,
5315 best: Option<LanguageTag>,
5316 },
5317 AbiTags {
5320 package: PackageName,
5321 version: Option<Version>,
5322 tags: BTreeSet<AbiTag>,
5323 best: Option<AbiTag>,
5324 },
5325 PlatformTags {
5328 package: PackageName,
5329 version: Option<Version>,
5330 tags: BTreeSet<PlatformTag>,
5331 best: Option<PlatformTag>,
5332 markers: MarkerEnvironment,
5333 },
5334}
5335
5336impl WheelTagHint {
5337 fn from_wheels(
5339 name: &PackageName,
5340 version: Option<&Version>,
5341 filenames: &[&WheelFilename],
5342 tags: &Tags,
5343 markers: &MarkerEnvironment,
5344 ) -> Option<Self> {
5345 let incompatibility = filenames
5346 .iter()
5347 .map(|filename| {
5348 tags.compatibility(
5349 filename.python_tags(),
5350 filename.abi_tags(),
5351 filename.platform_tags(),
5352 )
5353 })
5354 .max()?;
5355 match incompatibility {
5356 TagCompatibility::Incompatible(IncompatibleTag::Python) => {
5357 let best = tags.python_tag();
5358 let tags = Self::python_tags(filenames.iter().copied()).collect::<BTreeSet<_>>();
5359 if tags.is_empty() {
5360 None
5361 } else {
5362 Some(Self::LanguageTags {
5363 package: name.clone(),
5364 version: version.cloned(),
5365 tags,
5366 best,
5367 })
5368 }
5369 }
5370 TagCompatibility::Incompatible(IncompatibleTag::Abi) => {
5371 let best = tags.abi_tag();
5372 let tags = Self::abi_tags(filenames.iter().copied())
5373 .filter(|tag| *tag != AbiTag::None)
5382 .collect::<BTreeSet<_>>();
5383 if tags.is_empty() {
5384 None
5385 } else {
5386 Some(Self::AbiTags {
5387 package: name.clone(),
5388 version: version.cloned(),
5389 tags,
5390 best,
5391 })
5392 }
5393 }
5394 TagCompatibility::Incompatible(IncompatibleTag::Platform) => {
5395 let best = tags.platform_tag().cloned();
5396 let incompatible_tags = Self::platform_tags(filenames.iter().copied(), tags)
5397 .cloned()
5398 .collect::<BTreeSet<_>>();
5399 if incompatible_tags.is_empty() {
5400 None
5401 } else {
5402 Some(Self::PlatformTags {
5403 package: name.clone(),
5404 version: version.cloned(),
5405 tags: incompatible_tags,
5406 best,
5407 markers: markers.clone(),
5408 })
5409 }
5410 }
5411 _ => None,
5412 }
5413 }
5414
5415 fn python_tags<'a>(
5417 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5418 ) -> impl Iterator<Item = LanguageTag> + 'a {
5419 filenames.flat_map(WheelFilename::python_tags).copied()
5420 }
5421
5422 fn abi_tags<'a>(
5424 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5425 ) -> impl Iterator<Item = AbiTag> + 'a {
5426 filenames.flat_map(WheelFilename::abi_tags).copied()
5427 }
5428
5429 fn platform_tags<'a>(
5432 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5433 tags: &'a Tags,
5434 ) -> impl Iterator<Item = &'a PlatformTag> + 'a {
5435 filenames.flat_map(move |filename| {
5436 if filename.python_tags().iter().any(|wheel_py| {
5437 filename
5438 .abi_tags()
5439 .iter()
5440 .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
5441 }) {
5442 filename.platform_tags().iter()
5443 } else {
5444 [].iter()
5445 }
5446 })
5447 }
5448
5449 fn suggest_environment_marker(markers: &MarkerEnvironment) -> String {
5450 let sys_platform = markers.sys_platform();
5451 let platform_machine = markers.platform_machine();
5452
5453 if platform_machine.is_empty() {
5455 format!("sys_platform == '{sys_platform}'")
5456 } else {
5457 format!("sys_platform == '{sys_platform}' and platform_machine == '{platform_machine}'")
5458 }
5459 }
5460}
5461
5462impl std::fmt::Display for WheelTagHint {
5463 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5464 match self {
5465 Self::LanguageTags {
5466 package,
5467 version,
5468 tags,
5469 best,
5470 } => {
5471 if let Some(best) = best {
5472 let s = if tags.len() == 1 { "" } else { "s" };
5473 let best = if let Some(pretty) = best.pretty() {
5474 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5475 } else {
5476 format!("{}", best.cyan())
5477 };
5478 if let Some(version) = version {
5479 write!(
5480 f,
5481 "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python implementation tag{s}: {}",
5482 "hint".bold().cyan(),
5483 ":".bold(),
5484 best,
5485 package.cyan(),
5486 format!("v{version}").cyan(),
5487 tags.iter()
5488 .map(|tag| format!("`{}`", tag.cyan()))
5489 .join(", "),
5490 )
5491 } else {
5492 write!(
5493 f,
5494 "{}{} You're using {}, but `{}` only has wheels with the following Python implementation tag{s}: {}",
5495 "hint".bold().cyan(),
5496 ":".bold(),
5497 best,
5498 package.cyan(),
5499 tags.iter()
5500 .map(|tag| format!("`{}`", tag.cyan()))
5501 .join(", "),
5502 )
5503 }
5504 } else {
5505 let s = if tags.len() == 1 { "" } else { "s" };
5506 if let Some(version) = version {
5507 write!(
5508 f,
5509 "{}{} Wheels are available for `{}` ({}) with the following Python implementation tag{s}: {}",
5510 "hint".bold().cyan(),
5511 ":".bold(),
5512 package.cyan(),
5513 format!("v{version}").cyan(),
5514 tags.iter()
5515 .map(|tag| format!("`{}`", tag.cyan()))
5516 .join(", "),
5517 )
5518 } else {
5519 write!(
5520 f,
5521 "{}{} Wheels are available for `{}` with the following Python implementation tag{s}: {}",
5522 "hint".bold().cyan(),
5523 ":".bold(),
5524 package.cyan(),
5525 tags.iter()
5526 .map(|tag| format!("`{}`", tag.cyan()))
5527 .join(", "),
5528 )
5529 }
5530 }
5531 }
5532 Self::AbiTags {
5533 package,
5534 version,
5535 tags,
5536 best,
5537 } => {
5538 if let Some(best) = best {
5539 let s = if tags.len() == 1 { "" } else { "s" };
5540 let best = if let Some(pretty) = best.pretty() {
5541 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5542 } else {
5543 format!("{}", best.cyan())
5544 };
5545 if let Some(version) = version {
5546 write!(
5547 f,
5548 "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python ABI tag{s}: {}",
5549 "hint".bold().cyan(),
5550 ":".bold(),
5551 best,
5552 package.cyan(),
5553 format!("v{version}").cyan(),
5554 tags.iter()
5555 .map(|tag| format!("`{}`", tag.cyan()))
5556 .join(", "),
5557 )
5558 } else {
5559 write!(
5560 f,
5561 "{}{} You're using {}, but `{}` only has wheels with the following Python ABI tag{s}: {}",
5562 "hint".bold().cyan(),
5563 ":".bold(),
5564 best,
5565 package.cyan(),
5566 tags.iter()
5567 .map(|tag| format!("`{}`", tag.cyan()))
5568 .join(", "),
5569 )
5570 }
5571 } else {
5572 let s = if tags.len() == 1 { "" } else { "s" };
5573 if let Some(version) = version {
5574 write!(
5575 f,
5576 "{}{} Wheels are available for `{}` ({}) with the following Python ABI tag{s}: {}",
5577 "hint".bold().cyan(),
5578 ":".bold(),
5579 package.cyan(),
5580 format!("v{version}").cyan(),
5581 tags.iter()
5582 .map(|tag| format!("`{}`", tag.cyan()))
5583 .join(", "),
5584 )
5585 } else {
5586 write!(
5587 f,
5588 "{}{} Wheels are available for `{}` with the following Python ABI tag{s}: {}",
5589 "hint".bold().cyan(),
5590 ":".bold(),
5591 package.cyan(),
5592 tags.iter()
5593 .map(|tag| format!("`{}`", tag.cyan()))
5594 .join(", "),
5595 )
5596 }
5597 }
5598 }
5599 Self::PlatformTags {
5600 package,
5601 version,
5602 tags,
5603 best,
5604 markers,
5605 } => {
5606 let s = if tags.len() == 1 { "" } else { "s" };
5607 if let Some(best) = best {
5608 let example_marker = Self::suggest_environment_marker(markers);
5609 let best = if let Some(pretty) = best.pretty() {
5610 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5611 } else {
5612 format!("`{}`", best.cyan())
5613 };
5614 let package_ref = if let Some(version) = version {
5615 format!("`{}` ({})", package.cyan(), format!("v{version}").cyan())
5616 } else {
5617 format!("`{}`", package.cyan())
5618 };
5619 write!(
5620 f,
5621 "{}{} 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",
5622 "hint".bold().cyan(),
5623 ":".bold(),
5624 best,
5625 package_ref,
5626 tags.iter()
5627 .map(|tag| format!("`{}`", tag.cyan()))
5628 .join(", "),
5629 format!("\"{example_marker}\"").cyan(),
5630 "tool.uv.required-environments".green()
5631 )
5632 } else {
5633 if let Some(version) = version {
5634 write!(
5635 f,
5636 "{}{} Wheels are available for `{}` ({}) on the following platform{s}: {}",
5637 "hint".bold().cyan(),
5638 ":".bold(),
5639 package.cyan(),
5640 format!("v{version}").cyan(),
5641 tags.iter()
5642 .map(|tag| format!("`{}`", tag.cyan()))
5643 .join(", "),
5644 )
5645 } else {
5646 write!(
5647 f,
5648 "{}{} Wheels are available for `{}` on the following platform{s}: {}",
5649 "hint".bold().cyan(),
5650 ":".bold(),
5651 package.cyan(),
5652 tags.iter()
5653 .map(|tag| format!("`{}`", tag.cyan()))
5654 .join(", "),
5655 )
5656 }
5657 }
5658 }
5659 }
5660 }
5661}
5662
5663#[derive(Debug, thiserror::Error)]
5670enum LockErrorKind {
5671 #[error("Found duplicate package `{id}`", id = id.cyan())]
5674 DuplicatePackage {
5675 id: PackageId,
5677 },
5678 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = id.cyan(), dependency = dependency.cyan())]
5681 DuplicateDependency {
5682 id: PackageId,
5685 dependency: Dependency,
5687 },
5688 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}[{extra}]").cyan(), dependency = dependency.cyan())]
5692 DuplicateOptionalDependency {
5693 id: PackageId,
5696 extra: ExtraName,
5698 dependency: Dependency,
5700 },
5701 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}:{group}").cyan(), dependency = dependency.cyan())]
5705 DuplicateDevDependency {
5706 id: PackageId,
5709 group: GroupName,
5711 dependency: Dependency,
5713 },
5714 #[error(transparent)]
5717 InvalidUrl(
5718 #[from]
5721 ToUrlError,
5722 ),
5723 #[error("Failed to parse file extension for `{id}`; expected one of: {err}", id = id.cyan())]
5726 MissingExtension {
5727 id: PackageId,
5729 err: ExtensionError,
5731 },
5732 #[error("Failed to parse Git URL")]
5734 InvalidGitSourceUrl(
5735 #[source]
5738 SourceParseError,
5739 ),
5740 #[error("Failed to parse timestamp")]
5741 InvalidTimestamp(
5742 #[source]
5745 jiff::Error,
5746 ),
5747 #[error("For package `{id}`, found dependency `{dependency}` with no locked package", id = id.cyan(), dependency = dependency.cyan())]
5751 UnrecognizedDependency {
5752 id: PackageId,
5754 dependency: Dependency,
5757 },
5758 #[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" })]
5761 Hash {
5762 id: PackageId,
5764 artifact_type: &'static str,
5767 expected: bool,
5769 },
5770 #[error("Found package `{id}` with extra `{extra}` but no base package", id = id.cyan(), extra = extra.cyan())]
5773 MissingExtraBase {
5774 id: PackageId,
5776 extra: ExtraName,
5778 },
5779 #[error("Found package `{id}` with development dependency group `{group}` but no base package", id = id.cyan())]
5783 MissingDevBase {
5784 id: PackageId,
5786 group: GroupName,
5788 },
5789 #[error("Wheels cannot come from {source_type} sources")]
5792 InvalidWheelSource {
5793 id: PackageId,
5795 source_type: &'static str,
5797 },
5798 #[error("Found registry distribution `{name}` ({version}) without a valid URL", name = name.cyan(), version = format!("v{version}").cyan())]
5801 MissingUrl {
5802 name: PackageName,
5804 version: Version,
5806 },
5807 #[error("Found registry distribution `{name}` ({version}) without a valid path", name = name.cyan(), version = format!("v{version}").cyan())]
5810 MissingPath {
5811 name: PackageName,
5813 version: Version,
5815 },
5816 #[error("Found registry distribution `{id}` without a valid filename", id = id.cyan())]
5819 MissingFilename {
5820 id: PackageId,
5822 },
5823 #[error("Distribution `{id}` can't be installed because it doesn't have a source distribution or wheel for the current platform", id = id.cyan())]
5826 NeitherSourceDistNorWheel {
5827 id: PackageId,
5829 },
5830 #[error("Distribution `{id}` can't be installed because it is marked as both `--no-binary` and `--no-build`", id = id.cyan())]
5832 NoBinaryNoBuild {
5833 id: PackageId,
5835 },
5836 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but has no source distribution", id = id.cyan())]
5839 NoBinary {
5840 id: PackageId,
5842 },
5843 #[error("Distribution `{id}` can't be installed because it is marked as `--no-build` but has no binary distribution", id = id.cyan())]
5846 NoBuild {
5847 id: PackageId,
5849 },
5850 #[error("Distribution `{id}` can't be installed because the binary distribution is incompatible with the current platform", id = id.cyan())]
5853 IncompatibleWheelOnly {
5854 id: PackageId,
5856 },
5857 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but is itself a binary distribution", id = id.cyan())]
5859 NoBinaryWheelOnly {
5860 id: PackageId,
5862 },
5863 #[error("Found dependency `{id}` with no locked distribution", id = id.cyan())]
5865 VerbatimUrl {
5866 id: PackageId,
5868 #[source]
5870 err: VerbatimUrlError,
5871 },
5872 #[error("Could not compute relative path between workspace and distribution")]
5874 DistributionRelativePath(
5875 #[source]
5877 io::Error,
5878 ),
5879 #[error("Could not compute relative path between workspace and index")]
5881 IndexRelativePath(
5882 #[source]
5884 io::Error,
5885 ),
5886 #[error("Could not compute absolute path from workspace root and lockfile path")]
5888 AbsolutePath(
5889 #[source]
5891 io::Error,
5892 ),
5893 #[error("Dependency `{name}` has missing `version` field but has more than one matching package", name = name.cyan())]
5896 MissingDependencyVersion {
5897 name: PackageName,
5899 },
5900 #[error("Dependency `{name}` has missing `source` field but has more than one matching package", name = name.cyan())]
5903 MissingDependencySource {
5904 name: PackageName,
5906 },
5907 #[error("Could not compute relative path between workspace and requirement")]
5909 RequirementRelativePath(
5910 #[source]
5912 io::Error,
5913 ),
5914 #[error("Could not convert between URL and path")]
5916 RequirementVerbatimUrl(
5917 #[source]
5919 VerbatimUrlError,
5920 ),
5921 #[error("Could not convert between URL and path")]
5923 RegistryVerbatimUrl(
5924 #[source]
5926 VerbatimUrlError,
5927 ),
5928 #[error("Failed to convert path to URL: {path}", path = path.display().cyan())]
5930 PathToUrl { path: Box<Path> },
5931 #[error("Failed to convert URL to path: {url}", url = url.cyan())]
5933 UrlToPath { url: DisplaySafeUrl },
5934 #[error("Found multiple packages matching `{name}`", name = name.cyan())]
5937 MultipleRootPackages {
5938 name: PackageName,
5940 },
5941 #[error("Could not find root package `{name}`", name = name.cyan())]
5943 MissingRootPackage {
5944 name: PackageName,
5946 },
5947 #[error("Failed to generate package metadata for `{id}`", id = id.cyan())]
5949 Resolution {
5950 id: PackageId,
5952 #[source]
5954 err: uv_distribution::Error,
5955 },
5956 #[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())]
5959 InconsistentVersions {
5960 name: PackageName,
5962 version: Version,
5964 wheel: Wheel,
5966 },
5967 #[error(
5968 "Found conflicting extras `{package1}[{extra1}]` \
5969 and `{package2}[{extra2}]` enabled simultaneously"
5970 )]
5971 ConflictingExtra {
5972 package1: PackageName,
5973 extra1: ExtraName,
5974 package2: PackageName,
5975 extra2: ExtraName,
5976 },
5977 #[error(transparent)]
5978 GitUrlParse(#[from] GitUrlParseError),
5979 #[error("Failed to read `{path}`")]
5980 UnreadablePyprojectToml {
5981 path: PathBuf,
5982 #[source]
5983 err: std::io::Error,
5984 },
5985 #[error("Failed to parse `{path}`")]
5986 InvalidPyprojectToml {
5987 path: PathBuf,
5988 #[source]
5989 err: toml::de::Error,
5990 },
5991 #[error("Workspace member `{id}` has non-local source", id = id.cyan())]
5993 NonLocalWorkspaceMember {
5994 id: PackageId,
5996 },
5997}
5998
5999#[derive(Debug, thiserror::Error)]
6001enum SourceParseError {
6002 #[error("Invalid URL in source `{given}`")]
6004 InvalidUrl {
6005 given: String,
6007 #[source]
6009 err: DisplaySafeUrlError,
6010 },
6011 #[error("Missing SHA in source `{given}`")]
6013 MissingSha {
6014 given: String,
6016 },
6017 #[error("Invalid SHA in source `{given}`")]
6019 InvalidSha {
6020 given: String,
6022 },
6023}
6024
6025#[derive(Clone, Debug, Eq, PartialEq)]
6027struct HashParseError(&'static str);
6028
6029impl std::error::Error for HashParseError {}
6030
6031impl Display for HashParseError {
6032 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
6033 Display::fmt(self.0, f)
6034 }
6035}
6036
6037fn each_element_on_its_line_array(elements: impl Iterator<Item = impl Into<Value>>) -> Array {
6048 let mut array = elements
6049 .map(|item| {
6050 let mut value = item.into();
6051 value.decor_mut().set_prefix("\n ");
6053 value
6054 })
6055 .collect::<Array>();
6056 array.set_trailing_comma(true);
6059 array.set_trailing("\n");
6061 array
6062}
6063
6064fn simplified_universal_markers(
6069 markers: &[UniversalMarker],
6070 requires_python: &RequiresPython,
6071) -> Vec<String> {
6072 let mut pep508_only = vec![];
6073 let mut seen = FxHashSet::default();
6074 for marker in markers {
6075 let simplified =
6076 SimplifiedMarkerTree::new(requires_python, marker.pep508()).as_simplified_marker_tree();
6077 if seen.insert(simplified) {
6078 pep508_only.push(simplified);
6079 }
6080 }
6081 let any_overlap = pep508_only
6082 .iter()
6083 .tuple_combinations()
6084 .any(|(&marker1, &marker2)| !marker1.is_disjoint(marker2));
6085 let markers = if !any_overlap {
6086 pep508_only
6087 } else {
6088 markers
6089 .iter()
6090 .map(|marker| {
6091 SimplifiedMarkerTree::new(requires_python, marker.combined())
6092 .as_simplified_marker_tree()
6093 })
6094 .collect()
6095 };
6096 markers
6097 .into_iter()
6098 .filter_map(MarkerTree::try_to_string)
6099 .collect()
6100}
6101
6102pub(crate) fn is_wheel_unreachable(
6110 filename: &WheelFilename,
6111 graph: &ResolverOutput,
6112 requires_python: &RequiresPython,
6113 node_index: NodeIndex,
6114 tags: Option<&Tags>,
6115) -> bool {
6116 if let Some(tags) = tags
6117 && !filename.compatibility(tags).is_compatible()
6118 {
6119 return true;
6120 }
6121 if !requires_python.matches_wheel_tag(filename) {
6123 return true;
6124 }
6125
6126 let platform_tags = filename.platform_tags();
6135
6136 if platform_tags.iter().all(PlatformTag::is_any) {
6137 return false;
6138 }
6139
6140 if platform_tags.iter().all(PlatformTag::is_linux) {
6141 if platform_tags.iter().all(PlatformTag::is_arm) {
6142 if graph.graph[node_index]
6143 .marker()
6144 .is_disjoint(*LINUX_ARM_MARKERS)
6145 {
6146 return true;
6147 }
6148 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6149 if graph.graph[node_index]
6150 .marker()
6151 .is_disjoint(*LINUX_X86_64_MARKERS)
6152 {
6153 return true;
6154 }
6155 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6156 if graph.graph[node_index]
6157 .marker()
6158 .is_disjoint(*LINUX_X86_MARKERS)
6159 {
6160 return true;
6161 }
6162 } else if platform_tags.iter().all(PlatformTag::is_ppc64le) {
6163 if graph.graph[node_index]
6164 .marker()
6165 .is_disjoint(*LINUX_PPC64LE_MARKERS)
6166 {
6167 return true;
6168 }
6169 } else if platform_tags.iter().all(PlatformTag::is_ppc64) {
6170 if graph.graph[node_index]
6171 .marker()
6172 .is_disjoint(*LINUX_PPC64_MARKERS)
6173 {
6174 return true;
6175 }
6176 } else if platform_tags.iter().all(PlatformTag::is_s390x) {
6177 if graph.graph[node_index]
6178 .marker()
6179 .is_disjoint(*LINUX_S390X_MARKERS)
6180 {
6181 return true;
6182 }
6183 } else if platform_tags.iter().all(PlatformTag::is_riscv64) {
6184 if graph.graph[node_index]
6185 .marker()
6186 .is_disjoint(*LINUX_RISCV64_MARKERS)
6187 {
6188 return true;
6189 }
6190 } else if platform_tags.iter().all(PlatformTag::is_loongarch64) {
6191 if graph.graph[node_index]
6192 .marker()
6193 .is_disjoint(*LINUX_LOONGARCH64_MARKERS)
6194 {
6195 return true;
6196 }
6197 } else if platform_tags.iter().all(PlatformTag::is_armv7l) {
6198 if graph.graph[node_index]
6199 .marker()
6200 .is_disjoint(*LINUX_ARMV7L_MARKERS)
6201 {
6202 return true;
6203 }
6204 } else if platform_tags.iter().all(PlatformTag::is_armv6l) {
6205 if graph.graph[node_index]
6206 .marker()
6207 .is_disjoint(*LINUX_ARMV6L_MARKERS)
6208 {
6209 return true;
6210 }
6211 } else if graph.graph[node_index].marker().is_disjoint(*LINUX_MARKERS) {
6212 return true;
6213 }
6214 }
6215
6216 if platform_tags.iter().all(PlatformTag::is_windows) {
6217 if platform_tags.iter().all(PlatformTag::is_arm) {
6218 if graph.graph[node_index]
6219 .marker()
6220 .is_disjoint(*WINDOWS_ARM_MARKERS)
6221 {
6222 return true;
6223 }
6224 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6225 if graph.graph[node_index]
6226 .marker()
6227 .is_disjoint(*WINDOWS_X86_64_MARKERS)
6228 {
6229 return true;
6230 }
6231 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6232 if graph.graph[node_index]
6233 .marker()
6234 .is_disjoint(*WINDOWS_X86_MARKERS)
6235 {
6236 return true;
6237 }
6238 } else if graph.graph[node_index]
6239 .marker()
6240 .is_disjoint(*WINDOWS_MARKERS)
6241 {
6242 return true;
6243 }
6244 }
6245
6246 if platform_tags.iter().all(PlatformTag::is_macos) {
6247 if platform_tags.iter().all(PlatformTag::is_arm) {
6248 if graph.graph[node_index]
6249 .marker()
6250 .is_disjoint(*MAC_ARM_MARKERS)
6251 {
6252 return true;
6253 }
6254 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6255 if graph.graph[node_index]
6256 .marker()
6257 .is_disjoint(*MAC_X86_64_MARKERS)
6258 {
6259 return true;
6260 }
6261 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6262 if graph.graph[node_index]
6263 .marker()
6264 .is_disjoint(*MAC_X86_MARKERS)
6265 {
6266 return true;
6267 }
6268 } else if graph.graph[node_index].marker().is_disjoint(*MAC_MARKERS) {
6269 return true;
6270 }
6271 }
6272
6273 if platform_tags.iter().all(PlatformTag::is_android) {
6274 if platform_tags.iter().all(PlatformTag::is_arm) {
6275 if graph.graph[node_index]
6276 .marker()
6277 .is_disjoint(*ANDROID_ARM_MARKERS)
6278 {
6279 return true;
6280 }
6281 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6282 if graph.graph[node_index]
6283 .marker()
6284 .is_disjoint(*ANDROID_X86_64_MARKERS)
6285 {
6286 return true;
6287 }
6288 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6289 if graph.graph[node_index]
6290 .marker()
6291 .is_disjoint(*ANDROID_X86_MARKERS)
6292 {
6293 return true;
6294 }
6295 } else if graph.graph[node_index]
6296 .marker()
6297 .is_disjoint(*ANDROID_MARKERS)
6298 {
6299 return true;
6300 }
6301 }
6302
6303 if platform_tags.iter().all(PlatformTag::is_arm) {
6304 if graph.graph[node_index].marker().is_disjoint(*ARM_MARKERS) {
6305 return true;
6306 }
6307 }
6308
6309 if platform_tags.iter().all(PlatformTag::is_x86_64) {
6310 if graph.graph[node_index]
6311 .marker()
6312 .is_disjoint(*X86_64_MARKERS)
6313 {
6314 return true;
6315 }
6316 }
6317
6318 if platform_tags.iter().all(PlatformTag::is_x86) {
6319 if graph.graph[node_index].marker().is_disjoint(*X86_MARKERS) {
6320 return true;
6321 }
6322 }
6323
6324 if platform_tags.iter().all(PlatformTag::is_ppc64le) {
6325 if graph.graph[node_index]
6326 .marker()
6327 .is_disjoint(*PPC64LE_MARKERS)
6328 {
6329 return true;
6330 }
6331 }
6332
6333 if platform_tags.iter().all(PlatformTag::is_ppc64) {
6334 if graph.graph[node_index].marker().is_disjoint(*PPC64_MARKERS) {
6335 return true;
6336 }
6337 }
6338
6339 if platform_tags.iter().all(PlatformTag::is_s390x) {
6340 if graph.graph[node_index].marker().is_disjoint(*S390X_MARKERS) {
6341 return true;
6342 }
6343 }
6344
6345 if platform_tags.iter().all(PlatformTag::is_riscv64) {
6346 if graph.graph[node_index]
6347 .marker()
6348 .is_disjoint(*RISCV64_MARKERS)
6349 {
6350 return true;
6351 }
6352 }
6353
6354 if platform_tags.iter().all(PlatformTag::is_loongarch64) {
6355 if graph.graph[node_index]
6356 .marker()
6357 .is_disjoint(*LOONGARCH64_MARKERS)
6358 {
6359 return true;
6360 }
6361 }
6362
6363 if platform_tags.iter().all(PlatformTag::is_armv7l) {
6364 if graph.graph[node_index]
6365 .marker()
6366 .is_disjoint(*ARMV7L_MARKERS)
6367 {
6368 return true;
6369 }
6370 }
6371
6372 if platform_tags.iter().all(PlatformTag::is_armv6l) {
6373 if graph.graph[node_index]
6374 .marker()
6375 .is_disjoint(*ARMV6L_MARKERS)
6376 {
6377 return true;
6378 }
6379 }
6380
6381 false
6382}
6383
6384#[cfg(test)]
6385mod tests {
6386 use uv_warnings::anstream;
6387
6388 use super::*;
6389
6390 macro_rules! assert_stripped_snapshot {
6392 ($expr:expr, @$snapshot:literal) => {{
6393 let expr = format!("{}", $expr);
6394 let expr = format!("{}", anstream::adapter::strip_str(&expr));
6395 insta::assert_snapshot!(expr, @$snapshot);
6396 }};
6397 }
6398
6399 #[test]
6400 fn missing_dependency_source_unambiguous() {
6401 let data = r#"
6402version = 1
6403requires-python = ">=3.12"
6404
6405[[package]]
6406name = "a"
6407version = "0.1.0"
6408source = { registry = "https://pypi.org/simple" }
6409sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6410
6411[[package]]
6412name = "b"
6413version = "0.1.0"
6414source = { registry = "https://pypi.org/simple" }
6415sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6416
6417[[package.dependencies]]
6418name = "a"
6419version = "0.1.0"
6420"#;
6421 let result: Result<Lock, _> = toml::from_str(data);
6422 insta::assert_debug_snapshot!(result);
6423 }
6424
6425 #[test]
6426 fn missing_dependency_version_unambiguous() {
6427 let data = r#"
6428version = 1
6429requires-python = ">=3.12"
6430
6431[[package]]
6432name = "a"
6433version = "0.1.0"
6434source = { registry = "https://pypi.org/simple" }
6435sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6436
6437[[package]]
6438name = "b"
6439version = "0.1.0"
6440source = { registry = "https://pypi.org/simple" }
6441sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6442
6443[[package.dependencies]]
6444name = "a"
6445source = { registry = "https://pypi.org/simple" }
6446"#;
6447 let result: Result<Lock, _> = toml::from_str(data);
6448 insta::assert_debug_snapshot!(result);
6449 }
6450
6451 #[test]
6452 fn missing_dependency_source_version_unambiguous() {
6453 let data = r#"
6454version = 1
6455requires-python = ">=3.12"
6456
6457[[package]]
6458name = "a"
6459version = "0.1.0"
6460source = { registry = "https://pypi.org/simple" }
6461sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6462
6463[[package]]
6464name = "b"
6465version = "0.1.0"
6466source = { registry = "https://pypi.org/simple" }
6467sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6468
6469[[package.dependencies]]
6470name = "a"
6471"#;
6472 let result: Result<Lock, _> = toml::from_str(data);
6473 insta::assert_debug_snapshot!(result);
6474 }
6475
6476 #[test]
6477 fn missing_dependency_source_ambiguous() {
6478 let data = r#"
6479version = 1
6480requires-python = ">=3.12"
6481
6482[[package]]
6483name = "a"
6484version = "0.1.0"
6485source = { registry = "https://pypi.org/simple" }
6486sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6487
6488[[package]]
6489name = "a"
6490version = "0.1.1"
6491source = { registry = "https://pypi.org/simple" }
6492sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6493
6494[[package]]
6495name = "b"
6496version = "0.1.0"
6497source = { registry = "https://pypi.org/simple" }
6498sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6499
6500[[package.dependencies]]
6501name = "a"
6502version = "0.1.0"
6503"#;
6504 let result = toml::from_str::<Lock>(data).unwrap_err();
6505 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6506 }
6507
6508 #[test]
6509 fn missing_dependency_version_ambiguous() {
6510 let data = r#"
6511version = 1
6512requires-python = ">=3.12"
6513
6514[[package]]
6515name = "a"
6516version = "0.1.0"
6517source = { registry = "https://pypi.org/simple" }
6518sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6519
6520[[package]]
6521name = "a"
6522version = "0.1.1"
6523source = { registry = "https://pypi.org/simple" }
6524sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6525
6526[[package]]
6527name = "b"
6528version = "0.1.0"
6529source = { registry = "https://pypi.org/simple" }
6530sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6531
6532[[package.dependencies]]
6533name = "a"
6534source = { registry = "https://pypi.org/simple" }
6535"#;
6536 let result = toml::from_str::<Lock>(data).unwrap_err();
6537 assert_stripped_snapshot!(result, @"Dependency `a` has missing `version` field but has more than one matching package");
6538 }
6539
6540 #[test]
6541 fn missing_dependency_source_version_ambiguous() {
6542 let data = r#"
6543version = 1
6544requires-python = ">=3.12"
6545
6546[[package]]
6547name = "a"
6548version = "0.1.0"
6549source = { registry = "https://pypi.org/simple" }
6550sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6551
6552[[package]]
6553name = "a"
6554version = "0.1.1"
6555source = { registry = "https://pypi.org/simple" }
6556sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6557
6558[[package]]
6559name = "b"
6560version = "0.1.0"
6561source = { registry = "https://pypi.org/simple" }
6562sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6563
6564[[package.dependencies]]
6565name = "a"
6566"#;
6567 let result = toml::from_str::<Lock>(data).unwrap_err();
6568 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6569 }
6570
6571 #[test]
6572 fn missing_dependency_version_dynamic() {
6573 let data = r#"
6574version = 1
6575requires-python = ">=3.12"
6576
6577[[package]]
6578name = "a"
6579source = { editable = "path/to/a" }
6580
6581[[package]]
6582name = "a"
6583version = "0.1.1"
6584source = { registry = "https://pypi.org/simple" }
6585sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6586
6587[[package]]
6588name = "b"
6589version = "0.1.0"
6590source = { registry = "https://pypi.org/simple" }
6591sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6592
6593[[package.dependencies]]
6594name = "a"
6595source = { editable = "path/to/a" }
6596"#;
6597 let result = toml::from_str::<Lock>(data);
6598 insta::assert_debug_snapshot!(result);
6599 }
6600
6601 #[test]
6602 fn hash_optional_missing() {
6603 let data = r#"
6604version = 1
6605requires-python = ">=3.12"
6606
6607[[package]]
6608name = "anyio"
6609version = "4.3.0"
6610source = { registry = "https://pypi.org/simple" }
6611wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl" }]
6612"#;
6613 let result: Result<Lock, _> = toml::from_str(data);
6614 insta::assert_debug_snapshot!(result);
6615 }
6616
6617 #[test]
6618 fn hash_optional_present() {
6619 let data = r#"
6620version = 1
6621requires-python = ">=3.12"
6622
6623[[package]]
6624name = "anyio"
6625version = "4.3.0"
6626source = { registry = "https://pypi.org/simple" }
6627wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6628"#;
6629 let result: Result<Lock, _> = toml::from_str(data);
6630 insta::assert_debug_snapshot!(result);
6631 }
6632
6633 #[test]
6634 fn hash_required_present() {
6635 let data = r#"
6636version = 1
6637requires-python = ">=3.12"
6638
6639[[package]]
6640name = "anyio"
6641version = "4.3.0"
6642source = { path = "file:///foo/bar" }
6643wheels = [{ url = "file:///foo/bar/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6644"#;
6645 let result: Result<Lock, _> = toml::from_str(data);
6646 insta::assert_debug_snapshot!(result);
6647 }
6648
6649 #[test]
6650 fn source_direct_no_subdir() {
6651 let data = r#"
6652version = 1
6653requires-python = ">=3.12"
6654
6655[[package]]
6656name = "anyio"
6657version = "4.3.0"
6658source = { url = "https://burntsushi.net" }
6659"#;
6660 let result: Result<Lock, _> = toml::from_str(data);
6661 insta::assert_debug_snapshot!(result);
6662 }
6663
6664 #[test]
6665 fn source_direct_has_subdir() {
6666 let data = r#"
6667version = 1
6668requires-python = ">=3.12"
6669
6670[[package]]
6671name = "anyio"
6672version = "4.3.0"
6673source = { url = "https://burntsushi.net", subdirectory = "wat/foo/bar" }
6674"#;
6675 let result: Result<Lock, _> = toml::from_str(data);
6676 insta::assert_debug_snapshot!(result);
6677 }
6678
6679 #[test]
6680 fn source_directory() {
6681 let data = r#"
6682version = 1
6683requires-python = ">=3.12"
6684
6685[[package]]
6686name = "anyio"
6687version = "4.3.0"
6688source = { directory = "path/to/dir" }
6689"#;
6690 let result: Result<Lock, _> = toml::from_str(data);
6691 insta::assert_debug_snapshot!(result);
6692 }
6693
6694 #[test]
6695 fn source_editable() {
6696 let data = r#"
6697version = 1
6698requires-python = ">=3.12"
6699
6700[[package]]
6701name = "anyio"
6702version = "4.3.0"
6703source = { editable = "path/to/dir" }
6704"#;
6705 let result: Result<Lock, _> = toml::from_str(data);
6706 insta::assert_debug_snapshot!(result);
6707 }
6708}