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