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 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 LINUX_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
114 let mut marker = *LINUX_MARKERS;
115 marker.and(*ARM_MARKERS);
116 marker
117});
118static LINUX_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
119 let mut marker = *LINUX_MARKERS;
120 marker.and(*X86_64_MARKERS);
121 marker
122});
123static LINUX_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
124 let mut marker = *LINUX_MARKERS;
125 marker.and(*X86_MARKERS);
126 marker
127});
128static WINDOWS_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
129 let mut marker = *WINDOWS_MARKERS;
130 marker.and(*ARM_MARKERS);
131 marker
132});
133static WINDOWS_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
134 let mut marker = *WINDOWS_MARKERS;
135 marker.and(*X86_64_MARKERS);
136 marker
137});
138static WINDOWS_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
139 let mut marker = *WINDOWS_MARKERS;
140 marker.and(*X86_MARKERS);
141 marker
142});
143static MAC_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
144 let mut marker = *MAC_MARKERS;
145 marker.and(*ARM_MARKERS);
146 marker
147});
148static MAC_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
149 let mut marker = *MAC_MARKERS;
150 marker.and(*X86_64_MARKERS);
151 marker
152});
153static MAC_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
154 let mut marker = *MAC_MARKERS;
155 marker.and(*X86_MARKERS);
156 marker
157});
158static ANDROID_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
159 let mut marker = *ANDROID_MARKERS;
160 marker.and(*ARM_MARKERS);
161 marker
162});
163static ANDROID_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
164 let mut marker = *ANDROID_MARKERS;
165 marker.and(*X86_64_MARKERS);
166 marker
167});
168static ANDROID_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
169 let mut marker = *ANDROID_MARKERS;
170 marker.and(*X86_MARKERS);
171 marker
172});
173
174pub(crate) struct HashedDist {
179 pub(crate) dist: Dist,
180 pub(crate) hashes: HashDigests,
181}
182
183#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
184#[serde(try_from = "LockWire")]
185pub struct Lock {
186 version: u32,
195 revision: u32,
201 fork_markers: Vec<UniversalMarker>,
204 conflicts: Conflicts,
206 supported_environments: Vec<MarkerTree>,
208 required_environments: Vec<MarkerTree>,
210 requires_python: RequiresPython,
212 options: ResolverOptions,
214 packages: Vec<Package>,
216 by_id: FxHashMap<PackageId, usize>,
228 manifest: ResolverManifest,
230}
231
232impl Lock {
233 pub fn from_resolution(resolution: &ResolverOutput, root: &Path) -> Result<Self, LockError> {
235 let mut packages = BTreeMap::new();
236 let requires_python = resolution.requires_python.clone();
237
238 let mut seen = FxHashSet::default();
240 let mut duplicates = FxHashSet::default();
241 for node_index in resolution.graph.node_indices() {
242 let ResolutionGraphNode::Dist(dist) = &resolution.graph[node_index] else {
243 continue;
244 };
245 if !dist.is_base() {
246 continue;
247 }
248 if !seen.insert(dist.name()) {
249 duplicates.insert(dist.name());
250 }
251 }
252
253 for node_index in resolution.graph.node_indices() {
255 let ResolutionGraphNode::Dist(dist) = &resolution.graph[node_index] else {
256 continue;
257 };
258 if !dist.is_base() {
259 continue;
260 }
261
262 let fork_markers = if duplicates.contains(dist.name()) {
265 resolution
266 .fork_markers
267 .iter()
268 .filter(|fork_markers| !fork_markers.is_disjoint(dist.marker))
269 .copied()
270 .collect()
271 } else {
272 vec![]
273 };
274
275 let mut package = Package::from_annotated_dist(dist, fork_markers, root)?;
276 Self::remove_unreachable_wheels(resolution, &requires_python, node_index, &mut package);
277
278 for edge in resolution.graph.edges(node_index) {
280 let ResolutionGraphNode::Dist(dependency_dist) = &resolution.graph[edge.target()]
281 else {
282 continue;
283 };
284 let marker = *edge.weight();
285 package.add_dependency(&requires_python, dependency_dist, marker, root)?;
286 }
287
288 let id = package.id.clone();
289 if let Some(locked_dist) = packages.insert(id, package) {
290 return Err(LockErrorKind::DuplicatePackage {
291 id: locked_dist.id.clone(),
292 }
293 .into());
294 }
295 }
296
297 for node_index in resolution.graph.node_indices() {
299 let ResolutionGraphNode::Dist(dist) = &resolution.graph[node_index] else {
300 continue;
301 };
302 if let Some(extra) = dist.extra.as_ref() {
303 let id = PackageId::from_annotated_dist(dist, root)?;
304 let Some(package) = packages.get_mut(&id) else {
305 return Err(LockErrorKind::MissingExtraBase {
306 id,
307 extra: extra.clone(),
308 }
309 .into());
310 };
311 for edge in resolution.graph.edges(node_index) {
312 let ResolutionGraphNode::Dist(dependency_dist) =
313 &resolution.graph[edge.target()]
314 else {
315 continue;
316 };
317 let marker = *edge.weight();
318 package.add_optional_dependency(
319 &requires_python,
320 extra.clone(),
321 dependency_dist,
322 marker,
323 root,
324 )?;
325 }
326 }
327 if let Some(group) = dist.group.as_ref() {
328 let id = PackageId::from_annotated_dist(dist, root)?;
329 let Some(package) = packages.get_mut(&id) else {
330 return Err(LockErrorKind::MissingDevBase {
331 id,
332 group: group.clone(),
333 }
334 .into());
335 };
336 for edge in resolution.graph.edges(node_index) {
337 let ResolutionGraphNode::Dist(dependency_dist) =
338 &resolution.graph[edge.target()]
339 else {
340 continue;
341 };
342 let marker = *edge.weight();
343 package.add_group_dependency(
344 &requires_python,
345 group.clone(),
346 dependency_dist,
347 marker,
348 root,
349 )?;
350 }
351 }
352 }
353
354 let packages = packages.into_values().collect();
355
356 let options = ResolverOptions {
357 resolution_mode: resolution.options.resolution_mode,
358 prerelease_mode: resolution.options.prerelease_mode,
359 fork_strategy: resolution.options.fork_strategy,
360 exclude_newer: resolution.options.exclude_newer.clone().into(),
361 };
362 let lock = Self::new(
363 VERSION,
364 REVISION,
365 packages,
366 requires_python,
367 options,
368 ResolverManifest::default(),
369 Conflicts::empty(),
370 vec![],
371 vec![],
372 resolution.fork_markers.clone(),
373 )?;
374 Ok(lock)
375 }
376
377 fn remove_unreachable_wheels(
382 graph: &ResolverOutput,
383 requires_python: &RequiresPython,
384 node_index: NodeIndex,
385 locked_dist: &mut Package,
386 ) {
387 locked_dist
389 .wheels
390 .retain(|wheel| requires_python.matches_wheel_tag(&wheel.filename));
391
392 locked_dist.wheels.retain(|wheel| {
394 let platform_tags = wheel.filename.platform_tags();
401
402 if platform_tags.iter().all(PlatformTag::is_any) {
403 return true;
404 }
405
406 if platform_tags.iter().all(PlatformTag::is_linux) {
407 if platform_tags.iter().all(PlatformTag::is_arm) {
408 if graph.graph[node_index]
409 .marker()
410 .is_disjoint(*LINUX_ARM_MARKERS)
411 {
412 return false;
413 }
414 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
415 if graph.graph[node_index]
416 .marker()
417 .is_disjoint(*LINUX_X86_64_MARKERS)
418 {
419 return false;
420 }
421 } else if platform_tags.iter().all(PlatformTag::is_x86) {
422 if graph.graph[node_index]
423 .marker()
424 .is_disjoint(*LINUX_X86_MARKERS)
425 {
426 return false;
427 }
428 } else if graph.graph[node_index].marker().is_disjoint(*LINUX_MARKERS) {
429 return false;
430 }
431 }
432
433 if platform_tags.iter().all(PlatformTag::is_windows) {
434 if platform_tags.iter().all(PlatformTag::is_arm) {
435 if graph.graph[node_index]
436 .marker()
437 .is_disjoint(*WINDOWS_ARM_MARKERS)
438 {
439 return false;
440 }
441 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
442 if graph.graph[node_index]
443 .marker()
444 .is_disjoint(*WINDOWS_X86_64_MARKERS)
445 {
446 return false;
447 }
448 } else if platform_tags.iter().all(PlatformTag::is_x86) {
449 if graph.graph[node_index]
450 .marker()
451 .is_disjoint(*WINDOWS_X86_MARKERS)
452 {
453 return false;
454 }
455 } else if graph.graph[node_index]
456 .marker()
457 .is_disjoint(*WINDOWS_MARKERS)
458 {
459 return false;
460 }
461 }
462
463 if platform_tags.iter().all(PlatformTag::is_macos) {
464 if platform_tags.iter().all(PlatformTag::is_arm) {
465 if graph.graph[node_index]
466 .marker()
467 .is_disjoint(*MAC_ARM_MARKERS)
468 {
469 return false;
470 }
471 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
472 if graph.graph[node_index]
473 .marker()
474 .is_disjoint(*MAC_X86_64_MARKERS)
475 {
476 return false;
477 }
478 } else if platform_tags.iter().all(PlatformTag::is_x86) {
479 if graph.graph[node_index]
480 .marker()
481 .is_disjoint(*MAC_X86_MARKERS)
482 {
483 return false;
484 }
485 } else if graph.graph[node_index].marker().is_disjoint(*MAC_MARKERS) {
486 return false;
487 }
488 }
489
490 if platform_tags.iter().all(PlatformTag::is_android) {
491 if platform_tags.iter().all(PlatformTag::is_arm) {
492 if graph.graph[node_index]
493 .marker()
494 .is_disjoint(*ANDROID_ARM_MARKERS)
495 {
496 return false;
497 }
498 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
499 if graph.graph[node_index]
500 .marker()
501 .is_disjoint(*ANDROID_X86_64_MARKERS)
502 {
503 return false;
504 }
505 } else if platform_tags.iter().all(PlatformTag::is_x86) {
506 if graph.graph[node_index]
507 .marker()
508 .is_disjoint(*ANDROID_X86_MARKERS)
509 {
510 return false;
511 }
512 } else if graph.graph[node_index]
513 .marker()
514 .is_disjoint(*ANDROID_MARKERS)
515 {
516 return false;
517 }
518 }
519
520 if platform_tags.iter().all(PlatformTag::is_arm) {
521 if graph.graph[node_index].marker().is_disjoint(*ARM_MARKERS) {
522 return false;
523 }
524 }
525
526 if platform_tags.iter().all(PlatformTag::is_x86_64) {
527 if graph.graph[node_index]
528 .marker()
529 .is_disjoint(*X86_64_MARKERS)
530 {
531 return false;
532 }
533 }
534
535 if platform_tags.iter().all(PlatformTag::is_x86) {
536 if graph.graph[node_index].marker().is_disjoint(*X86_MARKERS) {
537 return false;
538 }
539 }
540
541 true
542 });
543 }
544
545 fn new(
547 version: u32,
548 revision: u32,
549 mut packages: Vec<Package>,
550 requires_python: RequiresPython,
551 options: ResolverOptions,
552 manifest: ResolverManifest,
553 conflicts: Conflicts,
554 supported_environments: Vec<MarkerTree>,
555 required_environments: Vec<MarkerTree>,
556 fork_markers: Vec<UniversalMarker>,
557 ) -> Result<Self, LockError> {
558 for package in &mut packages {
561 package.dependencies.sort();
562 for windows in package.dependencies.windows(2) {
563 let (dep1, dep2) = (&windows[0], &windows[1]);
564 if dep1 == dep2 {
565 return Err(LockErrorKind::DuplicateDependency {
566 id: package.id.clone(),
567 dependency: dep1.clone(),
568 }
569 .into());
570 }
571 }
572
573 for (extra, dependencies) in &mut package.optional_dependencies {
575 dependencies.sort();
576 for windows in dependencies.windows(2) {
577 let (dep1, dep2) = (&windows[0], &windows[1]);
578 if dep1 == dep2 {
579 return Err(LockErrorKind::DuplicateOptionalDependency {
580 id: package.id.clone(),
581 extra: extra.clone(),
582 dependency: dep1.clone(),
583 }
584 .into());
585 }
586 }
587 }
588
589 for (group, dependencies) in &mut package.dependency_groups {
591 dependencies.sort();
592 for windows in dependencies.windows(2) {
593 let (dep1, dep2) = (&windows[0], &windows[1]);
594 if dep1 == dep2 {
595 return Err(LockErrorKind::DuplicateDevDependency {
596 id: package.id.clone(),
597 group: group.clone(),
598 dependency: dep1.clone(),
599 }
600 .into());
601 }
602 }
603 }
604 }
605 packages.sort_by(|dist1, dist2| dist1.id.cmp(&dist2.id));
606
607 let mut by_id = FxHashMap::default();
610 for (i, dist) in packages.iter().enumerate() {
611 if by_id.insert(dist.id.clone(), i).is_some() {
612 return Err(LockErrorKind::DuplicatePackage {
613 id: dist.id.clone(),
614 }
615 .into());
616 }
617 }
618
619 let mut extras_by_id = FxHashMap::default();
621 for dist in &packages {
622 for extra in dist.optional_dependencies.keys() {
623 extras_by_id
624 .entry(dist.id.clone())
625 .or_insert_with(FxHashSet::default)
626 .insert(extra.clone());
627 }
628 }
629
630 for dist in &mut packages {
632 for dep in dist
633 .dependencies
634 .iter_mut()
635 .chain(dist.optional_dependencies.values_mut().flatten())
636 .chain(dist.dependency_groups.values_mut().flatten())
637 {
638 dep.extra.retain(|extra| {
639 extras_by_id
640 .get(&dep.package_id)
641 .is_some_and(|extras| extras.contains(extra))
642 });
643 }
644 }
645
646 for dist in &packages {
650 for dep in &dist.dependencies {
651 if !by_id.contains_key(&dep.package_id) {
652 return Err(LockErrorKind::UnrecognizedDependency {
653 id: dist.id.clone(),
654 dependency: dep.clone(),
655 }
656 .into());
657 }
658 }
659
660 for dependencies in dist.optional_dependencies.values() {
662 for dep in dependencies {
663 if !by_id.contains_key(&dep.package_id) {
664 return Err(LockErrorKind::UnrecognizedDependency {
665 id: dist.id.clone(),
666 dependency: dep.clone(),
667 }
668 .into());
669 }
670 }
671 }
672
673 for dependencies in dist.dependency_groups.values() {
675 for dep in dependencies {
676 if !by_id.contains_key(&dep.package_id) {
677 return Err(LockErrorKind::UnrecognizedDependency {
678 id: dist.id.clone(),
679 dependency: dep.clone(),
680 }
681 .into());
682 }
683 }
684 }
685
686 if let Some(requires_hash) = dist.id.source.requires_hash() {
689 for wheel in &dist.wheels {
690 if requires_hash != wheel.hash.is_some() {
691 return Err(LockErrorKind::Hash {
692 id: dist.id.clone(),
693 artifact_type: "wheel",
694 expected: requires_hash,
695 }
696 .into());
697 }
698 }
699 }
700 }
701 let lock = Self {
702 version,
703 revision,
704 fork_markers,
705 conflicts,
706 supported_environments,
707 required_environments,
708 requires_python,
709 options,
710 packages,
711 by_id,
712 manifest,
713 };
714 Ok(lock)
715 }
716
717 #[must_use]
719 pub fn with_manifest(mut self, manifest: ResolverManifest) -> Self {
720 self.manifest = manifest;
721 self
722 }
723
724 #[must_use]
726 pub fn with_conflicts(mut self, conflicts: Conflicts) -> Self {
727 self.conflicts = conflicts;
728 self
729 }
730
731 #[must_use]
733 pub fn with_supported_environments(mut self, supported_environments: Vec<MarkerTree>) -> Self {
734 self.supported_environments = supported_environments
744 .into_iter()
745 .map(|marker| self.requires_python.complexify_markers(marker))
746 .collect();
747 self
748 }
749
750 #[must_use]
752 pub fn with_required_environments(mut self, required_environments: Vec<MarkerTree>) -> Self {
753 self.required_environments = required_environments
754 .into_iter()
755 .map(|marker| self.requires_python.complexify_markers(marker))
756 .collect();
757 self
758 }
759
760 pub fn supports_provides_extra(&self) -> bool {
762 (self.version(), self.revision()) >= (1, 1)
764 }
765
766 pub fn includes_empty_groups(&self) -> bool {
768 (self.version(), self.revision()) >= (1, 1)
771 }
772
773 pub fn version(&self) -> u32 {
775 self.version
776 }
777
778 pub fn revision(&self) -> u32 {
780 self.revision
781 }
782
783 pub fn len(&self) -> usize {
785 self.packages.len()
786 }
787
788 pub fn is_empty(&self) -> bool {
790 self.packages.is_empty()
791 }
792
793 pub fn packages(&self) -> &[Package] {
795 &self.packages
796 }
797
798 pub fn requires_python(&self) -> &RequiresPython {
800 &self.requires_python
801 }
802
803 pub fn resolution_mode(&self) -> ResolutionMode {
805 self.options.resolution_mode
806 }
807
808 pub fn prerelease_mode(&self) -> PrereleaseMode {
810 self.options.prerelease_mode
811 }
812
813 pub fn fork_strategy(&self) -> ForkStrategy {
815 self.options.fork_strategy
816 }
817
818 pub fn exclude_newer(&self) -> ExcludeNewer {
820 self.options.exclude_newer.clone().into()
823 }
824
825 pub fn conflicts(&self) -> &Conflicts {
827 &self.conflicts
828 }
829
830 pub fn supported_environments(&self) -> &[MarkerTree] {
832 &self.supported_environments
833 }
834
835 pub fn required_environments(&self) -> &[MarkerTree] {
837 &self.required_environments
838 }
839
840 pub fn members(&self) -> &BTreeSet<PackageName> {
842 &self.manifest.members
843 }
844
845 pub fn requirements(&self) -> &BTreeSet<Requirement> {
847 &self.manifest.requirements
848 }
849
850 pub fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
852 &self.manifest.dependency_groups
853 }
854
855 pub fn build_constraints(&self, root: &Path) -> Constraints {
857 Constraints::from_requirements(
858 self.manifest
859 .build_constraints
860 .iter()
861 .cloned()
862 .map(|requirement| requirement.to_absolute(root)),
863 )
864 }
865
866 pub fn root(&self) -> Option<&Package> {
868 self.packages.iter().find(|package| {
869 let (Source::Editable(path) | Source::Virtual(path)) = &package.id.source else {
870 return false;
871 };
872 path.as_ref() == Path::new("")
873 })
874 }
875
876 pub fn simplified_supported_environments(&self) -> Vec<MarkerTree> {
886 self.supported_environments()
887 .iter()
888 .copied()
889 .map(|marker| self.simplify_environment(marker))
890 .collect()
891 }
892
893 pub fn simplified_required_environments(&self) -> Vec<MarkerTree> {
896 self.required_environments()
897 .iter()
898 .copied()
899 .map(|marker| self.simplify_environment(marker))
900 .collect()
901 }
902
903 pub fn simplify_environment(&self, marker: MarkerTree) -> MarkerTree {
906 self.requires_python.simplify_markers(marker)
907 }
908
909 pub fn fork_markers(&self) -> &[UniversalMarker] {
912 self.fork_markers.as_slice()
913 }
914
915 pub fn check_marker_coverage(&self) -> Result<(), (MarkerTree, MarkerTree)> {
919 let fork_markers_union = if self.fork_markers().is_empty() {
920 self.requires_python.to_marker_tree()
921 } else {
922 let mut fork_markers_union = MarkerTree::FALSE;
923 for fork_marker in self.fork_markers() {
924 fork_markers_union.or(fork_marker.pep508());
925 }
926 fork_markers_union
927 };
928 let mut environments_union = if !self.supported_environments.is_empty() {
929 let mut environments_union = MarkerTree::FALSE;
930 for fork_marker in &self.supported_environments {
931 environments_union.or(*fork_marker);
932 }
933 environments_union
934 } else {
935 MarkerTree::TRUE
936 };
937 environments_union.and(self.requires_python.to_marker_tree());
939 if fork_markers_union.negate().is_disjoint(environments_union) {
940 Ok(())
941 } else {
942 Err((fork_markers_union, environments_union))
943 }
944 }
945
946 pub fn requires_python_coverage(
956 &self,
957 new_requires_python: &RequiresPython,
958 ) -> Result<(), (MarkerTree, MarkerTree)> {
959 let fork_markers_union = if self.fork_markers().is_empty() {
960 self.requires_python.to_marker_tree()
961 } else {
962 let mut fork_markers_union = MarkerTree::FALSE;
963 for fork_marker in self.fork_markers() {
964 fork_markers_union.or(fork_marker.pep508());
965 }
966 fork_markers_union
967 };
968 let new_requires_python = new_requires_python.to_marker_tree();
969 if fork_markers_union.is_disjoint(new_requires_python) {
970 Err((fork_markers_union, new_requires_python))
971 } else {
972 Ok(())
973 }
974 }
975
976 pub fn to_toml(&self) -> Result<String, toml_edit::ser::Error> {
978 debug_assert!(self.check_marker_coverage().is_ok());
981
982 let mut doc = toml_edit::DocumentMut::new();
985 doc.insert("version", value(i64::from(self.version)));
986
987 if self.revision > 0 {
988 doc.insert("revision", value(i64::from(self.revision)));
989 }
990
991 doc.insert("requires-python", value(self.requires_python.to_string()));
992
993 if !self.fork_markers.is_empty() {
994 let fork_markers = each_element_on_its_line_array(
995 simplified_universal_markers(&self.fork_markers, &self.requires_python).into_iter(),
996 );
997 if !fork_markers.is_empty() {
998 doc.insert("resolution-markers", value(fork_markers));
999 }
1000 }
1001
1002 if !self.supported_environments.is_empty() {
1003 let supported_environments = each_element_on_its_line_array(
1004 self.supported_environments
1005 .iter()
1006 .copied()
1007 .map(|marker| SimplifiedMarkerTree::new(&self.requires_python, marker))
1008 .filter_map(SimplifiedMarkerTree::try_to_string),
1009 );
1010 doc.insert("supported-markers", value(supported_environments));
1011 }
1012
1013 if !self.required_environments.is_empty() {
1014 let required_environments = each_element_on_its_line_array(
1015 self.required_environments
1016 .iter()
1017 .copied()
1018 .map(|marker| SimplifiedMarkerTree::new(&self.requires_python, marker))
1019 .filter_map(SimplifiedMarkerTree::try_to_string),
1020 );
1021 doc.insert("required-markers", value(required_environments));
1022 }
1023
1024 if !self.conflicts.is_empty() {
1025 let mut list = Array::new();
1026 for set in self.conflicts.iter() {
1027 list.push(each_element_on_its_line_array(set.iter().map(|item| {
1028 let mut table = InlineTable::new();
1029 table.insert("package", Value::from(item.package().to_string()));
1030 match item.kind() {
1031 ConflictKind::Project => {}
1032 ConflictKind::Extra(extra) => {
1033 table.insert("extra", Value::from(extra.to_string()));
1034 }
1035 ConflictKind::Group(group) => {
1036 table.insert("group", Value::from(group.to_string()));
1037 }
1038 }
1039 table
1040 })));
1041 }
1042 doc.insert("conflicts", value(list));
1043 }
1044
1045 {
1049 let mut options_table = Table::new();
1050
1051 if self.options.resolution_mode != ResolutionMode::default() {
1052 options_table.insert(
1053 "resolution-mode",
1054 value(self.options.resolution_mode.to_string()),
1055 );
1056 }
1057 if self.options.prerelease_mode != PrereleaseMode::default() {
1058 options_table.insert(
1059 "prerelease-mode",
1060 value(self.options.prerelease_mode.to_string()),
1061 );
1062 }
1063 if self.options.fork_strategy != ForkStrategy::default() {
1064 options_table.insert(
1065 "fork-strategy",
1066 value(self.options.fork_strategy.to_string()),
1067 );
1068 }
1069 let exclude_newer = ExcludeNewer::from(self.options.exclude_newer.clone());
1070 if !exclude_newer.is_empty() {
1071 if let Some(global) = &exclude_newer.global {
1073 options_table.insert("exclude-newer", value(global.to_string()));
1074 if let Some(span) = global.span() {
1076 options_table.insert("exclude-newer-span", value(span.to_string()));
1077 }
1078 }
1079
1080 if !exclude_newer.package.is_empty() {
1082 let mut package_table = toml_edit::Table::new();
1083 for (name, exclude_newer_value) in &exclude_newer.package {
1084 if let Some(span) = exclude_newer_value.span() {
1085 let mut inline = toml_edit::InlineTable::new();
1087 inline.insert(
1088 "timestamp",
1089 exclude_newer_value.timestamp().to_string().into(),
1090 );
1091 inline.insert("span", span.to_string().into());
1092 package_table.insert(name.as_ref(), Item::Value(inline.into()));
1093 } else {
1094 package_table
1096 .insert(name.as_ref(), value(exclude_newer_value.to_string()));
1097 }
1098 }
1099 options_table.insert("exclude-newer-package", Item::Table(package_table));
1100 }
1101 }
1102
1103 if !options_table.is_empty() {
1104 doc.insert("options", Item::Table(options_table));
1105 }
1106 }
1107
1108 {
1110 let mut manifest_table = Table::new();
1111
1112 if !self.manifest.members.is_empty() {
1113 manifest_table.insert(
1114 "members",
1115 value(each_element_on_its_line_array(
1116 self.manifest
1117 .members
1118 .iter()
1119 .map(std::string::ToString::to_string),
1120 )),
1121 );
1122 }
1123
1124 if !self.manifest.requirements.is_empty() {
1125 let requirements = self
1126 .manifest
1127 .requirements
1128 .iter()
1129 .map(|requirement| {
1130 serde::Serialize::serialize(
1131 &requirement,
1132 toml_edit::ser::ValueSerializer::new(),
1133 )
1134 })
1135 .collect::<Result<Vec<_>, _>>()?;
1136 let requirements = match requirements.as_slice() {
1137 [] => Array::new(),
1138 [requirement] => Array::from_iter([requirement]),
1139 requirements => each_element_on_its_line_array(requirements.iter()),
1140 };
1141 manifest_table.insert("requirements", value(requirements));
1142 }
1143
1144 if !self.manifest.constraints.is_empty() {
1145 let constraints = self
1146 .manifest
1147 .constraints
1148 .iter()
1149 .map(|requirement| {
1150 serde::Serialize::serialize(
1151 &requirement,
1152 toml_edit::ser::ValueSerializer::new(),
1153 )
1154 })
1155 .collect::<Result<Vec<_>, _>>()?;
1156 let constraints = match constraints.as_slice() {
1157 [] => Array::new(),
1158 [requirement] => Array::from_iter([requirement]),
1159 constraints => each_element_on_its_line_array(constraints.iter()),
1160 };
1161 manifest_table.insert("constraints", value(constraints));
1162 }
1163
1164 if !self.manifest.overrides.is_empty() {
1165 let overrides = self
1166 .manifest
1167 .overrides
1168 .iter()
1169 .map(|requirement| {
1170 serde::Serialize::serialize(
1171 &requirement,
1172 toml_edit::ser::ValueSerializer::new(),
1173 )
1174 })
1175 .collect::<Result<Vec<_>, _>>()?;
1176 let overrides = match overrides.as_slice() {
1177 [] => Array::new(),
1178 [requirement] => Array::from_iter([requirement]),
1179 overrides => each_element_on_its_line_array(overrides.iter()),
1180 };
1181 manifest_table.insert("overrides", value(overrides));
1182 }
1183
1184 if !self.manifest.excludes.is_empty() {
1185 let excludes = self
1186 .manifest
1187 .excludes
1188 .iter()
1189 .map(|name| {
1190 serde::Serialize::serialize(&name, toml_edit::ser::ValueSerializer::new())
1191 })
1192 .collect::<Result<Vec<_>, _>>()?;
1193 let excludes = match excludes.as_slice() {
1194 [] => Array::new(),
1195 [name] => Array::from_iter([name]),
1196 excludes => each_element_on_its_line_array(excludes.iter()),
1197 };
1198 manifest_table.insert("excludes", value(excludes));
1199 }
1200
1201 if !self.manifest.build_constraints.is_empty() {
1202 let build_constraints = self
1203 .manifest
1204 .build_constraints
1205 .iter()
1206 .map(|requirement| {
1207 serde::Serialize::serialize(
1208 &requirement,
1209 toml_edit::ser::ValueSerializer::new(),
1210 )
1211 })
1212 .collect::<Result<Vec<_>, _>>()?;
1213 let build_constraints = match build_constraints.as_slice() {
1214 [] => Array::new(),
1215 [requirement] => Array::from_iter([requirement]),
1216 build_constraints => each_element_on_its_line_array(build_constraints.iter()),
1217 };
1218 manifest_table.insert("build-constraints", value(build_constraints));
1219 }
1220
1221 if !self.manifest.dependency_groups.is_empty() {
1222 let mut dependency_groups = Table::new();
1223 for (extra, requirements) in &self.manifest.dependency_groups {
1224 let requirements = requirements
1225 .iter()
1226 .map(|requirement| {
1227 serde::Serialize::serialize(
1228 &requirement,
1229 toml_edit::ser::ValueSerializer::new(),
1230 )
1231 })
1232 .collect::<Result<Vec<_>, _>>()?;
1233 let requirements = match requirements.as_slice() {
1234 [] => Array::new(),
1235 [requirement] => Array::from_iter([requirement]),
1236 requirements => each_element_on_its_line_array(requirements.iter()),
1237 };
1238 if !requirements.is_empty() {
1239 dependency_groups.insert(extra.as_ref(), value(requirements));
1240 }
1241 }
1242 if !dependency_groups.is_empty() {
1243 manifest_table.insert("dependency-groups", Item::Table(dependency_groups));
1244 }
1245 }
1246
1247 if !self.manifest.dependency_metadata.is_empty() {
1248 let mut tables = ArrayOfTables::new();
1249 for metadata in &self.manifest.dependency_metadata {
1250 let mut table = Table::new();
1251 table.insert("name", value(metadata.name.to_string()));
1252 if let Some(version) = metadata.version.as_ref() {
1253 table.insert("version", value(version.to_string()));
1254 }
1255 if !metadata.requires_dist.is_empty() {
1256 table.insert(
1257 "requires-dist",
1258 value(serde::Serialize::serialize(
1259 &metadata.requires_dist,
1260 toml_edit::ser::ValueSerializer::new(),
1261 )?),
1262 );
1263 }
1264 if let Some(requires_python) = metadata.requires_python.as_ref() {
1265 table.insert("requires-python", value(requires_python.to_string()));
1266 }
1267 if !metadata.provides_extra.is_empty() {
1268 table.insert(
1269 "provides-extras",
1270 value(serde::Serialize::serialize(
1271 &metadata.provides_extra,
1272 toml_edit::ser::ValueSerializer::new(),
1273 )?),
1274 );
1275 }
1276 tables.push(table);
1277 }
1278 manifest_table.insert("dependency-metadata", Item::ArrayOfTables(tables));
1279 }
1280
1281 if !manifest_table.is_empty() {
1282 doc.insert("manifest", Item::Table(manifest_table));
1283 }
1284 }
1285
1286 let mut dist_count_by_name: FxHashMap<PackageName, u64> = FxHashMap::default();
1291 for dist in &self.packages {
1292 *dist_count_by_name.entry(dist.id.name.clone()).or_default() += 1;
1293 }
1294
1295 let mut packages = ArrayOfTables::new();
1296 for dist in &self.packages {
1297 packages.push(dist.to_toml(&self.requires_python, &dist_count_by_name)?);
1298 }
1299
1300 doc.insert("package", Item::ArrayOfTables(packages));
1301 Ok(doc.to_string())
1302 }
1303
1304 pub fn find_by_name(&self, name: &PackageName) -> Result<Option<&Package>, String> {
1308 let mut found_dist = None;
1309 for dist in &self.packages {
1310 if &dist.id.name == name {
1311 if found_dist.is_some() {
1312 return Err(format!("found multiple packages matching `{name}`"));
1313 }
1314 found_dist = Some(dist);
1315 }
1316 }
1317 Ok(found_dist)
1318 }
1319
1320 fn find_by_markers(
1330 &self,
1331 name: &PackageName,
1332 marker_env: &MarkerEnvironment,
1333 ) -> Result<Option<&Package>, String> {
1334 let mut found_dist = None;
1335 for dist in &self.packages {
1336 if &dist.id.name == name {
1337 if dist.fork_markers.is_empty()
1338 || dist
1339 .fork_markers
1340 .iter()
1341 .any(|marker| marker.evaluate_no_extras(marker_env))
1342 {
1343 if found_dist.is_some() {
1344 return Err(format!("found multiple packages matching `{name}`"));
1345 }
1346 found_dist = Some(dist);
1347 }
1348 }
1349 }
1350 Ok(found_dist)
1351 }
1352
1353 fn find_by_id(&self, id: &PackageId) -> &Package {
1354 let index = *self.by_id.get(id).expect("locked package for ID");
1355
1356 (self.packages.get(index).expect("valid index for package")) as _
1357 }
1358
1359 fn satisfies_provides_extra<'lock>(
1361 &self,
1362 provides_extra: Box<[ExtraName]>,
1363 package: &'lock Package,
1364 ) -> SatisfiesResult<'lock> {
1365 if !self.supports_provides_extra() {
1366 return SatisfiesResult::Satisfied;
1367 }
1368
1369 let expected: BTreeSet<_> = provides_extra.iter().collect();
1370 let actual: BTreeSet<_> = package.metadata.provides_extra.iter().collect();
1371
1372 if expected != actual {
1373 let expected = Box::into_iter(provides_extra).collect();
1374 return SatisfiesResult::MismatchedPackageProvidesExtra(
1375 &package.id.name,
1376 package.id.version.as_ref(),
1377 expected,
1378 actual,
1379 );
1380 }
1381
1382 SatisfiesResult::Satisfied
1383 }
1384
1385 #[allow(clippy::unused_self)]
1387 fn satisfies_requires_dist<'lock>(
1388 &self,
1389 requires_dist: Box<[Requirement]>,
1390 dependency_groups: BTreeMap<GroupName, Box<[Requirement]>>,
1391 package: &'lock Package,
1392 root: &Path,
1393 ) -> Result<SatisfiesResult<'lock>, LockError> {
1394 let flattened = if package.is_dynamic() {
1396 Some(
1397 FlatRequiresDist::from_requirements(requires_dist.clone(), &package.id.name)
1398 .into_iter()
1399 .map(|requirement| {
1400 normalize_requirement(requirement, root, &self.requires_python)
1401 })
1402 .collect::<Result<BTreeSet<_>, _>>()?,
1403 )
1404 } else {
1405 None
1406 };
1407
1408 let expected: BTreeSet<_> = Box::into_iter(requires_dist)
1410 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1411 .collect::<Result<_, _>>()?;
1412 let actual: BTreeSet<_> = package
1413 .metadata
1414 .requires_dist
1415 .iter()
1416 .cloned()
1417 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1418 .collect::<Result<_, _>>()?;
1419
1420 if expected != actual && flattened.is_none_or(|expected| expected != actual) {
1421 return Ok(SatisfiesResult::MismatchedPackageRequirements(
1422 &package.id.name,
1423 package.id.version.as_ref(),
1424 expected,
1425 actual,
1426 ));
1427 }
1428
1429 let expected: BTreeMap<GroupName, BTreeSet<Requirement>> = dependency_groups
1431 .into_iter()
1432 .filter(|(_, requirements)| self.includes_empty_groups() || !requirements.is_empty())
1433 .map(|(group, requirements)| {
1434 Ok::<_, LockError>((
1435 group,
1436 Box::into_iter(requirements)
1437 .map(|requirement| {
1438 normalize_requirement(requirement, root, &self.requires_python)
1439 })
1440 .collect::<Result<_, _>>()?,
1441 ))
1442 })
1443 .collect::<Result<_, _>>()?;
1444 let actual: BTreeMap<GroupName, BTreeSet<Requirement>> = package
1445 .metadata
1446 .dependency_groups
1447 .iter()
1448 .filter(|(_, requirements)| self.includes_empty_groups() || !requirements.is_empty())
1449 .map(|(group, requirements)| {
1450 Ok::<_, LockError>((
1451 group.clone(),
1452 requirements
1453 .iter()
1454 .cloned()
1455 .map(|requirement| {
1456 normalize_requirement(requirement, root, &self.requires_python)
1457 })
1458 .collect::<Result<_, _>>()?,
1459 ))
1460 })
1461 .collect::<Result<_, _>>()?;
1462
1463 if expected != actual {
1464 return Ok(SatisfiesResult::MismatchedPackageDependencyGroups(
1465 &package.id.name,
1466 package.id.version.as_ref(),
1467 expected,
1468 actual,
1469 ));
1470 }
1471
1472 Ok(SatisfiesResult::Satisfied)
1473 }
1474
1475 pub async fn satisfies<Context: BuildContext>(
1477 &self,
1478 root: &Path,
1479 packages: &BTreeMap<PackageName, WorkspaceMember>,
1480 members: &[PackageName],
1481 required_members: &BTreeMap<PackageName, Editability>,
1482 requirements: &[Requirement],
1483 constraints: &[Requirement],
1484 overrides: &[Requirement],
1485 excludes: &[PackageName],
1486 build_constraints: &[Requirement],
1487 dependency_groups: &BTreeMap<GroupName, Vec<Requirement>>,
1488 dependency_metadata: &DependencyMetadata,
1489 indexes: Option<&IndexLocations>,
1490 tags: &Tags,
1491 markers: &MarkerEnvironment,
1492 hasher: &HashStrategy,
1493 index: &InMemoryIndex,
1494 database: &DistributionDatabase<'_, Context>,
1495 ) -> Result<SatisfiesResult<'_>, LockError> {
1496 let mut queue: VecDeque<&Package> = VecDeque::new();
1497 let mut seen = FxHashSet::default();
1498
1499 {
1501 let expected = members.iter().cloned().collect::<BTreeSet<_>>();
1502 let actual = &self.manifest.members;
1503 if expected != *actual {
1504 return Ok(SatisfiesResult::MismatchedMembers(expected, actual));
1505 }
1506 }
1507
1508 for (name, member) in packages {
1511 let source = self.find_by_name(name).ok().flatten();
1512
1513 let value = required_members.get(name);
1515 let is_required_member = value.is_some();
1516 let editability = value.copied().flatten();
1517
1518 let expected_virtual = !member.pyproject_toml().is_package(!is_required_member);
1520 let actual_virtual =
1521 source.map(|package| matches!(package.id.source, Source::Virtual(..)));
1522 if actual_virtual != Some(expected_virtual) {
1523 return Ok(SatisfiesResult::MismatchedVirtual(
1524 name.clone(),
1525 expected_virtual,
1526 ));
1527 }
1528
1529 let expected_editable = if expected_virtual {
1531 false
1532 } else {
1533 editability.unwrap_or(true)
1534 };
1535 let actual_editable =
1536 source.map(|package| matches!(package.id.source, Source::Editable(..)));
1537 if actual_editable != Some(expected_editable) {
1538 return Ok(SatisfiesResult::MismatchedEditable(
1539 name.clone(),
1540 expected_editable,
1541 ));
1542 }
1543 }
1544
1545 {
1547 let expected: BTreeSet<_> = requirements
1548 .iter()
1549 .cloned()
1550 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1551 .collect::<Result<_, _>>()?;
1552 let actual: BTreeSet<_> = self
1553 .manifest
1554 .requirements
1555 .iter()
1556 .cloned()
1557 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1558 .collect::<Result<_, _>>()?;
1559 if expected != actual {
1560 return Ok(SatisfiesResult::MismatchedRequirements(expected, actual));
1561 }
1562 }
1563
1564 {
1566 let expected: BTreeSet<_> = constraints
1567 .iter()
1568 .cloned()
1569 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1570 .collect::<Result<_, _>>()?;
1571 let actual: BTreeSet<_> = self
1572 .manifest
1573 .constraints
1574 .iter()
1575 .cloned()
1576 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1577 .collect::<Result<_, _>>()?;
1578 if expected != actual {
1579 return Ok(SatisfiesResult::MismatchedConstraints(expected, actual));
1580 }
1581 }
1582
1583 {
1585 let expected: BTreeSet<_> = overrides
1586 .iter()
1587 .cloned()
1588 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1589 .collect::<Result<_, _>>()?;
1590 let actual: BTreeSet<_> = self
1591 .manifest
1592 .overrides
1593 .iter()
1594 .cloned()
1595 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1596 .collect::<Result<_, _>>()?;
1597 if expected != actual {
1598 return Ok(SatisfiesResult::MismatchedOverrides(expected, actual));
1599 }
1600 }
1601
1602 {
1604 let expected: BTreeSet<_> = excludes.iter().cloned().collect();
1605 let actual: BTreeSet<_> = self.manifest.excludes.iter().cloned().collect();
1606 if expected != actual {
1607 return Ok(SatisfiesResult::MismatchedExcludes(expected, actual));
1608 }
1609 }
1610
1611 {
1613 let expected: BTreeSet<_> = build_constraints
1614 .iter()
1615 .cloned()
1616 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1617 .collect::<Result<_, _>>()?;
1618 let actual: BTreeSet<_> = self
1619 .manifest
1620 .build_constraints
1621 .iter()
1622 .cloned()
1623 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1624 .collect::<Result<_, _>>()?;
1625 if expected != actual {
1626 return Ok(SatisfiesResult::MismatchedBuildConstraints(
1627 expected, actual,
1628 ));
1629 }
1630 }
1631
1632 {
1634 let expected: BTreeMap<GroupName, BTreeSet<Requirement>> = dependency_groups
1635 .iter()
1636 .filter(|(_, requirements)| !requirements.is_empty())
1637 .map(|(group, requirements)| {
1638 Ok::<_, LockError>((
1639 group.clone(),
1640 requirements
1641 .iter()
1642 .cloned()
1643 .map(|requirement| {
1644 normalize_requirement(requirement, root, &self.requires_python)
1645 })
1646 .collect::<Result<_, _>>()?,
1647 ))
1648 })
1649 .collect::<Result<_, _>>()?;
1650 let actual: BTreeMap<GroupName, BTreeSet<Requirement>> = self
1651 .manifest
1652 .dependency_groups
1653 .iter()
1654 .filter(|(_, requirements)| !requirements.is_empty())
1655 .map(|(group, requirements)| {
1656 Ok::<_, LockError>((
1657 group.clone(),
1658 requirements
1659 .iter()
1660 .cloned()
1661 .map(|requirement| {
1662 normalize_requirement(requirement, root, &self.requires_python)
1663 })
1664 .collect::<Result<_, _>>()?,
1665 ))
1666 })
1667 .collect::<Result<_, _>>()?;
1668 if expected != actual {
1669 return Ok(SatisfiesResult::MismatchedDependencyGroups(
1670 expected, actual,
1671 ));
1672 }
1673 }
1674
1675 {
1677 let expected = dependency_metadata
1678 .values()
1679 .cloned()
1680 .collect::<BTreeSet<_>>();
1681 let actual = &self.manifest.dependency_metadata;
1682 if expected != *actual {
1683 return Ok(SatisfiesResult::MismatchedStaticMetadata(expected, actual));
1684 }
1685 }
1686
1687 let mut remotes = indexes.map(|locations| {
1689 locations
1690 .allowed_indexes()
1691 .into_iter()
1692 .filter_map(|index| match index.url() {
1693 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
1694 Some(UrlString::from(index.url().without_credentials().as_ref()))
1695 }
1696 IndexUrl::Path(_) => None,
1697 })
1698 .collect::<BTreeSet<_>>()
1699 });
1700
1701 let mut locals = indexes.map(|locations| {
1702 locations
1703 .allowed_indexes()
1704 .into_iter()
1705 .filter_map(|index| match index.url() {
1706 IndexUrl::Pypi(_) | IndexUrl::Url(_) => None,
1707 IndexUrl::Path(url) => {
1708 let path = url.to_file_path().ok()?;
1709 let path = relative_to(&path, root)
1710 .or_else(|_| std::path::absolute(path))
1711 .ok()?
1712 .into_boxed_path();
1713 Some(path)
1714 }
1715 })
1716 .collect::<BTreeSet<_>>()
1717 });
1718
1719 for root_name in packages.keys() {
1721 let root = self
1722 .find_by_name(root_name)
1723 .expect("found too many packages matching root");
1724
1725 let Some(root) = root else {
1726 return Ok(SatisfiesResult::MissingRoot(root_name.clone()));
1728 };
1729
1730 queue.push_back(root);
1732 }
1733
1734 while let Some(package) = queue.pop_front() {
1735 if let Source::Registry(index) = &package.id.source {
1737 match index {
1738 RegistrySource::Url(url) => {
1739 if remotes
1740 .as_ref()
1741 .is_some_and(|remotes| !remotes.contains(url))
1742 {
1743 let name = &package.id.name;
1744 let version = &package
1745 .id
1746 .version
1747 .as_ref()
1748 .expect("version for registry source");
1749 return Ok(SatisfiesResult::MissingRemoteIndex(name, version, url));
1750 }
1751 }
1752 RegistrySource::Path(path) => {
1753 if locals.as_ref().is_some_and(|locals| !locals.contains(path)) {
1754 let name = &package.id.name;
1755 let version = &package
1756 .id
1757 .version
1758 .as_ref()
1759 .expect("version for registry source");
1760 return Ok(SatisfiesResult::MissingLocalIndex(name, version, path));
1761 }
1762 }
1763 }
1764 }
1765
1766 if package.id.source.is_immutable() {
1768 continue;
1769 }
1770
1771 if let Some(version) = package.id.version.as_ref() {
1772 let HashedDist { dist, .. } = package.to_dist(
1774 root,
1775 TagPolicy::Preferred(tags),
1776 &BuildOptions::default(),
1777 markers,
1778 )?;
1779
1780 let metadata = {
1781 let id = dist.version_id();
1782 if let Some(archive) =
1783 index
1784 .distributions()
1785 .get(&id)
1786 .as_deref()
1787 .and_then(|response| {
1788 if let MetadataResponse::Found(archive, ..) = response {
1789 Some(archive)
1790 } else {
1791 None
1792 }
1793 })
1794 {
1795 archive.metadata.clone()
1797 } else {
1798 let archive = database
1800 .get_or_build_wheel_metadata(&dist, hasher.get(&dist))
1801 .await
1802 .map_err(|err| LockErrorKind::Resolution {
1803 id: package.id.clone(),
1804 err,
1805 })?;
1806
1807 let metadata = archive.metadata.clone();
1808
1809 index
1811 .distributions()
1812 .done(id, Arc::new(MetadataResponse::Found(archive)));
1813
1814 metadata
1815 }
1816 };
1817
1818 if package.id.source.is_source_tree() {
1821 if metadata.dynamic {
1822 return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, false));
1823 }
1824 }
1825
1826 if metadata.version != *version {
1828 return Ok(SatisfiesResult::MismatchedVersion(
1829 &package.id.name,
1830 version.clone(),
1831 Some(metadata.version.clone()),
1832 ));
1833 }
1834
1835 match self.satisfies_provides_extra(metadata.provides_extra, package) {
1837 SatisfiesResult::Satisfied => {}
1838 result => return Ok(result),
1839 }
1840
1841 match self.satisfies_requires_dist(
1843 metadata.requires_dist,
1844 metadata.dependency_groups,
1845 package,
1846 root,
1847 )? {
1848 SatisfiesResult::Satisfied => {}
1849 result => return Ok(result),
1850 }
1851 } else if let Some(source_tree) = package.id.source.as_source_tree() {
1852 let parent = root.join(source_tree);
1862 let path = parent.join("pyproject.toml");
1863 let metadata =
1864 match fs_err::tokio::read_to_string(&path).await {
1865 Ok(contents) => {
1866 let pyproject_toml = toml::from_str::<PyProjectToml>(&contents)
1867 .map_err(|err| LockErrorKind::InvalidPyprojectToml {
1868 path: path.clone(),
1869 err,
1870 })?;
1871 database
1872 .requires_dist(&parent, &pyproject_toml)
1873 .await
1874 .map_err(|err| LockErrorKind::Resolution {
1875 id: package.id.clone(),
1876 err,
1877 })?
1878 }
1879 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
1880 Err(err) => {
1881 return Err(LockErrorKind::UnreadablePyprojectToml { path, err }.into());
1882 }
1883 };
1884
1885 let satisfied = metadata.is_some_and(|metadata| {
1886 if !metadata.dynamic {
1888 debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
1889 return false;
1890 }
1891
1892 if let SatisfiesResult::Satisfied = self.satisfies_provides_extra(metadata.provides_extra, package, ) {
1894 debug!("Static `provides-extra` for `{}` is up-to-date", package.id);
1895 } else {
1896 debug!("Static `provides-extra` for `{}` is out-of-date; falling back to distribution database", package.id);
1897 return false;
1898 }
1899
1900 match self.satisfies_requires_dist(metadata.requires_dist, metadata.dependency_groups, package, root) {
1902 Ok(SatisfiesResult::Satisfied) => {
1903 debug!("Static `requires-dist` for `{}` is up-to-date", package.id);
1904 },
1905 Ok(..) => {
1906 debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
1907 return false;
1908 },
1909 Err(..) => {
1910 debug!("Static `requires-dist` for `{}` is invalid; falling back to distribution database", package.id);
1911 return false;
1912 },
1913 }
1914
1915 true
1916 });
1917
1918 if !satisfied {
1924 let HashedDist { dist, .. } = package.to_dist(
1925 root,
1926 TagPolicy::Preferred(tags),
1927 &BuildOptions::default(),
1928 markers,
1929 )?;
1930
1931 let metadata = {
1932 let id = dist.version_id();
1933 if let Some(archive) =
1934 index
1935 .distributions()
1936 .get(&id)
1937 .as_deref()
1938 .and_then(|response| {
1939 if let MetadataResponse::Found(archive, ..) = response {
1940 Some(archive)
1941 } else {
1942 None
1943 }
1944 })
1945 {
1946 archive.metadata.clone()
1948 } else {
1949 let archive = database
1951 .get_or_build_wheel_metadata(&dist, hasher.get(&dist))
1952 .await
1953 .map_err(|err| LockErrorKind::Resolution {
1954 id: package.id.clone(),
1955 err,
1956 })?;
1957
1958 let metadata = archive.metadata.clone();
1959
1960 index
1962 .distributions()
1963 .done(id, Arc::new(MetadataResponse::Found(archive)));
1964
1965 metadata
1966 }
1967 };
1968
1969 if !metadata.dynamic {
1971 return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, true));
1972 }
1973
1974 match self.satisfies_provides_extra(metadata.provides_extra, package) {
1976 SatisfiesResult::Satisfied => {}
1977 result => return Ok(result),
1978 }
1979
1980 match self.satisfies_requires_dist(
1982 metadata.requires_dist,
1983 metadata.dependency_groups,
1984 package,
1985 root,
1986 )? {
1987 SatisfiesResult::Satisfied => {}
1988 result => return Ok(result),
1989 }
1990 }
1991 } else {
1992 return Ok(SatisfiesResult::MissingVersion(&package.id.name));
1993 }
1994
1995 for requirement in package
2000 .metadata
2001 .requires_dist
2002 .iter()
2003 .chain(package.metadata.dependency_groups.values().flatten())
2004 {
2005 if let RequirementSource::Registry {
2006 index: Some(index), ..
2007 } = &requirement.source
2008 {
2009 match &index.url {
2010 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
2011 if let Some(remotes) = remotes.as_mut() {
2012 remotes.insert(UrlString::from(
2013 index.url().without_credentials().as_ref(),
2014 ));
2015 }
2016 }
2017 IndexUrl::Path(url) => {
2018 if let Some(locals) = locals.as_mut() {
2019 if let Some(path) = url.to_file_path().ok().and_then(|path| {
2020 relative_to(&path, root)
2021 .or_else(|_| std::path::absolute(path))
2022 .ok()
2023 }) {
2024 locals.insert(path.into_boxed_path());
2025 }
2026 }
2027 }
2028 }
2029 }
2030 }
2031
2032 for dep in &package.dependencies {
2034 if seen.insert(&dep.package_id) {
2035 let dep_dist = self.find_by_id(&dep.package_id);
2036 queue.push_back(dep_dist);
2037 }
2038 }
2039
2040 for dependencies in package.optional_dependencies.values() {
2041 for dep in dependencies {
2042 if seen.insert(&dep.package_id) {
2043 let dep_dist = self.find_by_id(&dep.package_id);
2044 queue.push_back(dep_dist);
2045 }
2046 }
2047 }
2048
2049 for dependencies in package.dependency_groups.values() {
2050 for dep in dependencies {
2051 if seen.insert(&dep.package_id) {
2052 let dep_dist = self.find_by_id(&dep.package_id);
2053 queue.push_back(dep_dist);
2054 }
2055 }
2056 }
2057 }
2058
2059 Ok(SatisfiesResult::Satisfied)
2060 }
2061}
2062
2063#[derive(Debug, Copy, Clone)]
2064enum TagPolicy<'tags> {
2065 Required(&'tags Tags),
2067 Preferred(&'tags Tags),
2070}
2071
2072impl<'tags> TagPolicy<'tags> {
2073 fn tags(&self) -> &'tags Tags {
2075 match self {
2076 Self::Required(tags) | Self::Preferred(tags) => tags,
2077 }
2078 }
2079}
2080
2081#[derive(Debug)]
2083pub enum SatisfiesResult<'lock> {
2084 Satisfied,
2086 MismatchedMembers(BTreeSet<PackageName>, &'lock BTreeSet<PackageName>),
2088 MismatchedVirtual(PackageName, bool),
2090 MismatchedEditable(PackageName, bool),
2092 MismatchedDynamic(&'lock PackageName, bool),
2094 MismatchedVersion(&'lock PackageName, Version, Option<Version>),
2096 MismatchedRequirements(BTreeSet<Requirement>, BTreeSet<Requirement>),
2098 MismatchedConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
2100 MismatchedOverrides(BTreeSet<Requirement>, BTreeSet<Requirement>),
2102 MismatchedExcludes(BTreeSet<PackageName>, BTreeSet<PackageName>),
2104 MismatchedBuildConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
2106 MismatchedDependencyGroups(
2108 BTreeMap<GroupName, BTreeSet<Requirement>>,
2109 BTreeMap<GroupName, BTreeSet<Requirement>>,
2110 ),
2111 MismatchedStaticMetadata(BTreeSet<StaticMetadata>, &'lock BTreeSet<StaticMetadata>),
2113 MissingRoot(PackageName),
2115 MissingRemoteIndex(&'lock PackageName, &'lock Version, &'lock UrlString),
2117 MissingLocalIndex(&'lock PackageName, &'lock Version, &'lock Path),
2119 MismatchedPackageRequirements(
2121 &'lock PackageName,
2122 Option<&'lock Version>,
2123 BTreeSet<Requirement>,
2124 BTreeSet<Requirement>,
2125 ),
2126 MismatchedPackageProvidesExtra(
2128 &'lock PackageName,
2129 Option<&'lock Version>,
2130 BTreeSet<ExtraName>,
2131 BTreeSet<&'lock ExtraName>,
2132 ),
2133 MismatchedPackageDependencyGroups(
2135 &'lock PackageName,
2136 Option<&'lock Version>,
2137 BTreeMap<GroupName, BTreeSet<Requirement>>,
2138 BTreeMap<GroupName, BTreeSet<Requirement>>,
2139 ),
2140 MissingVersion(&'lock PackageName),
2142}
2143
2144#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2146#[serde(rename_all = "kebab-case")]
2147struct ResolverOptions {
2148 #[serde(default)]
2150 resolution_mode: ResolutionMode,
2151 #[serde(default)]
2153 prerelease_mode: PrereleaseMode,
2154 #[serde(default)]
2156 fork_strategy: ForkStrategy,
2157 #[serde(flatten)]
2159 exclude_newer: ExcludeNewerWire,
2160}
2161
2162#[allow(clippy::struct_field_names)]
2163#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2164#[serde(rename_all = "kebab-case")]
2165struct ExcludeNewerWire {
2166 exclude_newer: Option<Timestamp>,
2167 exclude_newer_span: Option<ExcludeNewerSpan>,
2168 #[serde(default, skip_serializing_if = "ExcludeNewerPackage::is_empty")]
2169 exclude_newer_package: ExcludeNewerPackage,
2170}
2171
2172impl From<ExcludeNewerWire> for ExcludeNewer {
2173 fn from(wire: ExcludeNewerWire) -> Self {
2174 Self {
2175 global: wire
2176 .exclude_newer
2177 .map(|timestamp| ExcludeNewerValue::new(timestamp, wire.exclude_newer_span)),
2178 package: wire.exclude_newer_package,
2179 }
2180 }
2181}
2182
2183impl From<ExcludeNewer> for ExcludeNewerWire {
2184 fn from(exclude_newer: ExcludeNewer) -> Self {
2185 let (timestamp, span) = exclude_newer
2186 .global
2187 .map(ExcludeNewerValue::into_parts)
2188 .map_or((None, None), |(t, s)| (Some(t), s));
2189 Self {
2190 exclude_newer: timestamp,
2191 exclude_newer_span: span,
2192 exclude_newer_package: exclude_newer.package,
2193 }
2194 }
2195}
2196
2197#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2198#[serde(rename_all = "kebab-case")]
2199pub struct ResolverManifest {
2200 #[serde(default)]
2202 members: BTreeSet<PackageName>,
2203 #[serde(default)]
2208 requirements: BTreeSet<Requirement>,
2209 #[serde(default)]
2215 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
2216 #[serde(default)]
2218 constraints: BTreeSet<Requirement>,
2219 #[serde(default)]
2221 overrides: BTreeSet<Requirement>,
2222 #[serde(default)]
2224 excludes: BTreeSet<PackageName>,
2225 #[serde(default)]
2227 build_constraints: BTreeSet<Requirement>,
2228 #[serde(default)]
2230 dependency_metadata: BTreeSet<StaticMetadata>,
2231}
2232
2233impl ResolverManifest {
2234 pub fn new(
2237 members: impl IntoIterator<Item = PackageName>,
2238 requirements: impl IntoIterator<Item = Requirement>,
2239 constraints: impl IntoIterator<Item = Requirement>,
2240 overrides: impl IntoIterator<Item = Requirement>,
2241 excludes: impl IntoIterator<Item = PackageName>,
2242 build_constraints: impl IntoIterator<Item = Requirement>,
2243 dependency_groups: impl IntoIterator<Item = (GroupName, Vec<Requirement>)>,
2244 dependency_metadata: impl IntoIterator<Item = StaticMetadata>,
2245 ) -> Self {
2246 Self {
2247 members: members.into_iter().collect(),
2248 requirements: requirements.into_iter().collect(),
2249 constraints: constraints.into_iter().collect(),
2250 overrides: overrides.into_iter().collect(),
2251 excludes: excludes.into_iter().collect(),
2252 build_constraints: build_constraints.into_iter().collect(),
2253 dependency_groups: dependency_groups
2254 .into_iter()
2255 .map(|(group, requirements)| (group, requirements.into_iter().collect()))
2256 .collect(),
2257 dependency_metadata: dependency_metadata.into_iter().collect(),
2258 }
2259 }
2260
2261 pub fn relative_to(self, root: &Path) -> Result<Self, io::Error> {
2263 Ok(Self {
2264 members: self.members,
2265 requirements: self
2266 .requirements
2267 .into_iter()
2268 .map(|requirement| requirement.relative_to(root))
2269 .collect::<Result<BTreeSet<_>, _>>()?,
2270 constraints: self
2271 .constraints
2272 .into_iter()
2273 .map(|requirement| requirement.relative_to(root))
2274 .collect::<Result<BTreeSet<_>, _>>()?,
2275 overrides: self
2276 .overrides
2277 .into_iter()
2278 .map(|requirement| requirement.relative_to(root))
2279 .collect::<Result<BTreeSet<_>, _>>()?,
2280 excludes: self.excludes,
2281 build_constraints: self
2282 .build_constraints
2283 .into_iter()
2284 .map(|requirement| requirement.relative_to(root))
2285 .collect::<Result<BTreeSet<_>, _>>()?,
2286 dependency_groups: self
2287 .dependency_groups
2288 .into_iter()
2289 .map(|(group, requirements)| {
2290 Ok::<_, io::Error>((
2291 group,
2292 requirements
2293 .into_iter()
2294 .map(|requirement| requirement.relative_to(root))
2295 .collect::<Result<BTreeSet<_>, _>>()?,
2296 ))
2297 })
2298 .collect::<Result<BTreeMap<_, _>, _>>()?,
2299 dependency_metadata: self.dependency_metadata,
2300 })
2301 }
2302}
2303
2304#[derive(Clone, Debug, serde::Deserialize)]
2305#[serde(rename_all = "kebab-case")]
2306struct LockWire {
2307 version: u32,
2308 revision: Option<u32>,
2309 requires_python: RequiresPython,
2310 #[serde(rename = "resolution-markers", default)]
2313 fork_markers: Vec<SimplifiedMarkerTree>,
2314 #[serde(rename = "supported-markers", default)]
2315 supported_environments: Vec<SimplifiedMarkerTree>,
2316 #[serde(rename = "required-markers", default)]
2317 required_environments: Vec<SimplifiedMarkerTree>,
2318 #[serde(rename = "conflicts", default)]
2319 conflicts: Option<Conflicts>,
2320 #[serde(default)]
2322 options: ResolverOptions,
2323 #[serde(default)]
2324 manifest: ResolverManifest,
2325 #[serde(rename = "package", alias = "distribution", default)]
2326 packages: Vec<PackageWire>,
2327}
2328
2329impl TryFrom<LockWire> for Lock {
2330 type Error = LockError;
2331
2332 fn try_from(wire: LockWire) -> Result<Self, LockError> {
2333 let mut unambiguous_package_ids: FxHashMap<PackageName, PackageId> = FxHashMap::default();
2338 let mut ambiguous = FxHashSet::default();
2339 for dist in &wire.packages {
2340 if ambiguous.contains(&dist.id.name) {
2341 continue;
2342 }
2343 if let Some(id) = unambiguous_package_ids.remove(&dist.id.name) {
2344 ambiguous.insert(id.name);
2345 continue;
2346 }
2347 unambiguous_package_ids.insert(dist.id.name.clone(), dist.id.clone());
2348 }
2349
2350 let packages = wire
2351 .packages
2352 .into_iter()
2353 .map(|dist| dist.unwire(&wire.requires_python, &unambiguous_package_ids))
2354 .collect::<Result<Vec<_>, _>>()?;
2355 let supported_environments = wire
2356 .supported_environments
2357 .into_iter()
2358 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2359 .collect();
2360 let required_environments = wire
2361 .required_environments
2362 .into_iter()
2363 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2364 .collect();
2365 let fork_markers = wire
2366 .fork_markers
2367 .into_iter()
2368 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2369 .map(UniversalMarker::from_combined)
2370 .collect();
2371 let lock = Self::new(
2372 wire.version,
2373 wire.revision.unwrap_or(0),
2374 packages,
2375 wire.requires_python,
2376 wire.options,
2377 wire.manifest,
2378 wire.conflicts.unwrap_or_else(Conflicts::empty),
2379 supported_environments,
2380 required_environments,
2381 fork_markers,
2382 )?;
2383
2384 Ok(lock)
2385 }
2386}
2387
2388#[derive(Clone, Debug, serde::Deserialize)]
2392#[serde(rename_all = "kebab-case")]
2393pub struct LockVersion {
2394 version: u32,
2395}
2396
2397impl LockVersion {
2398 pub fn version(&self) -> u32 {
2400 self.version
2401 }
2402}
2403
2404#[derive(Clone, Debug, PartialEq, Eq)]
2405pub struct Package {
2406 pub(crate) id: PackageId,
2407 sdist: Option<SourceDist>,
2408 wheels: Vec<Wheel>,
2409 fork_markers: Vec<UniversalMarker>,
2415 dependencies: Vec<Dependency>,
2417 optional_dependencies: BTreeMap<ExtraName, Vec<Dependency>>,
2419 dependency_groups: BTreeMap<GroupName, Vec<Dependency>>,
2421 metadata: PackageMetadata,
2423}
2424
2425impl Package {
2426 fn from_annotated_dist(
2427 annotated_dist: &AnnotatedDist,
2428 fork_markers: Vec<UniversalMarker>,
2429 root: &Path,
2430 ) -> Result<Self, LockError> {
2431 let id = PackageId::from_annotated_dist(annotated_dist, root)?;
2432 let sdist = SourceDist::from_annotated_dist(&id, annotated_dist)?;
2433 let wheels = Wheel::from_annotated_dist(annotated_dist)?;
2434 let requires_dist = if id.source.is_immutable() {
2435 BTreeSet::default()
2436 } else {
2437 annotated_dist
2438 .metadata
2439 .as_ref()
2440 .expect("metadata is present")
2441 .requires_dist
2442 .iter()
2443 .cloned()
2444 .map(|requirement| requirement.relative_to(root))
2445 .collect::<Result<_, _>>()
2446 .map_err(LockErrorKind::RequirementRelativePath)?
2447 };
2448 let provides_extra = if id.source.is_immutable() {
2449 Box::default()
2450 } else {
2451 annotated_dist
2452 .metadata
2453 .as_ref()
2454 .expect("metadata is present")
2455 .provides_extra
2456 .clone()
2457 };
2458 let dependency_groups = if id.source.is_immutable() {
2459 BTreeMap::default()
2460 } else {
2461 annotated_dist
2462 .metadata
2463 .as_ref()
2464 .expect("metadata is present")
2465 .dependency_groups
2466 .iter()
2467 .map(|(group, requirements)| {
2468 let requirements = requirements
2469 .iter()
2470 .cloned()
2471 .map(|requirement| requirement.relative_to(root))
2472 .collect::<Result<_, _>>()
2473 .map_err(LockErrorKind::RequirementRelativePath)?;
2474 Ok::<_, LockError>((group.clone(), requirements))
2475 })
2476 .collect::<Result<_, _>>()?
2477 };
2478 Ok(Self {
2479 id,
2480 sdist,
2481 wheels,
2482 fork_markers,
2483 dependencies: vec![],
2484 optional_dependencies: BTreeMap::default(),
2485 dependency_groups: BTreeMap::default(),
2486 metadata: PackageMetadata {
2487 requires_dist,
2488 provides_extra,
2489 dependency_groups,
2490 },
2491 })
2492 }
2493
2494 fn add_dependency(
2496 &mut self,
2497 requires_python: &RequiresPython,
2498 annotated_dist: &AnnotatedDist,
2499 marker: UniversalMarker,
2500 root: &Path,
2501 ) -> Result<(), LockError> {
2502 let new_dep =
2503 Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2504 for existing_dep in &mut self.dependencies {
2505 if existing_dep.package_id == new_dep.package_id
2506 && existing_dep.simplified_marker == new_dep.simplified_marker
2529 {
2530 existing_dep.extra.extend(new_dep.extra);
2531 return Ok(());
2532 }
2533 }
2534
2535 self.dependencies.push(new_dep);
2536 Ok(())
2537 }
2538
2539 fn add_optional_dependency(
2541 &mut self,
2542 requires_python: &RequiresPython,
2543 extra: ExtraName,
2544 annotated_dist: &AnnotatedDist,
2545 marker: UniversalMarker,
2546 root: &Path,
2547 ) -> Result<(), LockError> {
2548 let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2549 let optional_deps = self.optional_dependencies.entry(extra).or_default();
2550 for existing_dep in &mut *optional_deps {
2551 if existing_dep.package_id == dep.package_id
2552 && existing_dep.simplified_marker == dep.simplified_marker
2555 {
2556 existing_dep.extra.extend(dep.extra);
2557 return Ok(());
2558 }
2559 }
2560
2561 optional_deps.push(dep);
2562 Ok(())
2563 }
2564
2565 fn add_group_dependency(
2567 &mut self,
2568 requires_python: &RequiresPython,
2569 group: GroupName,
2570 annotated_dist: &AnnotatedDist,
2571 marker: UniversalMarker,
2572 root: &Path,
2573 ) -> Result<(), LockError> {
2574 let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2575 let deps = self.dependency_groups.entry(group).or_default();
2576 for existing_dep in &mut *deps {
2577 if existing_dep.package_id == dep.package_id
2578 && existing_dep.simplified_marker == dep.simplified_marker
2581 {
2582 existing_dep.extra.extend(dep.extra);
2583 return Ok(());
2584 }
2585 }
2586
2587 deps.push(dep);
2588 Ok(())
2589 }
2590
2591 fn to_dist(
2593 &self,
2594 workspace_root: &Path,
2595 tag_policy: TagPolicy<'_>,
2596 build_options: &BuildOptions,
2597 markers: &MarkerEnvironment,
2598 ) -> Result<HashedDist, LockError> {
2599 let no_binary = build_options.no_binary_package(&self.id.name);
2600 let no_build = build_options.no_build_package(&self.id.name);
2601
2602 if !no_binary {
2603 if let Some(best_wheel_index) = self.find_best_wheel(tag_policy) {
2604 let hashes = self.wheels[best_wheel_index]
2605 .hash
2606 .as_ref()
2607 .map(|hash| HashDigests::from(vec![hash.0.clone()]))
2608 .unwrap_or_else(|| HashDigests::from(vec![]));
2609
2610 let dist = match &self.id.source {
2611 Source::Registry(source) => {
2612 let wheels = self
2613 .wheels
2614 .iter()
2615 .map(|wheel| wheel.to_registry_wheel(source, workspace_root))
2616 .collect::<Result<_, LockError>>()?;
2617 let reg_built_dist = RegistryBuiltDist {
2618 wheels,
2619 best_wheel_index,
2620 sdist: None,
2621 };
2622 Dist::Built(BuiltDist::Registry(reg_built_dist))
2623 }
2624 Source::Path(path) => {
2625 let filename: WheelFilename =
2626 self.wheels[best_wheel_index].filename.clone();
2627 let install_path = absolute_path(workspace_root, path)?;
2628 let path_dist = PathBuiltDist {
2629 filename,
2630 url: verbatim_url(&install_path, &self.id)?,
2631 install_path: absolute_path(workspace_root, path)?.into_boxed_path(),
2632 };
2633 let built_dist = BuiltDist::Path(path_dist);
2634 Dist::Built(built_dist)
2635 }
2636 Source::Direct(url, direct) => {
2637 let filename: WheelFilename =
2638 self.wheels[best_wheel_index].filename.clone();
2639 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
2640 url: url.to_url().map_err(LockErrorKind::InvalidUrl)?,
2641 subdirectory: direct.subdirectory.clone(),
2642 ext: DistExtension::Wheel,
2643 });
2644 let direct_dist = DirectUrlBuiltDist {
2645 filename,
2646 location: Box::new(url.clone()),
2647 url: VerbatimUrl::from_url(url),
2648 };
2649 let built_dist = BuiltDist::DirectUrl(direct_dist);
2650 Dist::Built(built_dist)
2651 }
2652 Source::Git(_, _) => {
2653 return Err(LockErrorKind::InvalidWheelSource {
2654 id: self.id.clone(),
2655 source_type: "Git",
2656 }
2657 .into());
2658 }
2659 Source::Directory(_) => {
2660 return Err(LockErrorKind::InvalidWheelSource {
2661 id: self.id.clone(),
2662 source_type: "directory",
2663 }
2664 .into());
2665 }
2666 Source::Editable(_) => {
2667 return Err(LockErrorKind::InvalidWheelSource {
2668 id: self.id.clone(),
2669 source_type: "editable",
2670 }
2671 .into());
2672 }
2673 Source::Virtual(_) => {
2674 return Err(LockErrorKind::InvalidWheelSource {
2675 id: self.id.clone(),
2676 source_type: "virtual",
2677 }
2678 .into());
2679 }
2680 };
2681
2682 return Ok(HashedDist { dist, hashes });
2683 }
2684 }
2685
2686 if let Some(sdist) = self.to_source_dist(workspace_root)? {
2687 if !no_build || sdist.is_virtual() {
2691 let hashes = self
2692 .sdist
2693 .as_ref()
2694 .and_then(|s| s.hash())
2695 .map(|hash| HashDigests::from(vec![hash.0.clone()]))
2696 .unwrap_or_else(|| HashDigests::from(vec![]));
2697 return Ok(HashedDist {
2698 dist: Dist::Source(sdist),
2699 hashes,
2700 });
2701 }
2702 }
2703
2704 match (no_binary, no_build) {
2705 (true, true) => Err(LockErrorKind::NoBinaryNoBuild {
2706 id: self.id.clone(),
2707 }
2708 .into()),
2709 (true, false) if self.id.source.is_wheel() => Err(LockErrorKind::NoBinaryWheelOnly {
2710 id: self.id.clone(),
2711 }
2712 .into()),
2713 (true, false) => Err(LockErrorKind::NoBinary {
2714 id: self.id.clone(),
2715 }
2716 .into()),
2717 (false, true) => Err(LockErrorKind::NoBuild {
2718 id: self.id.clone(),
2719 }
2720 .into()),
2721 (false, false) if self.id.source.is_wheel() => Err(LockError {
2722 kind: Box::new(LockErrorKind::IncompatibleWheelOnly {
2723 id: self.id.clone(),
2724 }),
2725 hint: self.tag_hint(tag_policy, markers),
2726 }),
2727 (false, false) => Err(LockError {
2728 kind: Box::new(LockErrorKind::NeitherSourceDistNorWheel {
2729 id: self.id.clone(),
2730 }),
2731 hint: self.tag_hint(tag_policy, markers),
2732 }),
2733 }
2734 }
2735
2736 fn tag_hint(
2738 &self,
2739 tag_policy: TagPolicy<'_>,
2740 markers: &MarkerEnvironment,
2741 ) -> Option<WheelTagHint> {
2742 let filenames = self
2743 .wheels
2744 .iter()
2745 .map(|wheel| &wheel.filename)
2746 .collect::<Vec<_>>();
2747 WheelTagHint::from_wheels(
2748 &self.id.name,
2749 self.id.version.as_ref(),
2750 &filenames,
2751 tag_policy.tags(),
2752 markers,
2753 )
2754 }
2755
2756 fn to_source_dist(
2761 &self,
2762 workspace_root: &Path,
2763 ) -> Result<Option<uv_distribution_types::SourceDist>, LockError> {
2764 let sdist = match &self.id.source {
2765 Source::Path(path) => {
2766 let DistExtension::Source(ext) = DistExtension::from_path(path).map_err(|err| {
2768 LockErrorKind::MissingExtension {
2769 id: self.id.clone(),
2770 err,
2771 }
2772 })?
2773 else {
2774 return Ok(None);
2775 };
2776 let install_path = absolute_path(workspace_root, path)?;
2777 let path_dist = PathSourceDist {
2778 name: self.id.name.clone(),
2779 version: self.id.version.clone(),
2780 url: verbatim_url(&install_path, &self.id)?,
2781 install_path: install_path.into_boxed_path(),
2782 ext,
2783 };
2784 uv_distribution_types::SourceDist::Path(path_dist)
2785 }
2786 Source::Directory(path) => {
2787 let install_path = absolute_path(workspace_root, path)?;
2788 let dir_dist = DirectorySourceDist {
2789 name: self.id.name.clone(),
2790 url: verbatim_url(&install_path, &self.id)?,
2791 install_path: install_path.into_boxed_path(),
2792 editable: Some(false),
2793 r#virtual: Some(false),
2794 };
2795 uv_distribution_types::SourceDist::Directory(dir_dist)
2796 }
2797 Source::Editable(path) => {
2798 let install_path = absolute_path(workspace_root, path)?;
2799 let dir_dist = DirectorySourceDist {
2800 name: self.id.name.clone(),
2801 url: verbatim_url(&install_path, &self.id)?,
2802 install_path: install_path.into_boxed_path(),
2803 editable: Some(true),
2804 r#virtual: Some(false),
2805 };
2806 uv_distribution_types::SourceDist::Directory(dir_dist)
2807 }
2808 Source::Virtual(path) => {
2809 let install_path = absolute_path(workspace_root, path)?;
2810 let dir_dist = DirectorySourceDist {
2811 name: self.id.name.clone(),
2812 url: verbatim_url(&install_path, &self.id)?,
2813 install_path: install_path.into_boxed_path(),
2814 editable: Some(false),
2815 r#virtual: Some(true),
2816 };
2817 uv_distribution_types::SourceDist::Directory(dir_dist)
2818 }
2819 Source::Git(url, git) => {
2820 let mut url = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
2823 url.set_fragment(None);
2824 url.set_query(None);
2825
2826 let git_url = GitUrl::from_commit(
2828 url,
2829 GitReference::from(git.kind.clone()),
2830 git.precise,
2831 git.lfs,
2832 )?;
2833
2834 let url = DisplaySafeUrl::from(ParsedGitUrl {
2836 url: git_url.clone(),
2837 subdirectory: git.subdirectory.clone(),
2838 });
2839
2840 let git_dist = GitSourceDist {
2841 name: self.id.name.clone(),
2842 url: VerbatimUrl::from_url(url),
2843 git: Box::new(git_url),
2844 subdirectory: git.subdirectory.clone(),
2845 };
2846 uv_distribution_types::SourceDist::Git(git_dist)
2847 }
2848 Source::Direct(url, direct) => {
2849 let DistExtension::Source(ext) =
2851 DistExtension::from_path(url.base_str()).map_err(|err| {
2852 LockErrorKind::MissingExtension {
2853 id: self.id.clone(),
2854 err,
2855 }
2856 })?
2857 else {
2858 return Ok(None);
2859 };
2860 let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
2861 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
2862 url: location.clone(),
2863 subdirectory: direct.subdirectory.clone(),
2864 ext: DistExtension::Source(ext),
2865 });
2866 let direct_dist = DirectUrlSourceDist {
2867 name: self.id.name.clone(),
2868 location: Box::new(location),
2869 subdirectory: direct.subdirectory.clone(),
2870 ext,
2871 url: VerbatimUrl::from_url(url),
2872 };
2873 uv_distribution_types::SourceDist::DirectUrl(direct_dist)
2874 }
2875 Source::Registry(RegistrySource::Url(url)) => {
2876 let Some(ref sdist) = self.sdist else {
2877 return Ok(None);
2878 };
2879
2880 let name = &self.id.name;
2881 let version = self
2882 .id
2883 .version
2884 .as_ref()
2885 .expect("version for registry source");
2886
2887 let file_url = sdist.url().ok_or_else(|| LockErrorKind::MissingUrl {
2888 name: name.clone(),
2889 version: version.clone(),
2890 })?;
2891 let filename = sdist
2892 .filename()
2893 .ok_or_else(|| LockErrorKind::MissingFilename {
2894 id: self.id.clone(),
2895 })?;
2896 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
2897 LockErrorKind::MissingExtension {
2898 id: self.id.clone(),
2899 err,
2900 }
2901 })?;
2902 let file = Box::new(uv_distribution_types::File {
2903 dist_info_metadata: false,
2904 filename: SmallString::from(filename),
2905 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
2906 HashDigests::from(hash.0.clone())
2907 }),
2908 requires_python: None,
2909 size: sdist.size(),
2910 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
2911 url: FileLocation::AbsoluteUrl(file_url.clone()),
2912 yanked: None,
2913 zstd: None,
2914 });
2915
2916 let index = IndexUrl::from(VerbatimUrl::from_url(
2917 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
2918 ));
2919
2920 let reg_dist = RegistrySourceDist {
2921 name: name.clone(),
2922 version: version.clone(),
2923 file,
2924 ext,
2925 index,
2926 wheels: vec![],
2927 };
2928 uv_distribution_types::SourceDist::Registry(reg_dist)
2929 }
2930 Source::Registry(RegistrySource::Path(path)) => {
2931 let Some(ref sdist) = self.sdist else {
2932 return Ok(None);
2933 };
2934
2935 let name = &self.id.name;
2936 let version = self
2937 .id
2938 .version
2939 .as_ref()
2940 .expect("version for registry source");
2941
2942 let file_url = match sdist {
2943 SourceDist::Url { url: file_url, .. } => {
2944 FileLocation::AbsoluteUrl(file_url.clone())
2945 }
2946 SourceDist::Path {
2947 path: file_path, ..
2948 } => {
2949 let file_path = workspace_root.join(path).join(file_path);
2950 let file_url =
2951 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
2952 LockErrorKind::PathToUrl {
2953 path: file_path.into_boxed_path(),
2954 }
2955 })?;
2956 FileLocation::AbsoluteUrl(UrlString::from(file_url))
2957 }
2958 SourceDist::Metadata { .. } => {
2959 return Err(LockErrorKind::MissingPath {
2960 name: name.clone(),
2961 version: version.clone(),
2962 }
2963 .into());
2964 }
2965 };
2966 let filename = sdist
2967 .filename()
2968 .ok_or_else(|| LockErrorKind::MissingFilename {
2969 id: self.id.clone(),
2970 })?;
2971 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
2972 LockErrorKind::MissingExtension {
2973 id: self.id.clone(),
2974 err,
2975 }
2976 })?;
2977 let file = Box::new(uv_distribution_types::File {
2978 dist_info_metadata: false,
2979 filename: SmallString::from(filename),
2980 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
2981 HashDigests::from(hash.0.clone())
2982 }),
2983 requires_python: None,
2984 size: sdist.size(),
2985 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
2986 url: file_url,
2987 yanked: None,
2988 zstd: None,
2989 });
2990
2991 let index = IndexUrl::from(
2992 VerbatimUrl::from_absolute_path(workspace_root.join(path))
2993 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
2994 );
2995
2996 let reg_dist = RegistrySourceDist {
2997 name: name.clone(),
2998 version: version.clone(),
2999 file,
3000 ext,
3001 index,
3002 wheels: vec![],
3003 };
3004 uv_distribution_types::SourceDist::Registry(reg_dist)
3005 }
3006 };
3007
3008 Ok(Some(sdist))
3009 }
3010
3011 fn to_toml(
3012 &self,
3013 requires_python: &RequiresPython,
3014 dist_count_by_name: &FxHashMap<PackageName, u64>,
3015 ) -> Result<Table, toml_edit::ser::Error> {
3016 let mut table = Table::new();
3017
3018 self.id.to_toml(None, &mut table);
3019
3020 if !self.fork_markers.is_empty() {
3021 let fork_markers = each_element_on_its_line_array(
3022 simplified_universal_markers(&self.fork_markers, requires_python).into_iter(),
3023 );
3024 if !fork_markers.is_empty() {
3025 table.insert("resolution-markers", value(fork_markers));
3026 }
3027 }
3028
3029 if !self.dependencies.is_empty() {
3030 let deps = each_element_on_its_line_array(self.dependencies.iter().map(|dep| {
3031 dep.to_toml(requires_python, dist_count_by_name)
3032 .into_inline_table()
3033 }));
3034 table.insert("dependencies", value(deps));
3035 }
3036
3037 if !self.optional_dependencies.is_empty() {
3038 let mut optional_deps = Table::new();
3039 for (extra, deps) in &self.optional_dependencies {
3040 let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3041 dep.to_toml(requires_python, dist_count_by_name)
3042 .into_inline_table()
3043 }));
3044 if !deps.is_empty() {
3045 optional_deps.insert(extra.as_ref(), value(deps));
3046 }
3047 }
3048 if !optional_deps.is_empty() {
3049 table.insert("optional-dependencies", Item::Table(optional_deps));
3050 }
3051 }
3052
3053 if !self.dependency_groups.is_empty() {
3054 let mut dependency_groups = Table::new();
3055 for (extra, deps) in &self.dependency_groups {
3056 let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3057 dep.to_toml(requires_python, dist_count_by_name)
3058 .into_inline_table()
3059 }));
3060 if !deps.is_empty() {
3061 dependency_groups.insert(extra.as_ref(), value(deps));
3062 }
3063 }
3064 if !dependency_groups.is_empty() {
3065 table.insert("dev-dependencies", Item::Table(dependency_groups));
3066 }
3067 }
3068
3069 if let Some(ref sdist) = self.sdist {
3070 table.insert("sdist", value(sdist.to_toml()?));
3071 }
3072
3073 if !self.wheels.is_empty() {
3074 let wheels = each_element_on_its_line_array(
3075 self.wheels
3076 .iter()
3077 .map(Wheel::to_toml)
3078 .collect::<Result<Vec<_>, _>>()?
3079 .into_iter(),
3080 );
3081 table.insert("wheels", value(wheels));
3082 }
3083
3084 {
3086 let mut metadata_table = Table::new();
3087
3088 if !self.metadata.requires_dist.is_empty() {
3089 let requires_dist = self
3090 .metadata
3091 .requires_dist
3092 .iter()
3093 .map(|requirement| {
3094 serde::Serialize::serialize(
3095 &requirement,
3096 toml_edit::ser::ValueSerializer::new(),
3097 )
3098 })
3099 .collect::<Result<Vec<_>, _>>()?;
3100 let requires_dist = match requires_dist.as_slice() {
3101 [] => Array::new(),
3102 [requirement] => Array::from_iter([requirement]),
3103 requires_dist => each_element_on_its_line_array(requires_dist.iter()),
3104 };
3105 metadata_table.insert("requires-dist", value(requires_dist));
3106 }
3107
3108 if !self.metadata.dependency_groups.is_empty() {
3109 let mut dependency_groups = Table::new();
3110 for (extra, deps) in &self.metadata.dependency_groups {
3111 let deps = deps
3112 .iter()
3113 .map(|requirement| {
3114 serde::Serialize::serialize(
3115 &requirement,
3116 toml_edit::ser::ValueSerializer::new(),
3117 )
3118 })
3119 .collect::<Result<Vec<_>, _>>()?;
3120 let deps = match deps.as_slice() {
3121 [] => Array::new(),
3122 [requirement] => Array::from_iter([requirement]),
3123 deps => each_element_on_its_line_array(deps.iter()),
3124 };
3125 dependency_groups.insert(extra.as_ref(), value(deps));
3126 }
3127 if !dependency_groups.is_empty() {
3128 metadata_table.insert("requires-dev", Item::Table(dependency_groups));
3129 }
3130 }
3131
3132 if !self.metadata.provides_extra.is_empty() {
3133 let provides_extras = self
3134 .metadata
3135 .provides_extra
3136 .iter()
3137 .map(|extra| {
3138 serde::Serialize::serialize(&extra, toml_edit::ser::ValueSerializer::new())
3139 })
3140 .collect::<Result<Vec<_>, _>>()?;
3141 let provides_extras = Array::from_iter(provides_extras);
3143 metadata_table.insert("provides-extras", value(provides_extras));
3144 }
3145
3146 if !metadata_table.is_empty() {
3147 table.insert("metadata", Item::Table(metadata_table));
3148 }
3149 }
3150
3151 Ok(table)
3152 }
3153
3154 fn find_best_wheel(&self, tag_policy: TagPolicy<'_>) -> Option<usize> {
3155 type WheelPriority<'lock> = (TagPriority, Option<&'lock BuildTag>);
3156
3157 let mut best: Option<(WheelPriority, usize)> = None;
3158 for (i, wheel) in self.wheels.iter().enumerate() {
3159 let TagCompatibility::Compatible(tag_priority) =
3160 wheel.filename.compatibility(tag_policy.tags())
3161 else {
3162 continue;
3163 };
3164 let build_tag = wheel.filename.build_tag();
3165 let wheel_priority = (tag_priority, build_tag);
3166 match best {
3167 None => {
3168 best = Some((wheel_priority, i));
3169 }
3170 Some((best_priority, _)) => {
3171 if wheel_priority > best_priority {
3172 best = Some((wheel_priority, i));
3173 }
3174 }
3175 }
3176 }
3177
3178 let best = best.map(|(_, i)| i);
3179 match tag_policy {
3180 TagPolicy::Required(_) => best,
3181 TagPolicy::Preferred(_) => best.or_else(|| self.wheels.first().map(|_| 0)),
3182 }
3183 }
3184
3185 pub fn name(&self) -> &PackageName {
3187 &self.id.name
3188 }
3189
3190 pub fn version(&self) -> Option<&Version> {
3192 self.id.version.as_ref()
3193 }
3194
3195 pub fn git_sha(&self) -> Option<&GitOid> {
3197 match &self.id.source {
3198 Source::Git(_, git) => Some(&git.precise),
3199 _ => None,
3200 }
3201 }
3202
3203 pub fn fork_markers(&self) -> &[UniversalMarker] {
3205 self.fork_markers.as_slice()
3206 }
3207
3208 pub fn index(&self, root: &Path) -> Result<Option<IndexUrl>, LockError> {
3210 match &self.id.source {
3211 Source::Registry(RegistrySource::Url(url)) => {
3212 let index = IndexUrl::from(VerbatimUrl::from_url(
3213 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
3214 ));
3215 Ok(Some(index))
3216 }
3217 Source::Registry(RegistrySource::Path(path)) => {
3218 let index = IndexUrl::from(
3219 VerbatimUrl::from_absolute_path(root.join(path))
3220 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
3221 );
3222 Ok(Some(index))
3223 }
3224 _ => Ok(None),
3225 }
3226 }
3227
3228 fn hashes(&self) -> HashDigests {
3230 let mut hashes = Vec::with_capacity(
3231 usize::from(self.sdist.as_ref().and_then(|sdist| sdist.hash()).is_some())
3232 + self
3233 .wheels
3234 .iter()
3235 .map(|wheel| usize::from(wheel.hash.is_some()))
3236 .sum::<usize>(),
3237 );
3238 if let Some(ref sdist) = self.sdist {
3239 if let Some(hash) = sdist.hash() {
3240 hashes.push(hash.0.clone());
3241 }
3242 }
3243 for wheel in &self.wheels {
3244 hashes.extend(wheel.hash.as_ref().map(|h| h.0.clone()));
3245 if let Some(zstd) = wheel.zstd.as_ref() {
3246 hashes.extend(zstd.hash.as_ref().map(|h| h.0.clone()));
3247 }
3248 }
3249 HashDigests::from(hashes)
3250 }
3251
3252 pub fn as_git_ref(&self) -> Result<Option<ResolvedRepositoryReference>, LockError> {
3254 match &self.id.source {
3255 Source::Git(url, git) => Ok(Some(ResolvedRepositoryReference {
3256 reference: RepositoryReference {
3257 url: RepositoryUrl::new(&url.to_url().map_err(LockErrorKind::InvalidUrl)?),
3258 reference: GitReference::from(git.kind.clone()),
3259 },
3260 sha: git.precise,
3261 })),
3262 _ => Ok(None),
3263 }
3264 }
3265
3266 fn is_dynamic(&self) -> bool {
3268 self.id.version.is_none()
3269 }
3270
3271 pub fn provides_extras(&self) -> &[ExtraName] {
3273 &self.metadata.provides_extra
3274 }
3275
3276 pub fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
3278 &self.metadata.dependency_groups
3279 }
3280
3281 pub fn dependencies(&self) -> &[Dependency] {
3283 &self.dependencies
3284 }
3285
3286 pub fn optional_dependencies(&self) -> &BTreeMap<ExtraName, Vec<Dependency>> {
3288 &self.optional_dependencies
3289 }
3290
3291 pub fn resolved_dependency_groups(&self) -> &BTreeMap<GroupName, Vec<Dependency>> {
3293 &self.dependency_groups
3294 }
3295
3296 pub fn as_install_target(&self) -> InstallTarget<'_> {
3298 InstallTarget {
3299 name: self.name(),
3300 is_local: self.id.source.is_local(),
3301 }
3302 }
3303}
3304
3305fn verbatim_url(path: &Path, id: &PackageId) -> Result<VerbatimUrl, LockError> {
3307 let url =
3308 VerbatimUrl::from_normalized_path(path).map_err(|err| LockErrorKind::VerbatimUrl {
3309 id: id.clone(),
3310 err,
3311 })?;
3312 Ok(url)
3313}
3314
3315fn absolute_path(workspace_root: &Path, path: &Path) -> Result<PathBuf, LockError> {
3317 let path = uv_fs::normalize_absolute_path(&workspace_root.join(path))
3318 .map_err(LockErrorKind::AbsolutePath)?;
3319 Ok(path)
3320}
3321
3322#[derive(Clone, Debug, serde::Deserialize)]
3323#[serde(rename_all = "kebab-case")]
3324struct PackageWire {
3325 #[serde(flatten)]
3326 id: PackageId,
3327 #[serde(default)]
3328 metadata: PackageMetadata,
3329 #[serde(default)]
3330 sdist: Option<SourceDist>,
3331 #[serde(default)]
3332 wheels: Vec<Wheel>,
3333 #[serde(default, rename = "resolution-markers")]
3334 fork_markers: Vec<SimplifiedMarkerTree>,
3335 #[serde(default)]
3336 dependencies: Vec<DependencyWire>,
3337 #[serde(default)]
3338 optional_dependencies: BTreeMap<ExtraName, Vec<DependencyWire>>,
3339 #[serde(default, rename = "dev-dependencies", alias = "dependency-groups")]
3340 dependency_groups: BTreeMap<GroupName, Vec<DependencyWire>>,
3341}
3342
3343#[derive(Clone, Default, Debug, Eq, PartialEq, serde::Deserialize)]
3344#[serde(rename_all = "kebab-case")]
3345struct PackageMetadata {
3346 #[serde(default)]
3347 requires_dist: BTreeSet<Requirement>,
3348 #[serde(default, rename = "provides-extras")]
3349 provides_extra: Box<[ExtraName]>,
3350 #[serde(default, rename = "requires-dev", alias = "dependency-groups")]
3351 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
3352}
3353
3354impl PackageWire {
3355 fn unwire(
3356 self,
3357 requires_python: &RequiresPython,
3358 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3359 ) -> Result<Package, LockError> {
3360 if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) {
3362 if let Some(version) = &self.id.version {
3363 for wheel in &self.wheels {
3364 if *version != wheel.filename.version
3365 && *version != wheel.filename.version.clone().without_local()
3366 {
3367 return Err(LockError::from(LockErrorKind::InconsistentVersions {
3368 name: self.id.name,
3369 version: version.clone(),
3370 wheel: wheel.clone(),
3371 }));
3372 }
3373 }
3374 }
3377 }
3378
3379 let unwire_deps = |deps: Vec<DependencyWire>| -> Result<Vec<Dependency>, LockError> {
3380 deps.into_iter()
3381 .map(|dep| dep.unwire(requires_python, unambiguous_package_ids))
3382 .collect()
3383 };
3384
3385 Ok(Package {
3386 id: self.id,
3387 metadata: self.metadata,
3388 sdist: self.sdist,
3389 wheels: self.wheels,
3390 fork_markers: self
3391 .fork_markers
3392 .into_iter()
3393 .map(|simplified_marker| simplified_marker.into_marker(requires_python))
3394 .map(UniversalMarker::from_combined)
3395 .collect(),
3396 dependencies: unwire_deps(self.dependencies)?,
3397 optional_dependencies: self
3398 .optional_dependencies
3399 .into_iter()
3400 .map(|(extra, deps)| Ok((extra, unwire_deps(deps)?)))
3401 .collect::<Result<_, LockError>>()?,
3402 dependency_groups: self
3403 .dependency_groups
3404 .into_iter()
3405 .map(|(group, deps)| Ok((group, unwire_deps(deps)?)))
3406 .collect::<Result<_, LockError>>()?,
3407 })
3408 }
3409}
3410
3411#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3414#[serde(rename_all = "kebab-case")]
3415pub(crate) struct PackageId {
3416 pub(crate) name: PackageName,
3417 pub(crate) version: Option<Version>,
3418 source: Source,
3419}
3420
3421impl PackageId {
3422 fn from_annotated_dist(annotated_dist: &AnnotatedDist, root: &Path) -> Result<Self, LockError> {
3423 let source = Source::from_resolved_dist(&annotated_dist.dist, root)?;
3425 let version = if source.is_source_tree()
3427 && annotated_dist
3428 .metadata
3429 .as_ref()
3430 .is_some_and(|metadata| metadata.dynamic)
3431 {
3432 None
3433 } else {
3434 Some(annotated_dist.version.clone())
3435 };
3436 let name = annotated_dist.name.clone();
3437 Ok(Self {
3438 name,
3439 version,
3440 source,
3441 })
3442 }
3443
3444 fn to_toml(&self, dist_count_by_name: Option<&FxHashMap<PackageName, u64>>, table: &mut Table) {
3451 let count = dist_count_by_name.and_then(|map| map.get(&self.name).copied());
3452 table.insert("name", value(self.name.to_string()));
3453 if count.map(|count| count > 1).unwrap_or(true) {
3454 if let Some(version) = &self.version {
3455 table.insert("version", value(version.to_string()));
3456 }
3457 self.source.to_toml(table);
3458 }
3459 }
3460}
3461
3462impl Display for PackageId {
3463 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3464 if let Some(version) = &self.version {
3465 write!(f, "{}=={} @ {}", self.name, version, self.source)
3466 } else {
3467 write!(f, "{} @ {}", self.name, self.source)
3468 }
3469 }
3470}
3471
3472#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3473#[serde(rename_all = "kebab-case")]
3474struct PackageIdForDependency {
3475 name: PackageName,
3476 version: Option<Version>,
3477 source: Option<Source>,
3478}
3479
3480impl PackageIdForDependency {
3481 fn unwire(
3482 self,
3483 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3484 ) -> Result<PackageId, LockError> {
3485 let unambiguous_package_id = unambiguous_package_ids.get(&self.name);
3486 let source = self.source.map(Ok::<_, LockError>).unwrap_or_else(|| {
3487 let Some(package_id) = unambiguous_package_id else {
3488 return Err(LockErrorKind::MissingDependencySource {
3489 name: self.name.clone(),
3490 }
3491 .into());
3492 };
3493 Ok(package_id.source.clone())
3494 })?;
3495 let version = if let Some(version) = self.version {
3496 Some(version)
3497 } else {
3498 if let Some(package_id) = unambiguous_package_id {
3499 package_id.version.clone()
3500 } else {
3501 if source.is_source_tree() {
3504 None
3505 } else {
3506 return Err(LockErrorKind::MissingDependencyVersion {
3507 name: self.name.clone(),
3508 }
3509 .into());
3510 }
3511 }
3512 };
3513 Ok(PackageId {
3514 name: self.name,
3515 version,
3516 source,
3517 })
3518 }
3519}
3520
3521impl From<PackageId> for PackageIdForDependency {
3522 fn from(id: PackageId) -> Self {
3523 Self {
3524 name: id.name,
3525 version: id.version,
3526 source: Some(id.source),
3527 }
3528 }
3529}
3530
3531#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3539#[serde(try_from = "SourceWire")]
3540enum Source {
3541 Registry(RegistrySource),
3543 Git(UrlString, GitSource),
3545 Direct(UrlString, DirectSource),
3547 Path(Box<Path>),
3549 Directory(Box<Path>),
3551 Editable(Box<Path>),
3553 Virtual(Box<Path>),
3555}
3556
3557impl Source {
3558 fn from_resolved_dist(resolved_dist: &ResolvedDist, root: &Path) -> Result<Self, LockError> {
3559 match *resolved_dist {
3560 ResolvedDist::Installed { .. } => unreachable!(),
3562 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(dist, root),
3563 }
3564 }
3565
3566 fn from_dist(dist: &Dist, root: &Path) -> Result<Self, LockError> {
3567 match *dist {
3568 Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, root),
3569 Dist::Source(ref source_dist) => Self::from_source_dist(source_dist, root),
3570 }
3571 }
3572
3573 fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result<Self, LockError> {
3574 match *built_dist {
3575 BuiltDist::Registry(ref reg_dist) => Self::from_registry_built_dist(reg_dist, root),
3576 BuiltDist::DirectUrl(ref direct_dist) => Ok(Self::from_direct_built_dist(direct_dist)),
3577 BuiltDist::Path(ref path_dist) => Self::from_path_built_dist(path_dist, root),
3578 }
3579 }
3580
3581 fn from_source_dist(
3582 source_dist: &uv_distribution_types::SourceDist,
3583 root: &Path,
3584 ) -> Result<Self, LockError> {
3585 match *source_dist {
3586 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
3587 Self::from_registry_source_dist(reg_dist, root)
3588 }
3589 uv_distribution_types::SourceDist::DirectUrl(ref direct_dist) => {
3590 Ok(Self::from_direct_source_dist(direct_dist))
3591 }
3592 uv_distribution_types::SourceDist::Git(ref git_dist) => {
3593 Ok(Self::from_git_dist(git_dist))
3594 }
3595 uv_distribution_types::SourceDist::Path(ref path_dist) => {
3596 Self::from_path_source_dist(path_dist, root)
3597 }
3598 uv_distribution_types::SourceDist::Directory(ref directory) => {
3599 Self::from_directory_source_dist(directory, root)
3600 }
3601 }
3602 }
3603
3604 fn from_registry_built_dist(
3605 reg_dist: &RegistryBuiltDist,
3606 root: &Path,
3607 ) -> Result<Self, LockError> {
3608 Self::from_index_url(®_dist.best_wheel().index, root)
3609 }
3610
3611 fn from_registry_source_dist(
3612 reg_dist: &RegistrySourceDist,
3613 root: &Path,
3614 ) -> Result<Self, LockError> {
3615 Self::from_index_url(®_dist.index, root)
3616 }
3617
3618 fn from_direct_built_dist(direct_dist: &DirectUrlBuiltDist) -> Self {
3619 Self::Direct(
3620 normalize_url(direct_dist.url.to_url()),
3621 DirectSource { subdirectory: None },
3622 )
3623 }
3624
3625 fn from_direct_source_dist(direct_dist: &DirectUrlSourceDist) -> Self {
3626 Self::Direct(
3627 normalize_url(direct_dist.url.to_url()),
3628 DirectSource {
3629 subdirectory: direct_dist.subdirectory.clone(),
3630 },
3631 )
3632 }
3633
3634 fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result<Self, LockError> {
3635 let path = relative_to(&path_dist.install_path, root)
3636 .or_else(|_| std::path::absolute(&path_dist.install_path))
3637 .map_err(LockErrorKind::DistributionRelativePath)?;
3638 Ok(Self::Path(path.into_boxed_path()))
3639 }
3640
3641 fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Self, LockError> {
3642 let path = relative_to(&path_dist.install_path, root)
3643 .or_else(|_| std::path::absolute(&path_dist.install_path))
3644 .map_err(LockErrorKind::DistributionRelativePath)?;
3645 Ok(Self::Path(path.into_boxed_path()))
3646 }
3647
3648 fn from_directory_source_dist(
3649 directory_dist: &DirectorySourceDist,
3650 root: &Path,
3651 ) -> Result<Self, LockError> {
3652 let path = relative_to(&directory_dist.install_path, root)
3653 .or_else(|_| std::path::absolute(&directory_dist.install_path))
3654 .map_err(LockErrorKind::DistributionRelativePath)?;
3655 if directory_dist.editable.unwrap_or(false) {
3656 Ok(Self::Editable(path.into_boxed_path()))
3657 } else if directory_dist.r#virtual.unwrap_or(false) {
3658 Ok(Self::Virtual(path.into_boxed_path()))
3659 } else {
3660 Ok(Self::Directory(path.into_boxed_path()))
3661 }
3662 }
3663
3664 fn from_index_url(index_url: &IndexUrl, root: &Path) -> Result<Self, LockError> {
3665 match index_url {
3666 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
3667 let redacted = index_url.without_credentials();
3669 let source = RegistrySource::Url(UrlString::from(redacted.as_ref()));
3670 Ok(Self::Registry(source))
3671 }
3672 IndexUrl::Path(url) => {
3673 let path = url
3674 .to_file_path()
3675 .map_err(|()| LockErrorKind::UrlToPath { url: url.to_url() })?;
3676 let path = relative_to(&path, root)
3677 .or_else(|_| std::path::absolute(&path))
3678 .map_err(LockErrorKind::IndexRelativePath)?;
3679 let source = RegistrySource::Path(path.into_boxed_path());
3680 Ok(Self::Registry(source))
3681 }
3682 }
3683 }
3684
3685 fn from_git_dist(git_dist: &GitSourceDist) -> Self {
3686 Self::Git(
3687 UrlString::from(locked_git_url(git_dist)),
3688 GitSource {
3689 kind: GitSourceKind::from(git_dist.git.reference().clone()),
3690 precise: git_dist.git.precise().unwrap_or_else(|| {
3691 panic!("Git distribution is missing a precise hash: {git_dist}")
3692 }),
3693 subdirectory: git_dist.subdirectory.clone(),
3694 lfs: git_dist.git.lfs(),
3695 },
3696 )
3697 }
3698
3699 fn is_immutable(&self) -> bool {
3706 matches!(self, Self::Registry(..) | Self::Git(_, _))
3707 }
3708
3709 fn is_wheel(&self) -> bool {
3711 match self {
3712 Self::Path(path) => {
3713 matches!(
3714 DistExtension::from_path(path).ok(),
3715 Some(DistExtension::Wheel)
3716 )
3717 }
3718 Self::Direct(url, _) => {
3719 matches!(
3720 DistExtension::from_path(url.as_ref()).ok(),
3721 Some(DistExtension::Wheel)
3722 )
3723 }
3724 Self::Directory(..) => false,
3725 Self::Editable(..) => false,
3726 Self::Virtual(..) => false,
3727 Self::Git(..) => false,
3728 Self::Registry(..) => false,
3729 }
3730 }
3731
3732 fn is_source_tree(&self) -> bool {
3734 match self {
3735 Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => true,
3736 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => false,
3737 }
3738 }
3739
3740 fn as_source_tree(&self) -> Option<&Path> {
3742 match self {
3743 Self::Directory(path) | Self::Editable(path) | Self::Virtual(path) => Some(path),
3744 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => None,
3745 }
3746 }
3747
3748 fn to_toml(&self, table: &mut Table) {
3749 let mut source_table = InlineTable::new();
3750 match self {
3751 Self::Registry(source) => match source {
3752 RegistrySource::Url(url) => {
3753 source_table.insert("registry", Value::from(url.as_ref()));
3754 }
3755 RegistrySource::Path(path) => {
3756 source_table.insert(
3757 "registry",
3758 Value::from(PortablePath::from(path).to_string()),
3759 );
3760 }
3761 },
3762 Self::Git(url, _) => {
3763 source_table.insert("git", Value::from(url.as_ref()));
3764 }
3765 Self::Direct(url, DirectSource { subdirectory }) => {
3766 source_table.insert("url", Value::from(url.as_ref()));
3767 if let Some(ref subdirectory) = *subdirectory {
3768 source_table.insert(
3769 "subdirectory",
3770 Value::from(PortablePath::from(subdirectory).to_string()),
3771 );
3772 }
3773 }
3774 Self::Path(path) => {
3775 source_table.insert("path", Value::from(PortablePath::from(path).to_string()));
3776 }
3777 Self::Directory(path) => {
3778 source_table.insert(
3779 "directory",
3780 Value::from(PortablePath::from(path).to_string()),
3781 );
3782 }
3783 Self::Editable(path) => {
3784 source_table.insert(
3785 "editable",
3786 Value::from(PortablePath::from(path).to_string()),
3787 );
3788 }
3789 Self::Virtual(path) => {
3790 source_table.insert("virtual", Value::from(PortablePath::from(path).to_string()));
3791 }
3792 }
3793 table.insert("source", value(source_table));
3794 }
3795
3796 pub(crate) fn is_local(&self) -> bool {
3798 matches!(
3799 self,
3800 Self::Path(_) | Self::Directory(_) | Self::Editable(_) | Self::Virtual(_)
3801 )
3802 }
3803}
3804
3805impl Display for Source {
3806 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3807 match self {
3808 Self::Registry(RegistrySource::Url(url)) | Self::Git(url, _) | Self::Direct(url, _) => {
3809 write!(f, "{}+{}", self.name(), url)
3810 }
3811 Self::Registry(RegistrySource::Path(path))
3812 | Self::Path(path)
3813 | Self::Directory(path)
3814 | Self::Editable(path)
3815 | Self::Virtual(path) => {
3816 write!(f, "{}+{}", self.name(), PortablePath::from(path))
3817 }
3818 }
3819 }
3820}
3821
3822impl Source {
3823 fn name(&self) -> &str {
3824 match self {
3825 Self::Registry(..) => "registry",
3826 Self::Git(..) => "git",
3827 Self::Direct(..) => "direct",
3828 Self::Path(..) => "path",
3829 Self::Directory(..) => "directory",
3830 Self::Editable(..) => "editable",
3831 Self::Virtual(..) => "virtual",
3832 }
3833 }
3834
3835 fn requires_hash(&self) -> Option<bool> {
3843 match self {
3844 Self::Registry(..) => None,
3845 Self::Direct(..) | Self::Path(..) => Some(true),
3846 Self::Git(..) | Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => {
3847 Some(false)
3848 }
3849 }
3850 }
3851}
3852
3853#[derive(Clone, Debug, serde::Deserialize)]
3854#[serde(untagged, rename_all = "kebab-case")]
3855enum SourceWire {
3856 Registry {
3857 registry: RegistrySourceWire,
3858 },
3859 Git {
3860 git: String,
3861 },
3862 Direct {
3863 url: UrlString,
3864 subdirectory: Option<PortablePathBuf>,
3865 },
3866 Path {
3867 path: PortablePathBuf,
3868 },
3869 Directory {
3870 directory: PortablePathBuf,
3871 },
3872 Editable {
3873 editable: PortablePathBuf,
3874 },
3875 Virtual {
3876 r#virtual: PortablePathBuf,
3877 },
3878}
3879
3880impl TryFrom<SourceWire> for Source {
3881 type Error = LockError;
3882
3883 fn try_from(wire: SourceWire) -> Result<Self, LockError> {
3884 #[allow(clippy::enum_glob_use)]
3885 use self::SourceWire::*;
3886
3887 match wire {
3888 Registry { registry } => Ok(Self::Registry(registry.into())),
3889 Git { git } => {
3890 let url = DisplaySafeUrl::parse(&git)
3891 .map_err(|err| SourceParseError::InvalidUrl {
3892 given: git.clone(),
3893 err,
3894 })
3895 .map_err(LockErrorKind::InvalidGitSourceUrl)?;
3896
3897 let git_source = GitSource::from_url(&url)
3898 .map_err(|err| match err {
3899 GitSourceError::InvalidSha => SourceParseError::InvalidSha { given: git },
3900 GitSourceError::MissingSha => SourceParseError::MissingSha { given: git },
3901 })
3902 .map_err(LockErrorKind::InvalidGitSourceUrl)?;
3903
3904 Ok(Self::Git(UrlString::from(url), git_source))
3905 }
3906 Direct { url, subdirectory } => Ok(Self::Direct(
3907 url,
3908 DirectSource {
3909 subdirectory: subdirectory.map(Box::<std::path::Path>::from),
3910 },
3911 )),
3912 Path { path } => Ok(Self::Path(path.into())),
3913 Directory { directory } => Ok(Self::Directory(directory.into())),
3914 Editable { editable } => Ok(Self::Editable(editable.into())),
3915 Virtual { r#virtual } => Ok(Self::Virtual(r#virtual.into())),
3916 }
3917 }
3918}
3919
3920#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
3922enum RegistrySource {
3923 Url(UrlString),
3925 Path(Box<Path>),
3927}
3928
3929impl Display for RegistrySource {
3930 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3931 match self {
3932 Self::Url(url) => write!(f, "{url}"),
3933 Self::Path(path) => write!(f, "{}", path.display()),
3934 }
3935 }
3936}
3937
3938#[derive(Clone, Debug)]
3939enum RegistrySourceWire {
3940 Url(UrlString),
3942 Path(PortablePathBuf),
3944}
3945
3946impl<'de> serde::de::Deserialize<'de> for RegistrySourceWire {
3947 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3948 where
3949 D: serde::de::Deserializer<'de>,
3950 {
3951 struct Visitor;
3952
3953 impl serde::de::Visitor<'_> for Visitor {
3954 type Value = RegistrySourceWire;
3955
3956 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
3957 formatter.write_str("a valid URL or a file path")
3958 }
3959
3960 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
3961 where
3962 E: serde::de::Error,
3963 {
3964 if split_scheme(value).is_some() {
3965 Ok(
3966 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
3967 value,
3968 ))
3969 .map(RegistrySourceWire::Url)?,
3970 )
3971 } else {
3972 Ok(
3973 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
3974 value,
3975 ))
3976 .map(RegistrySourceWire::Path)?,
3977 )
3978 }
3979 }
3980 }
3981
3982 deserializer.deserialize_str(Visitor)
3983 }
3984}
3985
3986impl From<RegistrySourceWire> for RegistrySource {
3987 fn from(wire: RegistrySourceWire) -> Self {
3988 match wire {
3989 RegistrySourceWire::Url(url) => Self::Url(url),
3990 RegistrySourceWire::Path(path) => Self::Path(path.into()),
3991 }
3992 }
3993}
3994
3995#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3996#[serde(rename_all = "kebab-case")]
3997struct DirectSource {
3998 subdirectory: Option<Box<Path>>,
3999}
4000
4001#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
4006struct GitSource {
4007 precise: GitOid,
4008 subdirectory: Option<Box<Path>>,
4009 kind: GitSourceKind,
4010 lfs: GitLfs,
4011}
4012
4013#[derive(Clone, Debug, Eq, PartialEq)]
4015enum GitSourceError {
4016 InvalidSha,
4017 MissingSha,
4018}
4019
4020impl GitSource {
4021 fn from_url(url: &Url) -> Result<Self, GitSourceError> {
4024 let mut kind = GitSourceKind::DefaultBranch;
4025 let mut subdirectory = None;
4026 let mut lfs = GitLfs::Disabled;
4027 for (key, val) in url.query_pairs() {
4028 match &*key {
4029 "tag" => kind = GitSourceKind::Tag(val.into_owned()),
4030 "branch" => kind = GitSourceKind::Branch(val.into_owned()),
4031 "rev" => kind = GitSourceKind::Rev(val.into_owned()),
4032 "subdirectory" => subdirectory = Some(PortablePathBuf::from(val.as_ref()).into()),
4033 "lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
4034 _ => {}
4035 }
4036 }
4037
4038 let precise = GitOid::from_str(url.fragment().ok_or(GitSourceError::MissingSha)?)
4039 .map_err(|_| GitSourceError::InvalidSha)?;
4040
4041 Ok(Self {
4042 precise,
4043 subdirectory,
4044 kind,
4045 lfs,
4046 })
4047 }
4048}
4049
4050#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4051#[serde(rename_all = "kebab-case")]
4052enum GitSourceKind {
4053 Tag(String),
4054 Branch(String),
4055 Rev(String),
4056 DefaultBranch,
4057}
4058
4059#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4061#[serde(rename_all = "kebab-case")]
4062struct SourceDistMetadata {
4063 hash: Option<Hash>,
4065 size: Option<u64>,
4069 #[serde(alias = "upload_time")]
4071 upload_time: Option<Timestamp>,
4072}
4073
4074#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4079#[serde(from = "SourceDistWire")]
4080enum SourceDist {
4081 Url {
4082 url: UrlString,
4083 #[serde(flatten)]
4084 metadata: SourceDistMetadata,
4085 },
4086 Path {
4087 path: Box<Path>,
4088 #[serde(flatten)]
4089 metadata: SourceDistMetadata,
4090 },
4091 Metadata {
4092 #[serde(flatten)]
4093 metadata: SourceDistMetadata,
4094 },
4095}
4096
4097impl SourceDist {
4098 fn filename(&self) -> Option<Cow<'_, str>> {
4099 match self {
4100 Self::Metadata { .. } => None,
4101 Self::Url { url, .. } => url.filename().ok(),
4102 Self::Path { path, .. } => path.file_name().map(|filename| filename.to_string_lossy()),
4103 }
4104 }
4105
4106 fn url(&self) -> Option<&UrlString> {
4107 match self {
4108 Self::Metadata { .. } => None,
4109 Self::Url { url, .. } => Some(url),
4110 Self::Path { .. } => None,
4111 }
4112 }
4113
4114 pub(crate) fn hash(&self) -> Option<&Hash> {
4115 match self {
4116 Self::Metadata { metadata } => metadata.hash.as_ref(),
4117 Self::Url { metadata, .. } => metadata.hash.as_ref(),
4118 Self::Path { metadata, .. } => metadata.hash.as_ref(),
4119 }
4120 }
4121
4122 pub(crate) fn size(&self) -> Option<u64> {
4123 match self {
4124 Self::Metadata { metadata } => metadata.size,
4125 Self::Url { metadata, .. } => metadata.size,
4126 Self::Path { metadata, .. } => metadata.size,
4127 }
4128 }
4129
4130 pub(crate) fn upload_time(&self) -> Option<Timestamp> {
4131 match self {
4132 Self::Metadata { metadata } => metadata.upload_time,
4133 Self::Url { metadata, .. } => metadata.upload_time,
4134 Self::Path { metadata, .. } => metadata.upload_time,
4135 }
4136 }
4137}
4138
4139impl SourceDist {
4140 fn from_annotated_dist(
4141 id: &PackageId,
4142 annotated_dist: &AnnotatedDist,
4143 ) -> Result<Option<Self>, LockError> {
4144 match annotated_dist.dist {
4145 ResolvedDist::Installed { .. } => unreachable!(),
4147 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4148 id,
4149 dist,
4150 annotated_dist.hashes.as_slice(),
4151 annotated_dist.index(),
4152 ),
4153 }
4154 }
4155
4156 fn from_dist(
4157 id: &PackageId,
4158 dist: &Dist,
4159 hashes: &[HashDigest],
4160 index: Option<&IndexUrl>,
4161 ) -> Result<Option<Self>, LockError> {
4162 match *dist {
4163 Dist::Built(BuiltDist::Registry(ref built_dist)) => {
4164 let Some(sdist) = built_dist.sdist.as_ref() else {
4165 return Ok(None);
4166 };
4167 Self::from_registry_dist(sdist, index)
4168 }
4169 Dist::Built(_) => Ok(None),
4170 Dist::Source(ref source_dist) => Self::from_source_dist(id, source_dist, hashes, index),
4171 }
4172 }
4173
4174 fn from_source_dist(
4175 id: &PackageId,
4176 source_dist: &uv_distribution_types::SourceDist,
4177 hashes: &[HashDigest],
4178 index: Option<&IndexUrl>,
4179 ) -> Result<Option<Self>, LockError> {
4180 match *source_dist {
4181 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
4182 Self::from_registry_dist(reg_dist, index)
4183 }
4184 uv_distribution_types::SourceDist::DirectUrl(_) => {
4185 Self::from_direct_dist(id, hashes).map(Some)
4186 }
4187 uv_distribution_types::SourceDist::Path(_) => {
4188 Self::from_path_dist(id, hashes).map(Some)
4189 }
4190 uv_distribution_types::SourceDist::Git(_)
4194 | uv_distribution_types::SourceDist::Directory(_) => Ok(None),
4195 }
4196 }
4197
4198 fn from_registry_dist(
4199 reg_dist: &RegistrySourceDist,
4200 index: Option<&IndexUrl>,
4201 ) -> Result<Option<Self>, LockError> {
4202 if index.is_none_or(|index| *index != reg_dist.index) {
4205 return Ok(None);
4206 }
4207
4208 match ®_dist.index {
4209 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4210 let url = normalize_file_location(®_dist.file.url)
4211 .map_err(LockErrorKind::InvalidUrl)
4212 .map_err(LockError::from)?;
4213 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4214 let size = reg_dist.file.size;
4215 let upload_time = reg_dist
4216 .file
4217 .upload_time_utc_ms
4218 .map(Timestamp::from_millisecond)
4219 .transpose()
4220 .map_err(LockErrorKind::InvalidTimestamp)?;
4221 Ok(Some(Self::Url {
4222 url,
4223 metadata: SourceDistMetadata {
4224 hash,
4225 size,
4226 upload_time,
4227 },
4228 }))
4229 }
4230 IndexUrl::Path(path) => {
4231 let index_path = path
4232 .to_file_path()
4233 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4234 let url = reg_dist
4235 .file
4236 .url
4237 .to_url()
4238 .map_err(LockErrorKind::InvalidUrl)?;
4239
4240 if url.scheme() == "file" {
4241 let reg_dist_path = url
4242 .to_file_path()
4243 .map_err(|()| LockErrorKind::UrlToPath { url })?;
4244 let path = relative_to(®_dist_path, index_path)
4245 .or_else(|_| std::path::absolute(®_dist_path))
4246 .map_err(LockErrorKind::DistributionRelativePath)?
4247 .into_boxed_path();
4248 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4249 let size = reg_dist.file.size;
4250 let upload_time = reg_dist
4251 .file
4252 .upload_time_utc_ms
4253 .map(Timestamp::from_millisecond)
4254 .transpose()
4255 .map_err(LockErrorKind::InvalidTimestamp)?;
4256 Ok(Some(Self::Path {
4257 path,
4258 metadata: SourceDistMetadata {
4259 hash,
4260 size,
4261 upload_time,
4262 },
4263 }))
4264 } else {
4265 let url = normalize_file_location(®_dist.file.url)
4266 .map_err(LockErrorKind::InvalidUrl)
4267 .map_err(LockError::from)?;
4268 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4269 let size = reg_dist.file.size;
4270 let upload_time = reg_dist
4271 .file
4272 .upload_time_utc_ms
4273 .map(Timestamp::from_millisecond)
4274 .transpose()
4275 .map_err(LockErrorKind::InvalidTimestamp)?;
4276 Ok(Some(Self::Url {
4277 url,
4278 metadata: SourceDistMetadata {
4279 hash,
4280 size,
4281 upload_time,
4282 },
4283 }))
4284 }
4285 }
4286 }
4287 }
4288
4289 fn from_direct_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4290 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4291 let kind = LockErrorKind::Hash {
4292 id: id.clone(),
4293 artifact_type: "direct URL source distribution",
4294 expected: true,
4295 };
4296 return Err(kind.into());
4297 };
4298 Ok(Self::Metadata {
4299 metadata: SourceDistMetadata {
4300 hash: Some(hash),
4301 size: None,
4302 upload_time: None,
4303 },
4304 })
4305 }
4306
4307 fn from_path_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4308 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4309 let kind = LockErrorKind::Hash {
4310 id: id.clone(),
4311 artifact_type: "path source distribution",
4312 expected: true,
4313 };
4314 return Err(kind.into());
4315 };
4316 Ok(Self::Metadata {
4317 metadata: SourceDistMetadata {
4318 hash: Some(hash),
4319 size: None,
4320 upload_time: None,
4321 },
4322 })
4323 }
4324}
4325
4326#[derive(Clone, Debug, serde::Deserialize)]
4327#[serde(untagged, rename_all = "kebab-case")]
4328enum SourceDistWire {
4329 Url {
4330 url: UrlString,
4331 #[serde(flatten)]
4332 metadata: SourceDistMetadata,
4333 },
4334 Path {
4335 path: PortablePathBuf,
4336 #[serde(flatten)]
4337 metadata: SourceDistMetadata,
4338 },
4339 Metadata {
4340 #[serde(flatten)]
4341 metadata: SourceDistMetadata,
4342 },
4343}
4344
4345impl SourceDist {
4346 fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
4348 let mut table = InlineTable::new();
4349 match self {
4350 Self::Metadata { .. } => {}
4351 Self::Url { url, .. } => {
4352 table.insert("url", Value::from(url.as_ref()));
4353 }
4354 Self::Path { path, .. } => {
4355 table.insert("path", Value::from(PortablePath::from(path).to_string()));
4356 }
4357 }
4358 if let Some(hash) = self.hash() {
4359 table.insert("hash", Value::from(hash.to_string()));
4360 }
4361 if let Some(size) = self.size() {
4362 table.insert(
4363 "size",
4364 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4365 );
4366 }
4367 if let Some(upload_time) = self.upload_time() {
4368 table.insert("upload-time", Value::from(upload_time.to_string()));
4369 }
4370 Ok(table)
4371 }
4372}
4373
4374impl From<SourceDistWire> for SourceDist {
4375 fn from(wire: SourceDistWire) -> Self {
4376 match wire {
4377 SourceDistWire::Url { url, metadata } => Self::Url { url, metadata },
4378 SourceDistWire::Path { path, metadata } => Self::Path {
4379 path: path.into(),
4380 metadata,
4381 },
4382 SourceDistWire::Metadata { metadata } => Self::Metadata { metadata },
4383 }
4384 }
4385}
4386
4387impl From<GitReference> for GitSourceKind {
4388 fn from(value: GitReference) -> Self {
4389 match value {
4390 GitReference::Branch(branch) => Self::Branch(branch),
4391 GitReference::Tag(tag) => Self::Tag(tag),
4392 GitReference::BranchOrTag(rev) => Self::Rev(rev),
4393 GitReference::BranchOrTagOrCommit(rev) => Self::Rev(rev),
4394 GitReference::NamedRef(rev) => Self::Rev(rev),
4395 GitReference::DefaultBranch => Self::DefaultBranch,
4396 }
4397 }
4398}
4399
4400impl From<GitSourceKind> for GitReference {
4401 fn from(value: GitSourceKind) -> Self {
4402 match value {
4403 GitSourceKind::Branch(branch) => Self::Branch(branch),
4404 GitSourceKind::Tag(tag) => Self::Tag(tag),
4405 GitSourceKind::Rev(rev) => Self::from_rev(rev),
4406 GitSourceKind::DefaultBranch => Self::DefaultBranch,
4407 }
4408 }
4409}
4410
4411fn locked_git_url(git_dist: &GitSourceDist) -> DisplaySafeUrl {
4413 let mut url = git_dist.git.repository().clone();
4414
4415 url.remove_credentials();
4417
4418 url.set_fragment(None);
4420 url.set_query(None);
4421
4422 if let Some(subdirectory) = git_dist
4424 .subdirectory
4425 .as_deref()
4426 .map(PortablePath::from)
4427 .as_ref()
4428 .map(PortablePath::to_string)
4429 {
4430 url.query_pairs_mut()
4431 .append_pair("subdirectory", &subdirectory);
4432 }
4433
4434 if git_dist.git.lfs().enabled() {
4436 url.query_pairs_mut().append_pair("lfs", "true");
4437 }
4438
4439 match git_dist.git.reference() {
4441 GitReference::Branch(branch) => {
4442 url.query_pairs_mut().append_pair("branch", branch.as_str());
4443 }
4444 GitReference::Tag(tag) => {
4445 url.query_pairs_mut().append_pair("tag", tag.as_str());
4446 }
4447 GitReference::BranchOrTag(rev)
4448 | GitReference::BranchOrTagOrCommit(rev)
4449 | GitReference::NamedRef(rev) => {
4450 url.query_pairs_mut().append_pair("rev", rev.as_str());
4451 }
4452 GitReference::DefaultBranch => {}
4453 }
4454
4455 url.set_fragment(
4457 git_dist
4458 .git
4459 .precise()
4460 .as_ref()
4461 .map(GitOid::to_string)
4462 .as_deref(),
4463 );
4464
4465 url
4466}
4467
4468#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4469struct ZstdWheel {
4470 hash: Option<Hash>,
4471 size: Option<u64>,
4472}
4473
4474#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4476#[serde(try_from = "WheelWire")]
4477struct Wheel {
4478 url: WheelWireSource,
4483 hash: Option<Hash>,
4489 size: Option<u64>,
4493 upload_time: Option<Timestamp>,
4497 filename: WheelFilename,
4504 zstd: Option<ZstdWheel>,
4506}
4507
4508impl Wheel {
4509 fn from_annotated_dist(annotated_dist: &AnnotatedDist) -> Result<Vec<Self>, LockError> {
4510 match annotated_dist.dist {
4511 ResolvedDist::Installed { .. } => unreachable!(),
4513 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4514 dist,
4515 annotated_dist.hashes.as_slice(),
4516 annotated_dist.index(),
4517 ),
4518 }
4519 }
4520
4521 fn from_dist(
4522 dist: &Dist,
4523 hashes: &[HashDigest],
4524 index: Option<&IndexUrl>,
4525 ) -> Result<Vec<Self>, LockError> {
4526 match *dist {
4527 Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, hashes, index),
4528 Dist::Source(uv_distribution_types::SourceDist::Registry(ref source_dist)) => {
4529 source_dist
4530 .wheels
4531 .iter()
4532 .filter(|wheel| {
4533 index.is_some_and(|index| *index == wheel.index)
4536 })
4537 .map(Self::from_registry_wheel)
4538 .collect()
4539 }
4540 Dist::Source(_) => Ok(vec![]),
4541 }
4542 }
4543
4544 fn from_built_dist(
4545 built_dist: &BuiltDist,
4546 hashes: &[HashDigest],
4547 index: Option<&IndexUrl>,
4548 ) -> Result<Vec<Self>, LockError> {
4549 match *built_dist {
4550 BuiltDist::Registry(ref reg_dist) => Self::from_registry_dist(reg_dist, index),
4551 BuiltDist::DirectUrl(ref direct_dist) => {
4552 Ok(vec![Self::from_direct_dist(direct_dist, hashes)])
4553 }
4554 BuiltDist::Path(ref path_dist) => Ok(vec![Self::from_path_dist(path_dist, hashes)]),
4555 }
4556 }
4557
4558 fn from_registry_dist(
4559 reg_dist: &RegistryBuiltDist,
4560 index: Option<&IndexUrl>,
4561 ) -> Result<Vec<Self>, LockError> {
4562 reg_dist
4563 .wheels
4564 .iter()
4565 .filter(|wheel| {
4566 index.is_some_and(|index| *index == wheel.index)
4569 })
4570 .map(Self::from_registry_wheel)
4571 .collect()
4572 }
4573
4574 fn from_registry_wheel(wheel: &RegistryBuiltWheel) -> Result<Self, LockError> {
4575 let url = match &wheel.index {
4576 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4577 let url = normalize_file_location(&wheel.file.url)
4578 .map_err(LockErrorKind::InvalidUrl)
4579 .map_err(LockError::from)?;
4580 WheelWireSource::Url { url }
4581 }
4582 IndexUrl::Path(path) => {
4583 let index_path = path
4584 .to_file_path()
4585 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4586 let wheel_url = wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?;
4587
4588 if wheel_url.scheme() == "file" {
4589 let wheel_path = wheel_url
4590 .to_file_path()
4591 .map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?;
4592 let path = relative_to(&wheel_path, index_path)
4593 .or_else(|_| std::path::absolute(&wheel_path))
4594 .map_err(LockErrorKind::DistributionRelativePath)?
4595 .into_boxed_path();
4596 WheelWireSource::Path { path }
4597 } else {
4598 let url = normalize_file_location(&wheel.file.url)
4599 .map_err(LockErrorKind::InvalidUrl)
4600 .map_err(LockError::from)?;
4601 WheelWireSource::Url { url }
4602 }
4603 }
4604 };
4605 let filename = wheel.filename.clone();
4606 let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from);
4607 let size = wheel.file.size;
4608 let upload_time = wheel
4609 .file
4610 .upload_time_utc_ms
4611 .map(Timestamp::from_millisecond)
4612 .transpose()
4613 .map_err(LockErrorKind::InvalidTimestamp)?;
4614 let zstd = wheel.file.zstd.as_ref().map(|zstd| ZstdWheel {
4615 hash: zstd.hashes.iter().max().cloned().map(Hash::from),
4616 size: zstd.size,
4617 });
4618 Ok(Self {
4619 url,
4620 hash,
4621 size,
4622 upload_time,
4623 filename,
4624 zstd,
4625 })
4626 }
4627
4628 fn from_direct_dist(direct_dist: &DirectUrlBuiltDist, hashes: &[HashDigest]) -> Self {
4629 Self {
4630 url: WheelWireSource::Url {
4631 url: normalize_url(direct_dist.url.to_url()),
4632 },
4633 hash: hashes.iter().max().cloned().map(Hash::from),
4634 size: None,
4635 upload_time: None,
4636 filename: direct_dist.filename.clone(),
4637 zstd: None,
4638 }
4639 }
4640
4641 fn from_path_dist(path_dist: &PathBuiltDist, hashes: &[HashDigest]) -> Self {
4642 Self {
4643 url: WheelWireSource::Filename {
4644 filename: path_dist.filename.clone(),
4645 },
4646 hash: hashes.iter().max().cloned().map(Hash::from),
4647 size: None,
4648 upload_time: None,
4649 filename: path_dist.filename.clone(),
4650 zstd: None,
4651 }
4652 }
4653
4654 pub(crate) fn to_registry_wheel(
4655 &self,
4656 source: &RegistrySource,
4657 root: &Path,
4658 ) -> Result<RegistryBuiltWheel, LockError> {
4659 let filename: WheelFilename = self.filename.clone();
4660
4661 match source {
4662 RegistrySource::Url(url) => {
4663 let file_location = match &self.url {
4664 WheelWireSource::Url { url: file_url } => {
4665 FileLocation::AbsoluteUrl(file_url.clone())
4666 }
4667 WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => {
4668 return Err(LockErrorKind::MissingUrl {
4669 name: filename.name,
4670 version: filename.version,
4671 }
4672 .into());
4673 }
4674 };
4675 let file = Box::new(uv_distribution_types::File {
4676 dist_info_metadata: false,
4677 filename: SmallString::from(filename.to_string()),
4678 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
4679 requires_python: None,
4680 size: self.size,
4681 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
4682 url: file_location,
4683 yanked: None,
4684 zstd: self
4685 .zstd
4686 .as_ref()
4687 .map(|zstd| uv_distribution_types::Zstd {
4688 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
4689 size: zstd.size,
4690 })
4691 .map(Box::new),
4692 });
4693 let index = IndexUrl::from(VerbatimUrl::from_url(
4694 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
4695 ));
4696 Ok(RegistryBuiltWheel {
4697 filename,
4698 file,
4699 index,
4700 })
4701 }
4702 RegistrySource::Path(index_path) => {
4703 let file_location = match &self.url {
4704 WheelWireSource::Url { url: file_url } => {
4705 FileLocation::AbsoluteUrl(file_url.clone())
4706 }
4707 WheelWireSource::Path { path: file_path } => {
4708 let file_path = root.join(index_path).join(file_path);
4709 let file_url =
4710 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
4711 LockErrorKind::PathToUrl {
4712 path: file_path.into_boxed_path(),
4713 }
4714 })?;
4715 FileLocation::AbsoluteUrl(UrlString::from(file_url))
4716 }
4717 WheelWireSource::Filename { .. } => {
4718 return Err(LockErrorKind::MissingPath {
4719 name: filename.name,
4720 version: filename.version,
4721 }
4722 .into());
4723 }
4724 };
4725 let file = Box::new(uv_distribution_types::File {
4726 dist_info_metadata: false,
4727 filename: SmallString::from(filename.to_string()),
4728 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
4729 requires_python: None,
4730 size: self.size,
4731 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
4732 url: file_location,
4733 yanked: None,
4734 zstd: self
4735 .zstd
4736 .as_ref()
4737 .map(|zstd| uv_distribution_types::Zstd {
4738 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
4739 size: zstd.size,
4740 })
4741 .map(Box::new),
4742 });
4743 let index = IndexUrl::from(
4744 VerbatimUrl::from_absolute_path(root.join(index_path))
4745 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
4746 );
4747 Ok(RegistryBuiltWheel {
4748 filename,
4749 file,
4750 index,
4751 })
4752 }
4753 }
4754 }
4755}
4756
4757#[derive(Clone, Debug, serde::Deserialize)]
4758#[serde(rename_all = "kebab-case")]
4759struct WheelWire {
4760 #[serde(flatten)]
4761 url: WheelWireSource,
4762 hash: Option<Hash>,
4768 size: Option<u64>,
4772 #[serde(alias = "upload_time")]
4776 upload_time: Option<Timestamp>,
4777 #[serde(alias = "zstd")]
4779 zstd: Option<ZstdWheel>,
4780}
4781
4782#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4783#[serde(untagged, rename_all = "kebab-case")]
4784enum WheelWireSource {
4785 Url {
4787 url: UrlString,
4792 },
4793 Path {
4795 path: Box<Path>,
4797 },
4798 Filename {
4802 filename: WheelFilename,
4805 },
4806}
4807
4808impl Wheel {
4809 fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
4811 let mut table = InlineTable::new();
4812 match &self.url {
4813 WheelWireSource::Url { url } => {
4814 table.insert("url", Value::from(url.as_ref()));
4815 }
4816 WheelWireSource::Path { path } => {
4817 table.insert("path", Value::from(PortablePath::from(path).to_string()));
4818 }
4819 WheelWireSource::Filename { filename } => {
4820 table.insert("filename", Value::from(filename.to_string()));
4821 }
4822 }
4823 if let Some(ref hash) = self.hash {
4824 table.insert("hash", Value::from(hash.to_string()));
4825 }
4826 if let Some(size) = self.size {
4827 table.insert(
4828 "size",
4829 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4830 );
4831 }
4832 if let Some(upload_time) = self.upload_time {
4833 table.insert("upload-time", Value::from(upload_time.to_string()));
4834 }
4835 if let Some(zstd) = &self.zstd {
4836 let mut inner = InlineTable::new();
4837 if let Some(ref hash) = zstd.hash {
4838 inner.insert("hash", Value::from(hash.to_string()));
4839 }
4840 if let Some(size) = zstd.size {
4841 inner.insert(
4842 "size",
4843 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4844 );
4845 }
4846 table.insert("zstd", Value::from(inner));
4847 }
4848 Ok(table)
4849 }
4850}
4851
4852impl TryFrom<WheelWire> for Wheel {
4853 type Error = String;
4854
4855 fn try_from(wire: WheelWire) -> Result<Self, String> {
4856 let filename = match &wire.url {
4857 WheelWireSource::Url { url } => {
4858 let filename = url.filename().map_err(|err| err.to_string())?;
4859 filename.parse::<WheelFilename>().map_err(|err| {
4860 format!("failed to parse `{filename}` as wheel filename: {err}")
4861 })?
4862 }
4863 WheelWireSource::Path { path } => {
4864 let filename = path
4865 .file_name()
4866 .and_then(|file_name| file_name.to_str())
4867 .ok_or_else(|| {
4868 format!("path `{}` has no filename component", path.display())
4869 })?;
4870 filename.parse::<WheelFilename>().map_err(|err| {
4871 format!("failed to parse `{filename}` as wheel filename: {err}")
4872 })?
4873 }
4874 WheelWireSource::Filename { filename } => filename.clone(),
4875 };
4876
4877 Ok(Self {
4878 url: wire.url,
4879 hash: wire.hash,
4880 size: wire.size,
4881 upload_time: wire.upload_time,
4882 zstd: wire.zstd,
4883 filename,
4884 })
4885 }
4886}
4887
4888#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
4890pub struct Dependency {
4891 package_id: PackageId,
4892 extra: BTreeSet<ExtraName>,
4893 simplified_marker: SimplifiedMarkerTree,
4913 complexified_marker: UniversalMarker,
4917}
4918
4919impl Dependency {
4920 fn new(
4921 requires_python: &RequiresPython,
4922 package_id: PackageId,
4923 extra: BTreeSet<ExtraName>,
4924 complexified_marker: UniversalMarker,
4925 ) -> Self {
4926 let simplified_marker =
4927 SimplifiedMarkerTree::new(requires_python, complexified_marker.combined());
4928 let complexified_marker = simplified_marker.into_marker(requires_python);
4929 Self {
4930 package_id,
4931 extra,
4932 simplified_marker,
4933 complexified_marker: UniversalMarker::from_combined(complexified_marker),
4934 }
4935 }
4936
4937 fn from_annotated_dist(
4938 requires_python: &RequiresPython,
4939 annotated_dist: &AnnotatedDist,
4940 complexified_marker: UniversalMarker,
4941 root: &Path,
4942 ) -> Result<Self, LockError> {
4943 let package_id = PackageId::from_annotated_dist(annotated_dist, root)?;
4944 let extra = annotated_dist.extra.iter().cloned().collect();
4945 Ok(Self::new(
4946 requires_python,
4947 package_id,
4948 extra,
4949 complexified_marker,
4950 ))
4951 }
4952
4953 fn to_toml(
4955 &self,
4956 _requires_python: &RequiresPython,
4957 dist_count_by_name: &FxHashMap<PackageName, u64>,
4958 ) -> Table {
4959 let mut table = Table::new();
4960 self.package_id
4961 .to_toml(Some(dist_count_by_name), &mut table);
4962 if !self.extra.is_empty() {
4963 let extra_array = self
4964 .extra
4965 .iter()
4966 .map(ToString::to_string)
4967 .collect::<Array>();
4968 table.insert("extra", value(extra_array));
4969 }
4970 if let Some(marker) = self.simplified_marker.try_to_string() {
4971 table.insert("marker", value(marker));
4972 }
4973
4974 table
4975 }
4976
4977 pub fn package_name(&self) -> &PackageName {
4979 &self.package_id.name
4980 }
4981
4982 pub fn extra(&self) -> &BTreeSet<ExtraName> {
4984 &self.extra
4985 }
4986}
4987
4988impl Display for Dependency {
4989 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
4990 match (self.extra.is_empty(), self.package_id.version.as_ref()) {
4991 (true, Some(version)) => write!(f, "{}=={}", self.package_id.name, version),
4992 (true, None) => write!(f, "{}", self.package_id.name),
4993 (false, Some(version)) => write!(
4994 f,
4995 "{}[{}]=={}",
4996 self.package_id.name,
4997 self.extra.iter().join(","),
4998 version
4999 ),
5000 (false, None) => write!(
5001 f,
5002 "{}[{}]",
5003 self.package_id.name,
5004 self.extra.iter().join(",")
5005 ),
5006 }
5007 }
5008}
5009
5010#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)]
5012#[serde(rename_all = "kebab-case")]
5013struct DependencyWire {
5014 #[serde(flatten)]
5015 package_id: PackageIdForDependency,
5016 #[serde(default)]
5017 extra: BTreeSet<ExtraName>,
5018 #[serde(default)]
5019 marker: SimplifiedMarkerTree,
5020}
5021
5022impl DependencyWire {
5023 fn unwire(
5024 self,
5025 requires_python: &RequiresPython,
5026 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
5027 ) -> Result<Dependency, LockError> {
5028 let complexified_marker = self.marker.into_marker(requires_python);
5029 Ok(Dependency {
5030 package_id: self.package_id.unwire(unambiguous_package_ids)?,
5031 extra: self.extra,
5032 simplified_marker: self.marker,
5033 complexified_marker: UniversalMarker::from_combined(complexified_marker),
5034 })
5035 }
5036}
5037
5038#[derive(Clone, Debug, PartialEq, Eq)]
5043struct Hash(HashDigest);
5044
5045impl From<HashDigest> for Hash {
5046 fn from(hd: HashDigest) -> Self {
5047 Self(hd)
5048 }
5049}
5050
5051impl FromStr for Hash {
5052 type Err = HashParseError;
5053
5054 fn from_str(s: &str) -> Result<Self, HashParseError> {
5055 let (algorithm, digest) = s.split_once(':').ok_or(HashParseError(
5056 "expected '{algorithm}:{digest}', but found no ':' in hash digest",
5057 ))?;
5058 let algorithm = algorithm
5059 .parse()
5060 .map_err(|_| HashParseError("unrecognized hash algorithm"))?;
5061 Ok(Self(HashDigest {
5062 algorithm,
5063 digest: digest.into(),
5064 }))
5065 }
5066}
5067
5068impl Display for Hash {
5069 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5070 write!(f, "{}:{}", self.0.algorithm, self.0.digest)
5071 }
5072}
5073
5074impl<'de> serde::Deserialize<'de> for Hash {
5075 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5076 where
5077 D: serde::de::Deserializer<'de>,
5078 {
5079 struct Visitor;
5080
5081 impl serde::de::Visitor<'_> for Visitor {
5082 type Value = Hash;
5083
5084 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
5085 f.write_str("a string")
5086 }
5087
5088 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
5089 Hash::from_str(v).map_err(serde::de::Error::custom)
5090 }
5091 }
5092
5093 deserializer.deserialize_str(Visitor)
5094 }
5095}
5096
5097impl From<Hash> for Hashes {
5098 fn from(value: Hash) -> Self {
5099 match value.0.algorithm {
5100 HashAlgorithm::Md5 => Self {
5101 md5: Some(value.0.digest),
5102 sha256: None,
5103 sha384: None,
5104 sha512: None,
5105 blake2b: None,
5106 },
5107 HashAlgorithm::Sha256 => Self {
5108 md5: None,
5109 sha256: Some(value.0.digest),
5110 sha384: None,
5111 sha512: None,
5112 blake2b: None,
5113 },
5114 HashAlgorithm::Sha384 => Self {
5115 md5: None,
5116 sha256: None,
5117 sha384: Some(value.0.digest),
5118 sha512: None,
5119 blake2b: None,
5120 },
5121 HashAlgorithm::Sha512 => Self {
5122 md5: None,
5123 sha256: None,
5124 sha384: None,
5125 sha512: Some(value.0.digest),
5126 blake2b: None,
5127 },
5128 HashAlgorithm::Blake2b => Self {
5129 md5: None,
5130 sha256: None,
5131 sha384: None,
5132 sha512: None,
5133 blake2b: Some(value.0.digest),
5134 },
5135 }
5136 }
5137}
5138
5139fn normalize_file_location(location: &FileLocation) -> Result<UrlString, ToUrlError> {
5141 match location {
5142 FileLocation::AbsoluteUrl(absolute) => Ok(absolute.without_fragment().into_owned()),
5143 FileLocation::RelativeUrl(_, _) => Ok(normalize_url(location.to_url()?)),
5144 }
5145}
5146
5147fn normalize_url(mut url: DisplaySafeUrl) -> UrlString {
5149 url.set_fragment(None);
5150 UrlString::from(url)
5151}
5152
5153fn normalize_requirement(
5163 mut requirement: Requirement,
5164 root: &Path,
5165 requires_python: &RequiresPython,
5166) -> Result<Requirement, LockError> {
5167 requirement.extras.sort();
5169 requirement.groups.sort();
5170
5171 match requirement.source {
5173 RequirementSource::Git {
5174 git,
5175 subdirectory,
5176 url: _,
5177 } => {
5178 let git = {
5180 let mut repository = git.repository().clone();
5181
5182 repository.remove_credentials();
5184
5185 repository.set_fragment(None);
5187 repository.set_query(None);
5188
5189 GitUrl::from_fields(
5190 repository,
5191 git.reference().clone(),
5192 git.precise(),
5193 git.lfs(),
5194 )?
5195 };
5196
5197 let url = DisplaySafeUrl::from(ParsedGitUrl {
5199 url: git.clone(),
5200 subdirectory: subdirectory.clone(),
5201 });
5202
5203 Ok(Requirement {
5204 name: requirement.name,
5205 extras: requirement.extras,
5206 groups: requirement.groups,
5207 marker: requires_python.simplify_markers(requirement.marker),
5208 source: RequirementSource::Git {
5209 git,
5210 subdirectory,
5211 url: VerbatimUrl::from_url(url),
5212 },
5213 origin: None,
5214 })
5215 }
5216 RequirementSource::Path {
5217 install_path,
5218 ext,
5219 url: _,
5220 } => {
5221 let install_path =
5222 uv_fs::normalize_path_buf(root.join(&install_path)).into_boxed_path();
5223 let url = VerbatimUrl::from_normalized_path(&install_path)
5224 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5225
5226 Ok(Requirement {
5227 name: requirement.name,
5228 extras: requirement.extras,
5229 groups: requirement.groups,
5230 marker: requires_python.simplify_markers(requirement.marker),
5231 source: RequirementSource::Path {
5232 install_path,
5233 ext,
5234 url,
5235 },
5236 origin: None,
5237 })
5238 }
5239 RequirementSource::Directory {
5240 install_path,
5241 editable,
5242 r#virtual,
5243 url: _,
5244 } => {
5245 let install_path =
5246 uv_fs::normalize_path_buf(root.join(&install_path)).into_boxed_path();
5247 let url = VerbatimUrl::from_normalized_path(&install_path)
5248 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5249
5250 Ok(Requirement {
5251 name: requirement.name,
5252 extras: requirement.extras,
5253 groups: requirement.groups,
5254 marker: requires_python.simplify_markers(requirement.marker),
5255 source: RequirementSource::Directory {
5256 install_path,
5257 editable: Some(editable.unwrap_or(false)),
5258 r#virtual: Some(r#virtual.unwrap_or(false)),
5259 url,
5260 },
5261 origin: None,
5262 })
5263 }
5264 RequirementSource::Registry {
5265 specifier,
5266 index,
5267 conflict,
5268 } => {
5269 let index = index
5271 .map(|index| index.url.into_url())
5272 .map(|mut index| {
5273 index.remove_credentials();
5274 index
5275 })
5276 .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index))));
5277 Ok(Requirement {
5278 name: requirement.name,
5279 extras: requirement.extras,
5280 groups: requirement.groups,
5281 marker: requires_python.simplify_markers(requirement.marker),
5282 source: RequirementSource::Registry {
5283 specifier,
5284 index,
5285 conflict,
5286 },
5287 origin: None,
5288 })
5289 }
5290 RequirementSource::Url {
5291 mut location,
5292 subdirectory,
5293 ext,
5294 url: _,
5295 } => {
5296 location.remove_credentials();
5298
5299 location.set_fragment(None);
5301
5302 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
5304 url: location.clone(),
5305 subdirectory: subdirectory.clone(),
5306 ext,
5307 });
5308
5309 Ok(Requirement {
5310 name: requirement.name,
5311 extras: requirement.extras,
5312 groups: requirement.groups,
5313 marker: requires_python.simplify_markers(requirement.marker),
5314 source: RequirementSource::Url {
5315 location,
5316 subdirectory,
5317 ext,
5318 url: VerbatimUrl::from_url(url),
5319 },
5320 origin: None,
5321 })
5322 }
5323 }
5324}
5325
5326#[derive(Debug)]
5327pub struct LockError {
5328 kind: Box<LockErrorKind>,
5329 hint: Option<WheelTagHint>,
5330}
5331
5332impl std::error::Error for LockError {
5333 fn source(&self) -> Option<&(dyn Error + 'static)> {
5334 self.kind.source()
5335 }
5336}
5337
5338impl std::fmt::Display for LockError {
5339 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5340 write!(f, "{}", self.kind)?;
5341 if let Some(hint) = &self.hint {
5342 write!(f, "\n\n{hint}")?;
5343 }
5344 Ok(())
5345 }
5346}
5347
5348impl LockError {
5349 pub fn is_resolution(&self) -> bool {
5351 matches!(&*self.kind, LockErrorKind::Resolution { .. })
5352 }
5353}
5354
5355impl<E> From<E> for LockError
5356where
5357 LockErrorKind: From<E>,
5358{
5359 fn from(err: E) -> Self {
5360 Self {
5361 kind: Box::new(LockErrorKind::from(err)),
5362 hint: None,
5363 }
5364 }
5365}
5366
5367#[derive(Debug, Clone, PartialEq, Eq)]
5368#[allow(clippy::enum_variant_names)]
5369enum WheelTagHint {
5370 LanguageTags {
5373 package: PackageName,
5374 version: Option<Version>,
5375 tags: BTreeSet<LanguageTag>,
5376 best: Option<LanguageTag>,
5377 },
5378 AbiTags {
5381 package: PackageName,
5382 version: Option<Version>,
5383 tags: BTreeSet<AbiTag>,
5384 best: Option<AbiTag>,
5385 },
5386 PlatformTags {
5389 package: PackageName,
5390 version: Option<Version>,
5391 tags: BTreeSet<PlatformTag>,
5392 best: Option<PlatformTag>,
5393 markers: MarkerEnvironment,
5394 },
5395}
5396
5397impl WheelTagHint {
5398 fn from_wheels(
5400 name: &PackageName,
5401 version: Option<&Version>,
5402 filenames: &[&WheelFilename],
5403 tags: &Tags,
5404 markers: &MarkerEnvironment,
5405 ) -> Option<Self> {
5406 let incompatibility = filenames
5407 .iter()
5408 .map(|filename| {
5409 tags.compatibility(
5410 filename.python_tags(),
5411 filename.abi_tags(),
5412 filename.platform_tags(),
5413 )
5414 })
5415 .max()?;
5416 match incompatibility {
5417 TagCompatibility::Incompatible(IncompatibleTag::Python) => {
5418 let best = tags.python_tag();
5419 let tags = Self::python_tags(filenames.iter().copied()).collect::<BTreeSet<_>>();
5420 if tags.is_empty() {
5421 None
5422 } else {
5423 Some(Self::LanguageTags {
5424 package: name.clone(),
5425 version: version.cloned(),
5426 tags,
5427 best,
5428 })
5429 }
5430 }
5431 TagCompatibility::Incompatible(IncompatibleTag::Abi) => {
5432 let best = tags.abi_tag();
5433 let tags = Self::abi_tags(filenames.iter().copied())
5434 .filter(|tag| *tag != AbiTag::None)
5443 .collect::<BTreeSet<_>>();
5444 if tags.is_empty() {
5445 None
5446 } else {
5447 Some(Self::AbiTags {
5448 package: name.clone(),
5449 version: version.cloned(),
5450 tags,
5451 best,
5452 })
5453 }
5454 }
5455 TagCompatibility::Incompatible(IncompatibleTag::Platform) => {
5456 let best = tags.platform_tag().cloned();
5457 let incompatible_tags = Self::platform_tags(filenames.iter().copied(), tags)
5458 .cloned()
5459 .collect::<BTreeSet<_>>();
5460 if incompatible_tags.is_empty() {
5461 None
5462 } else {
5463 Some(Self::PlatformTags {
5464 package: name.clone(),
5465 version: version.cloned(),
5466 tags: incompatible_tags,
5467 best,
5468 markers: markers.clone(),
5469 })
5470 }
5471 }
5472 _ => None,
5473 }
5474 }
5475
5476 fn python_tags<'a>(
5478 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5479 ) -> impl Iterator<Item = LanguageTag> + 'a {
5480 filenames.flat_map(WheelFilename::python_tags).copied()
5481 }
5482
5483 fn abi_tags<'a>(
5485 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5486 ) -> impl Iterator<Item = AbiTag> + 'a {
5487 filenames.flat_map(WheelFilename::abi_tags).copied()
5488 }
5489
5490 fn platform_tags<'a>(
5493 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5494 tags: &'a Tags,
5495 ) -> impl Iterator<Item = &'a PlatformTag> + 'a {
5496 filenames.flat_map(move |filename| {
5497 if filename.python_tags().iter().any(|wheel_py| {
5498 filename
5499 .abi_tags()
5500 .iter()
5501 .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
5502 }) {
5503 filename.platform_tags().iter()
5504 } else {
5505 [].iter()
5506 }
5507 })
5508 }
5509
5510 fn suggest_environment_marker(markers: &MarkerEnvironment) -> String {
5511 let sys_platform = markers.sys_platform();
5512 let platform_machine = markers.platform_machine();
5513
5514 if platform_machine.is_empty() {
5516 format!("sys_platform == '{sys_platform}'")
5517 } else {
5518 format!("sys_platform == '{sys_platform}' and platform_machine == '{platform_machine}'")
5519 }
5520 }
5521}
5522
5523impl std::fmt::Display for WheelTagHint {
5524 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5525 match self {
5526 Self::LanguageTags {
5527 package,
5528 version,
5529 tags,
5530 best,
5531 } => {
5532 if let Some(best) = best {
5533 let s = if tags.len() == 1 { "" } else { "s" };
5534 let best = if let Some(pretty) = best.pretty() {
5535 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5536 } else {
5537 format!("{}", best.cyan())
5538 };
5539 if let Some(version) = version {
5540 write!(
5541 f,
5542 "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python implementation tag{s}: {}",
5543 "hint".bold().cyan(),
5544 ":".bold(),
5545 best,
5546 package.cyan(),
5547 format!("v{version}").cyan(),
5548 tags.iter()
5549 .map(|tag| format!("`{}`", tag.cyan()))
5550 .join(", "),
5551 )
5552 } else {
5553 write!(
5554 f,
5555 "{}{} You're using {}, but `{}` only has wheels with the following Python implementation tag{s}: {}",
5556 "hint".bold().cyan(),
5557 ":".bold(),
5558 best,
5559 package.cyan(),
5560 tags.iter()
5561 .map(|tag| format!("`{}`", tag.cyan()))
5562 .join(", "),
5563 )
5564 }
5565 } else {
5566 let s = if tags.len() == 1 { "" } else { "s" };
5567 if let Some(version) = version {
5568 write!(
5569 f,
5570 "{}{} Wheels are available for `{}` ({}) with the following Python implementation tag{s}: {}",
5571 "hint".bold().cyan(),
5572 ":".bold(),
5573 package.cyan(),
5574 format!("v{version}").cyan(),
5575 tags.iter()
5576 .map(|tag| format!("`{}`", tag.cyan()))
5577 .join(", "),
5578 )
5579 } else {
5580 write!(
5581 f,
5582 "{}{} Wheels are available for `{}` with the following Python implementation tag{s}: {}",
5583 "hint".bold().cyan(),
5584 ":".bold(),
5585 package.cyan(),
5586 tags.iter()
5587 .map(|tag| format!("`{}`", tag.cyan()))
5588 .join(", "),
5589 )
5590 }
5591 }
5592 }
5593 Self::AbiTags {
5594 package,
5595 version,
5596 tags,
5597 best,
5598 } => {
5599 if let Some(best) = best {
5600 let s = if tags.len() == 1 { "" } else { "s" };
5601 let best = if let Some(pretty) = best.pretty() {
5602 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5603 } else {
5604 format!("{}", best.cyan())
5605 };
5606 if let Some(version) = version {
5607 write!(
5608 f,
5609 "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python ABI tag{s}: {}",
5610 "hint".bold().cyan(),
5611 ":".bold(),
5612 best,
5613 package.cyan(),
5614 format!("v{version}").cyan(),
5615 tags.iter()
5616 .map(|tag| format!("`{}`", tag.cyan()))
5617 .join(", "),
5618 )
5619 } else {
5620 write!(
5621 f,
5622 "{}{} You're using {}, but `{}` only has wheels with the following Python ABI tag{s}: {}",
5623 "hint".bold().cyan(),
5624 ":".bold(),
5625 best,
5626 package.cyan(),
5627 tags.iter()
5628 .map(|tag| format!("`{}`", tag.cyan()))
5629 .join(", "),
5630 )
5631 }
5632 } else {
5633 let s = if tags.len() == 1 { "" } else { "s" };
5634 if let Some(version) = version {
5635 write!(
5636 f,
5637 "{}{} Wheels are available for `{}` ({}) with the following Python ABI tag{s}: {}",
5638 "hint".bold().cyan(),
5639 ":".bold(),
5640 package.cyan(),
5641 format!("v{version}").cyan(),
5642 tags.iter()
5643 .map(|tag| format!("`{}`", tag.cyan()))
5644 .join(", "),
5645 )
5646 } else {
5647 write!(
5648 f,
5649 "{}{} Wheels are available for `{}` with the following Python ABI tag{s}: {}",
5650 "hint".bold().cyan(),
5651 ":".bold(),
5652 package.cyan(),
5653 tags.iter()
5654 .map(|tag| format!("`{}`", tag.cyan()))
5655 .join(", "),
5656 )
5657 }
5658 }
5659 }
5660 Self::PlatformTags {
5661 package,
5662 version,
5663 tags,
5664 best,
5665 markers,
5666 } => {
5667 let s = if tags.len() == 1 { "" } else { "s" };
5668 if let Some(best) = best {
5669 let example_marker = Self::suggest_environment_marker(markers);
5670 let best = if let Some(pretty) = best.pretty() {
5671 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5672 } else {
5673 format!("`{}`", best.cyan())
5674 };
5675 let package_ref = if let Some(version) = version {
5676 format!("`{}` ({})", package.cyan(), format!("v{version}").cyan())
5677 } else {
5678 format!("`{}`", package.cyan())
5679 };
5680 write!(
5681 f,
5682 "{}{} 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",
5683 "hint".bold().cyan(),
5684 ":".bold(),
5685 best,
5686 package_ref,
5687 tags.iter()
5688 .map(|tag| format!("`{}`", tag.cyan()))
5689 .join(", "),
5690 format!("\"{example_marker}\"").cyan(),
5691 "tool.uv.required-environments".green()
5692 )
5693 } else {
5694 if let Some(version) = version {
5695 write!(
5696 f,
5697 "{}{} Wheels are available for `{}` ({}) on the following platform{s}: {}",
5698 "hint".bold().cyan(),
5699 ":".bold(),
5700 package.cyan(),
5701 format!("v{version}").cyan(),
5702 tags.iter()
5703 .map(|tag| format!("`{}`", tag.cyan()))
5704 .join(", "),
5705 )
5706 } else {
5707 write!(
5708 f,
5709 "{}{} Wheels are available for `{}` on the following platform{s}: {}",
5710 "hint".bold().cyan(),
5711 ":".bold(),
5712 package.cyan(),
5713 tags.iter()
5714 .map(|tag| format!("`{}`", tag.cyan()))
5715 .join(", "),
5716 )
5717 }
5718 }
5719 }
5720 }
5721 }
5722}
5723
5724#[derive(Debug, thiserror::Error)]
5731enum LockErrorKind {
5732 #[error("Found duplicate package `{id}`", id = id.cyan())]
5735 DuplicatePackage {
5736 id: PackageId,
5738 },
5739 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = id.cyan(), dependency = dependency.cyan())]
5742 DuplicateDependency {
5743 id: PackageId,
5746 dependency: Dependency,
5748 },
5749 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}[{extra}]").cyan(), dependency = dependency.cyan())]
5753 DuplicateOptionalDependency {
5754 id: PackageId,
5757 extra: ExtraName,
5759 dependency: Dependency,
5761 },
5762 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}:{group}").cyan(), dependency = dependency.cyan())]
5766 DuplicateDevDependency {
5767 id: PackageId,
5770 group: GroupName,
5772 dependency: Dependency,
5774 },
5775 #[error(transparent)]
5778 InvalidUrl(
5779 #[from]
5782 ToUrlError,
5783 ),
5784 #[error("Failed to parse file extension for `{id}`; expected one of: {err}", id = id.cyan())]
5787 MissingExtension {
5788 id: PackageId,
5790 err: ExtensionError,
5792 },
5793 #[error("Failed to parse Git URL")]
5795 InvalidGitSourceUrl(
5796 #[source]
5799 SourceParseError,
5800 ),
5801 #[error("Failed to parse timestamp")]
5802 InvalidTimestamp(
5803 #[source]
5806 jiff::Error,
5807 ),
5808 #[error("For package `{id}`, found dependency `{dependency}` with no locked package", id = id.cyan(), dependency = dependency.cyan())]
5812 UnrecognizedDependency {
5813 id: PackageId,
5815 dependency: Dependency,
5818 },
5819 #[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" })]
5822 Hash {
5823 id: PackageId,
5825 artifact_type: &'static str,
5828 expected: bool,
5830 },
5831 #[error("Found package `{id}` with extra `{extra}` but no base package", id = id.cyan(), extra = extra.cyan())]
5834 MissingExtraBase {
5835 id: PackageId,
5837 extra: ExtraName,
5839 },
5840 #[error("Found package `{id}` with development dependency group `{group}` but no base package", id = id.cyan())]
5844 MissingDevBase {
5845 id: PackageId,
5847 group: GroupName,
5849 },
5850 #[error("Wheels cannot come from {source_type} sources")]
5853 InvalidWheelSource {
5854 id: PackageId,
5856 source_type: &'static str,
5858 },
5859 #[error("Found registry distribution `{name}` ({version}) without a valid URL", name = name.cyan(), version = format!("v{version}").cyan())]
5862 MissingUrl {
5863 name: PackageName,
5865 version: Version,
5867 },
5868 #[error("Found registry distribution `{name}` ({version}) without a valid path", name = name.cyan(), version = format!("v{version}").cyan())]
5871 MissingPath {
5872 name: PackageName,
5874 version: Version,
5876 },
5877 #[error("Found registry distribution `{id}` without a valid filename", id = id.cyan())]
5880 MissingFilename {
5881 id: PackageId,
5883 },
5884 #[error("Distribution `{id}` can't be installed because it doesn't have a source distribution or wheel for the current platform", id = id.cyan())]
5887 NeitherSourceDistNorWheel {
5888 id: PackageId,
5890 },
5891 #[error("Distribution `{id}` can't be installed because it is marked as both `--no-binary` and `--no-build`", id = id.cyan())]
5893 NoBinaryNoBuild {
5894 id: PackageId,
5896 },
5897 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but has no source distribution", id = id.cyan())]
5900 NoBinary {
5901 id: PackageId,
5903 },
5904 #[error("Distribution `{id}` can't be installed because it is marked as `--no-build` but has no binary distribution", id = id.cyan())]
5907 NoBuild {
5908 id: PackageId,
5910 },
5911 #[error("Distribution `{id}` can't be installed because the binary distribution is incompatible with the current platform", id = id.cyan())]
5914 IncompatibleWheelOnly {
5915 id: PackageId,
5917 },
5918 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but is itself a binary distribution", id = id.cyan())]
5920 NoBinaryWheelOnly {
5921 id: PackageId,
5923 },
5924 #[error("Found dependency `{id}` with no locked distribution", id = id.cyan())]
5926 VerbatimUrl {
5927 id: PackageId,
5929 #[source]
5931 err: VerbatimUrlError,
5932 },
5933 #[error("Could not compute relative path between workspace and distribution")]
5935 DistributionRelativePath(
5936 #[source]
5938 io::Error,
5939 ),
5940 #[error("Could not compute relative path between workspace and index")]
5942 IndexRelativePath(
5943 #[source]
5945 io::Error,
5946 ),
5947 #[error("Could not compute absolute path from workspace root and lockfile path")]
5949 AbsolutePath(
5950 #[source]
5952 io::Error,
5953 ),
5954 #[error("Dependency `{name}` has missing `version` field but has more than one matching package", name = name.cyan())]
5957 MissingDependencyVersion {
5958 name: PackageName,
5960 },
5961 #[error("Dependency `{name}` has missing `source` field but has more than one matching package", name = name.cyan())]
5964 MissingDependencySource {
5965 name: PackageName,
5967 },
5968 #[error("Could not compute relative path between workspace and requirement")]
5970 RequirementRelativePath(
5971 #[source]
5973 io::Error,
5974 ),
5975 #[error("Could not convert between URL and path")]
5977 RequirementVerbatimUrl(
5978 #[source]
5980 VerbatimUrlError,
5981 ),
5982 #[error("Could not convert between URL and path")]
5984 RegistryVerbatimUrl(
5985 #[source]
5987 VerbatimUrlError,
5988 ),
5989 #[error("Failed to convert path to URL: {path}", path = path.display().cyan())]
5991 PathToUrl { path: Box<Path> },
5992 #[error("Failed to convert URL to path: {url}", url = url.cyan())]
5994 UrlToPath { url: DisplaySafeUrl },
5995 #[error("Found multiple packages matching `{name}`", name = name.cyan())]
5998 MultipleRootPackages {
5999 name: PackageName,
6001 },
6002 #[error("Could not find root package `{name}`", name = name.cyan())]
6004 MissingRootPackage {
6005 name: PackageName,
6007 },
6008 #[error("Failed to generate package metadata for `{id}`", id = id.cyan())]
6010 Resolution {
6011 id: PackageId,
6013 #[source]
6015 err: uv_distribution::Error,
6016 },
6017 #[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())]
6020 InconsistentVersions {
6021 name: PackageName,
6023 version: Version,
6025 wheel: Wheel,
6027 },
6028 #[error(
6029 "Found conflicting extras `{package1}[{extra1}]` \
6030 and `{package2}[{extra2}]` enabled simultaneously"
6031 )]
6032 ConflictingExtra {
6033 package1: PackageName,
6034 extra1: ExtraName,
6035 package2: PackageName,
6036 extra2: ExtraName,
6037 },
6038 #[error(transparent)]
6039 GitUrlParse(#[from] GitUrlParseError),
6040 #[error("Failed to read `{path}`")]
6041 UnreadablePyprojectToml {
6042 path: PathBuf,
6043 #[source]
6044 err: std::io::Error,
6045 },
6046 #[error("Failed to parse `{path}`")]
6047 InvalidPyprojectToml {
6048 path: PathBuf,
6049 #[source]
6050 err: toml::de::Error,
6051 },
6052 #[error("Workspace member `{id}` has non-local source", id = id.cyan())]
6054 NonLocalWorkspaceMember {
6055 id: PackageId,
6057 },
6058}
6059
6060#[derive(Debug, thiserror::Error)]
6062enum SourceParseError {
6063 #[error("Invalid URL in source `{given}`")]
6065 InvalidUrl {
6066 given: String,
6068 #[source]
6070 err: DisplaySafeUrlError,
6071 },
6072 #[error("Missing SHA in source `{given}`")]
6074 MissingSha {
6075 given: String,
6077 },
6078 #[error("Invalid SHA in source `{given}`")]
6080 InvalidSha {
6081 given: String,
6083 },
6084}
6085
6086#[derive(Clone, Debug, Eq, PartialEq)]
6088struct HashParseError(&'static str);
6089
6090impl std::error::Error for HashParseError {}
6091
6092impl Display for HashParseError {
6093 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
6094 Display::fmt(self.0, f)
6095 }
6096}
6097
6098fn each_element_on_its_line_array(elements: impl Iterator<Item = impl Into<Value>>) -> Array {
6109 let mut array = elements
6110 .map(|item| {
6111 let mut value = item.into();
6112 value.decor_mut().set_prefix("\n ");
6114 value
6115 })
6116 .collect::<Array>();
6117 array.set_trailing_comma(true);
6120 array.set_trailing("\n");
6122 array
6123}
6124
6125fn simplified_universal_markers(
6130 markers: &[UniversalMarker],
6131 requires_python: &RequiresPython,
6132) -> Vec<String> {
6133 let mut pep508_only = vec![];
6134 let mut seen = FxHashSet::default();
6135 for marker in markers {
6136 let simplified =
6137 SimplifiedMarkerTree::new(requires_python, marker.pep508()).as_simplified_marker_tree();
6138 if seen.insert(simplified) {
6139 pep508_only.push(simplified);
6140 }
6141 }
6142 let any_overlap = pep508_only
6143 .iter()
6144 .tuple_combinations()
6145 .any(|(&marker1, &marker2)| !marker1.is_disjoint(marker2));
6146 let markers = if !any_overlap {
6147 pep508_only
6148 } else {
6149 markers
6150 .iter()
6151 .map(|marker| {
6152 SimplifiedMarkerTree::new(requires_python, marker.combined())
6153 .as_simplified_marker_tree()
6154 })
6155 .collect()
6156 };
6157 markers
6158 .into_iter()
6159 .filter_map(MarkerTree::try_to_string)
6160 .collect()
6161}
6162
6163#[cfg(test)]
6164mod tests {
6165 use uv_warnings::anstream;
6166
6167 use super::*;
6168
6169 macro_rules! assert_stripped_snapshot {
6171 ($expr:expr, @$snapshot:literal) => {{
6172 let expr = format!("{}", $expr);
6173 let expr = format!("{}", anstream::adapter::strip_str(&expr));
6174 insta::assert_snapshot!(expr, @$snapshot);
6175 }};
6176 }
6177
6178 #[test]
6179 fn missing_dependency_source_unambiguous() {
6180 let data = r#"
6181version = 1
6182requires-python = ">=3.12"
6183
6184[[package]]
6185name = "a"
6186version = "0.1.0"
6187source = { registry = "https://pypi.org/simple" }
6188sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6189
6190[[package]]
6191name = "b"
6192version = "0.1.0"
6193source = { registry = "https://pypi.org/simple" }
6194sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6195
6196[[package.dependencies]]
6197name = "a"
6198version = "0.1.0"
6199"#;
6200 let result: Result<Lock, _> = toml::from_str(data);
6201 insta::assert_debug_snapshot!(result);
6202 }
6203
6204 #[test]
6205 fn missing_dependency_version_unambiguous() {
6206 let data = r#"
6207version = 1
6208requires-python = ">=3.12"
6209
6210[[package]]
6211name = "a"
6212version = "0.1.0"
6213source = { registry = "https://pypi.org/simple" }
6214sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6215
6216[[package]]
6217name = "b"
6218version = "0.1.0"
6219source = { registry = "https://pypi.org/simple" }
6220sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6221
6222[[package.dependencies]]
6223name = "a"
6224source = { registry = "https://pypi.org/simple" }
6225"#;
6226 let result: Result<Lock, _> = toml::from_str(data);
6227 insta::assert_debug_snapshot!(result);
6228 }
6229
6230 #[test]
6231 fn missing_dependency_source_version_unambiguous() {
6232 let data = r#"
6233version = 1
6234requires-python = ">=3.12"
6235
6236[[package]]
6237name = "a"
6238version = "0.1.0"
6239source = { registry = "https://pypi.org/simple" }
6240sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6241
6242[[package]]
6243name = "b"
6244version = "0.1.0"
6245source = { registry = "https://pypi.org/simple" }
6246sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6247
6248[[package.dependencies]]
6249name = "a"
6250"#;
6251 let result: Result<Lock, _> = toml::from_str(data);
6252 insta::assert_debug_snapshot!(result);
6253 }
6254
6255 #[test]
6256 fn missing_dependency_source_ambiguous() {
6257 let data = r#"
6258version = 1
6259requires-python = ">=3.12"
6260
6261[[package]]
6262name = "a"
6263version = "0.1.0"
6264source = { registry = "https://pypi.org/simple" }
6265sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6266
6267[[package]]
6268name = "a"
6269version = "0.1.1"
6270source = { registry = "https://pypi.org/simple" }
6271sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6272
6273[[package]]
6274name = "b"
6275version = "0.1.0"
6276source = { registry = "https://pypi.org/simple" }
6277sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6278
6279[[package.dependencies]]
6280name = "a"
6281version = "0.1.0"
6282"#;
6283 let result = toml::from_str::<Lock>(data).unwrap_err();
6284 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6285 }
6286
6287 #[test]
6288 fn missing_dependency_version_ambiguous() {
6289 let data = r#"
6290version = 1
6291requires-python = ">=3.12"
6292
6293[[package]]
6294name = "a"
6295version = "0.1.0"
6296source = { registry = "https://pypi.org/simple" }
6297sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6298
6299[[package]]
6300name = "a"
6301version = "0.1.1"
6302source = { registry = "https://pypi.org/simple" }
6303sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6304
6305[[package]]
6306name = "b"
6307version = "0.1.0"
6308source = { registry = "https://pypi.org/simple" }
6309sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6310
6311[[package.dependencies]]
6312name = "a"
6313source = { registry = "https://pypi.org/simple" }
6314"#;
6315 let result = toml::from_str::<Lock>(data).unwrap_err();
6316 assert_stripped_snapshot!(result, @"Dependency `a` has missing `version` field but has more than one matching package");
6317 }
6318
6319 #[test]
6320 fn missing_dependency_source_version_ambiguous() {
6321 let data = r#"
6322version = 1
6323requires-python = ">=3.12"
6324
6325[[package]]
6326name = "a"
6327version = "0.1.0"
6328source = { registry = "https://pypi.org/simple" }
6329sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6330
6331[[package]]
6332name = "a"
6333version = "0.1.1"
6334source = { registry = "https://pypi.org/simple" }
6335sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6336
6337[[package]]
6338name = "b"
6339version = "0.1.0"
6340source = { registry = "https://pypi.org/simple" }
6341sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6342
6343[[package.dependencies]]
6344name = "a"
6345"#;
6346 let result = toml::from_str::<Lock>(data).unwrap_err();
6347 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6348 }
6349
6350 #[test]
6351 fn missing_dependency_version_dynamic() {
6352 let data = r#"
6353version = 1
6354requires-python = ">=3.12"
6355
6356[[package]]
6357name = "a"
6358source = { editable = "path/to/a" }
6359
6360[[package]]
6361name = "a"
6362version = "0.1.1"
6363source = { registry = "https://pypi.org/simple" }
6364sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6365
6366[[package]]
6367name = "b"
6368version = "0.1.0"
6369source = { registry = "https://pypi.org/simple" }
6370sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6371
6372[[package.dependencies]]
6373name = "a"
6374source = { editable = "path/to/a" }
6375"#;
6376 let result = toml::from_str::<Lock>(data);
6377 insta::assert_debug_snapshot!(result);
6378 }
6379
6380 #[test]
6381 fn hash_optional_missing() {
6382 let data = r#"
6383version = 1
6384requires-python = ">=3.12"
6385
6386[[package]]
6387name = "anyio"
6388version = "4.3.0"
6389source = { registry = "https://pypi.org/simple" }
6390wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl" }]
6391"#;
6392 let result: Result<Lock, _> = toml::from_str(data);
6393 insta::assert_debug_snapshot!(result);
6394 }
6395
6396 #[test]
6397 fn hash_optional_present() {
6398 let data = r#"
6399version = 1
6400requires-python = ">=3.12"
6401
6402[[package]]
6403name = "anyio"
6404version = "4.3.0"
6405source = { registry = "https://pypi.org/simple" }
6406wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6407"#;
6408 let result: Result<Lock, _> = toml::from_str(data);
6409 insta::assert_debug_snapshot!(result);
6410 }
6411
6412 #[test]
6413 fn hash_required_present() {
6414 let data = r#"
6415version = 1
6416requires-python = ">=3.12"
6417
6418[[package]]
6419name = "anyio"
6420version = "4.3.0"
6421source = { path = "file:///foo/bar" }
6422wheels = [{ url = "file:///foo/bar/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6423"#;
6424 let result: Result<Lock, _> = toml::from_str(data);
6425 insta::assert_debug_snapshot!(result);
6426 }
6427
6428 #[test]
6429 fn source_direct_no_subdir() {
6430 let data = r#"
6431version = 1
6432requires-python = ">=3.12"
6433
6434[[package]]
6435name = "anyio"
6436version = "4.3.0"
6437source = { url = "https://burntsushi.net" }
6438"#;
6439 let result: Result<Lock, _> = toml::from_str(data);
6440 insta::assert_debug_snapshot!(result);
6441 }
6442
6443 #[test]
6444 fn source_direct_has_subdir() {
6445 let data = r#"
6446version = 1
6447requires-python = ">=3.12"
6448
6449[[package]]
6450name = "anyio"
6451version = "4.3.0"
6452source = { url = "https://burntsushi.net", subdirectory = "wat/foo/bar" }
6453"#;
6454 let result: Result<Lock, _> = toml::from_str(data);
6455 insta::assert_debug_snapshot!(result);
6456 }
6457
6458 #[test]
6459 fn source_directory() {
6460 let data = r#"
6461version = 1
6462requires-python = ">=3.12"
6463
6464[[package]]
6465name = "anyio"
6466version = "4.3.0"
6467source = { directory = "path/to/dir" }
6468"#;
6469 let result: Result<Lock, _> = toml::from_str(data);
6470 insta::assert_debug_snapshot!(result);
6471 }
6472
6473 #[test]
6474 fn source_editable() {
6475 let data = r#"
6476version = 1
6477requires-python = ">=3.12"
6478
6479[[package]]
6480name = "anyio"
6481version = "4.3.0"
6482source = { editable = "path/to/dir" }
6483"#;
6484 let result: Result<Lock, _> = toml::from_str(data);
6485 insta::assert_debug_snapshot!(result);
6486 }
6487}