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 = {
2605 let wheel = &self.wheels[best_wheel_index];
2606 HashDigests::from(
2607 wheel
2608 .hash
2609 .iter()
2610 .chain(wheel.zstd.iter().flat_map(|z| z.hash.iter()))
2611 .map(|h| h.0.clone())
2612 .collect::<Vec<_>>(),
2613 )
2614 };
2615
2616 let dist = match &self.id.source {
2617 Source::Registry(source) => {
2618 let wheels = self
2619 .wheels
2620 .iter()
2621 .map(|wheel| wheel.to_registry_wheel(source, workspace_root))
2622 .collect::<Result<_, LockError>>()?;
2623 let reg_built_dist = RegistryBuiltDist {
2624 wheels,
2625 best_wheel_index,
2626 sdist: None,
2627 };
2628 Dist::Built(BuiltDist::Registry(reg_built_dist))
2629 }
2630 Source::Path(path) => {
2631 let filename: WheelFilename =
2632 self.wheels[best_wheel_index].filename.clone();
2633 let install_path = absolute_path(workspace_root, path)?;
2634 let path_dist = PathBuiltDist {
2635 filename,
2636 url: verbatim_url(&install_path, &self.id)?,
2637 install_path: absolute_path(workspace_root, path)?.into_boxed_path(),
2638 };
2639 let built_dist = BuiltDist::Path(path_dist);
2640 Dist::Built(built_dist)
2641 }
2642 Source::Direct(url, direct) => {
2643 let filename: WheelFilename =
2644 self.wheels[best_wheel_index].filename.clone();
2645 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
2646 url: url.to_url().map_err(LockErrorKind::InvalidUrl)?,
2647 subdirectory: direct.subdirectory.clone(),
2648 ext: DistExtension::Wheel,
2649 });
2650 let direct_dist = DirectUrlBuiltDist {
2651 filename,
2652 location: Box::new(url.clone()),
2653 url: VerbatimUrl::from_url(url),
2654 };
2655 let built_dist = BuiltDist::DirectUrl(direct_dist);
2656 Dist::Built(built_dist)
2657 }
2658 Source::Git(_, _) => {
2659 return Err(LockErrorKind::InvalidWheelSource {
2660 id: self.id.clone(),
2661 source_type: "Git",
2662 }
2663 .into());
2664 }
2665 Source::Directory(_) => {
2666 return Err(LockErrorKind::InvalidWheelSource {
2667 id: self.id.clone(),
2668 source_type: "directory",
2669 }
2670 .into());
2671 }
2672 Source::Editable(_) => {
2673 return Err(LockErrorKind::InvalidWheelSource {
2674 id: self.id.clone(),
2675 source_type: "editable",
2676 }
2677 .into());
2678 }
2679 Source::Virtual(_) => {
2680 return Err(LockErrorKind::InvalidWheelSource {
2681 id: self.id.clone(),
2682 source_type: "virtual",
2683 }
2684 .into());
2685 }
2686 };
2687
2688 return Ok(HashedDist { dist, hashes });
2689 }
2690 }
2691
2692 if let Some(sdist) = self.to_source_dist(workspace_root)? {
2693 if !no_build || sdist.is_virtual() {
2697 let hashes = self
2698 .sdist
2699 .as_ref()
2700 .and_then(|s| s.hash())
2701 .map(|hash| HashDigests::from(vec![hash.0.clone()]))
2702 .unwrap_or_else(|| HashDigests::from(vec![]));
2703 return Ok(HashedDist {
2704 dist: Dist::Source(sdist),
2705 hashes,
2706 });
2707 }
2708 }
2709
2710 match (no_binary, no_build) {
2711 (true, true) => Err(LockErrorKind::NoBinaryNoBuild {
2712 id: self.id.clone(),
2713 }
2714 .into()),
2715 (true, false) if self.id.source.is_wheel() => Err(LockErrorKind::NoBinaryWheelOnly {
2716 id: self.id.clone(),
2717 }
2718 .into()),
2719 (true, false) => Err(LockErrorKind::NoBinary {
2720 id: self.id.clone(),
2721 }
2722 .into()),
2723 (false, true) => Err(LockErrorKind::NoBuild {
2724 id: self.id.clone(),
2725 }
2726 .into()),
2727 (false, false) if self.id.source.is_wheel() => Err(LockError {
2728 kind: Box::new(LockErrorKind::IncompatibleWheelOnly {
2729 id: self.id.clone(),
2730 }),
2731 hint: self.tag_hint(tag_policy, markers),
2732 }),
2733 (false, false) => Err(LockError {
2734 kind: Box::new(LockErrorKind::NeitherSourceDistNorWheel {
2735 id: self.id.clone(),
2736 }),
2737 hint: self.tag_hint(tag_policy, markers),
2738 }),
2739 }
2740 }
2741
2742 fn tag_hint(
2744 &self,
2745 tag_policy: TagPolicy<'_>,
2746 markers: &MarkerEnvironment,
2747 ) -> Option<WheelTagHint> {
2748 let filenames = self
2749 .wheels
2750 .iter()
2751 .map(|wheel| &wheel.filename)
2752 .collect::<Vec<_>>();
2753 WheelTagHint::from_wheels(
2754 &self.id.name,
2755 self.id.version.as_ref(),
2756 &filenames,
2757 tag_policy.tags(),
2758 markers,
2759 )
2760 }
2761
2762 fn to_source_dist(
2767 &self,
2768 workspace_root: &Path,
2769 ) -> Result<Option<uv_distribution_types::SourceDist>, LockError> {
2770 let sdist = match &self.id.source {
2771 Source::Path(path) => {
2772 let DistExtension::Source(ext) = DistExtension::from_path(path).map_err(|err| {
2774 LockErrorKind::MissingExtension {
2775 id: self.id.clone(),
2776 err,
2777 }
2778 })?
2779 else {
2780 return Ok(None);
2781 };
2782 let install_path = absolute_path(workspace_root, path)?;
2783 let path_dist = PathSourceDist {
2784 name: self.id.name.clone(),
2785 version: self.id.version.clone(),
2786 url: verbatim_url(&install_path, &self.id)?,
2787 install_path: install_path.into_boxed_path(),
2788 ext,
2789 };
2790 uv_distribution_types::SourceDist::Path(path_dist)
2791 }
2792 Source::Directory(path) => {
2793 let install_path = absolute_path(workspace_root, path)?;
2794 let dir_dist = DirectorySourceDist {
2795 name: self.id.name.clone(),
2796 url: verbatim_url(&install_path, &self.id)?,
2797 install_path: install_path.into_boxed_path(),
2798 editable: Some(false),
2799 r#virtual: Some(false),
2800 };
2801 uv_distribution_types::SourceDist::Directory(dir_dist)
2802 }
2803 Source::Editable(path) => {
2804 let install_path = absolute_path(workspace_root, path)?;
2805 let dir_dist = DirectorySourceDist {
2806 name: self.id.name.clone(),
2807 url: verbatim_url(&install_path, &self.id)?,
2808 install_path: install_path.into_boxed_path(),
2809 editable: Some(true),
2810 r#virtual: Some(false),
2811 };
2812 uv_distribution_types::SourceDist::Directory(dir_dist)
2813 }
2814 Source::Virtual(path) => {
2815 let install_path = absolute_path(workspace_root, path)?;
2816 let dir_dist = DirectorySourceDist {
2817 name: self.id.name.clone(),
2818 url: verbatim_url(&install_path, &self.id)?,
2819 install_path: install_path.into_boxed_path(),
2820 editable: Some(false),
2821 r#virtual: Some(true),
2822 };
2823 uv_distribution_types::SourceDist::Directory(dir_dist)
2824 }
2825 Source::Git(url, git) => {
2826 let mut url = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
2829 url.set_fragment(None);
2830 url.set_query(None);
2831
2832 let git_url = GitUrl::from_commit(
2834 url,
2835 GitReference::from(git.kind.clone()),
2836 git.precise,
2837 git.lfs,
2838 )?;
2839
2840 let url = DisplaySafeUrl::from(ParsedGitUrl {
2842 url: git_url.clone(),
2843 subdirectory: git.subdirectory.clone(),
2844 });
2845
2846 let git_dist = GitSourceDist {
2847 name: self.id.name.clone(),
2848 url: VerbatimUrl::from_url(url),
2849 git: Box::new(git_url),
2850 subdirectory: git.subdirectory.clone(),
2851 };
2852 uv_distribution_types::SourceDist::Git(git_dist)
2853 }
2854 Source::Direct(url, direct) => {
2855 let DistExtension::Source(ext) =
2857 DistExtension::from_path(url.base_str()).map_err(|err| {
2858 LockErrorKind::MissingExtension {
2859 id: self.id.clone(),
2860 err,
2861 }
2862 })?
2863 else {
2864 return Ok(None);
2865 };
2866 let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
2867 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
2868 url: location.clone(),
2869 subdirectory: direct.subdirectory.clone(),
2870 ext: DistExtension::Source(ext),
2871 });
2872 let direct_dist = DirectUrlSourceDist {
2873 name: self.id.name.clone(),
2874 location: Box::new(location),
2875 subdirectory: direct.subdirectory.clone(),
2876 ext,
2877 url: VerbatimUrl::from_url(url),
2878 };
2879 uv_distribution_types::SourceDist::DirectUrl(direct_dist)
2880 }
2881 Source::Registry(RegistrySource::Url(url)) => {
2882 let Some(ref sdist) = self.sdist else {
2883 return Ok(None);
2884 };
2885
2886 let name = &self.id.name;
2887 let version = self
2888 .id
2889 .version
2890 .as_ref()
2891 .expect("version for registry source");
2892
2893 let file_url = sdist.url().ok_or_else(|| LockErrorKind::MissingUrl {
2894 name: name.clone(),
2895 version: version.clone(),
2896 })?;
2897 let filename = sdist
2898 .filename()
2899 .ok_or_else(|| LockErrorKind::MissingFilename {
2900 id: self.id.clone(),
2901 })?;
2902 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
2903 LockErrorKind::MissingExtension {
2904 id: self.id.clone(),
2905 err,
2906 }
2907 })?;
2908 let file = Box::new(uv_distribution_types::File {
2909 dist_info_metadata: false,
2910 filename: SmallString::from(filename),
2911 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
2912 HashDigests::from(hash.0.clone())
2913 }),
2914 requires_python: None,
2915 size: sdist.size(),
2916 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
2917 url: FileLocation::AbsoluteUrl(file_url.clone()),
2918 yanked: None,
2919 zstd: None,
2920 });
2921
2922 let index = IndexUrl::from(VerbatimUrl::from_url(
2923 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
2924 ));
2925
2926 let reg_dist = RegistrySourceDist {
2927 name: name.clone(),
2928 version: version.clone(),
2929 file,
2930 ext,
2931 index,
2932 wheels: vec![],
2933 };
2934 uv_distribution_types::SourceDist::Registry(reg_dist)
2935 }
2936 Source::Registry(RegistrySource::Path(path)) => {
2937 let Some(ref sdist) = self.sdist else {
2938 return Ok(None);
2939 };
2940
2941 let name = &self.id.name;
2942 let version = self
2943 .id
2944 .version
2945 .as_ref()
2946 .expect("version for registry source");
2947
2948 let file_url = match sdist {
2949 SourceDist::Url { url: file_url, .. } => {
2950 FileLocation::AbsoluteUrl(file_url.clone())
2951 }
2952 SourceDist::Path {
2953 path: file_path, ..
2954 } => {
2955 let file_path = workspace_root.join(path).join(file_path);
2956 let file_url =
2957 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
2958 LockErrorKind::PathToUrl {
2959 path: file_path.into_boxed_path(),
2960 }
2961 })?;
2962 FileLocation::AbsoluteUrl(UrlString::from(file_url))
2963 }
2964 SourceDist::Metadata { .. } => {
2965 return Err(LockErrorKind::MissingPath {
2966 name: name.clone(),
2967 version: version.clone(),
2968 }
2969 .into());
2970 }
2971 };
2972 let filename = sdist
2973 .filename()
2974 .ok_or_else(|| LockErrorKind::MissingFilename {
2975 id: self.id.clone(),
2976 })?;
2977 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
2978 LockErrorKind::MissingExtension {
2979 id: self.id.clone(),
2980 err,
2981 }
2982 })?;
2983 let file = Box::new(uv_distribution_types::File {
2984 dist_info_metadata: false,
2985 filename: SmallString::from(filename),
2986 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
2987 HashDigests::from(hash.0.clone())
2988 }),
2989 requires_python: None,
2990 size: sdist.size(),
2991 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
2992 url: file_url,
2993 yanked: None,
2994 zstd: None,
2995 });
2996
2997 let index = IndexUrl::from(
2998 VerbatimUrl::from_absolute_path(workspace_root.join(path))
2999 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
3000 );
3001
3002 let reg_dist = RegistrySourceDist {
3003 name: name.clone(),
3004 version: version.clone(),
3005 file,
3006 ext,
3007 index,
3008 wheels: vec![],
3009 };
3010 uv_distribution_types::SourceDist::Registry(reg_dist)
3011 }
3012 };
3013
3014 Ok(Some(sdist))
3015 }
3016
3017 fn to_toml(
3018 &self,
3019 requires_python: &RequiresPython,
3020 dist_count_by_name: &FxHashMap<PackageName, u64>,
3021 ) -> Result<Table, toml_edit::ser::Error> {
3022 let mut table = Table::new();
3023
3024 self.id.to_toml(None, &mut table);
3025
3026 if !self.fork_markers.is_empty() {
3027 let fork_markers = each_element_on_its_line_array(
3028 simplified_universal_markers(&self.fork_markers, requires_python).into_iter(),
3029 );
3030 if !fork_markers.is_empty() {
3031 table.insert("resolution-markers", value(fork_markers));
3032 }
3033 }
3034
3035 if !self.dependencies.is_empty() {
3036 let deps = each_element_on_its_line_array(self.dependencies.iter().map(|dep| {
3037 dep.to_toml(requires_python, dist_count_by_name)
3038 .into_inline_table()
3039 }));
3040 table.insert("dependencies", value(deps));
3041 }
3042
3043 if !self.optional_dependencies.is_empty() {
3044 let mut optional_deps = Table::new();
3045 for (extra, deps) in &self.optional_dependencies {
3046 let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3047 dep.to_toml(requires_python, dist_count_by_name)
3048 .into_inline_table()
3049 }));
3050 if !deps.is_empty() {
3051 optional_deps.insert(extra.as_ref(), value(deps));
3052 }
3053 }
3054 if !optional_deps.is_empty() {
3055 table.insert("optional-dependencies", Item::Table(optional_deps));
3056 }
3057 }
3058
3059 if !self.dependency_groups.is_empty() {
3060 let mut dependency_groups = Table::new();
3061 for (extra, deps) in &self.dependency_groups {
3062 let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3063 dep.to_toml(requires_python, dist_count_by_name)
3064 .into_inline_table()
3065 }));
3066 if !deps.is_empty() {
3067 dependency_groups.insert(extra.as_ref(), value(deps));
3068 }
3069 }
3070 if !dependency_groups.is_empty() {
3071 table.insert("dev-dependencies", Item::Table(dependency_groups));
3072 }
3073 }
3074
3075 if let Some(ref sdist) = self.sdist {
3076 table.insert("sdist", value(sdist.to_toml()?));
3077 }
3078
3079 if !self.wheels.is_empty() {
3080 let wheels = each_element_on_its_line_array(
3081 self.wheels
3082 .iter()
3083 .map(Wheel::to_toml)
3084 .collect::<Result<Vec<_>, _>>()?
3085 .into_iter(),
3086 );
3087 table.insert("wheels", value(wheels));
3088 }
3089
3090 {
3092 let mut metadata_table = Table::new();
3093
3094 if !self.metadata.requires_dist.is_empty() {
3095 let requires_dist = self
3096 .metadata
3097 .requires_dist
3098 .iter()
3099 .map(|requirement| {
3100 serde::Serialize::serialize(
3101 &requirement,
3102 toml_edit::ser::ValueSerializer::new(),
3103 )
3104 })
3105 .collect::<Result<Vec<_>, _>>()?;
3106 let requires_dist = match requires_dist.as_slice() {
3107 [] => Array::new(),
3108 [requirement] => Array::from_iter([requirement]),
3109 requires_dist => each_element_on_its_line_array(requires_dist.iter()),
3110 };
3111 metadata_table.insert("requires-dist", value(requires_dist));
3112 }
3113
3114 if !self.metadata.dependency_groups.is_empty() {
3115 let mut dependency_groups = Table::new();
3116 for (extra, deps) in &self.metadata.dependency_groups {
3117 let deps = deps
3118 .iter()
3119 .map(|requirement| {
3120 serde::Serialize::serialize(
3121 &requirement,
3122 toml_edit::ser::ValueSerializer::new(),
3123 )
3124 })
3125 .collect::<Result<Vec<_>, _>>()?;
3126 let deps = match deps.as_slice() {
3127 [] => Array::new(),
3128 [requirement] => Array::from_iter([requirement]),
3129 deps => each_element_on_its_line_array(deps.iter()),
3130 };
3131 dependency_groups.insert(extra.as_ref(), value(deps));
3132 }
3133 if !dependency_groups.is_empty() {
3134 metadata_table.insert("requires-dev", Item::Table(dependency_groups));
3135 }
3136 }
3137
3138 if !self.metadata.provides_extra.is_empty() {
3139 let provides_extras = self
3140 .metadata
3141 .provides_extra
3142 .iter()
3143 .map(|extra| {
3144 serde::Serialize::serialize(&extra, toml_edit::ser::ValueSerializer::new())
3145 })
3146 .collect::<Result<Vec<_>, _>>()?;
3147 let provides_extras = Array::from_iter(provides_extras);
3149 metadata_table.insert("provides-extras", value(provides_extras));
3150 }
3151
3152 if !metadata_table.is_empty() {
3153 table.insert("metadata", Item::Table(metadata_table));
3154 }
3155 }
3156
3157 Ok(table)
3158 }
3159
3160 fn find_best_wheel(&self, tag_policy: TagPolicy<'_>) -> Option<usize> {
3161 type WheelPriority<'lock> = (TagPriority, Option<&'lock BuildTag>);
3162
3163 let mut best: Option<(WheelPriority, usize)> = None;
3164 for (i, wheel) in self.wheels.iter().enumerate() {
3165 let TagCompatibility::Compatible(tag_priority) =
3166 wheel.filename.compatibility(tag_policy.tags())
3167 else {
3168 continue;
3169 };
3170 let build_tag = wheel.filename.build_tag();
3171 let wheel_priority = (tag_priority, build_tag);
3172 match best {
3173 None => {
3174 best = Some((wheel_priority, i));
3175 }
3176 Some((best_priority, _)) => {
3177 if wheel_priority > best_priority {
3178 best = Some((wheel_priority, i));
3179 }
3180 }
3181 }
3182 }
3183
3184 let best = best.map(|(_, i)| i);
3185 match tag_policy {
3186 TagPolicy::Required(_) => best,
3187 TagPolicy::Preferred(_) => best.or_else(|| self.wheels.first().map(|_| 0)),
3188 }
3189 }
3190
3191 pub fn name(&self) -> &PackageName {
3193 &self.id.name
3194 }
3195
3196 pub fn version(&self) -> Option<&Version> {
3198 self.id.version.as_ref()
3199 }
3200
3201 pub fn git_sha(&self) -> Option<&GitOid> {
3203 match &self.id.source {
3204 Source::Git(_, git) => Some(&git.precise),
3205 _ => None,
3206 }
3207 }
3208
3209 pub fn fork_markers(&self) -> &[UniversalMarker] {
3211 self.fork_markers.as_slice()
3212 }
3213
3214 pub fn index(&self, root: &Path) -> Result<Option<IndexUrl>, LockError> {
3216 match &self.id.source {
3217 Source::Registry(RegistrySource::Url(url)) => {
3218 let index = IndexUrl::from(VerbatimUrl::from_url(
3219 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
3220 ));
3221 Ok(Some(index))
3222 }
3223 Source::Registry(RegistrySource::Path(path)) => {
3224 let index = IndexUrl::from(
3225 VerbatimUrl::from_absolute_path(root.join(path))
3226 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
3227 );
3228 Ok(Some(index))
3229 }
3230 _ => Ok(None),
3231 }
3232 }
3233
3234 fn hashes(&self) -> HashDigests {
3236 let mut hashes = Vec::with_capacity(
3237 usize::from(self.sdist.as_ref().and_then(|sdist| sdist.hash()).is_some())
3238 + self
3239 .wheels
3240 .iter()
3241 .map(|wheel| usize::from(wheel.hash.is_some()))
3242 .sum::<usize>(),
3243 );
3244 if let Some(ref sdist) = self.sdist {
3245 if let Some(hash) = sdist.hash() {
3246 hashes.push(hash.0.clone());
3247 }
3248 }
3249 for wheel in &self.wheels {
3250 hashes.extend(wheel.hash.as_ref().map(|h| h.0.clone()));
3251 if let Some(zstd) = wheel.zstd.as_ref() {
3252 hashes.extend(zstd.hash.as_ref().map(|h| h.0.clone()));
3253 }
3254 }
3255 HashDigests::from(hashes)
3256 }
3257
3258 pub fn as_git_ref(&self) -> Result<Option<ResolvedRepositoryReference>, LockError> {
3260 match &self.id.source {
3261 Source::Git(url, git) => Ok(Some(ResolvedRepositoryReference {
3262 reference: RepositoryReference {
3263 url: RepositoryUrl::new(&url.to_url().map_err(LockErrorKind::InvalidUrl)?),
3264 reference: GitReference::from(git.kind.clone()),
3265 },
3266 sha: git.precise,
3267 })),
3268 _ => Ok(None),
3269 }
3270 }
3271
3272 fn is_dynamic(&self) -> bool {
3274 self.id.version.is_none()
3275 }
3276
3277 pub fn provides_extras(&self) -> &[ExtraName] {
3279 &self.metadata.provides_extra
3280 }
3281
3282 pub fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
3284 &self.metadata.dependency_groups
3285 }
3286
3287 pub fn dependencies(&self) -> &[Dependency] {
3289 &self.dependencies
3290 }
3291
3292 pub fn optional_dependencies(&self) -> &BTreeMap<ExtraName, Vec<Dependency>> {
3294 &self.optional_dependencies
3295 }
3296
3297 pub fn resolved_dependency_groups(&self) -> &BTreeMap<GroupName, Vec<Dependency>> {
3299 &self.dependency_groups
3300 }
3301
3302 pub fn as_install_target(&self) -> InstallTarget<'_> {
3304 InstallTarget {
3305 name: self.name(),
3306 is_local: self.id.source.is_local(),
3307 }
3308 }
3309}
3310
3311fn verbatim_url(path: &Path, id: &PackageId) -> Result<VerbatimUrl, LockError> {
3313 let url =
3314 VerbatimUrl::from_normalized_path(path).map_err(|err| LockErrorKind::VerbatimUrl {
3315 id: id.clone(),
3316 err,
3317 })?;
3318 Ok(url)
3319}
3320
3321fn absolute_path(workspace_root: &Path, path: &Path) -> Result<PathBuf, LockError> {
3323 let path = uv_fs::normalize_absolute_path(&workspace_root.join(path))
3324 .map_err(LockErrorKind::AbsolutePath)?;
3325 Ok(path)
3326}
3327
3328#[derive(Clone, Debug, serde::Deserialize)]
3329#[serde(rename_all = "kebab-case")]
3330struct PackageWire {
3331 #[serde(flatten)]
3332 id: PackageId,
3333 #[serde(default)]
3334 metadata: PackageMetadata,
3335 #[serde(default)]
3336 sdist: Option<SourceDist>,
3337 #[serde(default)]
3338 wheels: Vec<Wheel>,
3339 #[serde(default, rename = "resolution-markers")]
3340 fork_markers: Vec<SimplifiedMarkerTree>,
3341 #[serde(default)]
3342 dependencies: Vec<DependencyWire>,
3343 #[serde(default)]
3344 optional_dependencies: BTreeMap<ExtraName, Vec<DependencyWire>>,
3345 #[serde(default, rename = "dev-dependencies", alias = "dependency-groups")]
3346 dependency_groups: BTreeMap<GroupName, Vec<DependencyWire>>,
3347}
3348
3349#[derive(Clone, Default, Debug, Eq, PartialEq, serde::Deserialize)]
3350#[serde(rename_all = "kebab-case")]
3351struct PackageMetadata {
3352 #[serde(default)]
3353 requires_dist: BTreeSet<Requirement>,
3354 #[serde(default, rename = "provides-extras")]
3355 provides_extra: Box<[ExtraName]>,
3356 #[serde(default, rename = "requires-dev", alias = "dependency-groups")]
3357 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
3358}
3359
3360impl PackageWire {
3361 fn unwire(
3362 self,
3363 requires_python: &RequiresPython,
3364 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3365 ) -> Result<Package, LockError> {
3366 if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) {
3368 if let Some(version) = &self.id.version {
3369 for wheel in &self.wheels {
3370 if *version != wheel.filename.version
3371 && *version != wheel.filename.version.clone().without_local()
3372 {
3373 return Err(LockError::from(LockErrorKind::InconsistentVersions {
3374 name: self.id.name,
3375 version: version.clone(),
3376 wheel: wheel.clone(),
3377 }));
3378 }
3379 }
3380 }
3383 }
3384
3385 let unwire_deps = |deps: Vec<DependencyWire>| -> Result<Vec<Dependency>, LockError> {
3386 deps.into_iter()
3387 .map(|dep| dep.unwire(requires_python, unambiguous_package_ids))
3388 .collect()
3389 };
3390
3391 Ok(Package {
3392 id: self.id,
3393 metadata: self.metadata,
3394 sdist: self.sdist,
3395 wheels: self.wheels,
3396 fork_markers: self
3397 .fork_markers
3398 .into_iter()
3399 .map(|simplified_marker| simplified_marker.into_marker(requires_python))
3400 .map(UniversalMarker::from_combined)
3401 .collect(),
3402 dependencies: unwire_deps(self.dependencies)?,
3403 optional_dependencies: self
3404 .optional_dependencies
3405 .into_iter()
3406 .map(|(extra, deps)| Ok((extra, unwire_deps(deps)?)))
3407 .collect::<Result<_, LockError>>()?,
3408 dependency_groups: self
3409 .dependency_groups
3410 .into_iter()
3411 .map(|(group, deps)| Ok((group, unwire_deps(deps)?)))
3412 .collect::<Result<_, LockError>>()?,
3413 })
3414 }
3415}
3416
3417#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3420#[serde(rename_all = "kebab-case")]
3421pub(crate) struct PackageId {
3422 pub(crate) name: PackageName,
3423 pub(crate) version: Option<Version>,
3424 source: Source,
3425}
3426
3427impl PackageId {
3428 fn from_annotated_dist(annotated_dist: &AnnotatedDist, root: &Path) -> Result<Self, LockError> {
3429 let source = Source::from_resolved_dist(&annotated_dist.dist, root)?;
3431 let version = if source.is_source_tree()
3433 && annotated_dist
3434 .metadata
3435 .as_ref()
3436 .is_some_and(|metadata| metadata.dynamic)
3437 {
3438 None
3439 } else {
3440 Some(annotated_dist.version.clone())
3441 };
3442 let name = annotated_dist.name.clone();
3443 Ok(Self {
3444 name,
3445 version,
3446 source,
3447 })
3448 }
3449
3450 fn to_toml(&self, dist_count_by_name: Option<&FxHashMap<PackageName, u64>>, table: &mut Table) {
3457 let count = dist_count_by_name.and_then(|map| map.get(&self.name).copied());
3458 table.insert("name", value(self.name.to_string()));
3459 if count.map(|count| count > 1).unwrap_or(true) {
3460 if let Some(version) = &self.version {
3461 table.insert("version", value(version.to_string()));
3462 }
3463 self.source.to_toml(table);
3464 }
3465 }
3466}
3467
3468impl Display for PackageId {
3469 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3470 if let Some(version) = &self.version {
3471 write!(f, "{}=={} @ {}", self.name, version, self.source)
3472 } else {
3473 write!(f, "{} @ {}", self.name, self.source)
3474 }
3475 }
3476}
3477
3478#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3479#[serde(rename_all = "kebab-case")]
3480struct PackageIdForDependency {
3481 name: PackageName,
3482 version: Option<Version>,
3483 source: Option<Source>,
3484}
3485
3486impl PackageIdForDependency {
3487 fn unwire(
3488 self,
3489 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3490 ) -> Result<PackageId, LockError> {
3491 let unambiguous_package_id = unambiguous_package_ids.get(&self.name);
3492 let source = self.source.map(Ok::<_, LockError>).unwrap_or_else(|| {
3493 let Some(package_id) = unambiguous_package_id else {
3494 return Err(LockErrorKind::MissingDependencySource {
3495 name: self.name.clone(),
3496 }
3497 .into());
3498 };
3499 Ok(package_id.source.clone())
3500 })?;
3501 let version = if let Some(version) = self.version {
3502 Some(version)
3503 } else {
3504 if let Some(package_id) = unambiguous_package_id {
3505 package_id.version.clone()
3506 } else {
3507 if source.is_source_tree() {
3510 None
3511 } else {
3512 return Err(LockErrorKind::MissingDependencyVersion {
3513 name: self.name.clone(),
3514 }
3515 .into());
3516 }
3517 }
3518 };
3519 Ok(PackageId {
3520 name: self.name,
3521 version,
3522 source,
3523 })
3524 }
3525}
3526
3527impl From<PackageId> for PackageIdForDependency {
3528 fn from(id: PackageId) -> Self {
3529 Self {
3530 name: id.name,
3531 version: id.version,
3532 source: Some(id.source),
3533 }
3534 }
3535}
3536
3537#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3545#[serde(try_from = "SourceWire")]
3546enum Source {
3547 Registry(RegistrySource),
3549 Git(UrlString, GitSource),
3551 Direct(UrlString, DirectSource),
3553 Path(Box<Path>),
3555 Directory(Box<Path>),
3557 Editable(Box<Path>),
3559 Virtual(Box<Path>),
3561}
3562
3563impl Source {
3564 fn from_resolved_dist(resolved_dist: &ResolvedDist, root: &Path) -> Result<Self, LockError> {
3565 match *resolved_dist {
3566 ResolvedDist::Installed { .. } => unreachable!(),
3568 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(dist, root),
3569 }
3570 }
3571
3572 fn from_dist(dist: &Dist, root: &Path) -> Result<Self, LockError> {
3573 match *dist {
3574 Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, root),
3575 Dist::Source(ref source_dist) => Self::from_source_dist(source_dist, root),
3576 }
3577 }
3578
3579 fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result<Self, LockError> {
3580 match *built_dist {
3581 BuiltDist::Registry(ref reg_dist) => Self::from_registry_built_dist(reg_dist, root),
3582 BuiltDist::DirectUrl(ref direct_dist) => Ok(Self::from_direct_built_dist(direct_dist)),
3583 BuiltDist::Path(ref path_dist) => Self::from_path_built_dist(path_dist, root),
3584 }
3585 }
3586
3587 fn from_source_dist(
3588 source_dist: &uv_distribution_types::SourceDist,
3589 root: &Path,
3590 ) -> Result<Self, LockError> {
3591 match *source_dist {
3592 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
3593 Self::from_registry_source_dist(reg_dist, root)
3594 }
3595 uv_distribution_types::SourceDist::DirectUrl(ref direct_dist) => {
3596 Ok(Self::from_direct_source_dist(direct_dist))
3597 }
3598 uv_distribution_types::SourceDist::Git(ref git_dist) => {
3599 Ok(Self::from_git_dist(git_dist))
3600 }
3601 uv_distribution_types::SourceDist::Path(ref path_dist) => {
3602 Self::from_path_source_dist(path_dist, root)
3603 }
3604 uv_distribution_types::SourceDist::Directory(ref directory) => {
3605 Self::from_directory_source_dist(directory, root)
3606 }
3607 }
3608 }
3609
3610 fn from_registry_built_dist(
3611 reg_dist: &RegistryBuiltDist,
3612 root: &Path,
3613 ) -> Result<Self, LockError> {
3614 Self::from_index_url(®_dist.best_wheel().index, root)
3615 }
3616
3617 fn from_registry_source_dist(
3618 reg_dist: &RegistrySourceDist,
3619 root: &Path,
3620 ) -> Result<Self, LockError> {
3621 Self::from_index_url(®_dist.index, root)
3622 }
3623
3624 fn from_direct_built_dist(direct_dist: &DirectUrlBuiltDist) -> Self {
3625 Self::Direct(
3626 normalize_url(direct_dist.url.to_url()),
3627 DirectSource { subdirectory: None },
3628 )
3629 }
3630
3631 fn from_direct_source_dist(direct_dist: &DirectUrlSourceDist) -> Self {
3632 Self::Direct(
3633 normalize_url(direct_dist.url.to_url()),
3634 DirectSource {
3635 subdirectory: direct_dist.subdirectory.clone(),
3636 },
3637 )
3638 }
3639
3640 fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result<Self, LockError> {
3641 let path = relative_to(&path_dist.install_path, root)
3642 .or_else(|_| std::path::absolute(&path_dist.install_path))
3643 .map_err(LockErrorKind::DistributionRelativePath)?;
3644 Ok(Self::Path(path.into_boxed_path()))
3645 }
3646
3647 fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Self, LockError> {
3648 let path = relative_to(&path_dist.install_path, root)
3649 .or_else(|_| std::path::absolute(&path_dist.install_path))
3650 .map_err(LockErrorKind::DistributionRelativePath)?;
3651 Ok(Self::Path(path.into_boxed_path()))
3652 }
3653
3654 fn from_directory_source_dist(
3655 directory_dist: &DirectorySourceDist,
3656 root: &Path,
3657 ) -> Result<Self, LockError> {
3658 let path = relative_to(&directory_dist.install_path, root)
3659 .or_else(|_| std::path::absolute(&directory_dist.install_path))
3660 .map_err(LockErrorKind::DistributionRelativePath)?;
3661 if directory_dist.editable.unwrap_or(false) {
3662 Ok(Self::Editable(path.into_boxed_path()))
3663 } else if directory_dist.r#virtual.unwrap_or(false) {
3664 Ok(Self::Virtual(path.into_boxed_path()))
3665 } else {
3666 Ok(Self::Directory(path.into_boxed_path()))
3667 }
3668 }
3669
3670 fn from_index_url(index_url: &IndexUrl, root: &Path) -> Result<Self, LockError> {
3671 match index_url {
3672 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
3673 let redacted = index_url.without_credentials();
3675 let source = RegistrySource::Url(UrlString::from(redacted.as_ref()));
3676 Ok(Self::Registry(source))
3677 }
3678 IndexUrl::Path(url) => {
3679 let path = url
3680 .to_file_path()
3681 .map_err(|()| LockErrorKind::UrlToPath { url: url.to_url() })?;
3682 let path = relative_to(&path, root)
3683 .or_else(|_| std::path::absolute(&path))
3684 .map_err(LockErrorKind::IndexRelativePath)?;
3685 let source = RegistrySource::Path(path.into_boxed_path());
3686 Ok(Self::Registry(source))
3687 }
3688 }
3689 }
3690
3691 fn from_git_dist(git_dist: &GitSourceDist) -> Self {
3692 Self::Git(
3693 UrlString::from(locked_git_url(git_dist)),
3694 GitSource {
3695 kind: GitSourceKind::from(git_dist.git.reference().clone()),
3696 precise: git_dist.git.precise().unwrap_or_else(|| {
3697 panic!("Git distribution is missing a precise hash: {git_dist}")
3698 }),
3699 subdirectory: git_dist.subdirectory.clone(),
3700 lfs: git_dist.git.lfs(),
3701 },
3702 )
3703 }
3704
3705 fn is_immutable(&self) -> bool {
3712 matches!(self, Self::Registry(..) | Self::Git(_, _))
3713 }
3714
3715 fn is_wheel(&self) -> bool {
3717 match self {
3718 Self::Path(path) => {
3719 matches!(
3720 DistExtension::from_path(path).ok(),
3721 Some(DistExtension::Wheel)
3722 )
3723 }
3724 Self::Direct(url, _) => {
3725 matches!(
3726 DistExtension::from_path(url.as_ref()).ok(),
3727 Some(DistExtension::Wheel)
3728 )
3729 }
3730 Self::Directory(..) => false,
3731 Self::Editable(..) => false,
3732 Self::Virtual(..) => false,
3733 Self::Git(..) => false,
3734 Self::Registry(..) => false,
3735 }
3736 }
3737
3738 fn is_source_tree(&self) -> bool {
3740 match self {
3741 Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => true,
3742 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => false,
3743 }
3744 }
3745
3746 fn as_source_tree(&self) -> Option<&Path> {
3748 match self {
3749 Self::Directory(path) | Self::Editable(path) | Self::Virtual(path) => Some(path),
3750 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => None,
3751 }
3752 }
3753
3754 fn to_toml(&self, table: &mut Table) {
3755 let mut source_table = InlineTable::new();
3756 match self {
3757 Self::Registry(source) => match source {
3758 RegistrySource::Url(url) => {
3759 source_table.insert("registry", Value::from(url.as_ref()));
3760 }
3761 RegistrySource::Path(path) => {
3762 source_table.insert(
3763 "registry",
3764 Value::from(PortablePath::from(path).to_string()),
3765 );
3766 }
3767 },
3768 Self::Git(url, _) => {
3769 source_table.insert("git", Value::from(url.as_ref()));
3770 }
3771 Self::Direct(url, DirectSource { subdirectory }) => {
3772 source_table.insert("url", Value::from(url.as_ref()));
3773 if let Some(ref subdirectory) = *subdirectory {
3774 source_table.insert(
3775 "subdirectory",
3776 Value::from(PortablePath::from(subdirectory).to_string()),
3777 );
3778 }
3779 }
3780 Self::Path(path) => {
3781 source_table.insert("path", Value::from(PortablePath::from(path).to_string()));
3782 }
3783 Self::Directory(path) => {
3784 source_table.insert(
3785 "directory",
3786 Value::from(PortablePath::from(path).to_string()),
3787 );
3788 }
3789 Self::Editable(path) => {
3790 source_table.insert(
3791 "editable",
3792 Value::from(PortablePath::from(path).to_string()),
3793 );
3794 }
3795 Self::Virtual(path) => {
3796 source_table.insert("virtual", Value::from(PortablePath::from(path).to_string()));
3797 }
3798 }
3799 table.insert("source", value(source_table));
3800 }
3801
3802 pub(crate) fn is_local(&self) -> bool {
3804 matches!(
3805 self,
3806 Self::Path(_) | Self::Directory(_) | Self::Editable(_) | Self::Virtual(_)
3807 )
3808 }
3809}
3810
3811impl Display for Source {
3812 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3813 match self {
3814 Self::Registry(RegistrySource::Url(url)) | Self::Git(url, _) | Self::Direct(url, _) => {
3815 write!(f, "{}+{}", self.name(), url)
3816 }
3817 Self::Registry(RegistrySource::Path(path))
3818 | Self::Path(path)
3819 | Self::Directory(path)
3820 | Self::Editable(path)
3821 | Self::Virtual(path) => {
3822 write!(f, "{}+{}", self.name(), PortablePath::from(path))
3823 }
3824 }
3825 }
3826}
3827
3828impl Source {
3829 fn name(&self) -> &str {
3830 match self {
3831 Self::Registry(..) => "registry",
3832 Self::Git(..) => "git",
3833 Self::Direct(..) => "direct",
3834 Self::Path(..) => "path",
3835 Self::Directory(..) => "directory",
3836 Self::Editable(..) => "editable",
3837 Self::Virtual(..) => "virtual",
3838 }
3839 }
3840
3841 fn requires_hash(&self) -> Option<bool> {
3849 match self {
3850 Self::Registry(..) => None,
3851 Self::Direct(..) | Self::Path(..) => Some(true),
3852 Self::Git(..) | Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => {
3853 Some(false)
3854 }
3855 }
3856 }
3857}
3858
3859#[derive(Clone, Debug, serde::Deserialize)]
3860#[serde(untagged, rename_all = "kebab-case")]
3861enum SourceWire {
3862 Registry {
3863 registry: RegistrySourceWire,
3864 },
3865 Git {
3866 git: String,
3867 },
3868 Direct {
3869 url: UrlString,
3870 subdirectory: Option<PortablePathBuf>,
3871 },
3872 Path {
3873 path: PortablePathBuf,
3874 },
3875 Directory {
3876 directory: PortablePathBuf,
3877 },
3878 Editable {
3879 editable: PortablePathBuf,
3880 },
3881 Virtual {
3882 r#virtual: PortablePathBuf,
3883 },
3884}
3885
3886impl TryFrom<SourceWire> for Source {
3887 type Error = LockError;
3888
3889 fn try_from(wire: SourceWire) -> Result<Self, LockError> {
3890 #[allow(clippy::enum_glob_use)]
3891 use self::SourceWire::*;
3892
3893 match wire {
3894 Registry { registry } => Ok(Self::Registry(registry.into())),
3895 Git { git } => {
3896 let url = DisplaySafeUrl::parse(&git)
3897 .map_err(|err| SourceParseError::InvalidUrl {
3898 given: git.clone(),
3899 err,
3900 })
3901 .map_err(LockErrorKind::InvalidGitSourceUrl)?;
3902
3903 let git_source = GitSource::from_url(&url)
3904 .map_err(|err| match err {
3905 GitSourceError::InvalidSha => SourceParseError::InvalidSha { given: git },
3906 GitSourceError::MissingSha => SourceParseError::MissingSha { given: git },
3907 })
3908 .map_err(LockErrorKind::InvalidGitSourceUrl)?;
3909
3910 Ok(Self::Git(UrlString::from(url), git_source))
3911 }
3912 Direct { url, subdirectory } => Ok(Self::Direct(
3913 url,
3914 DirectSource {
3915 subdirectory: subdirectory.map(Box::<std::path::Path>::from),
3916 },
3917 )),
3918 Path { path } => Ok(Self::Path(path.into())),
3919 Directory { directory } => Ok(Self::Directory(directory.into())),
3920 Editable { editable } => Ok(Self::Editable(editable.into())),
3921 Virtual { r#virtual } => Ok(Self::Virtual(r#virtual.into())),
3922 }
3923 }
3924}
3925
3926#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
3928enum RegistrySource {
3929 Url(UrlString),
3931 Path(Box<Path>),
3933}
3934
3935impl Display for RegistrySource {
3936 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3937 match self {
3938 Self::Url(url) => write!(f, "{url}"),
3939 Self::Path(path) => write!(f, "{}", path.display()),
3940 }
3941 }
3942}
3943
3944#[derive(Clone, Debug)]
3945enum RegistrySourceWire {
3946 Url(UrlString),
3948 Path(PortablePathBuf),
3950}
3951
3952impl<'de> serde::de::Deserialize<'de> for RegistrySourceWire {
3953 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3954 where
3955 D: serde::de::Deserializer<'de>,
3956 {
3957 struct Visitor;
3958
3959 impl serde::de::Visitor<'_> for Visitor {
3960 type Value = RegistrySourceWire;
3961
3962 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
3963 formatter.write_str("a valid URL or a file path")
3964 }
3965
3966 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
3967 where
3968 E: serde::de::Error,
3969 {
3970 if split_scheme(value).is_some() {
3971 Ok(
3972 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
3973 value,
3974 ))
3975 .map(RegistrySourceWire::Url)?,
3976 )
3977 } else {
3978 Ok(
3979 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
3980 value,
3981 ))
3982 .map(RegistrySourceWire::Path)?,
3983 )
3984 }
3985 }
3986 }
3987
3988 deserializer.deserialize_str(Visitor)
3989 }
3990}
3991
3992impl From<RegistrySourceWire> for RegistrySource {
3993 fn from(wire: RegistrySourceWire) -> Self {
3994 match wire {
3995 RegistrySourceWire::Url(url) => Self::Url(url),
3996 RegistrySourceWire::Path(path) => Self::Path(path.into()),
3997 }
3998 }
3999}
4000
4001#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4002#[serde(rename_all = "kebab-case")]
4003struct DirectSource {
4004 subdirectory: Option<Box<Path>>,
4005}
4006
4007#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
4012struct GitSource {
4013 precise: GitOid,
4014 subdirectory: Option<Box<Path>>,
4015 kind: GitSourceKind,
4016 lfs: GitLfs,
4017}
4018
4019#[derive(Clone, Debug, Eq, PartialEq)]
4021enum GitSourceError {
4022 InvalidSha,
4023 MissingSha,
4024}
4025
4026impl GitSource {
4027 fn from_url(url: &Url) -> Result<Self, GitSourceError> {
4030 let mut kind = GitSourceKind::DefaultBranch;
4031 let mut subdirectory = None;
4032 let mut lfs = GitLfs::Disabled;
4033 for (key, val) in url.query_pairs() {
4034 match &*key {
4035 "tag" => kind = GitSourceKind::Tag(val.into_owned()),
4036 "branch" => kind = GitSourceKind::Branch(val.into_owned()),
4037 "rev" => kind = GitSourceKind::Rev(val.into_owned()),
4038 "subdirectory" => subdirectory = Some(PortablePathBuf::from(val.as_ref()).into()),
4039 "lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
4040 _ => {}
4041 }
4042 }
4043
4044 let precise = GitOid::from_str(url.fragment().ok_or(GitSourceError::MissingSha)?)
4045 .map_err(|_| GitSourceError::InvalidSha)?;
4046
4047 Ok(Self {
4048 precise,
4049 subdirectory,
4050 kind,
4051 lfs,
4052 })
4053 }
4054}
4055
4056#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4057#[serde(rename_all = "kebab-case")]
4058enum GitSourceKind {
4059 Tag(String),
4060 Branch(String),
4061 Rev(String),
4062 DefaultBranch,
4063}
4064
4065#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4067#[serde(rename_all = "kebab-case")]
4068struct SourceDistMetadata {
4069 hash: Option<Hash>,
4071 size: Option<u64>,
4075 #[serde(alias = "upload_time")]
4077 upload_time: Option<Timestamp>,
4078}
4079
4080#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4085#[serde(from = "SourceDistWire")]
4086enum SourceDist {
4087 Url {
4088 url: UrlString,
4089 #[serde(flatten)]
4090 metadata: SourceDistMetadata,
4091 },
4092 Path {
4093 path: Box<Path>,
4094 #[serde(flatten)]
4095 metadata: SourceDistMetadata,
4096 },
4097 Metadata {
4098 #[serde(flatten)]
4099 metadata: SourceDistMetadata,
4100 },
4101}
4102
4103impl SourceDist {
4104 fn filename(&self) -> Option<Cow<'_, str>> {
4105 match self {
4106 Self::Metadata { .. } => None,
4107 Self::Url { url, .. } => url.filename().ok(),
4108 Self::Path { path, .. } => path.file_name().map(|filename| filename.to_string_lossy()),
4109 }
4110 }
4111
4112 fn url(&self) -> Option<&UrlString> {
4113 match self {
4114 Self::Metadata { .. } => None,
4115 Self::Url { url, .. } => Some(url),
4116 Self::Path { .. } => None,
4117 }
4118 }
4119
4120 pub(crate) fn hash(&self) -> Option<&Hash> {
4121 match self {
4122 Self::Metadata { metadata } => metadata.hash.as_ref(),
4123 Self::Url { metadata, .. } => metadata.hash.as_ref(),
4124 Self::Path { metadata, .. } => metadata.hash.as_ref(),
4125 }
4126 }
4127
4128 pub(crate) fn size(&self) -> Option<u64> {
4129 match self {
4130 Self::Metadata { metadata } => metadata.size,
4131 Self::Url { metadata, .. } => metadata.size,
4132 Self::Path { metadata, .. } => metadata.size,
4133 }
4134 }
4135
4136 pub(crate) fn upload_time(&self) -> Option<Timestamp> {
4137 match self {
4138 Self::Metadata { metadata } => metadata.upload_time,
4139 Self::Url { metadata, .. } => metadata.upload_time,
4140 Self::Path { metadata, .. } => metadata.upload_time,
4141 }
4142 }
4143}
4144
4145impl SourceDist {
4146 fn from_annotated_dist(
4147 id: &PackageId,
4148 annotated_dist: &AnnotatedDist,
4149 ) -> Result<Option<Self>, LockError> {
4150 match annotated_dist.dist {
4151 ResolvedDist::Installed { .. } => unreachable!(),
4153 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4154 id,
4155 dist,
4156 annotated_dist.hashes.as_slice(),
4157 annotated_dist.index(),
4158 ),
4159 }
4160 }
4161
4162 fn from_dist(
4163 id: &PackageId,
4164 dist: &Dist,
4165 hashes: &[HashDigest],
4166 index: Option<&IndexUrl>,
4167 ) -> Result<Option<Self>, LockError> {
4168 match *dist {
4169 Dist::Built(BuiltDist::Registry(ref built_dist)) => {
4170 let Some(sdist) = built_dist.sdist.as_ref() else {
4171 return Ok(None);
4172 };
4173 Self::from_registry_dist(sdist, index)
4174 }
4175 Dist::Built(_) => Ok(None),
4176 Dist::Source(ref source_dist) => Self::from_source_dist(id, source_dist, hashes, index),
4177 }
4178 }
4179
4180 fn from_source_dist(
4181 id: &PackageId,
4182 source_dist: &uv_distribution_types::SourceDist,
4183 hashes: &[HashDigest],
4184 index: Option<&IndexUrl>,
4185 ) -> Result<Option<Self>, LockError> {
4186 match *source_dist {
4187 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
4188 Self::from_registry_dist(reg_dist, index)
4189 }
4190 uv_distribution_types::SourceDist::DirectUrl(_) => {
4191 Self::from_direct_dist(id, hashes).map(Some)
4192 }
4193 uv_distribution_types::SourceDist::Path(_) => {
4194 Self::from_path_dist(id, hashes).map(Some)
4195 }
4196 uv_distribution_types::SourceDist::Git(_)
4200 | uv_distribution_types::SourceDist::Directory(_) => Ok(None),
4201 }
4202 }
4203
4204 fn from_registry_dist(
4205 reg_dist: &RegistrySourceDist,
4206 index: Option<&IndexUrl>,
4207 ) -> Result<Option<Self>, LockError> {
4208 if index.is_none_or(|index| *index != reg_dist.index) {
4211 return Ok(None);
4212 }
4213
4214 match ®_dist.index {
4215 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4216 let url = normalize_file_location(®_dist.file.url)
4217 .map_err(LockErrorKind::InvalidUrl)
4218 .map_err(LockError::from)?;
4219 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4220 let size = reg_dist.file.size;
4221 let upload_time = reg_dist
4222 .file
4223 .upload_time_utc_ms
4224 .map(Timestamp::from_millisecond)
4225 .transpose()
4226 .map_err(LockErrorKind::InvalidTimestamp)?;
4227 Ok(Some(Self::Url {
4228 url,
4229 metadata: SourceDistMetadata {
4230 hash,
4231 size,
4232 upload_time,
4233 },
4234 }))
4235 }
4236 IndexUrl::Path(path) => {
4237 let index_path = path
4238 .to_file_path()
4239 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4240 let url = reg_dist
4241 .file
4242 .url
4243 .to_url()
4244 .map_err(LockErrorKind::InvalidUrl)?;
4245
4246 if url.scheme() == "file" {
4247 let reg_dist_path = url
4248 .to_file_path()
4249 .map_err(|()| LockErrorKind::UrlToPath { url })?;
4250 let path = relative_to(®_dist_path, index_path)
4251 .or_else(|_| std::path::absolute(®_dist_path))
4252 .map_err(LockErrorKind::DistributionRelativePath)?
4253 .into_boxed_path();
4254 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4255 let size = reg_dist.file.size;
4256 let upload_time = reg_dist
4257 .file
4258 .upload_time_utc_ms
4259 .map(Timestamp::from_millisecond)
4260 .transpose()
4261 .map_err(LockErrorKind::InvalidTimestamp)?;
4262 Ok(Some(Self::Path {
4263 path,
4264 metadata: SourceDistMetadata {
4265 hash,
4266 size,
4267 upload_time,
4268 },
4269 }))
4270 } else {
4271 let url = normalize_file_location(®_dist.file.url)
4272 .map_err(LockErrorKind::InvalidUrl)
4273 .map_err(LockError::from)?;
4274 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4275 let size = reg_dist.file.size;
4276 let upload_time = reg_dist
4277 .file
4278 .upload_time_utc_ms
4279 .map(Timestamp::from_millisecond)
4280 .transpose()
4281 .map_err(LockErrorKind::InvalidTimestamp)?;
4282 Ok(Some(Self::Url {
4283 url,
4284 metadata: SourceDistMetadata {
4285 hash,
4286 size,
4287 upload_time,
4288 },
4289 }))
4290 }
4291 }
4292 }
4293 }
4294
4295 fn from_direct_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4296 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4297 let kind = LockErrorKind::Hash {
4298 id: id.clone(),
4299 artifact_type: "direct URL source distribution",
4300 expected: true,
4301 };
4302 return Err(kind.into());
4303 };
4304 Ok(Self::Metadata {
4305 metadata: SourceDistMetadata {
4306 hash: Some(hash),
4307 size: None,
4308 upload_time: None,
4309 },
4310 })
4311 }
4312
4313 fn from_path_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4314 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4315 let kind = LockErrorKind::Hash {
4316 id: id.clone(),
4317 artifact_type: "path source distribution",
4318 expected: true,
4319 };
4320 return Err(kind.into());
4321 };
4322 Ok(Self::Metadata {
4323 metadata: SourceDistMetadata {
4324 hash: Some(hash),
4325 size: None,
4326 upload_time: None,
4327 },
4328 })
4329 }
4330}
4331
4332#[derive(Clone, Debug, serde::Deserialize)]
4333#[serde(untagged, rename_all = "kebab-case")]
4334enum SourceDistWire {
4335 Url {
4336 url: UrlString,
4337 #[serde(flatten)]
4338 metadata: SourceDistMetadata,
4339 },
4340 Path {
4341 path: PortablePathBuf,
4342 #[serde(flatten)]
4343 metadata: SourceDistMetadata,
4344 },
4345 Metadata {
4346 #[serde(flatten)]
4347 metadata: SourceDistMetadata,
4348 },
4349}
4350
4351impl SourceDist {
4352 fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
4354 let mut table = InlineTable::new();
4355 match self {
4356 Self::Metadata { .. } => {}
4357 Self::Url { url, .. } => {
4358 table.insert("url", Value::from(url.as_ref()));
4359 }
4360 Self::Path { path, .. } => {
4361 table.insert("path", Value::from(PortablePath::from(path).to_string()));
4362 }
4363 }
4364 if let Some(hash) = self.hash() {
4365 table.insert("hash", Value::from(hash.to_string()));
4366 }
4367 if let Some(size) = self.size() {
4368 table.insert(
4369 "size",
4370 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4371 );
4372 }
4373 if let Some(upload_time) = self.upload_time() {
4374 table.insert("upload-time", Value::from(upload_time.to_string()));
4375 }
4376 Ok(table)
4377 }
4378}
4379
4380impl From<SourceDistWire> for SourceDist {
4381 fn from(wire: SourceDistWire) -> Self {
4382 match wire {
4383 SourceDistWire::Url { url, metadata } => Self::Url { url, metadata },
4384 SourceDistWire::Path { path, metadata } => Self::Path {
4385 path: path.into(),
4386 metadata,
4387 },
4388 SourceDistWire::Metadata { metadata } => Self::Metadata { metadata },
4389 }
4390 }
4391}
4392
4393impl From<GitReference> for GitSourceKind {
4394 fn from(value: GitReference) -> Self {
4395 match value {
4396 GitReference::Branch(branch) => Self::Branch(branch),
4397 GitReference::Tag(tag) => Self::Tag(tag),
4398 GitReference::BranchOrTag(rev) => Self::Rev(rev),
4399 GitReference::BranchOrTagOrCommit(rev) => Self::Rev(rev),
4400 GitReference::NamedRef(rev) => Self::Rev(rev),
4401 GitReference::DefaultBranch => Self::DefaultBranch,
4402 }
4403 }
4404}
4405
4406impl From<GitSourceKind> for GitReference {
4407 fn from(value: GitSourceKind) -> Self {
4408 match value {
4409 GitSourceKind::Branch(branch) => Self::Branch(branch),
4410 GitSourceKind::Tag(tag) => Self::Tag(tag),
4411 GitSourceKind::Rev(rev) => Self::from_rev(rev),
4412 GitSourceKind::DefaultBranch => Self::DefaultBranch,
4413 }
4414 }
4415}
4416
4417fn locked_git_url(git_dist: &GitSourceDist) -> DisplaySafeUrl {
4419 let mut url = git_dist.git.repository().clone();
4420
4421 url.remove_credentials();
4423
4424 url.set_fragment(None);
4426 url.set_query(None);
4427
4428 if let Some(subdirectory) = git_dist
4430 .subdirectory
4431 .as_deref()
4432 .map(PortablePath::from)
4433 .as_ref()
4434 .map(PortablePath::to_string)
4435 {
4436 url.query_pairs_mut()
4437 .append_pair("subdirectory", &subdirectory);
4438 }
4439
4440 if git_dist.git.lfs().enabled() {
4442 url.query_pairs_mut().append_pair("lfs", "true");
4443 }
4444
4445 match git_dist.git.reference() {
4447 GitReference::Branch(branch) => {
4448 url.query_pairs_mut().append_pair("branch", branch.as_str());
4449 }
4450 GitReference::Tag(tag) => {
4451 url.query_pairs_mut().append_pair("tag", tag.as_str());
4452 }
4453 GitReference::BranchOrTag(rev)
4454 | GitReference::BranchOrTagOrCommit(rev)
4455 | GitReference::NamedRef(rev) => {
4456 url.query_pairs_mut().append_pair("rev", rev.as_str());
4457 }
4458 GitReference::DefaultBranch => {}
4459 }
4460
4461 url.set_fragment(
4463 git_dist
4464 .git
4465 .precise()
4466 .as_ref()
4467 .map(GitOid::to_string)
4468 .as_deref(),
4469 );
4470
4471 url
4472}
4473
4474#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4475struct ZstdWheel {
4476 hash: Option<Hash>,
4477 size: Option<u64>,
4478}
4479
4480#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4482#[serde(try_from = "WheelWire")]
4483struct Wheel {
4484 url: WheelWireSource,
4489 hash: Option<Hash>,
4495 size: Option<u64>,
4499 upload_time: Option<Timestamp>,
4503 filename: WheelFilename,
4510 zstd: Option<ZstdWheel>,
4512}
4513
4514impl Wheel {
4515 fn from_annotated_dist(annotated_dist: &AnnotatedDist) -> Result<Vec<Self>, LockError> {
4516 match annotated_dist.dist {
4517 ResolvedDist::Installed { .. } => unreachable!(),
4519 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4520 dist,
4521 annotated_dist.hashes.as_slice(),
4522 annotated_dist.index(),
4523 ),
4524 }
4525 }
4526
4527 fn from_dist(
4528 dist: &Dist,
4529 hashes: &[HashDigest],
4530 index: Option<&IndexUrl>,
4531 ) -> Result<Vec<Self>, LockError> {
4532 match *dist {
4533 Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, hashes, index),
4534 Dist::Source(uv_distribution_types::SourceDist::Registry(ref source_dist)) => {
4535 source_dist
4536 .wheels
4537 .iter()
4538 .filter(|wheel| {
4539 index.is_some_and(|index| *index == wheel.index)
4542 })
4543 .map(Self::from_registry_wheel)
4544 .collect()
4545 }
4546 Dist::Source(_) => Ok(vec![]),
4547 }
4548 }
4549
4550 fn from_built_dist(
4551 built_dist: &BuiltDist,
4552 hashes: &[HashDigest],
4553 index: Option<&IndexUrl>,
4554 ) -> Result<Vec<Self>, LockError> {
4555 match *built_dist {
4556 BuiltDist::Registry(ref reg_dist) => Self::from_registry_dist(reg_dist, index),
4557 BuiltDist::DirectUrl(ref direct_dist) => {
4558 Ok(vec![Self::from_direct_dist(direct_dist, hashes)])
4559 }
4560 BuiltDist::Path(ref path_dist) => Ok(vec![Self::from_path_dist(path_dist, hashes)]),
4561 }
4562 }
4563
4564 fn from_registry_dist(
4565 reg_dist: &RegistryBuiltDist,
4566 index: Option<&IndexUrl>,
4567 ) -> Result<Vec<Self>, LockError> {
4568 reg_dist
4569 .wheels
4570 .iter()
4571 .filter(|wheel| {
4572 index.is_some_and(|index| *index == wheel.index)
4575 })
4576 .map(Self::from_registry_wheel)
4577 .collect()
4578 }
4579
4580 fn from_registry_wheel(wheel: &RegistryBuiltWheel) -> Result<Self, LockError> {
4581 let url = match &wheel.index {
4582 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4583 let url = normalize_file_location(&wheel.file.url)
4584 .map_err(LockErrorKind::InvalidUrl)
4585 .map_err(LockError::from)?;
4586 WheelWireSource::Url { url }
4587 }
4588 IndexUrl::Path(path) => {
4589 let index_path = path
4590 .to_file_path()
4591 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4592 let wheel_url = wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?;
4593
4594 if wheel_url.scheme() == "file" {
4595 let wheel_path = wheel_url
4596 .to_file_path()
4597 .map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?;
4598 let path = relative_to(&wheel_path, index_path)
4599 .or_else(|_| std::path::absolute(&wheel_path))
4600 .map_err(LockErrorKind::DistributionRelativePath)?
4601 .into_boxed_path();
4602 WheelWireSource::Path { path }
4603 } else {
4604 let url = normalize_file_location(&wheel.file.url)
4605 .map_err(LockErrorKind::InvalidUrl)
4606 .map_err(LockError::from)?;
4607 WheelWireSource::Url { url }
4608 }
4609 }
4610 };
4611 let filename = wheel.filename.clone();
4612 let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from);
4613 let size = wheel.file.size;
4614 let upload_time = wheel
4615 .file
4616 .upload_time_utc_ms
4617 .map(Timestamp::from_millisecond)
4618 .transpose()
4619 .map_err(LockErrorKind::InvalidTimestamp)?;
4620 let zstd = wheel.file.zstd.as_ref().map(|zstd| ZstdWheel {
4621 hash: zstd.hashes.iter().max().cloned().map(Hash::from),
4622 size: zstd.size,
4623 });
4624 Ok(Self {
4625 url,
4626 hash,
4627 size,
4628 upload_time,
4629 filename,
4630 zstd,
4631 })
4632 }
4633
4634 fn from_direct_dist(direct_dist: &DirectUrlBuiltDist, hashes: &[HashDigest]) -> Self {
4635 Self {
4636 url: WheelWireSource::Url {
4637 url: normalize_url(direct_dist.url.to_url()),
4638 },
4639 hash: hashes.iter().max().cloned().map(Hash::from),
4640 size: None,
4641 upload_time: None,
4642 filename: direct_dist.filename.clone(),
4643 zstd: None,
4644 }
4645 }
4646
4647 fn from_path_dist(path_dist: &PathBuiltDist, hashes: &[HashDigest]) -> Self {
4648 Self {
4649 url: WheelWireSource::Filename {
4650 filename: path_dist.filename.clone(),
4651 },
4652 hash: hashes.iter().max().cloned().map(Hash::from),
4653 size: None,
4654 upload_time: None,
4655 filename: path_dist.filename.clone(),
4656 zstd: None,
4657 }
4658 }
4659
4660 pub(crate) fn to_registry_wheel(
4661 &self,
4662 source: &RegistrySource,
4663 root: &Path,
4664 ) -> Result<RegistryBuiltWheel, LockError> {
4665 let filename: WheelFilename = self.filename.clone();
4666
4667 match source {
4668 RegistrySource::Url(url) => {
4669 let file_location = match &self.url {
4670 WheelWireSource::Url { url: file_url } => {
4671 FileLocation::AbsoluteUrl(file_url.clone())
4672 }
4673 WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => {
4674 return Err(LockErrorKind::MissingUrl {
4675 name: filename.name,
4676 version: filename.version,
4677 }
4678 .into());
4679 }
4680 };
4681 let file = Box::new(uv_distribution_types::File {
4682 dist_info_metadata: false,
4683 filename: SmallString::from(filename.to_string()),
4684 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
4685 requires_python: None,
4686 size: self.size,
4687 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
4688 url: file_location,
4689 yanked: None,
4690 zstd: self
4691 .zstd
4692 .as_ref()
4693 .map(|zstd| uv_distribution_types::Zstd {
4694 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
4695 size: zstd.size,
4696 })
4697 .map(Box::new),
4698 });
4699 let index = IndexUrl::from(VerbatimUrl::from_url(
4700 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
4701 ));
4702 Ok(RegistryBuiltWheel {
4703 filename,
4704 file,
4705 index,
4706 })
4707 }
4708 RegistrySource::Path(index_path) => {
4709 let file_location = match &self.url {
4710 WheelWireSource::Url { url: file_url } => {
4711 FileLocation::AbsoluteUrl(file_url.clone())
4712 }
4713 WheelWireSource::Path { path: file_path } => {
4714 let file_path = root.join(index_path).join(file_path);
4715 let file_url =
4716 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
4717 LockErrorKind::PathToUrl {
4718 path: file_path.into_boxed_path(),
4719 }
4720 })?;
4721 FileLocation::AbsoluteUrl(UrlString::from(file_url))
4722 }
4723 WheelWireSource::Filename { .. } => {
4724 return Err(LockErrorKind::MissingPath {
4725 name: filename.name,
4726 version: filename.version,
4727 }
4728 .into());
4729 }
4730 };
4731 let file = Box::new(uv_distribution_types::File {
4732 dist_info_metadata: false,
4733 filename: SmallString::from(filename.to_string()),
4734 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
4735 requires_python: None,
4736 size: self.size,
4737 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
4738 url: file_location,
4739 yanked: None,
4740 zstd: self
4741 .zstd
4742 .as_ref()
4743 .map(|zstd| uv_distribution_types::Zstd {
4744 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
4745 size: zstd.size,
4746 })
4747 .map(Box::new),
4748 });
4749 let index = IndexUrl::from(
4750 VerbatimUrl::from_absolute_path(root.join(index_path))
4751 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
4752 );
4753 Ok(RegistryBuiltWheel {
4754 filename,
4755 file,
4756 index,
4757 })
4758 }
4759 }
4760 }
4761}
4762
4763#[derive(Clone, Debug, serde::Deserialize)]
4764#[serde(rename_all = "kebab-case")]
4765struct WheelWire {
4766 #[serde(flatten)]
4767 url: WheelWireSource,
4768 hash: Option<Hash>,
4774 size: Option<u64>,
4778 #[serde(alias = "upload_time")]
4782 upload_time: Option<Timestamp>,
4783 #[serde(alias = "zstd")]
4785 zstd: Option<ZstdWheel>,
4786}
4787
4788#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4789#[serde(untagged, rename_all = "kebab-case")]
4790enum WheelWireSource {
4791 Url {
4793 url: UrlString,
4798 },
4799 Path {
4801 path: Box<Path>,
4803 },
4804 Filename {
4808 filename: WheelFilename,
4811 },
4812}
4813
4814impl Wheel {
4815 fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
4817 let mut table = InlineTable::new();
4818 match &self.url {
4819 WheelWireSource::Url { url } => {
4820 table.insert("url", Value::from(url.as_ref()));
4821 }
4822 WheelWireSource::Path { path } => {
4823 table.insert("path", Value::from(PortablePath::from(path).to_string()));
4824 }
4825 WheelWireSource::Filename { filename } => {
4826 table.insert("filename", Value::from(filename.to_string()));
4827 }
4828 }
4829 if let Some(ref hash) = self.hash {
4830 table.insert("hash", Value::from(hash.to_string()));
4831 }
4832 if let Some(size) = self.size {
4833 table.insert(
4834 "size",
4835 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4836 );
4837 }
4838 if let Some(upload_time) = self.upload_time {
4839 table.insert("upload-time", Value::from(upload_time.to_string()));
4840 }
4841 if let Some(zstd) = &self.zstd {
4842 let mut inner = InlineTable::new();
4843 if let Some(ref hash) = zstd.hash {
4844 inner.insert("hash", Value::from(hash.to_string()));
4845 }
4846 if let Some(size) = zstd.size {
4847 inner.insert(
4848 "size",
4849 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4850 );
4851 }
4852 table.insert("zstd", Value::from(inner));
4853 }
4854 Ok(table)
4855 }
4856}
4857
4858impl TryFrom<WheelWire> for Wheel {
4859 type Error = String;
4860
4861 fn try_from(wire: WheelWire) -> Result<Self, String> {
4862 let filename = match &wire.url {
4863 WheelWireSource::Url { url } => {
4864 let filename = url.filename().map_err(|err| err.to_string())?;
4865 filename.parse::<WheelFilename>().map_err(|err| {
4866 format!("failed to parse `{filename}` as wheel filename: {err}")
4867 })?
4868 }
4869 WheelWireSource::Path { path } => {
4870 let filename = path
4871 .file_name()
4872 .and_then(|file_name| file_name.to_str())
4873 .ok_or_else(|| {
4874 format!("path `{}` has no filename component", path.display())
4875 })?;
4876 filename.parse::<WheelFilename>().map_err(|err| {
4877 format!("failed to parse `{filename}` as wheel filename: {err}")
4878 })?
4879 }
4880 WheelWireSource::Filename { filename } => filename.clone(),
4881 };
4882
4883 Ok(Self {
4884 url: wire.url,
4885 hash: wire.hash,
4886 size: wire.size,
4887 upload_time: wire.upload_time,
4888 zstd: wire.zstd,
4889 filename,
4890 })
4891 }
4892}
4893
4894#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
4896pub struct Dependency {
4897 package_id: PackageId,
4898 extra: BTreeSet<ExtraName>,
4899 simplified_marker: SimplifiedMarkerTree,
4919 complexified_marker: UniversalMarker,
4923}
4924
4925impl Dependency {
4926 fn new(
4927 requires_python: &RequiresPython,
4928 package_id: PackageId,
4929 extra: BTreeSet<ExtraName>,
4930 complexified_marker: UniversalMarker,
4931 ) -> Self {
4932 let simplified_marker =
4933 SimplifiedMarkerTree::new(requires_python, complexified_marker.combined());
4934 let complexified_marker = simplified_marker.into_marker(requires_python);
4935 Self {
4936 package_id,
4937 extra,
4938 simplified_marker,
4939 complexified_marker: UniversalMarker::from_combined(complexified_marker),
4940 }
4941 }
4942
4943 fn from_annotated_dist(
4944 requires_python: &RequiresPython,
4945 annotated_dist: &AnnotatedDist,
4946 complexified_marker: UniversalMarker,
4947 root: &Path,
4948 ) -> Result<Self, LockError> {
4949 let package_id = PackageId::from_annotated_dist(annotated_dist, root)?;
4950 let extra = annotated_dist.extra.iter().cloned().collect();
4951 Ok(Self::new(
4952 requires_python,
4953 package_id,
4954 extra,
4955 complexified_marker,
4956 ))
4957 }
4958
4959 fn to_toml(
4961 &self,
4962 _requires_python: &RequiresPython,
4963 dist_count_by_name: &FxHashMap<PackageName, u64>,
4964 ) -> Table {
4965 let mut table = Table::new();
4966 self.package_id
4967 .to_toml(Some(dist_count_by_name), &mut table);
4968 if !self.extra.is_empty() {
4969 let extra_array = self
4970 .extra
4971 .iter()
4972 .map(ToString::to_string)
4973 .collect::<Array>();
4974 table.insert("extra", value(extra_array));
4975 }
4976 if let Some(marker) = self.simplified_marker.try_to_string() {
4977 table.insert("marker", value(marker));
4978 }
4979
4980 table
4981 }
4982
4983 pub fn package_name(&self) -> &PackageName {
4985 &self.package_id.name
4986 }
4987
4988 pub fn extra(&self) -> &BTreeSet<ExtraName> {
4990 &self.extra
4991 }
4992}
4993
4994impl Display for Dependency {
4995 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
4996 match (self.extra.is_empty(), self.package_id.version.as_ref()) {
4997 (true, Some(version)) => write!(f, "{}=={}", self.package_id.name, version),
4998 (true, None) => write!(f, "{}", self.package_id.name),
4999 (false, Some(version)) => write!(
5000 f,
5001 "{}[{}]=={}",
5002 self.package_id.name,
5003 self.extra.iter().join(","),
5004 version
5005 ),
5006 (false, None) => write!(
5007 f,
5008 "{}[{}]",
5009 self.package_id.name,
5010 self.extra.iter().join(",")
5011 ),
5012 }
5013 }
5014}
5015
5016#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)]
5018#[serde(rename_all = "kebab-case")]
5019struct DependencyWire {
5020 #[serde(flatten)]
5021 package_id: PackageIdForDependency,
5022 #[serde(default)]
5023 extra: BTreeSet<ExtraName>,
5024 #[serde(default)]
5025 marker: SimplifiedMarkerTree,
5026}
5027
5028impl DependencyWire {
5029 fn unwire(
5030 self,
5031 requires_python: &RequiresPython,
5032 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
5033 ) -> Result<Dependency, LockError> {
5034 let complexified_marker = self.marker.into_marker(requires_python);
5035 Ok(Dependency {
5036 package_id: self.package_id.unwire(unambiguous_package_ids)?,
5037 extra: self.extra,
5038 simplified_marker: self.marker,
5039 complexified_marker: UniversalMarker::from_combined(complexified_marker),
5040 })
5041 }
5042}
5043
5044#[derive(Clone, Debug, PartialEq, Eq)]
5049struct Hash(HashDigest);
5050
5051impl From<HashDigest> for Hash {
5052 fn from(hd: HashDigest) -> Self {
5053 Self(hd)
5054 }
5055}
5056
5057impl FromStr for Hash {
5058 type Err = HashParseError;
5059
5060 fn from_str(s: &str) -> Result<Self, HashParseError> {
5061 let (algorithm, digest) = s.split_once(':').ok_or(HashParseError(
5062 "expected '{algorithm}:{digest}', but found no ':' in hash digest",
5063 ))?;
5064 let algorithm = algorithm
5065 .parse()
5066 .map_err(|_| HashParseError("unrecognized hash algorithm"))?;
5067 Ok(Self(HashDigest {
5068 algorithm,
5069 digest: digest.into(),
5070 }))
5071 }
5072}
5073
5074impl Display for Hash {
5075 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5076 write!(f, "{}:{}", self.0.algorithm, self.0.digest)
5077 }
5078}
5079
5080impl<'de> serde::Deserialize<'de> for Hash {
5081 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5082 where
5083 D: serde::de::Deserializer<'de>,
5084 {
5085 struct Visitor;
5086
5087 impl serde::de::Visitor<'_> for Visitor {
5088 type Value = Hash;
5089
5090 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
5091 f.write_str("a string")
5092 }
5093
5094 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
5095 Hash::from_str(v).map_err(serde::de::Error::custom)
5096 }
5097 }
5098
5099 deserializer.deserialize_str(Visitor)
5100 }
5101}
5102
5103impl From<Hash> for Hashes {
5104 fn from(value: Hash) -> Self {
5105 match value.0.algorithm {
5106 HashAlgorithm::Md5 => Self {
5107 md5: Some(value.0.digest),
5108 sha256: None,
5109 sha384: None,
5110 sha512: None,
5111 blake2b: None,
5112 },
5113 HashAlgorithm::Sha256 => Self {
5114 md5: None,
5115 sha256: Some(value.0.digest),
5116 sha384: None,
5117 sha512: None,
5118 blake2b: None,
5119 },
5120 HashAlgorithm::Sha384 => Self {
5121 md5: None,
5122 sha256: None,
5123 sha384: Some(value.0.digest),
5124 sha512: None,
5125 blake2b: None,
5126 },
5127 HashAlgorithm::Sha512 => Self {
5128 md5: None,
5129 sha256: None,
5130 sha384: None,
5131 sha512: Some(value.0.digest),
5132 blake2b: None,
5133 },
5134 HashAlgorithm::Blake2b => Self {
5135 md5: None,
5136 sha256: None,
5137 sha384: None,
5138 sha512: None,
5139 blake2b: Some(value.0.digest),
5140 },
5141 }
5142 }
5143}
5144
5145fn normalize_file_location(location: &FileLocation) -> Result<UrlString, ToUrlError> {
5147 match location {
5148 FileLocation::AbsoluteUrl(absolute) => Ok(absolute.without_fragment().into_owned()),
5149 FileLocation::RelativeUrl(_, _) => Ok(normalize_url(location.to_url()?)),
5150 }
5151}
5152
5153fn normalize_url(mut url: DisplaySafeUrl) -> UrlString {
5155 url.set_fragment(None);
5156 UrlString::from(url)
5157}
5158
5159fn normalize_requirement(
5169 mut requirement: Requirement,
5170 root: &Path,
5171 requires_python: &RequiresPython,
5172) -> Result<Requirement, LockError> {
5173 requirement.extras.sort();
5175 requirement.groups.sort();
5176
5177 match requirement.source {
5179 RequirementSource::Git {
5180 git,
5181 subdirectory,
5182 url: _,
5183 } => {
5184 let git = {
5186 let mut repository = git.repository().clone();
5187
5188 repository.remove_credentials();
5190
5191 repository.set_fragment(None);
5193 repository.set_query(None);
5194
5195 GitUrl::from_fields(
5196 repository,
5197 git.reference().clone(),
5198 git.precise(),
5199 git.lfs(),
5200 )?
5201 };
5202
5203 let url = DisplaySafeUrl::from(ParsedGitUrl {
5205 url: git.clone(),
5206 subdirectory: subdirectory.clone(),
5207 });
5208
5209 Ok(Requirement {
5210 name: requirement.name,
5211 extras: requirement.extras,
5212 groups: requirement.groups,
5213 marker: requires_python.simplify_markers(requirement.marker),
5214 source: RequirementSource::Git {
5215 git,
5216 subdirectory,
5217 url: VerbatimUrl::from_url(url),
5218 },
5219 origin: None,
5220 })
5221 }
5222 RequirementSource::Path {
5223 install_path,
5224 ext,
5225 url: _,
5226 } => {
5227 let install_path =
5228 uv_fs::normalize_path_buf(root.join(&install_path)).into_boxed_path();
5229 let url = VerbatimUrl::from_normalized_path(&install_path)
5230 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5231
5232 Ok(Requirement {
5233 name: requirement.name,
5234 extras: requirement.extras,
5235 groups: requirement.groups,
5236 marker: requires_python.simplify_markers(requirement.marker),
5237 source: RequirementSource::Path {
5238 install_path,
5239 ext,
5240 url,
5241 },
5242 origin: None,
5243 })
5244 }
5245 RequirementSource::Directory {
5246 install_path,
5247 editable,
5248 r#virtual,
5249 url: _,
5250 } => {
5251 let install_path =
5252 uv_fs::normalize_path_buf(root.join(&install_path)).into_boxed_path();
5253 let url = VerbatimUrl::from_normalized_path(&install_path)
5254 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5255
5256 Ok(Requirement {
5257 name: requirement.name,
5258 extras: requirement.extras,
5259 groups: requirement.groups,
5260 marker: requires_python.simplify_markers(requirement.marker),
5261 source: RequirementSource::Directory {
5262 install_path,
5263 editable: Some(editable.unwrap_or(false)),
5264 r#virtual: Some(r#virtual.unwrap_or(false)),
5265 url,
5266 },
5267 origin: None,
5268 })
5269 }
5270 RequirementSource::Registry {
5271 specifier,
5272 index,
5273 conflict,
5274 } => {
5275 let index = index
5277 .map(|index| index.url.into_url())
5278 .map(|mut index| {
5279 index.remove_credentials();
5280 index
5281 })
5282 .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index))));
5283 Ok(Requirement {
5284 name: requirement.name,
5285 extras: requirement.extras,
5286 groups: requirement.groups,
5287 marker: requires_python.simplify_markers(requirement.marker),
5288 source: RequirementSource::Registry {
5289 specifier,
5290 index,
5291 conflict,
5292 },
5293 origin: None,
5294 })
5295 }
5296 RequirementSource::Url {
5297 mut location,
5298 subdirectory,
5299 ext,
5300 url: _,
5301 } => {
5302 location.remove_credentials();
5304
5305 location.set_fragment(None);
5307
5308 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
5310 url: location.clone(),
5311 subdirectory: subdirectory.clone(),
5312 ext,
5313 });
5314
5315 Ok(Requirement {
5316 name: requirement.name,
5317 extras: requirement.extras,
5318 groups: requirement.groups,
5319 marker: requires_python.simplify_markers(requirement.marker),
5320 source: RequirementSource::Url {
5321 location,
5322 subdirectory,
5323 ext,
5324 url: VerbatimUrl::from_url(url),
5325 },
5326 origin: None,
5327 })
5328 }
5329 }
5330}
5331
5332#[derive(Debug)]
5333pub struct LockError {
5334 kind: Box<LockErrorKind>,
5335 hint: Option<WheelTagHint>,
5336}
5337
5338impl std::error::Error for LockError {
5339 fn source(&self) -> Option<&(dyn Error + 'static)> {
5340 self.kind.source()
5341 }
5342}
5343
5344impl std::fmt::Display for LockError {
5345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5346 write!(f, "{}", self.kind)?;
5347 if let Some(hint) = &self.hint {
5348 write!(f, "\n\n{hint}")?;
5349 }
5350 Ok(())
5351 }
5352}
5353
5354impl LockError {
5355 pub fn is_resolution(&self) -> bool {
5357 matches!(&*self.kind, LockErrorKind::Resolution { .. })
5358 }
5359}
5360
5361impl<E> From<E> for LockError
5362where
5363 LockErrorKind: From<E>,
5364{
5365 fn from(err: E) -> Self {
5366 Self {
5367 kind: Box::new(LockErrorKind::from(err)),
5368 hint: None,
5369 }
5370 }
5371}
5372
5373#[derive(Debug, Clone, PartialEq, Eq)]
5374#[allow(clippy::enum_variant_names)]
5375enum WheelTagHint {
5376 LanguageTags {
5379 package: PackageName,
5380 version: Option<Version>,
5381 tags: BTreeSet<LanguageTag>,
5382 best: Option<LanguageTag>,
5383 },
5384 AbiTags {
5387 package: PackageName,
5388 version: Option<Version>,
5389 tags: BTreeSet<AbiTag>,
5390 best: Option<AbiTag>,
5391 },
5392 PlatformTags {
5395 package: PackageName,
5396 version: Option<Version>,
5397 tags: BTreeSet<PlatformTag>,
5398 best: Option<PlatformTag>,
5399 markers: MarkerEnvironment,
5400 },
5401}
5402
5403impl WheelTagHint {
5404 fn from_wheels(
5406 name: &PackageName,
5407 version: Option<&Version>,
5408 filenames: &[&WheelFilename],
5409 tags: &Tags,
5410 markers: &MarkerEnvironment,
5411 ) -> Option<Self> {
5412 let incompatibility = filenames
5413 .iter()
5414 .map(|filename| {
5415 tags.compatibility(
5416 filename.python_tags(),
5417 filename.abi_tags(),
5418 filename.platform_tags(),
5419 )
5420 })
5421 .max()?;
5422 match incompatibility {
5423 TagCompatibility::Incompatible(IncompatibleTag::Python) => {
5424 let best = tags.python_tag();
5425 let tags = Self::python_tags(filenames.iter().copied()).collect::<BTreeSet<_>>();
5426 if tags.is_empty() {
5427 None
5428 } else {
5429 Some(Self::LanguageTags {
5430 package: name.clone(),
5431 version: version.cloned(),
5432 tags,
5433 best,
5434 })
5435 }
5436 }
5437 TagCompatibility::Incompatible(IncompatibleTag::Abi) => {
5438 let best = tags.abi_tag();
5439 let tags = Self::abi_tags(filenames.iter().copied())
5440 .filter(|tag| *tag != AbiTag::None)
5449 .collect::<BTreeSet<_>>();
5450 if tags.is_empty() {
5451 None
5452 } else {
5453 Some(Self::AbiTags {
5454 package: name.clone(),
5455 version: version.cloned(),
5456 tags,
5457 best,
5458 })
5459 }
5460 }
5461 TagCompatibility::Incompatible(IncompatibleTag::Platform) => {
5462 let best = tags.platform_tag().cloned();
5463 let incompatible_tags = Self::platform_tags(filenames.iter().copied(), tags)
5464 .cloned()
5465 .collect::<BTreeSet<_>>();
5466 if incompatible_tags.is_empty() {
5467 None
5468 } else {
5469 Some(Self::PlatformTags {
5470 package: name.clone(),
5471 version: version.cloned(),
5472 tags: incompatible_tags,
5473 best,
5474 markers: markers.clone(),
5475 })
5476 }
5477 }
5478 _ => None,
5479 }
5480 }
5481
5482 fn python_tags<'a>(
5484 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5485 ) -> impl Iterator<Item = LanguageTag> + 'a {
5486 filenames.flat_map(WheelFilename::python_tags).copied()
5487 }
5488
5489 fn abi_tags<'a>(
5491 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5492 ) -> impl Iterator<Item = AbiTag> + 'a {
5493 filenames.flat_map(WheelFilename::abi_tags).copied()
5494 }
5495
5496 fn platform_tags<'a>(
5499 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5500 tags: &'a Tags,
5501 ) -> impl Iterator<Item = &'a PlatformTag> + 'a {
5502 filenames.flat_map(move |filename| {
5503 if filename.python_tags().iter().any(|wheel_py| {
5504 filename
5505 .abi_tags()
5506 .iter()
5507 .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
5508 }) {
5509 filename.platform_tags().iter()
5510 } else {
5511 [].iter()
5512 }
5513 })
5514 }
5515
5516 fn suggest_environment_marker(markers: &MarkerEnvironment) -> String {
5517 let sys_platform = markers.sys_platform();
5518 let platform_machine = markers.platform_machine();
5519
5520 if platform_machine.is_empty() {
5522 format!("sys_platform == '{sys_platform}'")
5523 } else {
5524 format!("sys_platform == '{sys_platform}' and platform_machine == '{platform_machine}'")
5525 }
5526 }
5527}
5528
5529impl std::fmt::Display for WheelTagHint {
5530 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5531 match self {
5532 Self::LanguageTags {
5533 package,
5534 version,
5535 tags,
5536 best,
5537 } => {
5538 if let Some(best) = best {
5539 let s = if tags.len() == 1 { "" } else { "s" };
5540 let best = if let Some(pretty) = best.pretty() {
5541 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5542 } else {
5543 format!("{}", best.cyan())
5544 };
5545 if let Some(version) = version {
5546 write!(
5547 f,
5548 "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python implementation tag{s}: {}",
5549 "hint".bold().cyan(),
5550 ":".bold(),
5551 best,
5552 package.cyan(),
5553 format!("v{version}").cyan(),
5554 tags.iter()
5555 .map(|tag| format!("`{}`", tag.cyan()))
5556 .join(", "),
5557 )
5558 } else {
5559 write!(
5560 f,
5561 "{}{} You're using {}, but `{}` only has wheels with the following Python implementation tag{s}: {}",
5562 "hint".bold().cyan(),
5563 ":".bold(),
5564 best,
5565 package.cyan(),
5566 tags.iter()
5567 .map(|tag| format!("`{}`", tag.cyan()))
5568 .join(", "),
5569 )
5570 }
5571 } else {
5572 let s = if tags.len() == 1 { "" } else { "s" };
5573 if let Some(version) = version {
5574 write!(
5575 f,
5576 "{}{} Wheels are available for `{}` ({}) with the following Python implementation tag{s}: {}",
5577 "hint".bold().cyan(),
5578 ":".bold(),
5579 package.cyan(),
5580 format!("v{version}").cyan(),
5581 tags.iter()
5582 .map(|tag| format!("`{}`", tag.cyan()))
5583 .join(", "),
5584 )
5585 } else {
5586 write!(
5587 f,
5588 "{}{} Wheels are available for `{}` with the following Python implementation tag{s}: {}",
5589 "hint".bold().cyan(),
5590 ":".bold(),
5591 package.cyan(),
5592 tags.iter()
5593 .map(|tag| format!("`{}`", tag.cyan()))
5594 .join(", "),
5595 )
5596 }
5597 }
5598 }
5599 Self::AbiTags {
5600 package,
5601 version,
5602 tags,
5603 best,
5604 } => {
5605 if let Some(best) = best {
5606 let s = if tags.len() == 1 { "" } else { "s" };
5607 let best = if let Some(pretty) = best.pretty() {
5608 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5609 } else {
5610 format!("{}", best.cyan())
5611 };
5612 if let Some(version) = version {
5613 write!(
5614 f,
5615 "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python ABI tag{s}: {}",
5616 "hint".bold().cyan(),
5617 ":".bold(),
5618 best,
5619 package.cyan(),
5620 format!("v{version}").cyan(),
5621 tags.iter()
5622 .map(|tag| format!("`{}`", tag.cyan()))
5623 .join(", "),
5624 )
5625 } else {
5626 write!(
5627 f,
5628 "{}{} You're using {}, but `{}` only has wheels with the following Python ABI tag{s}: {}",
5629 "hint".bold().cyan(),
5630 ":".bold(),
5631 best,
5632 package.cyan(),
5633 tags.iter()
5634 .map(|tag| format!("`{}`", tag.cyan()))
5635 .join(", "),
5636 )
5637 }
5638 } else {
5639 let s = if tags.len() == 1 { "" } else { "s" };
5640 if let Some(version) = version {
5641 write!(
5642 f,
5643 "{}{} Wheels are available for `{}` ({}) with the following Python ABI tag{s}: {}",
5644 "hint".bold().cyan(),
5645 ":".bold(),
5646 package.cyan(),
5647 format!("v{version}").cyan(),
5648 tags.iter()
5649 .map(|tag| format!("`{}`", tag.cyan()))
5650 .join(", "),
5651 )
5652 } else {
5653 write!(
5654 f,
5655 "{}{} Wheels are available for `{}` with the following Python ABI tag{s}: {}",
5656 "hint".bold().cyan(),
5657 ":".bold(),
5658 package.cyan(),
5659 tags.iter()
5660 .map(|tag| format!("`{}`", tag.cyan()))
5661 .join(", "),
5662 )
5663 }
5664 }
5665 }
5666 Self::PlatformTags {
5667 package,
5668 version,
5669 tags,
5670 best,
5671 markers,
5672 } => {
5673 let s = if tags.len() == 1 { "" } else { "s" };
5674 if let Some(best) = best {
5675 let example_marker = Self::suggest_environment_marker(markers);
5676 let best = if let Some(pretty) = best.pretty() {
5677 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5678 } else {
5679 format!("`{}`", best.cyan())
5680 };
5681 let package_ref = if let Some(version) = version {
5682 format!("`{}` ({})", package.cyan(), format!("v{version}").cyan())
5683 } else {
5684 format!("`{}`", package.cyan())
5685 };
5686 write!(
5687 f,
5688 "{}{} 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",
5689 "hint".bold().cyan(),
5690 ":".bold(),
5691 best,
5692 package_ref,
5693 tags.iter()
5694 .map(|tag| format!("`{}`", tag.cyan()))
5695 .join(", "),
5696 format!("\"{example_marker}\"").cyan(),
5697 "tool.uv.required-environments".green()
5698 )
5699 } else {
5700 if let Some(version) = version {
5701 write!(
5702 f,
5703 "{}{} Wheels are available for `{}` ({}) on the following platform{s}: {}",
5704 "hint".bold().cyan(),
5705 ":".bold(),
5706 package.cyan(),
5707 format!("v{version}").cyan(),
5708 tags.iter()
5709 .map(|tag| format!("`{}`", tag.cyan()))
5710 .join(", "),
5711 )
5712 } else {
5713 write!(
5714 f,
5715 "{}{} Wheels are available for `{}` on the following platform{s}: {}",
5716 "hint".bold().cyan(),
5717 ":".bold(),
5718 package.cyan(),
5719 tags.iter()
5720 .map(|tag| format!("`{}`", tag.cyan()))
5721 .join(", "),
5722 )
5723 }
5724 }
5725 }
5726 }
5727 }
5728}
5729
5730#[derive(Debug, thiserror::Error)]
5737enum LockErrorKind {
5738 #[error("Found duplicate package `{id}`", id = id.cyan())]
5741 DuplicatePackage {
5742 id: PackageId,
5744 },
5745 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = id.cyan(), dependency = dependency.cyan())]
5748 DuplicateDependency {
5749 id: PackageId,
5752 dependency: Dependency,
5754 },
5755 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}[{extra}]").cyan(), dependency = dependency.cyan())]
5759 DuplicateOptionalDependency {
5760 id: PackageId,
5763 extra: ExtraName,
5765 dependency: Dependency,
5767 },
5768 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}:{group}").cyan(), dependency = dependency.cyan())]
5772 DuplicateDevDependency {
5773 id: PackageId,
5776 group: GroupName,
5778 dependency: Dependency,
5780 },
5781 #[error(transparent)]
5784 InvalidUrl(
5785 #[from]
5788 ToUrlError,
5789 ),
5790 #[error("Failed to parse file extension for `{id}`; expected one of: {err}", id = id.cyan())]
5793 MissingExtension {
5794 id: PackageId,
5796 err: ExtensionError,
5798 },
5799 #[error("Failed to parse Git URL")]
5801 InvalidGitSourceUrl(
5802 #[source]
5805 SourceParseError,
5806 ),
5807 #[error("Failed to parse timestamp")]
5808 InvalidTimestamp(
5809 #[source]
5812 jiff::Error,
5813 ),
5814 #[error("For package `{id}`, found dependency `{dependency}` with no locked package", id = id.cyan(), dependency = dependency.cyan())]
5818 UnrecognizedDependency {
5819 id: PackageId,
5821 dependency: Dependency,
5824 },
5825 #[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" })]
5828 Hash {
5829 id: PackageId,
5831 artifact_type: &'static str,
5834 expected: bool,
5836 },
5837 #[error("Found package `{id}` with extra `{extra}` but no base package", id = id.cyan(), extra = extra.cyan())]
5840 MissingExtraBase {
5841 id: PackageId,
5843 extra: ExtraName,
5845 },
5846 #[error("Found package `{id}` with development dependency group `{group}` but no base package", id = id.cyan())]
5850 MissingDevBase {
5851 id: PackageId,
5853 group: GroupName,
5855 },
5856 #[error("Wheels cannot come from {source_type} sources")]
5859 InvalidWheelSource {
5860 id: PackageId,
5862 source_type: &'static str,
5864 },
5865 #[error("Found registry distribution `{name}` ({version}) without a valid URL", name = name.cyan(), version = format!("v{version}").cyan())]
5868 MissingUrl {
5869 name: PackageName,
5871 version: Version,
5873 },
5874 #[error("Found registry distribution `{name}` ({version}) without a valid path", name = name.cyan(), version = format!("v{version}").cyan())]
5877 MissingPath {
5878 name: PackageName,
5880 version: Version,
5882 },
5883 #[error("Found registry distribution `{id}` without a valid filename", id = id.cyan())]
5886 MissingFilename {
5887 id: PackageId,
5889 },
5890 #[error("Distribution `{id}` can't be installed because it doesn't have a source distribution or wheel for the current platform", id = id.cyan())]
5893 NeitherSourceDistNorWheel {
5894 id: PackageId,
5896 },
5897 #[error("Distribution `{id}` can't be installed because it is marked as both `--no-binary` and `--no-build`", id = id.cyan())]
5899 NoBinaryNoBuild {
5900 id: PackageId,
5902 },
5903 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but has no source distribution", id = id.cyan())]
5906 NoBinary {
5907 id: PackageId,
5909 },
5910 #[error("Distribution `{id}` can't be installed because it is marked as `--no-build` but has no binary distribution", id = id.cyan())]
5913 NoBuild {
5914 id: PackageId,
5916 },
5917 #[error("Distribution `{id}` can't be installed because the binary distribution is incompatible with the current platform", id = id.cyan())]
5920 IncompatibleWheelOnly {
5921 id: PackageId,
5923 },
5924 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but is itself a binary distribution", id = id.cyan())]
5926 NoBinaryWheelOnly {
5927 id: PackageId,
5929 },
5930 #[error("Found dependency `{id}` with no locked distribution", id = id.cyan())]
5932 VerbatimUrl {
5933 id: PackageId,
5935 #[source]
5937 err: VerbatimUrlError,
5938 },
5939 #[error("Could not compute relative path between workspace and distribution")]
5941 DistributionRelativePath(
5942 #[source]
5944 io::Error,
5945 ),
5946 #[error("Could not compute relative path between workspace and index")]
5948 IndexRelativePath(
5949 #[source]
5951 io::Error,
5952 ),
5953 #[error("Could not compute absolute path from workspace root and lockfile path")]
5955 AbsolutePath(
5956 #[source]
5958 io::Error,
5959 ),
5960 #[error("Dependency `{name}` has missing `version` field but has more than one matching package", name = name.cyan())]
5963 MissingDependencyVersion {
5964 name: PackageName,
5966 },
5967 #[error("Dependency `{name}` has missing `source` field but has more than one matching package", name = name.cyan())]
5970 MissingDependencySource {
5971 name: PackageName,
5973 },
5974 #[error("Could not compute relative path between workspace and requirement")]
5976 RequirementRelativePath(
5977 #[source]
5979 io::Error,
5980 ),
5981 #[error("Could not convert between URL and path")]
5983 RequirementVerbatimUrl(
5984 #[source]
5986 VerbatimUrlError,
5987 ),
5988 #[error("Could not convert between URL and path")]
5990 RegistryVerbatimUrl(
5991 #[source]
5993 VerbatimUrlError,
5994 ),
5995 #[error("Failed to convert path to URL: {path}", path = path.display().cyan())]
5997 PathToUrl { path: Box<Path> },
5998 #[error("Failed to convert URL to path: {url}", url = url.cyan())]
6000 UrlToPath { url: DisplaySafeUrl },
6001 #[error("Found multiple packages matching `{name}`", name = name.cyan())]
6004 MultipleRootPackages {
6005 name: PackageName,
6007 },
6008 #[error("Could not find root package `{name}`", name = name.cyan())]
6010 MissingRootPackage {
6011 name: PackageName,
6013 },
6014 #[error("Failed to generate package metadata for `{id}`", id = id.cyan())]
6016 Resolution {
6017 id: PackageId,
6019 #[source]
6021 err: uv_distribution::Error,
6022 },
6023 #[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())]
6026 InconsistentVersions {
6027 name: PackageName,
6029 version: Version,
6031 wheel: Wheel,
6033 },
6034 #[error(
6035 "Found conflicting extras `{package1}[{extra1}]` \
6036 and `{package2}[{extra2}]` enabled simultaneously"
6037 )]
6038 ConflictingExtra {
6039 package1: PackageName,
6040 extra1: ExtraName,
6041 package2: PackageName,
6042 extra2: ExtraName,
6043 },
6044 #[error(transparent)]
6045 GitUrlParse(#[from] GitUrlParseError),
6046 #[error("Failed to read `{path}`")]
6047 UnreadablePyprojectToml {
6048 path: PathBuf,
6049 #[source]
6050 err: std::io::Error,
6051 },
6052 #[error("Failed to parse `{path}`")]
6053 InvalidPyprojectToml {
6054 path: PathBuf,
6055 #[source]
6056 err: toml::de::Error,
6057 },
6058 #[error("Workspace member `{id}` has non-local source", id = id.cyan())]
6060 NonLocalWorkspaceMember {
6061 id: PackageId,
6063 },
6064}
6065
6066#[derive(Debug, thiserror::Error)]
6068enum SourceParseError {
6069 #[error("Invalid URL in source `{given}`")]
6071 InvalidUrl {
6072 given: String,
6074 #[source]
6076 err: DisplaySafeUrlError,
6077 },
6078 #[error("Missing SHA in source `{given}`")]
6080 MissingSha {
6081 given: String,
6083 },
6084 #[error("Invalid SHA in source `{given}`")]
6086 InvalidSha {
6087 given: String,
6089 },
6090}
6091
6092#[derive(Clone, Debug, Eq, PartialEq)]
6094struct HashParseError(&'static str);
6095
6096impl std::error::Error for HashParseError {}
6097
6098impl Display for HashParseError {
6099 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
6100 Display::fmt(self.0, f)
6101 }
6102}
6103
6104fn each_element_on_its_line_array(elements: impl Iterator<Item = impl Into<Value>>) -> Array {
6115 let mut array = elements
6116 .map(|item| {
6117 let mut value = item.into();
6118 value.decor_mut().set_prefix("\n ");
6120 value
6121 })
6122 .collect::<Array>();
6123 array.set_trailing_comma(true);
6126 array.set_trailing("\n");
6128 array
6129}
6130
6131fn simplified_universal_markers(
6136 markers: &[UniversalMarker],
6137 requires_python: &RequiresPython,
6138) -> Vec<String> {
6139 let mut pep508_only = vec![];
6140 let mut seen = FxHashSet::default();
6141 for marker in markers {
6142 let simplified =
6143 SimplifiedMarkerTree::new(requires_python, marker.pep508()).as_simplified_marker_tree();
6144 if seen.insert(simplified) {
6145 pep508_only.push(simplified);
6146 }
6147 }
6148 let any_overlap = pep508_only
6149 .iter()
6150 .tuple_combinations()
6151 .any(|(&marker1, &marker2)| !marker1.is_disjoint(marker2));
6152 let markers = if !any_overlap {
6153 pep508_only
6154 } else {
6155 markers
6156 .iter()
6157 .map(|marker| {
6158 SimplifiedMarkerTree::new(requires_python, marker.combined())
6159 .as_simplified_marker_tree()
6160 })
6161 .collect()
6162 };
6163 markers
6164 .into_iter()
6165 .filter_map(MarkerTree::try_to_string)
6166 .collect()
6167}
6168
6169#[cfg(test)]
6170mod tests {
6171 use uv_warnings::anstream;
6172
6173 use super::*;
6174
6175 macro_rules! assert_stripped_snapshot {
6177 ($expr:expr, @$snapshot:literal) => {{
6178 let expr = format!("{}", $expr);
6179 let expr = format!("{}", anstream::adapter::strip_str(&expr));
6180 insta::assert_snapshot!(expr, @$snapshot);
6181 }};
6182 }
6183
6184 #[test]
6185 fn missing_dependency_source_unambiguous() {
6186 let data = r#"
6187version = 1
6188requires-python = ">=3.12"
6189
6190[[package]]
6191name = "a"
6192version = "0.1.0"
6193source = { registry = "https://pypi.org/simple" }
6194sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6195
6196[[package]]
6197name = "b"
6198version = "0.1.0"
6199source = { registry = "https://pypi.org/simple" }
6200sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6201
6202[[package.dependencies]]
6203name = "a"
6204version = "0.1.0"
6205"#;
6206 let result: Result<Lock, _> = toml::from_str(data);
6207 insta::assert_debug_snapshot!(result);
6208 }
6209
6210 #[test]
6211 fn missing_dependency_version_unambiguous() {
6212 let data = r#"
6213version = 1
6214requires-python = ">=3.12"
6215
6216[[package]]
6217name = "a"
6218version = "0.1.0"
6219source = { registry = "https://pypi.org/simple" }
6220sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6221
6222[[package]]
6223name = "b"
6224version = "0.1.0"
6225source = { registry = "https://pypi.org/simple" }
6226sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6227
6228[[package.dependencies]]
6229name = "a"
6230source = { registry = "https://pypi.org/simple" }
6231"#;
6232 let result: Result<Lock, _> = toml::from_str(data);
6233 insta::assert_debug_snapshot!(result);
6234 }
6235
6236 #[test]
6237 fn missing_dependency_source_version_unambiguous() {
6238 let data = r#"
6239version = 1
6240requires-python = ">=3.12"
6241
6242[[package]]
6243name = "a"
6244version = "0.1.0"
6245source = { registry = "https://pypi.org/simple" }
6246sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6247
6248[[package]]
6249name = "b"
6250version = "0.1.0"
6251source = { registry = "https://pypi.org/simple" }
6252sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6253
6254[[package.dependencies]]
6255name = "a"
6256"#;
6257 let result: Result<Lock, _> = toml::from_str(data);
6258 insta::assert_debug_snapshot!(result);
6259 }
6260
6261 #[test]
6262 fn missing_dependency_source_ambiguous() {
6263 let data = r#"
6264version = 1
6265requires-python = ">=3.12"
6266
6267[[package]]
6268name = "a"
6269version = "0.1.0"
6270source = { registry = "https://pypi.org/simple" }
6271sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6272
6273[[package]]
6274name = "a"
6275version = "0.1.1"
6276source = { registry = "https://pypi.org/simple" }
6277sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6278
6279[[package]]
6280name = "b"
6281version = "0.1.0"
6282source = { registry = "https://pypi.org/simple" }
6283sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6284
6285[[package.dependencies]]
6286name = "a"
6287version = "0.1.0"
6288"#;
6289 let result = toml::from_str::<Lock>(data).unwrap_err();
6290 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6291 }
6292
6293 #[test]
6294 fn missing_dependency_version_ambiguous() {
6295 let data = r#"
6296version = 1
6297requires-python = ">=3.12"
6298
6299[[package]]
6300name = "a"
6301version = "0.1.0"
6302source = { registry = "https://pypi.org/simple" }
6303sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6304
6305[[package]]
6306name = "a"
6307version = "0.1.1"
6308source = { registry = "https://pypi.org/simple" }
6309sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6310
6311[[package]]
6312name = "b"
6313version = "0.1.0"
6314source = { registry = "https://pypi.org/simple" }
6315sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6316
6317[[package.dependencies]]
6318name = "a"
6319source = { registry = "https://pypi.org/simple" }
6320"#;
6321 let result = toml::from_str::<Lock>(data).unwrap_err();
6322 assert_stripped_snapshot!(result, @"Dependency `a` has missing `version` field but has more than one matching package");
6323 }
6324
6325 #[test]
6326 fn missing_dependency_source_version_ambiguous() {
6327 let data = r#"
6328version = 1
6329requires-python = ">=3.12"
6330
6331[[package]]
6332name = "a"
6333version = "0.1.0"
6334source = { registry = "https://pypi.org/simple" }
6335sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6336
6337[[package]]
6338name = "a"
6339version = "0.1.1"
6340source = { registry = "https://pypi.org/simple" }
6341sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6342
6343[[package]]
6344name = "b"
6345version = "0.1.0"
6346source = { registry = "https://pypi.org/simple" }
6347sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6348
6349[[package.dependencies]]
6350name = "a"
6351"#;
6352 let result = toml::from_str::<Lock>(data).unwrap_err();
6353 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6354 }
6355
6356 #[test]
6357 fn missing_dependency_version_dynamic() {
6358 let data = r#"
6359version = 1
6360requires-python = ">=3.12"
6361
6362[[package]]
6363name = "a"
6364source = { editable = "path/to/a" }
6365
6366[[package]]
6367name = "a"
6368version = "0.1.1"
6369source = { registry = "https://pypi.org/simple" }
6370sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6371
6372[[package]]
6373name = "b"
6374version = "0.1.0"
6375source = { registry = "https://pypi.org/simple" }
6376sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6377
6378[[package.dependencies]]
6379name = "a"
6380source = { editable = "path/to/a" }
6381"#;
6382 let result = toml::from_str::<Lock>(data);
6383 insta::assert_debug_snapshot!(result);
6384 }
6385
6386 #[test]
6387 fn hash_optional_missing() {
6388 let data = r#"
6389version = 1
6390requires-python = ">=3.12"
6391
6392[[package]]
6393name = "anyio"
6394version = "4.3.0"
6395source = { registry = "https://pypi.org/simple" }
6396wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl" }]
6397"#;
6398 let result: Result<Lock, _> = toml::from_str(data);
6399 insta::assert_debug_snapshot!(result);
6400 }
6401
6402 #[test]
6403 fn hash_optional_present() {
6404 let data = r#"
6405version = 1
6406requires-python = ">=3.12"
6407
6408[[package]]
6409name = "anyio"
6410version = "4.3.0"
6411source = { registry = "https://pypi.org/simple" }
6412wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6413"#;
6414 let result: Result<Lock, _> = toml::from_str(data);
6415 insta::assert_debug_snapshot!(result);
6416 }
6417
6418 #[test]
6419 fn hash_required_present() {
6420 let data = r#"
6421version = 1
6422requires-python = ">=3.12"
6423
6424[[package]]
6425name = "anyio"
6426version = "4.3.0"
6427source = { path = "file:///foo/bar" }
6428wheels = [{ url = "file:///foo/bar/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6429"#;
6430 let result: Result<Lock, _> = toml::from_str(data);
6431 insta::assert_debug_snapshot!(result);
6432 }
6433
6434 #[test]
6435 fn source_direct_no_subdir() {
6436 let data = r#"
6437version = 1
6438requires-python = ">=3.12"
6439
6440[[package]]
6441name = "anyio"
6442version = "4.3.0"
6443source = { url = "https://burntsushi.net" }
6444"#;
6445 let result: Result<Lock, _> = toml::from_str(data);
6446 insta::assert_debug_snapshot!(result);
6447 }
6448
6449 #[test]
6450 fn source_direct_has_subdir() {
6451 let data = r#"
6452version = 1
6453requires-python = ">=3.12"
6454
6455[[package]]
6456name = "anyio"
6457version = "4.3.0"
6458source = { url = "https://burntsushi.net", subdirectory = "wat/foo/bar" }
6459"#;
6460 let result: Result<Lock, _> = toml::from_str(data);
6461 insta::assert_debug_snapshot!(result);
6462 }
6463
6464 #[test]
6465 fn source_directory() {
6466 let data = r#"
6467version = 1
6468requires-python = ">=3.12"
6469
6470[[package]]
6471name = "anyio"
6472version = "4.3.0"
6473source = { directory = "path/to/dir" }
6474"#;
6475 let result: Result<Lock, _> = toml::from_str(data);
6476 insta::assert_debug_snapshot!(result);
6477 }
6478
6479 #[test]
6480 fn source_editable() {
6481 let data = r#"
6482version = 1
6483requires-python = ">=3.12"
6484
6485[[package]]
6486name = "anyio"
6487version = "4.3.0"
6488source = { editable = "path/to/dir" }
6489"#;
6490 let result: Result<Lock, _> = toml::from_str(data);
6491 insta::assert_debug_snapshot!(result);
6492 }
6493}