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};
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 hasher: &HashStrategy,
1631 index: &InMemoryIndex,
1632 database: &DistributionDatabase<'_, Context>,
1633 ) -> Result<SatisfiesResult<'_>, LockError> {
1634 let mut queue: VecDeque<&Package> = VecDeque::new();
1635 let mut seen = FxHashSet::default();
1636
1637 {
1639 let expected = members.iter().cloned().collect::<BTreeSet<_>>();
1640 let actual = &self.manifest.members;
1641 if expected != *actual {
1642 return Ok(SatisfiesResult::MismatchedMembers(expected, actual));
1643 }
1644 }
1645
1646 for (name, member) in packages {
1649 let source = self.find_by_name(name).ok().flatten();
1650
1651 let value = required_members.get(name);
1653 let is_required_member = value.is_some();
1654 let editability = value.copied().flatten();
1655
1656 let expected_virtual = !member.pyproject_toml().is_package(!is_required_member);
1658 let actual_virtual =
1659 source.map(|package| matches!(package.id.source, Source::Virtual(..)));
1660 if actual_virtual != Some(expected_virtual) {
1661 return Ok(SatisfiesResult::MismatchedVirtual(
1662 name.clone(),
1663 expected_virtual,
1664 ));
1665 }
1666
1667 let expected_editable = if expected_virtual {
1669 false
1670 } else {
1671 editability.unwrap_or(true)
1672 };
1673 let actual_editable =
1674 source.map(|package| matches!(package.id.source, Source::Editable(..)));
1675 if actual_editable != Some(expected_editable) {
1676 return Ok(SatisfiesResult::MismatchedEditable(
1677 name.clone(),
1678 expected_editable,
1679 ));
1680 }
1681 }
1682
1683 {
1685 let expected: BTreeSet<_> = requirements
1686 .iter()
1687 .cloned()
1688 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1689 .collect::<Result<_, _>>()?;
1690 let actual: BTreeSet<_> = self
1691 .manifest
1692 .requirements
1693 .iter()
1694 .cloned()
1695 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1696 .collect::<Result<_, _>>()?;
1697 if expected != actual {
1698 return Ok(SatisfiesResult::MismatchedRequirements(expected, actual));
1699 }
1700 }
1701
1702 {
1704 let expected: BTreeSet<_> = constraints
1705 .iter()
1706 .cloned()
1707 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1708 .collect::<Result<_, _>>()?;
1709 let actual: BTreeSet<_> = self
1710 .manifest
1711 .constraints
1712 .iter()
1713 .cloned()
1714 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1715 .collect::<Result<_, _>>()?;
1716 if expected != actual {
1717 return Ok(SatisfiesResult::MismatchedConstraints(expected, actual));
1718 }
1719 }
1720
1721 {
1723 let expected: BTreeSet<_> = overrides
1724 .iter()
1725 .cloned()
1726 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1727 .collect::<Result<_, _>>()?;
1728 let actual: BTreeSet<_> = self
1729 .manifest
1730 .overrides
1731 .iter()
1732 .cloned()
1733 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1734 .collect::<Result<_, _>>()?;
1735 if expected != actual {
1736 return Ok(SatisfiesResult::MismatchedOverrides(expected, actual));
1737 }
1738 }
1739
1740 {
1742 let expected: BTreeSet<_> = excludes.iter().cloned().collect();
1743 let actual: BTreeSet<_> = self.manifest.excludes.iter().cloned().collect();
1744 if expected != actual {
1745 return Ok(SatisfiesResult::MismatchedExcludes(expected, actual));
1746 }
1747 }
1748
1749 {
1751 let expected: BTreeSet<_> = build_constraints
1752 .iter()
1753 .cloned()
1754 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1755 .collect::<Result<_, _>>()?;
1756 let actual: BTreeSet<_> = self
1757 .manifest
1758 .build_constraints
1759 .iter()
1760 .cloned()
1761 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1762 .collect::<Result<_, _>>()?;
1763 if expected != actual {
1764 return Ok(SatisfiesResult::MismatchedBuildConstraints(
1765 expected, actual,
1766 ));
1767 }
1768 }
1769
1770 {
1772 let expected: BTreeMap<GroupName, BTreeSet<Requirement>> = dependency_groups
1773 .iter()
1774 .filter(|(_, requirements)| !requirements.is_empty())
1775 .map(|(group, requirements)| {
1776 Ok::<_, LockError>((
1777 group.clone(),
1778 requirements
1779 .iter()
1780 .cloned()
1781 .map(|requirement| {
1782 normalize_requirement(requirement, root, &self.requires_python)
1783 })
1784 .collect::<Result<_, _>>()?,
1785 ))
1786 })
1787 .collect::<Result<_, _>>()?;
1788 let actual: BTreeMap<GroupName, BTreeSet<Requirement>> = self
1789 .manifest
1790 .dependency_groups
1791 .iter()
1792 .filter(|(_, requirements)| !requirements.is_empty())
1793 .map(|(group, requirements)| {
1794 Ok::<_, LockError>((
1795 group.clone(),
1796 requirements
1797 .iter()
1798 .cloned()
1799 .map(|requirement| {
1800 normalize_requirement(requirement, root, &self.requires_python)
1801 })
1802 .collect::<Result<_, _>>()?,
1803 ))
1804 })
1805 .collect::<Result<_, _>>()?;
1806 if expected != actual {
1807 return Ok(SatisfiesResult::MismatchedDependencyGroups(
1808 expected, actual,
1809 ));
1810 }
1811 }
1812
1813 {
1815 let expected = dependency_metadata
1816 .values()
1817 .cloned()
1818 .collect::<BTreeSet<_>>();
1819 let actual = &self.manifest.dependency_metadata;
1820 if expected != *actual {
1821 return Ok(SatisfiesResult::MismatchedStaticMetadata(expected, actual));
1822 }
1823 }
1824
1825 let mut remotes = indexes.map(|locations| {
1827 locations
1828 .allowed_indexes()
1829 .into_iter()
1830 .filter_map(|index| match index.url() {
1831 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
1832 Some(UrlString::from(index.url().without_credentials().as_ref()))
1833 }
1834 IndexUrl::Path(_) => None,
1835 })
1836 .collect::<BTreeSet<_>>()
1837 });
1838
1839 let mut locals = indexes.map(|locations| {
1840 locations
1841 .allowed_indexes()
1842 .into_iter()
1843 .filter_map(|index| match index.url() {
1844 IndexUrl::Pypi(_) | IndexUrl::Url(_) => None,
1845 IndexUrl::Path(url) => {
1846 let path = url.to_file_path().ok()?;
1847 let path = try_relative_to_if(&path, root, !url.was_given_absolute())
1848 .ok()?
1849 .into_boxed_path();
1850 Some(path)
1851 }
1852 })
1853 .collect::<BTreeSet<_>>()
1854 });
1855
1856 for root_name in packages.keys() {
1858 let root = self
1859 .find_by_name(root_name)
1860 .expect("found too many packages matching root");
1861
1862 let Some(root) = root else {
1863 return Ok(SatisfiesResult::MissingRoot(root_name.clone()));
1865 };
1866
1867 if seen.insert(&root.id) {
1868 queue.push_back(root);
1869 }
1870 }
1871
1872 let root_requirements = requirements
1875 .iter()
1876 .chain(dependency_groups.values().flatten())
1877 .collect::<Vec<_>>();
1878
1879 for requirement in &root_requirements {
1880 if let RequirementSource::Registry {
1881 index: Some(index), ..
1882 } = &requirement.source
1883 {
1884 match &index.url {
1885 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
1886 if let Some(remotes) = remotes.as_mut() {
1887 remotes.insert(UrlString::from(
1888 index.url().without_credentials().as_ref(),
1889 ));
1890 }
1891 }
1892 IndexUrl::Path(url) => {
1893 if let Some(locals) = locals.as_mut() {
1894 if let Some(path) = url.to_file_path().ok().and_then(|path| {
1895 try_relative_to_if(&path, root, !url.was_given_absolute()).ok()
1896 }) {
1897 locals.insert(path.into_boxed_path());
1898 }
1899 }
1900 }
1901 }
1902 }
1903 }
1904
1905 if !root_requirements.is_empty() {
1906 let names = root_requirements
1907 .iter()
1908 .map(|requirement| &requirement.name)
1909 .collect::<FxHashSet<_>>();
1910
1911 let by_name: FxHashMap<_, Vec<_>> = self.packages.iter().fold(
1912 FxHashMap::with_capacity_and_hasher(self.packages.len(), FxBuildHasher),
1913 |mut by_name, package| {
1914 if names.contains(&package.id.name) {
1915 by_name.entry(&package.id.name).or_default().push(package);
1916 }
1917 by_name
1918 },
1919 );
1920
1921 for requirement in root_requirements {
1922 for package in by_name.get(&requirement.name).into_iter().flatten() {
1923 if !package.id.source.is_source_tree() {
1924 continue;
1925 }
1926
1927 let marker = if package.fork_markers.is_empty() {
1928 requirement.marker
1929 } else {
1930 let mut combined = MarkerTree::FALSE;
1931 for fork_marker in &package.fork_markers {
1932 combined.or(fork_marker.pep508());
1933 }
1934 combined.and(requirement.marker);
1935 combined
1936 };
1937 if marker.is_false() {
1938 continue;
1939 }
1940 if !marker.evaluate(markers, &[]) {
1941 continue;
1942 }
1943
1944 if seen.insert(&package.id) {
1945 queue.push_back(package);
1946 }
1947 }
1948 }
1949 }
1950
1951 while let Some(package) = queue.pop_front() {
1952 if let Source::Registry(index) = &package.id.source {
1954 match index {
1955 RegistrySource::Url(url) => {
1956 if remotes
1957 .as_ref()
1958 .is_some_and(|remotes| !remotes.contains(url))
1959 {
1960 let name = &package.id.name;
1961 let version = &package
1962 .id
1963 .version
1964 .as_ref()
1965 .expect("version for registry source");
1966 return Ok(SatisfiesResult::MissingRemoteIndex(name, version, url));
1967 }
1968 }
1969 RegistrySource::Path(path) => {
1970 if locals.as_ref().is_some_and(|locals| !locals.contains(path)) {
1971 let name = &package.id.name;
1972 let version = &package
1973 .id
1974 .version
1975 .as_ref()
1976 .expect("version for registry source");
1977 return Ok(SatisfiesResult::MissingLocalIndex(name, version, path));
1978 }
1979 }
1980 }
1981 }
1982
1983 if package.id.source.is_immutable() {
1985 continue;
1986 }
1987
1988 if let Some(version) = package.id.version.as_ref() {
1989 let HashedDist { dist, .. } = package.to_dist(
1991 root,
1992 TagPolicy::Preferred(tags),
1993 &BuildOptions::default(),
1994 markers,
1995 )?;
1996
1997 let metadata = {
1998 let id = dist.distribution_id();
1999 if let Some(archive) =
2000 index
2001 .distributions()
2002 .get(&id)
2003 .as_deref()
2004 .and_then(|response| {
2005 if let MetadataResponse::Found(archive, ..) = response {
2006 Some(archive)
2007 } else {
2008 None
2009 }
2010 })
2011 {
2012 archive.metadata.clone()
2014 } else {
2015 let archive = database
2017 .get_or_build_wheel_metadata(&dist, hasher.get(&dist))
2018 .await
2019 .map_err(|err| LockErrorKind::Resolution {
2020 id: package.id.clone(),
2021 err,
2022 })?;
2023
2024 let metadata = archive.metadata.clone();
2025
2026 index
2028 .distributions()
2029 .done(id, Arc::new(MetadataResponse::Found(archive)));
2030
2031 metadata
2032 }
2033 };
2034
2035 if package.id.source.is_source_tree() {
2038 if metadata.dynamic {
2039 return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, false));
2040 }
2041 }
2042
2043 if metadata.version != *version {
2045 return Ok(SatisfiesResult::MismatchedVersion(
2046 &package.id.name,
2047 version.clone(),
2048 Some(metadata.version.clone()),
2049 ));
2050 }
2051
2052 match self.satisfies_provides_extra(metadata.provides_extra, package) {
2054 SatisfiesResult::Satisfied => {}
2055 result => return Ok(result),
2056 }
2057
2058 match self.satisfies_requires_dist(
2060 metadata.requires_dist,
2061 metadata.dependency_groups,
2062 package,
2063 root,
2064 )? {
2065 SatisfiesResult::Satisfied => {}
2066 result => return Ok(result),
2067 }
2068 } else if let Some(source_tree) = package.id.source.as_source_tree() {
2069 let parent = root.join(source_tree);
2079 let path = parent.join("pyproject.toml");
2080 let metadata = match fs_err::tokio::read_to_string(&path).await {
2081 Ok(contents) => {
2082 let pyproject_toml =
2083 PyProjectToml::from_toml(&contents, path.user_display()).map_err(
2084 |err| LockErrorKind::InvalidPyprojectToml {
2085 path: path.clone(),
2086 err,
2087 },
2088 )?;
2089 database
2090 .requires_dist(&parent, &pyproject_toml)
2091 .await
2092 .map_err(|err| LockErrorKind::Resolution {
2093 id: package.id.clone(),
2094 err,
2095 })?
2096 }
2097 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
2098 Err(err) => {
2099 return Err(LockErrorKind::UnreadablePyprojectToml { path, err }.into());
2100 }
2101 };
2102
2103 let satisfied = metadata.is_some_and(|metadata| {
2104 if !metadata.dynamic {
2106 debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
2107 return false;
2108 }
2109
2110 if let SatisfiesResult::Satisfied = self.satisfies_provides_extra(metadata.provides_extra, package, ) {
2112 debug!("Static `provides-extra` for `{}` is up-to-date", package.id);
2113 } else {
2114 debug!("Static `provides-extra` for `{}` is out-of-date; falling back to distribution database", package.id);
2115 return false;
2116 }
2117
2118 match self.satisfies_requires_dist(metadata.requires_dist, metadata.dependency_groups, package, root) {
2120 Ok(SatisfiesResult::Satisfied) => {
2121 debug!("Static `requires-dist` for `{}` is up-to-date", package.id);
2122 },
2123 Ok(..) => {
2124 debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
2125 return false;
2126 },
2127 Err(..) => {
2128 debug!("Static `requires-dist` for `{}` is invalid; falling back to distribution database", package.id);
2129 return false;
2130 },
2131 }
2132
2133 true
2134 });
2135
2136 if !satisfied {
2142 let HashedDist { dist, .. } = package.to_dist(
2143 root,
2144 TagPolicy::Preferred(tags),
2145 &BuildOptions::default(),
2146 markers,
2147 )?;
2148
2149 let metadata = {
2150 let id = dist.distribution_id();
2151 if let Some(archive) =
2152 index
2153 .distributions()
2154 .get(&id)
2155 .as_deref()
2156 .and_then(|response| {
2157 if let MetadataResponse::Found(archive, ..) = response {
2158 Some(archive)
2159 } else {
2160 None
2161 }
2162 })
2163 {
2164 archive.metadata.clone()
2166 } else {
2167 let archive = database
2169 .get_or_build_wheel_metadata(&dist, hasher.get(&dist))
2170 .await
2171 .map_err(|err| LockErrorKind::Resolution {
2172 id: package.id.clone(),
2173 err,
2174 })?;
2175
2176 let metadata = archive.metadata.clone();
2177
2178 index
2180 .distributions()
2181 .done(id, Arc::new(MetadataResponse::Found(archive)));
2182
2183 metadata
2184 }
2185 };
2186
2187 if !metadata.dynamic {
2189 return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, true));
2190 }
2191
2192 match self.satisfies_provides_extra(metadata.provides_extra, package) {
2194 SatisfiesResult::Satisfied => {}
2195 result => return Ok(result),
2196 }
2197
2198 match self.satisfies_requires_dist(
2200 metadata.requires_dist,
2201 metadata.dependency_groups,
2202 package,
2203 root,
2204 )? {
2205 SatisfiesResult::Satisfied => {}
2206 result => return Ok(result),
2207 }
2208 }
2209 } else {
2210 return Ok(SatisfiesResult::MissingVersion(&package.id.name));
2211 }
2212
2213 for requirement in package
2218 .metadata
2219 .requires_dist
2220 .iter()
2221 .chain(package.metadata.dependency_groups.values().flatten())
2222 {
2223 if let RequirementSource::Registry {
2224 index: Some(index), ..
2225 } = &requirement.source
2226 {
2227 match &index.url {
2228 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
2229 if let Some(remotes) = remotes.as_mut() {
2230 remotes.insert(UrlString::from(
2231 index.url().without_credentials().as_ref(),
2232 ));
2233 }
2234 }
2235 IndexUrl::Path(url) => {
2236 if let Some(locals) = locals.as_mut() {
2237 if let Some(path) = url.to_file_path().ok().and_then(|path| {
2238 try_relative_to_if(&path, root, !url.was_given_absolute()).ok()
2239 }) {
2240 locals.insert(path.into_boxed_path());
2241 }
2242 }
2243 }
2244 }
2245 }
2246 }
2247
2248 for dep in &package.dependencies {
2250 if seen.insert(&dep.package_id) {
2251 let dep_dist = self.find_by_id(&dep.package_id);
2252 queue.push_back(dep_dist);
2253 }
2254 }
2255
2256 for dependencies in package.optional_dependencies.values() {
2257 for dep in dependencies {
2258 if seen.insert(&dep.package_id) {
2259 let dep_dist = self.find_by_id(&dep.package_id);
2260 queue.push_back(dep_dist);
2261 }
2262 }
2263 }
2264
2265 for dependencies in package.dependency_groups.values() {
2266 for dep in dependencies {
2267 if seen.insert(&dep.package_id) {
2268 let dep_dist = self.find_by_id(&dep.package_id);
2269 queue.push_back(dep_dist);
2270 }
2271 }
2272 }
2273 }
2274
2275 Ok(SatisfiesResult::Satisfied)
2276 }
2277}
2278
2279#[derive(Debug)]
2287pub struct Auditable<'lock> {
2288 packages: Vec<(&'lock Package, &'lock Version)>,
2290}
2291
2292impl<'lock> Auditable<'lock> {
2293 pub fn len(&self) -> usize {
2295 self.packages.len()
2296 }
2297
2298 pub fn is_empty(&self) -> bool {
2300 self.packages.is_empty()
2301 }
2302
2303 pub fn packages(&self) -> impl Iterator<Item = (&'lock PackageName, &'lock Version)> + '_ {
2305 self.packages
2306 .iter()
2307 .map(|(package, version)| (package.name(), *version))
2308 }
2309
2310 pub fn projects(&self, root: &Path) -> Result<Vec<(&'lock PackageName, IndexUrl)>, LockError> {
2314 let mut seen: FxHashSet<(&PackageName, String)> = FxHashSet::default();
2315 let mut projects: Vec<(&PackageName, IndexUrl)> = Vec::with_capacity(self.packages.len());
2316 for (package, _version) in &self.packages {
2317 if let Some(index) = package.index(root)?
2318 && seen.insert((package.name(), index.url().to_string()))
2319 {
2320 projects.push((package.name(), index));
2321 }
2322 }
2323 Ok(projects)
2324 }
2325}
2326
2327#[derive(Debug, Copy, Clone)]
2328enum TagPolicy<'tags> {
2329 Required(&'tags Tags),
2331 Preferred(&'tags Tags),
2334}
2335
2336impl<'tags> TagPolicy<'tags> {
2337 fn tags(&self) -> &'tags Tags {
2339 match self {
2340 Self::Required(tags) | Self::Preferred(tags) => tags,
2341 }
2342 }
2343}
2344
2345#[derive(Debug)]
2347pub enum SatisfiesResult<'lock> {
2348 Satisfied,
2350 MismatchedMembers(BTreeSet<PackageName>, &'lock BTreeSet<PackageName>),
2352 MismatchedVirtual(PackageName, bool),
2354 MismatchedEditable(PackageName, bool),
2356 MismatchedDynamic(&'lock PackageName, bool),
2358 MismatchedVersion(&'lock PackageName, Version, Option<Version>),
2360 MismatchedRequirements(BTreeSet<Requirement>, BTreeSet<Requirement>),
2362 MismatchedConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
2364 MismatchedOverrides(BTreeSet<Requirement>, BTreeSet<Requirement>),
2366 MismatchedExcludes(BTreeSet<PackageName>, BTreeSet<PackageName>),
2368 MismatchedBuildConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
2370 MismatchedDependencyGroups(
2372 BTreeMap<GroupName, BTreeSet<Requirement>>,
2373 BTreeMap<GroupName, BTreeSet<Requirement>>,
2374 ),
2375 MismatchedStaticMetadata(BTreeSet<StaticMetadata>, &'lock BTreeSet<StaticMetadata>),
2377 MissingRoot(PackageName),
2379 MissingRemoteIndex(&'lock PackageName, &'lock Version, &'lock UrlString),
2381 MissingLocalIndex(&'lock PackageName, &'lock Version, &'lock Path),
2383 MismatchedPackageRequirements(
2385 &'lock PackageName,
2386 Option<&'lock Version>,
2387 BTreeSet<Requirement>,
2388 BTreeSet<Requirement>,
2389 ),
2390 MismatchedPackageProvidesExtra(
2392 &'lock PackageName,
2393 Option<&'lock Version>,
2394 BTreeSet<ExtraName>,
2395 BTreeSet<&'lock ExtraName>,
2396 ),
2397 MismatchedPackageDependencyGroups(
2399 &'lock PackageName,
2400 Option<&'lock Version>,
2401 BTreeMap<GroupName, BTreeSet<Requirement>>,
2402 BTreeMap<GroupName, BTreeSet<Requirement>>,
2403 ),
2404 MissingVersion(&'lock PackageName),
2406}
2407
2408#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2410#[serde(rename_all = "kebab-case")]
2411struct ResolverOptions {
2412 #[serde(default)]
2414 resolution_mode: ResolutionMode,
2415 #[serde(default)]
2417 prerelease_mode: PrereleaseMode,
2418 #[serde(default)]
2420 fork_strategy: ForkStrategy,
2421 #[serde(flatten)]
2423 exclude_newer: ExcludeNewerWire,
2424}
2425
2426#[expect(clippy::struct_field_names)]
2427#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2428#[serde(rename_all = "kebab-case")]
2429struct ExcludeNewerWire {
2430 exclude_newer: Option<Timestamp>,
2431 exclude_newer_span: Option<ExcludeNewerSpan>,
2432 #[serde(default, skip_serializing_if = "ExcludeNewerPackage::is_empty")]
2433 exclude_newer_package: ExcludeNewerPackage,
2434}
2435
2436impl From<ExcludeNewerWire> for ExcludeNewer {
2437 fn from(wire: ExcludeNewerWire) -> Self {
2438 let global = match (wire.exclude_newer, wire.exclude_newer_span) {
2439 (Some(timestamp), None) => Some(ExcludeNewerValue::absolute(timestamp)),
2440 (Some(_), Some(span)) => Some(ExcludeNewerValue::relative(span)),
2443 (None, Some(span)) => Some(ExcludeNewerValue::relative(span)),
2446 (None, None) => None,
2447 };
2448 Self {
2449 global,
2450 package: wire.exclude_newer_package,
2451 }
2452 }
2453}
2454
2455impl From<ExcludeNewer> for ExcludeNewerWire {
2456 fn from(exclude_newer: ExcludeNewer) -> Self {
2457 let (timestamp, span) = match exclude_newer.global {
2458 Some(ExcludeNewerValue::Absolute(timestamp)) => (Some(timestamp), None),
2459 Some(ExcludeNewerValue::Relative(span)) => (None, Some(span)),
2460 None => (None, None),
2461 };
2462 Self {
2463 exclude_newer: timestamp,
2464 exclude_newer_span: span,
2465 exclude_newer_package: exclude_newer.package,
2466 }
2467 }
2468}
2469
2470#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2471#[serde(rename_all = "kebab-case")]
2472pub struct ResolverManifest {
2473 #[serde(default)]
2475 members: BTreeSet<PackageName>,
2476 #[serde(default)]
2481 requirements: BTreeSet<Requirement>,
2482 #[serde(default)]
2488 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
2489 #[serde(default)]
2491 constraints: BTreeSet<Requirement>,
2492 #[serde(default)]
2494 overrides: BTreeSet<Requirement>,
2495 #[serde(default)]
2497 excludes: BTreeSet<PackageName>,
2498 #[serde(default)]
2500 build_constraints: BTreeSet<Requirement>,
2501 #[serde(default)]
2503 dependency_metadata: BTreeSet<StaticMetadata>,
2504}
2505
2506impl ResolverManifest {
2507 pub fn new(
2510 members: impl IntoIterator<Item = PackageName>,
2511 requirements: impl IntoIterator<Item = Requirement>,
2512 constraints: impl IntoIterator<Item = Requirement>,
2513 overrides: impl IntoIterator<Item = Requirement>,
2514 excludes: impl IntoIterator<Item = PackageName>,
2515 build_constraints: impl IntoIterator<Item = Requirement>,
2516 dependency_groups: impl IntoIterator<Item = (GroupName, Vec<Requirement>)>,
2517 dependency_metadata: impl IntoIterator<Item = StaticMetadata>,
2518 ) -> Self {
2519 Self {
2520 members: members.into_iter().collect(),
2521 requirements: requirements.into_iter().collect(),
2522 constraints: constraints.into_iter().collect(),
2523 overrides: overrides.into_iter().collect(),
2524 excludes: excludes.into_iter().collect(),
2525 build_constraints: build_constraints.into_iter().collect(),
2526 dependency_groups: dependency_groups
2527 .into_iter()
2528 .map(|(group, requirements)| (group, requirements.into_iter().collect()))
2529 .collect(),
2530 dependency_metadata: dependency_metadata.into_iter().collect(),
2531 }
2532 }
2533
2534 pub fn relative_to(self, root: &Path) -> Result<Self, io::Error> {
2536 Ok(Self {
2537 members: self.members,
2538 requirements: self
2539 .requirements
2540 .into_iter()
2541 .map(|requirement| requirement.relative_to(root))
2542 .collect::<Result<BTreeSet<_>, _>>()?,
2543 constraints: self
2544 .constraints
2545 .into_iter()
2546 .map(|requirement| requirement.relative_to(root))
2547 .collect::<Result<BTreeSet<_>, _>>()?,
2548 overrides: self
2549 .overrides
2550 .into_iter()
2551 .map(|requirement| requirement.relative_to(root))
2552 .collect::<Result<BTreeSet<_>, _>>()?,
2553 excludes: self.excludes,
2554 build_constraints: self
2555 .build_constraints
2556 .into_iter()
2557 .map(|requirement| requirement.relative_to(root))
2558 .collect::<Result<BTreeSet<_>, _>>()?,
2559 dependency_groups: self
2560 .dependency_groups
2561 .into_iter()
2562 .map(|(group, requirements)| {
2563 Ok::<_, io::Error>((
2564 group,
2565 requirements
2566 .into_iter()
2567 .map(|requirement| requirement.relative_to(root))
2568 .collect::<Result<BTreeSet<_>, _>>()?,
2569 ))
2570 })
2571 .collect::<Result<BTreeMap<_, _>, _>>()?,
2572 dependency_metadata: self.dependency_metadata,
2573 })
2574 }
2575}
2576
2577#[derive(Clone, Debug, serde::Deserialize)]
2578#[serde(rename_all = "kebab-case")]
2579struct LockWire {
2580 version: u32,
2581 revision: Option<u32>,
2582 requires_python: RequiresPython,
2583 #[serde(rename = "resolution-markers", default)]
2586 fork_markers: Vec<SimplifiedMarkerTree>,
2587 #[serde(rename = "supported-markers", default)]
2588 supported_environments: Vec<SimplifiedMarkerTree>,
2589 #[serde(rename = "required-markers", default)]
2590 required_environments: Vec<SimplifiedMarkerTree>,
2591 #[serde(rename = "conflicts", default)]
2592 conflicts: Option<Conflicts>,
2593 #[serde(default)]
2595 options: ResolverOptions,
2596 #[serde(default)]
2597 manifest: ResolverManifest,
2598 #[serde(rename = "package", alias = "distribution", default)]
2599 packages: Vec<PackageWire>,
2600}
2601
2602impl TryFrom<LockWire> for Lock {
2603 type Error = LockError;
2604
2605 fn try_from(wire: LockWire) -> Result<Self, LockError> {
2606 let mut unambiguous_package_ids: FxHashMap<PackageName, PackageId> = FxHashMap::default();
2611 let mut ambiguous = FxHashSet::default();
2612 for dist in &wire.packages {
2613 if ambiguous.contains(&dist.id.name) {
2614 continue;
2615 }
2616 if let Some(id) = unambiguous_package_ids.remove(&dist.id.name) {
2617 ambiguous.insert(id.name);
2618 continue;
2619 }
2620 unambiguous_package_ids.insert(dist.id.name.clone(), dist.id.clone());
2621 }
2622
2623 let packages = wire
2624 .packages
2625 .into_iter()
2626 .map(|dist| dist.unwire(&wire.requires_python, &unambiguous_package_ids))
2627 .collect::<Result<Vec<_>, _>>()?;
2628 let supported_environments = wire
2629 .supported_environments
2630 .into_iter()
2631 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2632 .collect();
2633 let required_environments = wire
2634 .required_environments
2635 .into_iter()
2636 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2637 .collect();
2638 let fork_markers = wire
2639 .fork_markers
2640 .into_iter()
2641 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2642 .map(UniversalMarker::from_combined)
2643 .collect();
2644 let mut options = wire.options;
2645 if options.exclude_newer.exclude_newer_span.is_some() {
2646 options.exclude_newer.exclude_newer = None;
2647 }
2648 let lock = Self::new(
2649 wire.version,
2650 wire.revision.unwrap_or(0),
2651 packages,
2652 wire.requires_python,
2653 options,
2654 wire.manifest,
2655 wire.conflicts.unwrap_or_else(Conflicts::empty),
2656 supported_environments,
2657 required_environments,
2658 fork_markers,
2659 )?;
2660
2661 Ok(lock)
2662 }
2663}
2664
2665#[derive(Clone, Debug, serde::Deserialize)]
2669#[serde(rename_all = "kebab-case")]
2670pub struct LockVersion {
2671 version: u32,
2672}
2673
2674impl LockVersion {
2675 pub fn version(&self) -> u32 {
2677 self.version
2678 }
2679}
2680
2681#[derive(Clone, Debug, PartialEq, Eq)]
2682pub struct Package {
2683 pub(crate) id: PackageId,
2684 sdist: Option<SourceDist>,
2685 wheels: Vec<Wheel>,
2686 fork_markers: Vec<UniversalMarker>,
2692 dependencies: Vec<Dependency>,
2694 optional_dependencies: BTreeMap<ExtraName, Vec<Dependency>>,
2696 dependency_groups: BTreeMap<GroupName, Vec<Dependency>>,
2698 metadata: PackageMetadata,
2700}
2701
2702impl Package {
2703 fn from_annotated_dist(
2704 annotated_dist: &AnnotatedDist,
2705 fork_markers: Vec<UniversalMarker>,
2706 root: &Path,
2707 ) -> Result<Self, LockError> {
2708 let id = PackageId::from_annotated_dist(annotated_dist, root)?;
2709 let sdist = SourceDist::from_annotated_dist(&id, annotated_dist)?;
2710 let wheels = Wheel::from_annotated_dist(annotated_dist)?;
2711 let requires_dist = if id.source.is_immutable() {
2712 BTreeSet::default()
2713 } else {
2714 annotated_dist
2715 .metadata
2716 .as_ref()
2717 .expect("metadata is present")
2718 .requires_dist
2719 .iter()
2720 .cloned()
2721 .map(|requirement| requirement.relative_to(root))
2722 .collect::<Result<_, _>>()
2723 .map_err(LockErrorKind::RequirementRelativePath)?
2724 };
2725 let provides_extra = if id.source.is_immutable() {
2726 Box::default()
2727 } else {
2728 annotated_dist
2729 .metadata
2730 .as_ref()
2731 .expect("metadata is present")
2732 .provides_extra
2733 .clone()
2734 };
2735 let dependency_groups = if id.source.is_immutable() {
2736 BTreeMap::default()
2737 } else {
2738 annotated_dist
2739 .metadata
2740 .as_ref()
2741 .expect("metadata is present")
2742 .dependency_groups
2743 .iter()
2744 .map(|(group, requirements)| {
2745 let requirements = requirements
2746 .iter()
2747 .cloned()
2748 .map(|requirement| requirement.relative_to(root))
2749 .collect::<Result<_, _>>()
2750 .map_err(LockErrorKind::RequirementRelativePath)?;
2751 Ok::<_, LockError>((group.clone(), requirements))
2752 })
2753 .collect::<Result<_, _>>()?
2754 };
2755 Ok(Self {
2756 id,
2757 sdist,
2758 wheels,
2759 fork_markers,
2760 dependencies: vec![],
2761 optional_dependencies: BTreeMap::default(),
2762 dependency_groups: BTreeMap::default(),
2763 metadata: PackageMetadata {
2764 requires_dist,
2765 provides_extra,
2766 dependency_groups,
2767 },
2768 })
2769 }
2770
2771 fn add_dependency(
2773 &mut self,
2774 requires_python: &RequiresPython,
2775 annotated_dist: &AnnotatedDist,
2776 marker: UniversalMarker,
2777 root: &Path,
2778 ) -> Result<(), LockError> {
2779 let new_dep =
2780 Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2781 for existing_dep in &mut self.dependencies {
2782 if existing_dep.package_id == new_dep.package_id
2783 && existing_dep.simplified_marker == new_dep.simplified_marker
2806 {
2807 existing_dep.extra.extend(new_dep.extra);
2808 return Ok(());
2809 }
2810 }
2811
2812 self.dependencies.push(new_dep);
2813 Ok(())
2814 }
2815
2816 fn add_optional_dependency(
2818 &mut self,
2819 requires_python: &RequiresPython,
2820 extra: ExtraName,
2821 annotated_dist: &AnnotatedDist,
2822 marker: UniversalMarker,
2823 root: &Path,
2824 ) -> Result<(), LockError> {
2825 let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2826 let optional_deps = self.optional_dependencies.entry(extra).or_default();
2827 for existing_dep in &mut *optional_deps {
2828 if existing_dep.package_id == dep.package_id
2829 && existing_dep.simplified_marker == dep.simplified_marker
2832 {
2833 existing_dep.extra.extend(dep.extra);
2834 return Ok(());
2835 }
2836 }
2837
2838 optional_deps.push(dep);
2839 Ok(())
2840 }
2841
2842 fn add_group_dependency(
2844 &mut self,
2845 requires_python: &RequiresPython,
2846 group: GroupName,
2847 annotated_dist: &AnnotatedDist,
2848 marker: UniversalMarker,
2849 root: &Path,
2850 ) -> Result<(), LockError> {
2851 let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2852 let deps = self.dependency_groups.entry(group).or_default();
2853 for existing_dep in &mut *deps {
2854 if existing_dep.package_id == dep.package_id
2855 && existing_dep.simplified_marker == dep.simplified_marker
2858 {
2859 existing_dep.extra.extend(dep.extra);
2860 return Ok(());
2861 }
2862 }
2863
2864 deps.push(dep);
2865 Ok(())
2866 }
2867
2868 fn to_dist(
2870 &self,
2871 workspace_root: &Path,
2872 tag_policy: TagPolicy<'_>,
2873 build_options: &BuildOptions,
2874 markers: &MarkerEnvironment,
2875 ) -> Result<HashedDist, LockError> {
2876 let no_binary = build_options.no_binary_package(&self.id.name);
2877 let no_build = build_options.no_build_package(&self.id.name);
2878
2879 if !no_binary {
2880 if let Some(best_wheel_index) = self.find_best_wheel(tag_policy) {
2881 let hashes = {
2882 let wheel = &self.wheels[best_wheel_index];
2883 HashDigests::from(
2884 wheel
2885 .hash
2886 .iter()
2887 .chain(wheel.zstd.iter().flat_map(|z| z.hash.iter()))
2888 .map(|h| h.0.clone())
2889 .collect::<Vec<_>>(),
2890 )
2891 };
2892
2893 let dist = match &self.id.source {
2894 Source::Registry(source) => {
2895 let wheels = self
2896 .wheels
2897 .iter()
2898 .map(|wheel| wheel.to_registry_wheel(source, workspace_root))
2899 .collect::<Result<_, LockError>>()?;
2900 let reg_built_dist = RegistryBuiltDist {
2901 wheels,
2902 best_wheel_index,
2903 sdist: None,
2904 };
2905 Dist::Built(BuiltDist::Registry(reg_built_dist))
2906 }
2907 Source::Path(path) => {
2908 let filename: WheelFilename =
2909 self.wheels[best_wheel_index].filename.clone();
2910 let install_path = absolute_path(workspace_root, path)?;
2911 let path_dist = PathBuiltDist {
2912 filename,
2913 url: verbatim_url(&install_path, &self.id)?,
2914 install_path: absolute_path(workspace_root, path)?.into_boxed_path(),
2915 };
2916 let built_dist = BuiltDist::Path(path_dist);
2917 Dist::Built(built_dist)
2918 }
2919 Source::Direct(url, direct) => {
2920 let filename: WheelFilename =
2921 self.wheels[best_wheel_index].filename.clone();
2922 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
2923 url: url.to_url().map_err(LockErrorKind::InvalidUrl)?,
2924 subdirectory: direct.subdirectory.clone(),
2925 ext: DistExtension::Wheel,
2926 });
2927 let direct_dist = DirectUrlBuiltDist {
2928 filename,
2929 location: Box::new(url.clone()),
2930 url: VerbatimUrl::from_url(url),
2931 };
2932 let built_dist = BuiltDist::DirectUrl(direct_dist);
2933 Dist::Built(built_dist)
2934 }
2935 Source::Git(_, _) => {
2936 return Err(LockErrorKind::InvalidWheelSource {
2937 id: self.id.clone(),
2938 source_type: "Git",
2939 }
2940 .into());
2941 }
2942 Source::Directory(_) => {
2943 return Err(LockErrorKind::InvalidWheelSource {
2944 id: self.id.clone(),
2945 source_type: "directory",
2946 }
2947 .into());
2948 }
2949 Source::Editable(_) => {
2950 return Err(LockErrorKind::InvalidWheelSource {
2951 id: self.id.clone(),
2952 source_type: "editable",
2953 }
2954 .into());
2955 }
2956 Source::Virtual(_) => {
2957 return Err(LockErrorKind::InvalidWheelSource {
2958 id: self.id.clone(),
2959 source_type: "virtual",
2960 }
2961 .into());
2962 }
2963 };
2964
2965 return Ok(HashedDist { dist, hashes });
2966 }
2967 }
2968
2969 if let Some(sdist) = self.to_source_dist(workspace_root)? {
2970 if !no_build || sdist.is_virtual() {
2974 let hashes = self
2975 .sdist
2976 .as_ref()
2977 .and_then(|s| s.hash())
2978 .map(|hash| HashDigests::from(vec![hash.0.clone()]))
2979 .unwrap_or_else(|| HashDigests::from(vec![]));
2980 return Ok(HashedDist {
2981 dist: Dist::Source(sdist),
2982 hashes,
2983 });
2984 }
2985 }
2986
2987 match (no_binary, no_build) {
2988 (true, true) => Err(LockErrorKind::NoBinaryNoBuild {
2989 id: self.id.clone(),
2990 }
2991 .into()),
2992 (true, false) if self.id.source.is_wheel() => Err(LockErrorKind::NoBinaryWheelOnly {
2993 id: self.id.clone(),
2994 }
2995 .into()),
2996 (true, false) => Err(LockErrorKind::NoBinary {
2997 id: self.id.clone(),
2998 }
2999 .into()),
3000 (false, true) => Err(LockErrorKind::NoBuild {
3001 id: self.id.clone(),
3002 }
3003 .into()),
3004 (false, false) if self.id.source.is_wheel() => Err(LockError {
3005 kind: Box::new(LockErrorKind::IncompatibleWheelOnly {
3006 id: self.id.clone(),
3007 }),
3008 hint: self.tag_hint(tag_policy, markers),
3009 }),
3010 (false, false) => Err(LockError {
3011 kind: Box::new(LockErrorKind::NeitherSourceDistNorWheel {
3012 id: self.id.clone(),
3013 }),
3014 hint: self.tag_hint(tag_policy, markers),
3015 }),
3016 }
3017 }
3018
3019 fn tag_hint(
3021 &self,
3022 tag_policy: TagPolicy<'_>,
3023 markers: &MarkerEnvironment,
3024 ) -> Option<WheelTagHint> {
3025 let filenames = self
3026 .wheels
3027 .iter()
3028 .map(|wheel| &wheel.filename)
3029 .collect::<Vec<_>>();
3030 WheelTagHint::from_wheels(
3031 &self.id.name,
3032 self.id.version.as_ref(),
3033 &filenames,
3034 tag_policy.tags(),
3035 markers,
3036 )
3037 }
3038
3039 fn to_source_dist(
3044 &self,
3045 workspace_root: &Path,
3046 ) -> Result<Option<uv_distribution_types::SourceDist>, LockError> {
3047 let sdist = match &self.id.source {
3048 Source::Path(path) => {
3049 let DistExtension::Source(ext) = DistExtension::from_path(path).map_err(|err| {
3051 LockErrorKind::MissingExtension {
3052 id: self.id.clone(),
3053 err,
3054 }
3055 })?
3056 else {
3057 return Ok(None);
3058 };
3059 let install_path = absolute_path(workspace_root, path)?;
3060 let given = path.to_str().expect("lock file paths must be UTF-8");
3061 let path_dist = PathSourceDist {
3062 name: self.id.name.clone(),
3063 version: self.id.version.clone(),
3064 url: verbatim_url(&install_path, &self.id)?.with_given(given),
3065 install_path: install_path.into_boxed_path(),
3066 ext,
3067 };
3068 uv_distribution_types::SourceDist::Path(path_dist)
3069 }
3070 Source::Directory(path) => {
3071 let install_path = absolute_path(workspace_root, path)?;
3072 let given = path.to_str().expect("lock file paths must be UTF-8");
3073 let dir_dist = DirectorySourceDist {
3074 name: self.id.name.clone(),
3075 url: verbatim_url(&install_path, &self.id)?.with_given(given),
3076 install_path: install_path.into_boxed_path(),
3077 editable: Some(false),
3078 r#virtual: Some(false),
3079 };
3080 uv_distribution_types::SourceDist::Directory(dir_dist)
3081 }
3082 Source::Editable(path) => {
3083 let install_path = absolute_path(workspace_root, path)?;
3084 let given = path.to_str().expect("lock file paths must be UTF-8");
3085 let dir_dist = DirectorySourceDist {
3086 name: self.id.name.clone(),
3087 url: verbatim_url(&install_path, &self.id)?.with_given(given),
3088 install_path: install_path.into_boxed_path(),
3089 editable: Some(true),
3090 r#virtual: Some(false),
3091 };
3092 uv_distribution_types::SourceDist::Directory(dir_dist)
3093 }
3094 Source::Virtual(path) => {
3095 let install_path = absolute_path(workspace_root, path)?;
3096 let given = path.to_str().expect("lock file paths must be UTF-8");
3097 let dir_dist = DirectorySourceDist {
3098 name: self.id.name.clone(),
3099 url: verbatim_url(&install_path, &self.id)?.with_given(given),
3100 install_path: install_path.into_boxed_path(),
3101 editable: Some(false),
3102 r#virtual: Some(true),
3103 };
3104 uv_distribution_types::SourceDist::Directory(dir_dist)
3105 }
3106 Source::Git(url, git) => {
3107 let mut url = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
3110 url.set_fragment(None);
3111 url.set_query(None);
3112
3113 let git_url = GitUrl::from_commit(
3115 url,
3116 GitReference::from(git.kind.clone()),
3117 git.precise,
3118 git.lfs,
3119 )?;
3120
3121 let url = DisplaySafeUrl::from(ParsedGitUrl {
3123 url: git_url.clone(),
3124 subdirectory: git.subdirectory.clone(),
3125 });
3126
3127 let git_dist = GitSourceDist {
3128 name: self.id.name.clone(),
3129 url: VerbatimUrl::from_url(url),
3130 git: Box::new(git_url),
3131 subdirectory: git.subdirectory.clone(),
3132 };
3133 uv_distribution_types::SourceDist::Git(git_dist)
3134 }
3135 Source::Direct(url, direct) => {
3136 let DistExtension::Source(ext) =
3138 DistExtension::from_path(url.base_str()).map_err(|err| {
3139 LockErrorKind::MissingExtension {
3140 id: self.id.clone(),
3141 err,
3142 }
3143 })?
3144 else {
3145 return Ok(None);
3146 };
3147 let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
3148 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
3149 url: location.clone(),
3150 subdirectory: direct.subdirectory.clone(),
3151 ext: DistExtension::Source(ext),
3152 });
3153 let direct_dist = DirectUrlSourceDist {
3154 name: self.id.name.clone(),
3155 location: Box::new(location),
3156 subdirectory: direct.subdirectory.clone(),
3157 ext,
3158 url: VerbatimUrl::from_url(url),
3159 };
3160 uv_distribution_types::SourceDist::DirectUrl(direct_dist)
3161 }
3162 Source::Registry(RegistrySource::Url(url)) => {
3163 let Some(ref sdist) = self.sdist else {
3164 return Ok(None);
3165 };
3166
3167 let name = &self.id.name;
3168 let version = self
3169 .id
3170 .version
3171 .as_ref()
3172 .expect("version for registry source");
3173
3174 let file_url = sdist.url().ok_or_else(|| LockErrorKind::MissingUrl {
3175 name: name.clone(),
3176 version: version.clone(),
3177 })?;
3178 let filename = sdist
3179 .filename()
3180 .ok_or_else(|| LockErrorKind::MissingFilename {
3181 id: self.id.clone(),
3182 })?;
3183 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
3184 LockErrorKind::MissingExtension {
3185 id: self.id.clone(),
3186 err,
3187 }
3188 })?;
3189 let file = Box::new(uv_distribution_types::File {
3190 dist_info_metadata: false,
3191 filename: SmallString::from(filename),
3192 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
3193 HashDigests::from(hash.0.clone())
3194 }),
3195 requires_python: None,
3196 size: sdist.size(),
3197 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
3198 url: FileLocation::AbsoluteUrl(file_url.clone()),
3199 yanked: None,
3200 zstd: None,
3201 });
3202
3203 let index = IndexUrl::from(VerbatimUrl::from_url(
3204 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
3205 ));
3206
3207 let reg_dist = RegistrySourceDist {
3208 name: name.clone(),
3209 version: version.clone(),
3210 file,
3211 ext,
3212 index,
3213 wheels: vec![],
3214 };
3215 uv_distribution_types::SourceDist::Registry(reg_dist)
3216 }
3217 Source::Registry(RegistrySource::Path(path)) => {
3218 let Some(ref sdist) = self.sdist else {
3219 return Ok(None);
3220 };
3221
3222 let name = &self.id.name;
3223 let version = self
3224 .id
3225 .version
3226 .as_ref()
3227 .expect("version for registry source");
3228
3229 let file_url = match sdist {
3230 SourceDist::Url { url: file_url, .. } => {
3231 FileLocation::AbsoluteUrl(file_url.clone())
3232 }
3233 SourceDist::Path {
3234 path: file_path, ..
3235 } => {
3236 let file_path = workspace_root.join(path).join(file_path);
3237 let file_url =
3238 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
3239 LockErrorKind::PathToUrl {
3240 path: file_path.into_boxed_path(),
3241 }
3242 })?;
3243 FileLocation::AbsoluteUrl(UrlString::from(file_url))
3244 }
3245 SourceDist::Metadata { .. } => {
3246 return Err(LockErrorKind::MissingPath {
3247 name: name.clone(),
3248 version: version.clone(),
3249 }
3250 .into());
3251 }
3252 };
3253 let filename = sdist
3254 .filename()
3255 .ok_or_else(|| LockErrorKind::MissingFilename {
3256 id: self.id.clone(),
3257 })?;
3258 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
3259 LockErrorKind::MissingExtension {
3260 id: self.id.clone(),
3261 err,
3262 }
3263 })?;
3264 let file = Box::new(uv_distribution_types::File {
3265 dist_info_metadata: false,
3266 filename: SmallString::from(filename),
3267 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
3268 HashDigests::from(hash.0.clone())
3269 }),
3270 requires_python: None,
3271 size: sdist.size(),
3272 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
3273 url: file_url,
3274 yanked: None,
3275 zstd: None,
3276 });
3277
3278 let index = IndexUrl::from(
3279 VerbatimUrl::from_absolute_path(workspace_root.join(path))
3280 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
3281 );
3282
3283 let reg_dist = RegistrySourceDist {
3284 name: name.clone(),
3285 version: version.clone(),
3286 file,
3287 ext,
3288 index,
3289 wheels: vec![],
3290 };
3291 uv_distribution_types::SourceDist::Registry(reg_dist)
3292 }
3293 };
3294
3295 Ok(Some(sdist))
3296 }
3297
3298 fn to_toml(
3299 &self,
3300 requires_python: &RequiresPython,
3301 dist_count_by_name: &FxHashMap<PackageName, u64>,
3302 ) -> Result<Table, toml_edit::ser::Error> {
3303 let mut table = Table::new();
3304
3305 self.id.to_toml(None, &mut table);
3306
3307 if !self.fork_markers.is_empty() {
3308 let fork_markers = each_element_on_its_line_array(
3309 simplified_universal_markers(&self.fork_markers, requires_python).into_iter(),
3310 );
3311 if !fork_markers.is_empty() {
3312 table.insert("resolution-markers", value(fork_markers));
3313 }
3314 }
3315
3316 if !self.dependencies.is_empty() {
3317 let deps = each_element_on_its_line_array(self.dependencies.iter().map(|dep| {
3318 dep.to_toml(requires_python, dist_count_by_name)
3319 .into_inline_table()
3320 }));
3321 table.insert("dependencies", value(deps));
3322 }
3323
3324 if !self.optional_dependencies.is_empty() {
3325 let mut optional_deps = Table::new();
3326 for (extra, deps) in &self.optional_dependencies {
3327 let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3328 dep.to_toml(requires_python, dist_count_by_name)
3329 .into_inline_table()
3330 }));
3331 if !deps.is_empty() {
3332 optional_deps.insert(extra.as_ref(), value(deps));
3333 }
3334 }
3335 if !optional_deps.is_empty() {
3336 table.insert("optional-dependencies", Item::Table(optional_deps));
3337 }
3338 }
3339
3340 if !self.dependency_groups.is_empty() {
3341 let mut dependency_groups = Table::new();
3342 for (extra, deps) in &self.dependency_groups {
3343 let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3344 dep.to_toml(requires_python, dist_count_by_name)
3345 .into_inline_table()
3346 }));
3347 if !deps.is_empty() {
3348 dependency_groups.insert(extra.as_ref(), value(deps));
3349 }
3350 }
3351 if !dependency_groups.is_empty() {
3352 table.insert("dev-dependencies", Item::Table(dependency_groups));
3353 }
3354 }
3355
3356 if let Some(ref sdist) = self.sdist {
3357 table.insert("sdist", value(sdist.to_toml()?));
3358 }
3359
3360 if !self.wheels.is_empty() {
3361 let wheels = each_element_on_its_line_array(
3362 self.wheels
3363 .iter()
3364 .map(Wheel::to_toml)
3365 .collect::<Result<Vec<_>, _>>()?
3366 .into_iter(),
3367 );
3368 table.insert("wheels", value(wheels));
3369 }
3370
3371 {
3373 let mut metadata_table = Table::new();
3374
3375 if !self.metadata.requires_dist.is_empty() {
3376 let requires_dist = self
3377 .metadata
3378 .requires_dist
3379 .iter()
3380 .map(|requirement| {
3381 serde::Serialize::serialize(
3382 &requirement,
3383 toml_edit::ser::ValueSerializer::new(),
3384 )
3385 })
3386 .collect::<Result<Vec<_>, _>>()?;
3387 let requires_dist = match requires_dist.as_slice() {
3388 [] => Array::new(),
3389 [requirement] => Array::from_iter([requirement]),
3390 requires_dist => each_element_on_its_line_array(requires_dist.iter()),
3391 };
3392 metadata_table.insert("requires-dist", value(requires_dist));
3393 }
3394
3395 if !self.metadata.dependency_groups.is_empty() {
3396 let mut dependency_groups = Table::new();
3397 for (extra, deps) in &self.metadata.dependency_groups {
3398 let deps = deps
3399 .iter()
3400 .map(|requirement| {
3401 serde::Serialize::serialize(
3402 &requirement,
3403 toml_edit::ser::ValueSerializer::new(),
3404 )
3405 })
3406 .collect::<Result<Vec<_>, _>>()?;
3407 let deps = match deps.as_slice() {
3408 [] => Array::new(),
3409 [requirement] => Array::from_iter([requirement]),
3410 deps => each_element_on_its_line_array(deps.iter()),
3411 };
3412 dependency_groups.insert(extra.as_ref(), value(deps));
3413 }
3414 if !dependency_groups.is_empty() {
3415 metadata_table.insert("requires-dev", Item::Table(dependency_groups));
3416 }
3417 }
3418
3419 if !self.metadata.provides_extra.is_empty() {
3420 let provides_extras = self
3421 .metadata
3422 .provides_extra
3423 .iter()
3424 .map(|extra| {
3425 serde::Serialize::serialize(&extra, toml_edit::ser::ValueSerializer::new())
3426 })
3427 .collect::<Result<Vec<_>, _>>()?;
3428 let provides_extras = Array::from_iter(provides_extras);
3430 metadata_table.insert("provides-extras", value(provides_extras));
3431 }
3432
3433 if !metadata_table.is_empty() {
3434 table.insert("metadata", Item::Table(metadata_table));
3435 }
3436 }
3437
3438 Ok(table)
3439 }
3440
3441 fn find_best_wheel(&self, tag_policy: TagPolicy<'_>) -> Option<usize> {
3442 type WheelPriority<'lock> = (TagPriority, Option<&'lock BuildTag>);
3443
3444 let mut best: Option<(WheelPriority, usize)> = None;
3445 for (i, wheel) in self.wheels.iter().enumerate() {
3446 let TagCompatibility::Compatible(tag_priority) =
3447 wheel.filename.compatibility(tag_policy.tags())
3448 else {
3449 continue;
3450 };
3451 let build_tag = wheel.filename.build_tag();
3452 let wheel_priority = (tag_priority, build_tag);
3453 match best {
3454 None => {
3455 best = Some((wheel_priority, i));
3456 }
3457 Some((best_priority, _)) => {
3458 if wheel_priority > best_priority {
3459 best = Some((wheel_priority, i));
3460 }
3461 }
3462 }
3463 }
3464
3465 let best = best.map(|(_, i)| i);
3466 match tag_policy {
3467 TagPolicy::Required(_) => best,
3468 TagPolicy::Preferred(_) => best.or_else(|| self.wheels.first().map(|_| 0)),
3469 }
3470 }
3471
3472 pub fn name(&self) -> &PackageName {
3474 &self.id.name
3475 }
3476
3477 pub fn version(&self) -> Option<&Version> {
3479 self.id.version.as_ref()
3480 }
3481
3482 pub fn git_sha(&self) -> Option<&GitOid> {
3484 match &self.id.source {
3485 Source::Git(_, git) => Some(&git.precise),
3486 _ => None,
3487 }
3488 }
3489
3490 pub fn fork_markers(&self) -> &[UniversalMarker] {
3492 self.fork_markers.as_slice()
3493 }
3494
3495 pub fn index(&self, root: &Path) -> Result<Option<IndexUrl>, LockError> {
3497 match &self.id.source {
3498 Source::Registry(RegistrySource::Url(url)) => {
3499 let index = IndexUrl::from(VerbatimUrl::from_url(
3500 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
3501 ));
3502 Ok(Some(index))
3503 }
3504 Source::Registry(RegistrySource::Path(path)) => {
3505 let index = IndexUrl::from(
3506 VerbatimUrl::from_absolute_path(root.join(path))
3507 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
3508 );
3509 Ok(Some(index))
3510 }
3511 _ => Ok(None),
3512 }
3513 }
3514
3515 fn hashes(&self) -> HashDigests {
3517 let mut hashes = Vec::with_capacity(
3518 usize::from(self.sdist.as_ref().and_then(|sdist| sdist.hash()).is_some())
3519 + self
3520 .wheels
3521 .iter()
3522 .map(|wheel| usize::from(wheel.hash.is_some()))
3523 .sum::<usize>(),
3524 );
3525 if let Some(ref sdist) = self.sdist {
3526 if let Some(hash) = sdist.hash() {
3527 hashes.push(hash.0.clone());
3528 }
3529 }
3530 for wheel in &self.wheels {
3531 hashes.extend(wheel.hash.as_ref().map(|h| h.0.clone()));
3532 if let Some(zstd) = wheel.zstd.as_ref() {
3533 hashes.extend(zstd.hash.as_ref().map(|h| h.0.clone()));
3534 }
3535 }
3536 HashDigests::from(hashes)
3537 }
3538
3539 pub fn as_git_ref(&self) -> Result<Option<ResolvedRepositoryReference>, LockError> {
3541 match &self.id.source {
3542 Source::Git(url, git) => Ok(Some(ResolvedRepositoryReference {
3543 reference: RepositoryReference {
3544 url: RepositoryUrl::new(&url.to_url().map_err(LockErrorKind::InvalidUrl)?),
3545 reference: GitReference::from(git.kind.clone()),
3546 },
3547 sha: git.precise,
3548 })),
3549 _ => Ok(None),
3550 }
3551 }
3552
3553 fn is_dynamic(&self) -> bool {
3555 self.id.version.is_none()
3556 }
3557
3558 pub fn provides_extras(&self) -> &[ExtraName] {
3560 &self.metadata.provides_extra
3561 }
3562
3563 pub fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
3565 &self.metadata.dependency_groups
3566 }
3567
3568 pub fn dependencies(&self) -> &[Dependency] {
3570 &self.dependencies
3571 }
3572
3573 pub fn optional_dependencies(&self) -> &BTreeMap<ExtraName, Vec<Dependency>> {
3575 &self.optional_dependencies
3576 }
3577
3578 pub fn resolved_dependency_groups(&self) -> &BTreeMap<GroupName, Vec<Dependency>> {
3580 &self.dependency_groups
3581 }
3582
3583 pub fn as_install_target(&self) -> InstallTarget<'_> {
3585 InstallTarget {
3586 name: self.name(),
3587 is_local: self.id.source.is_local(),
3588 }
3589 }
3590}
3591
3592fn verbatim_url(path: &Path, id: &PackageId) -> Result<VerbatimUrl, LockError> {
3594 let url =
3595 VerbatimUrl::from_normalized_path(path).map_err(|err| LockErrorKind::VerbatimUrl {
3596 id: id.clone(),
3597 err,
3598 })?;
3599 Ok(url)
3600}
3601
3602fn absolute_path(workspace_root: &Path, path: &Path) -> Result<PathBuf, LockError> {
3604 let path = uv_fs::normalize_absolute_path(&workspace_root.join(path))
3605 .map_err(LockErrorKind::AbsolutePath)?;
3606 Ok(path)
3607}
3608
3609#[derive(Clone, Debug, serde::Deserialize)]
3610#[serde(rename_all = "kebab-case")]
3611struct PackageWire {
3612 #[serde(flatten)]
3613 id: PackageId,
3614 #[serde(default)]
3615 metadata: PackageMetadata,
3616 #[serde(default)]
3617 sdist: Option<SourceDist>,
3618 #[serde(default)]
3619 wheels: Vec<Wheel>,
3620 #[serde(default, rename = "resolution-markers")]
3621 fork_markers: Vec<SimplifiedMarkerTree>,
3622 #[serde(default)]
3623 dependencies: Vec<DependencyWire>,
3624 #[serde(default)]
3625 optional_dependencies: BTreeMap<ExtraName, Vec<DependencyWire>>,
3626 #[serde(default, rename = "dev-dependencies", alias = "dependency-groups")]
3627 dependency_groups: BTreeMap<GroupName, Vec<DependencyWire>>,
3628}
3629
3630#[derive(Clone, Default, Debug, Eq, PartialEq, serde::Deserialize)]
3631#[serde(rename_all = "kebab-case")]
3632struct PackageMetadata {
3633 #[serde(default)]
3634 requires_dist: BTreeSet<Requirement>,
3635 #[serde(default, rename = "provides-extras")]
3636 provides_extra: Box<[ExtraName]>,
3637 #[serde(default, rename = "requires-dev", alias = "dependency-groups")]
3638 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
3639}
3640
3641impl PackageWire {
3642 fn unwire(
3643 self,
3644 requires_python: &RequiresPython,
3645 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3646 ) -> Result<Package, LockError> {
3647 if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) {
3649 if let Some(version) = &self.id.version {
3650 for wheel in &self.wheels {
3651 if *version != wheel.filename.version
3652 && *version != wheel.filename.version.clone().without_local()
3653 {
3654 return Err(LockError::from(LockErrorKind::InconsistentVersions {
3655 name: self.id.name,
3656 version: version.clone(),
3657 wheel: wheel.clone(),
3658 }));
3659 }
3660 }
3661 }
3664 }
3665
3666 let unwire_deps = |deps: Vec<DependencyWire>| -> Result<Vec<Dependency>, LockError> {
3667 deps.into_iter()
3668 .map(|dep| dep.unwire(requires_python, unambiguous_package_ids))
3669 .collect()
3670 };
3671
3672 Ok(Package {
3673 id: self.id,
3674 metadata: self.metadata,
3675 sdist: self.sdist,
3676 wheels: self.wheels,
3677 fork_markers: self
3678 .fork_markers
3679 .into_iter()
3680 .map(|simplified_marker| simplified_marker.into_marker(requires_python))
3681 .map(UniversalMarker::from_combined)
3682 .collect(),
3683 dependencies: unwire_deps(self.dependencies)?,
3684 optional_dependencies: self
3685 .optional_dependencies
3686 .into_iter()
3687 .map(|(extra, deps)| Ok((extra, unwire_deps(deps)?)))
3688 .collect::<Result<_, LockError>>()?,
3689 dependency_groups: self
3690 .dependency_groups
3691 .into_iter()
3692 .map(|(group, deps)| Ok((group, unwire_deps(deps)?)))
3693 .collect::<Result<_, LockError>>()?,
3694 })
3695 }
3696}
3697
3698#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3701#[serde(rename_all = "kebab-case")]
3702pub(crate) struct PackageId {
3703 pub(crate) name: PackageName,
3704 pub(crate) version: Option<Version>,
3705 source: Source,
3706}
3707
3708impl PackageId {
3709 fn from_annotated_dist(annotated_dist: &AnnotatedDist, root: &Path) -> Result<Self, LockError> {
3710 let source = Source::from_resolved_dist(&annotated_dist.dist, root)?;
3712 let version = if source.is_source_tree()
3714 && annotated_dist
3715 .metadata
3716 .as_ref()
3717 .is_some_and(|metadata| metadata.dynamic)
3718 {
3719 None
3720 } else {
3721 Some(annotated_dist.version.clone())
3722 };
3723 let name = annotated_dist.name.clone();
3724 Ok(Self {
3725 name,
3726 version,
3727 source,
3728 })
3729 }
3730
3731 fn to_toml(&self, dist_count_by_name: Option<&FxHashMap<PackageName, u64>>, table: &mut Table) {
3738 let count = dist_count_by_name.and_then(|map| map.get(&self.name).copied());
3739 table.insert("name", value(self.name.to_string()));
3740 if count.map(|count| count > 1).unwrap_or(true) {
3741 if let Some(version) = &self.version {
3742 table.insert("version", value(version.to_string()));
3743 }
3744 self.source.to_toml(table);
3745 }
3746 }
3747}
3748
3749impl Display for PackageId {
3750 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3751 if let Some(version) = &self.version {
3752 write!(f, "{}=={} @ {}", self.name, version, self.source)
3753 } else {
3754 write!(f, "{} @ {}", self.name, self.source)
3755 }
3756 }
3757}
3758
3759#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3760#[serde(rename_all = "kebab-case")]
3761struct PackageIdForDependency {
3762 name: PackageName,
3763 version: Option<Version>,
3764 source: Option<Source>,
3765}
3766
3767impl PackageIdForDependency {
3768 fn unwire(
3769 self,
3770 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3771 ) -> Result<PackageId, LockError> {
3772 let unambiguous_package_id = unambiguous_package_ids.get(&self.name);
3773 let source = self.source.map(Ok::<_, LockError>).unwrap_or_else(|| {
3774 let Some(package_id) = unambiguous_package_id else {
3775 return Err(LockErrorKind::MissingDependencySource {
3776 name: self.name.clone(),
3777 }
3778 .into());
3779 };
3780 Ok(package_id.source.clone())
3781 })?;
3782 let version = if let Some(version) = self.version {
3783 Some(version)
3784 } else {
3785 if let Some(package_id) = unambiguous_package_id {
3786 package_id.version.clone()
3787 } else {
3788 if source.is_source_tree() {
3791 None
3792 } else {
3793 return Err(LockErrorKind::MissingDependencyVersion {
3794 name: self.name.clone(),
3795 }
3796 .into());
3797 }
3798 }
3799 };
3800 Ok(PackageId {
3801 name: self.name,
3802 version,
3803 source,
3804 })
3805 }
3806}
3807
3808impl From<PackageId> for PackageIdForDependency {
3809 fn from(id: PackageId) -> Self {
3810 Self {
3811 name: id.name,
3812 version: id.version,
3813 source: Some(id.source),
3814 }
3815 }
3816}
3817
3818#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3826#[serde(try_from = "SourceWire")]
3827enum Source {
3828 Registry(RegistrySource),
3830 Git(UrlString, GitSource),
3832 Direct(UrlString, DirectSource),
3834 Path(Box<Path>),
3836 Directory(Box<Path>),
3838 Editable(Box<Path>),
3840 Virtual(Box<Path>),
3842}
3843
3844impl Source {
3845 fn from_resolved_dist(resolved_dist: &ResolvedDist, root: &Path) -> Result<Self, LockError> {
3846 match *resolved_dist {
3847 ResolvedDist::Installed { .. } => unreachable!(),
3849 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(dist, root),
3850 }
3851 }
3852
3853 fn from_dist(dist: &Dist, root: &Path) -> Result<Self, LockError> {
3854 match *dist {
3855 Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, root),
3856 Dist::Source(ref source_dist) => Self::from_source_dist(source_dist, root),
3857 }
3858 }
3859
3860 fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result<Self, LockError> {
3861 match *built_dist {
3862 BuiltDist::Registry(ref reg_dist) => Self::from_registry_built_dist(reg_dist, root),
3863 BuiltDist::DirectUrl(ref direct_dist) => Ok(Self::from_direct_built_dist(direct_dist)),
3864 BuiltDist::Path(ref path_dist) => Self::from_path_built_dist(path_dist, root),
3865 }
3866 }
3867
3868 fn from_source_dist(
3869 source_dist: &uv_distribution_types::SourceDist,
3870 root: &Path,
3871 ) -> Result<Self, LockError> {
3872 match *source_dist {
3873 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
3874 Self::from_registry_source_dist(reg_dist, root)
3875 }
3876 uv_distribution_types::SourceDist::DirectUrl(ref direct_dist) => {
3877 Ok(Self::from_direct_source_dist(direct_dist))
3878 }
3879 uv_distribution_types::SourceDist::Git(ref git_dist) => {
3880 Ok(Self::from_git_dist(git_dist))
3881 }
3882 uv_distribution_types::SourceDist::Path(ref path_dist) => {
3883 Self::from_path_source_dist(path_dist, root)
3884 }
3885 uv_distribution_types::SourceDist::Directory(ref directory) => {
3886 Self::from_directory_source_dist(directory, root)
3887 }
3888 }
3889 }
3890
3891 fn from_registry_built_dist(
3892 reg_dist: &RegistryBuiltDist,
3893 root: &Path,
3894 ) -> Result<Self, LockError> {
3895 Self::from_index_url(®_dist.best_wheel().index, root)
3896 }
3897
3898 fn from_registry_source_dist(
3899 reg_dist: &RegistrySourceDist,
3900 root: &Path,
3901 ) -> Result<Self, LockError> {
3902 Self::from_index_url(®_dist.index, root)
3903 }
3904
3905 fn from_direct_built_dist(direct_dist: &DirectUrlBuiltDist) -> Self {
3906 Self::Direct(
3907 normalize_url(direct_dist.url.to_url()),
3908 DirectSource { subdirectory: None },
3909 )
3910 }
3911
3912 fn from_direct_source_dist(direct_dist: &DirectUrlSourceDist) -> Self {
3913 Self::Direct(
3914 normalize_url(direct_dist.url.to_url()),
3915 DirectSource {
3916 subdirectory: direct_dist.subdirectory.clone(),
3917 },
3918 )
3919 }
3920
3921 fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result<Self, LockError> {
3922 let path = try_relative_to_if(
3923 &path_dist.install_path,
3924 root,
3925 !path_dist.url.was_given_absolute(),
3926 )
3927 .map_err(LockErrorKind::DistributionRelativePath)?;
3928 Ok(Self::Path(path.into_boxed_path()))
3929 }
3930
3931 fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Self, LockError> {
3932 let path = try_relative_to_if(
3933 &path_dist.install_path,
3934 root,
3935 !path_dist.url.was_given_absolute(),
3936 )
3937 .map_err(LockErrorKind::DistributionRelativePath)?;
3938 Ok(Self::Path(path.into_boxed_path()))
3939 }
3940
3941 fn from_directory_source_dist(
3942 directory_dist: &DirectorySourceDist,
3943 root: &Path,
3944 ) -> Result<Self, LockError> {
3945 let path = try_relative_to_if(
3946 &directory_dist.install_path,
3947 root,
3948 !directory_dist.url.was_given_absolute(),
3949 )
3950 .map_err(LockErrorKind::DistributionRelativePath)?;
3951 if directory_dist.editable.unwrap_or(false) {
3952 Ok(Self::Editable(path.into_boxed_path()))
3953 } else if directory_dist.r#virtual.unwrap_or(false) {
3954 Ok(Self::Virtual(path.into_boxed_path()))
3955 } else {
3956 Ok(Self::Directory(path.into_boxed_path()))
3957 }
3958 }
3959
3960 fn from_index_url(index_url: &IndexUrl, root: &Path) -> Result<Self, LockError> {
3961 match index_url {
3962 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
3963 let redacted = index_url.without_credentials();
3965 let source = RegistrySource::Url(UrlString::from(redacted.as_ref()));
3966 Ok(Self::Registry(source))
3967 }
3968 IndexUrl::Path(url) => {
3969 let path = url
3970 .to_file_path()
3971 .map_err(|()| LockErrorKind::UrlToPath { url: url.to_url() })?;
3972 let path = try_relative_to_if(&path, root, !url.was_given_absolute())
3973 .map_err(LockErrorKind::IndexRelativePath)?;
3974 let source = RegistrySource::Path(path.into_boxed_path());
3975 Ok(Self::Registry(source))
3976 }
3977 }
3978 }
3979
3980 fn from_git_dist(git_dist: &GitSourceDist) -> Self {
3981 Self::Git(
3982 UrlString::from(locked_git_url(git_dist)),
3983 GitSource {
3984 kind: GitSourceKind::from(git_dist.git.reference().clone()),
3985 precise: git_dist.git.precise().unwrap_or_else(|| {
3986 panic!("Git distribution is missing a precise hash: {git_dist}")
3987 }),
3988 subdirectory: git_dist.subdirectory.clone(),
3989 lfs: git_dist.git.lfs(),
3990 },
3991 )
3992 }
3993
3994 fn is_immutable(&self) -> bool {
4001 matches!(self, Self::Registry(..) | Self::Git(_, _))
4002 }
4003
4004 fn is_wheel(&self) -> bool {
4006 match self {
4007 Self::Path(path) => {
4008 matches!(
4009 DistExtension::from_path(path).ok(),
4010 Some(DistExtension::Wheel)
4011 )
4012 }
4013 Self::Direct(url, _) => {
4014 matches!(
4015 DistExtension::from_path(url.as_ref()).ok(),
4016 Some(DistExtension::Wheel)
4017 )
4018 }
4019 Self::Directory(..) => false,
4020 Self::Editable(..) => false,
4021 Self::Virtual(..) => false,
4022 Self::Git(..) => false,
4023 Self::Registry(..) => false,
4024 }
4025 }
4026
4027 fn is_source_tree(&self) -> bool {
4029 match self {
4030 Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => true,
4031 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => false,
4032 }
4033 }
4034
4035 fn as_source_tree(&self) -> Option<&Path> {
4037 match self {
4038 Self::Directory(path) | Self::Editable(path) | Self::Virtual(path) => Some(path),
4039 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => None,
4040 }
4041 }
4042
4043 fn to_toml(&self, table: &mut Table) {
4044 let mut source_table = InlineTable::new();
4045 match self {
4046 Self::Registry(source) => match source {
4047 RegistrySource::Url(url) => {
4048 source_table.insert("registry", Value::from(url.as_ref()));
4049 }
4050 RegistrySource::Path(path) => {
4051 source_table.insert(
4052 "registry",
4053 Value::from(PortablePath::from(path).to_string()),
4054 );
4055 }
4056 },
4057 Self::Git(url, _) => {
4058 source_table.insert("git", Value::from(url.as_ref()));
4059 }
4060 Self::Direct(url, DirectSource { subdirectory }) => {
4061 source_table.insert("url", Value::from(url.as_ref()));
4062 if let Some(ref subdirectory) = *subdirectory {
4063 source_table.insert(
4064 "subdirectory",
4065 Value::from(PortablePath::from(subdirectory).to_string()),
4066 );
4067 }
4068 }
4069 Self::Path(path) => {
4070 source_table.insert("path", Value::from(PortablePath::from(path).to_string()));
4071 }
4072 Self::Directory(path) => {
4073 source_table.insert(
4074 "directory",
4075 Value::from(PortablePath::from(path).to_string()),
4076 );
4077 }
4078 Self::Editable(path) => {
4079 source_table.insert(
4080 "editable",
4081 Value::from(PortablePath::from(path).to_string()),
4082 );
4083 }
4084 Self::Virtual(path) => {
4085 source_table.insert("virtual", Value::from(PortablePath::from(path).to_string()));
4086 }
4087 }
4088 table.insert("source", value(source_table));
4089 }
4090
4091 pub(crate) fn is_local(&self) -> bool {
4093 matches!(
4094 self,
4095 Self::Path(_) | Self::Directory(_) | Self::Editable(_) | Self::Virtual(_)
4096 )
4097 }
4098}
4099
4100impl Display for Source {
4101 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
4102 match self {
4103 Self::Registry(RegistrySource::Url(url)) | Self::Git(url, _) | Self::Direct(url, _) => {
4104 write!(f, "{}+{}", self.name(), url)
4105 }
4106 Self::Registry(RegistrySource::Path(path))
4107 | Self::Path(path)
4108 | Self::Directory(path)
4109 | Self::Editable(path)
4110 | Self::Virtual(path) => {
4111 write!(f, "{}+{}", self.name(), PortablePath::from(path))
4112 }
4113 }
4114 }
4115}
4116
4117impl Source {
4118 fn name(&self) -> &str {
4119 match self {
4120 Self::Registry(..) => "registry",
4121 Self::Git(..) => "git",
4122 Self::Direct(..) => "direct",
4123 Self::Path(..) => "path",
4124 Self::Directory(..) => "directory",
4125 Self::Editable(..) => "editable",
4126 Self::Virtual(..) => "virtual",
4127 }
4128 }
4129
4130 fn requires_hash(&self) -> Option<bool> {
4138 match self {
4139 Self::Registry(..) => None,
4140 Self::Direct(..) | Self::Path(..) => Some(true),
4141 Self::Git(..) | Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => {
4142 Some(false)
4143 }
4144 }
4145 }
4146}
4147
4148#[derive(Clone, Debug, serde::Deserialize)]
4149#[serde(untagged, rename_all = "kebab-case")]
4150enum SourceWire {
4151 Registry {
4152 registry: RegistrySourceWire,
4153 },
4154 Git {
4155 git: String,
4156 },
4157 Direct {
4158 url: UrlString,
4159 subdirectory: Option<PortablePathBuf>,
4160 },
4161 Path {
4162 path: PortablePathBuf,
4163 },
4164 Directory {
4165 directory: PortablePathBuf,
4166 },
4167 Editable {
4168 editable: PortablePathBuf,
4169 },
4170 Virtual {
4171 r#virtual: PortablePathBuf,
4172 },
4173}
4174
4175impl TryFrom<SourceWire> for Source {
4176 type Error = LockError;
4177
4178 fn try_from(wire: SourceWire) -> Result<Self, LockError> {
4179 use self::SourceWire::{Direct, Directory, Editable, Git, Path, Registry, Virtual};
4180
4181 match wire {
4182 Registry { registry } => Ok(Self::Registry(registry.into())),
4183 Git { git } => {
4184 let url = DisplaySafeUrl::parse(&git)
4185 .map_err(|err| SourceParseError::InvalidUrl {
4186 given: git.clone(),
4187 err,
4188 })
4189 .map_err(LockErrorKind::InvalidGitSourceUrl)?;
4190
4191 let git_source = GitSource::from_url(&url)
4192 .map_err(|err| match err {
4193 GitSourceError::InvalidSha => SourceParseError::InvalidSha { given: git },
4194 GitSourceError::MissingSha => SourceParseError::MissingSha { given: git },
4195 })
4196 .map_err(LockErrorKind::InvalidGitSourceUrl)?;
4197
4198 Ok(Self::Git(UrlString::from(url), git_source))
4199 }
4200 Direct { url, subdirectory } => Ok(Self::Direct(
4201 url,
4202 DirectSource {
4203 subdirectory: subdirectory.map(Box::<std::path::Path>::from),
4204 },
4205 )),
4206 Path { path } => Ok(Self::Path(path.into())),
4207 Directory { directory } => Ok(Self::Directory(directory.into())),
4208 Editable { editable } => Ok(Self::Editable(editable.into())),
4209 Virtual { r#virtual } => Ok(Self::Virtual(r#virtual.into())),
4210 }
4211 }
4212}
4213
4214#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
4216enum RegistrySource {
4217 Url(UrlString),
4219 Path(Box<Path>),
4221}
4222
4223impl Display for RegistrySource {
4224 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
4225 match self {
4226 Self::Url(url) => write!(f, "{url}"),
4227 Self::Path(path) => write!(f, "{}", path.display()),
4228 }
4229 }
4230}
4231
4232#[derive(Clone, Debug)]
4233enum RegistrySourceWire {
4234 Url(UrlString),
4236 Path(PortablePathBuf),
4238}
4239
4240impl<'de> serde::de::Deserialize<'de> for RegistrySourceWire {
4241 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4242 where
4243 D: serde::de::Deserializer<'de>,
4244 {
4245 struct Visitor;
4246
4247 impl serde::de::Visitor<'_> for Visitor {
4248 type Value = RegistrySourceWire;
4249
4250 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
4251 formatter.write_str("a valid URL or a file path")
4252 }
4253
4254 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
4255 where
4256 E: serde::de::Error,
4257 {
4258 if split_scheme(value).is_some_and(|(scheme, _)| Scheme::parse(scheme).is_some()) {
4259 Ok(
4260 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
4261 value,
4262 ))
4263 .map(RegistrySourceWire::Url)?,
4264 )
4265 } else {
4266 Ok(
4267 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
4268 value,
4269 ))
4270 .map(RegistrySourceWire::Path)?,
4271 )
4272 }
4273 }
4274 }
4275
4276 deserializer.deserialize_str(Visitor)
4277 }
4278}
4279
4280impl From<RegistrySourceWire> for RegistrySource {
4281 fn from(wire: RegistrySourceWire) -> Self {
4282 match wire {
4283 RegistrySourceWire::Url(url) => Self::Url(url),
4284 RegistrySourceWire::Path(path) => Self::Path(path.into()),
4285 }
4286 }
4287}
4288
4289#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4290#[serde(rename_all = "kebab-case")]
4291struct DirectSource {
4292 subdirectory: Option<Box<Path>>,
4293}
4294
4295#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
4300struct GitSource {
4301 precise: GitOid,
4302 subdirectory: Option<Box<Path>>,
4303 kind: GitSourceKind,
4304 lfs: GitLfs,
4305}
4306
4307#[derive(Clone, Debug, Eq, PartialEq)]
4309enum GitSourceError {
4310 InvalidSha,
4311 MissingSha,
4312}
4313
4314impl GitSource {
4315 fn from_url(url: &Url) -> Result<Self, GitSourceError> {
4318 let mut kind = GitSourceKind::DefaultBranch;
4319 let mut subdirectory = None;
4320 let mut lfs = GitLfs::Disabled;
4321 for (key, val) in url.query_pairs() {
4322 match &*key {
4323 "tag" => kind = GitSourceKind::Tag(val.into_owned()),
4324 "branch" => kind = GitSourceKind::Branch(val.into_owned()),
4325 "rev" => kind = GitSourceKind::Rev(val.into_owned()),
4326 "subdirectory" => subdirectory = Some(PortablePathBuf::from(val.as_ref()).into()),
4327 "lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
4328 _ => {}
4329 }
4330 }
4331
4332 let precise = GitOid::from_str(url.fragment().ok_or(GitSourceError::MissingSha)?)
4333 .map_err(|_| GitSourceError::InvalidSha)?;
4334
4335 Ok(Self {
4336 precise,
4337 subdirectory,
4338 kind,
4339 lfs,
4340 })
4341 }
4342}
4343
4344#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4345#[serde(rename_all = "kebab-case")]
4346enum GitSourceKind {
4347 Tag(String),
4348 Branch(String),
4349 Rev(String),
4350 DefaultBranch,
4351}
4352
4353#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4355#[serde(rename_all = "kebab-case")]
4356struct SourceDistMetadata {
4357 hash: Option<Hash>,
4359 size: Option<u64>,
4363 #[serde(alias = "upload_time")]
4365 upload_time: Option<Timestamp>,
4366}
4367
4368#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4373#[serde(from = "SourceDistWire")]
4374enum SourceDist {
4375 Url {
4376 url: UrlString,
4377 #[serde(flatten)]
4378 metadata: SourceDistMetadata,
4379 },
4380 Path {
4381 path: Box<Path>,
4382 #[serde(flatten)]
4383 metadata: SourceDistMetadata,
4384 },
4385 Metadata {
4386 #[serde(flatten)]
4387 metadata: SourceDistMetadata,
4388 },
4389}
4390
4391impl SourceDist {
4392 fn filename(&self) -> Option<Cow<'_, str>> {
4393 match self {
4394 Self::Metadata { .. } => None,
4395 Self::Url { url, .. } => url.filename().ok(),
4396 Self::Path { path, .. } => path.file_name().map(|filename| filename.to_string_lossy()),
4397 }
4398 }
4399
4400 fn url(&self) -> Option<&UrlString> {
4401 match self {
4402 Self::Metadata { .. } => None,
4403 Self::Url { url, .. } => Some(url),
4404 Self::Path { .. } => None,
4405 }
4406 }
4407
4408 pub(crate) fn hash(&self) -> Option<&Hash> {
4409 match self {
4410 Self::Metadata { metadata } => metadata.hash.as_ref(),
4411 Self::Url { metadata, .. } => metadata.hash.as_ref(),
4412 Self::Path { metadata, .. } => metadata.hash.as_ref(),
4413 }
4414 }
4415
4416 pub(crate) fn size(&self) -> Option<u64> {
4417 match self {
4418 Self::Metadata { metadata } => metadata.size,
4419 Self::Url { metadata, .. } => metadata.size,
4420 Self::Path { metadata, .. } => metadata.size,
4421 }
4422 }
4423
4424 pub(crate) fn upload_time(&self) -> Option<Timestamp> {
4425 match self {
4426 Self::Metadata { metadata } => metadata.upload_time,
4427 Self::Url { metadata, .. } => metadata.upload_time,
4428 Self::Path { metadata, .. } => metadata.upload_time,
4429 }
4430 }
4431}
4432
4433impl SourceDist {
4434 fn from_annotated_dist(
4435 id: &PackageId,
4436 annotated_dist: &AnnotatedDist,
4437 ) -> Result<Option<Self>, LockError> {
4438 match annotated_dist.dist {
4439 ResolvedDist::Installed { .. } => unreachable!(),
4441 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4442 id,
4443 dist,
4444 annotated_dist.hashes.as_slice(),
4445 annotated_dist.index(),
4446 ),
4447 }
4448 }
4449
4450 fn from_dist(
4451 id: &PackageId,
4452 dist: &Dist,
4453 hashes: &[HashDigest],
4454 index: Option<&IndexUrl>,
4455 ) -> Result<Option<Self>, LockError> {
4456 match *dist {
4457 Dist::Built(BuiltDist::Registry(ref built_dist)) => {
4458 let Some(sdist) = built_dist.sdist.as_ref() else {
4459 return Ok(None);
4460 };
4461 Self::from_registry_dist(sdist, index)
4462 }
4463 Dist::Built(_) => Ok(None),
4464 Dist::Source(ref source_dist) => Self::from_source_dist(id, source_dist, hashes, index),
4465 }
4466 }
4467
4468 fn from_source_dist(
4469 id: &PackageId,
4470 source_dist: &uv_distribution_types::SourceDist,
4471 hashes: &[HashDigest],
4472 index: Option<&IndexUrl>,
4473 ) -> Result<Option<Self>, LockError> {
4474 match *source_dist {
4475 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
4476 Self::from_registry_dist(reg_dist, index)
4477 }
4478 uv_distribution_types::SourceDist::DirectUrl(_) => {
4479 Self::from_direct_dist(id, hashes).map(Some)
4480 }
4481 uv_distribution_types::SourceDist::Path(_) => {
4482 Self::from_path_dist(id, hashes).map(Some)
4483 }
4484 uv_distribution_types::SourceDist::Git(_)
4488 | uv_distribution_types::SourceDist::Directory(_) => Ok(None),
4489 }
4490 }
4491
4492 fn from_registry_dist(
4493 reg_dist: &RegistrySourceDist,
4494 index: Option<&IndexUrl>,
4495 ) -> Result<Option<Self>, LockError> {
4496 if index.is_none_or(|index| *index != reg_dist.index) {
4499 return Ok(None);
4500 }
4501
4502 match ®_dist.index {
4503 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4504 let url = normalize_file_location(®_dist.file.url)
4505 .map_err(LockErrorKind::InvalidUrl)
4506 .map_err(LockError::from)?;
4507 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4508 let size = reg_dist.file.size;
4509 let upload_time = reg_dist
4510 .file
4511 .upload_time_utc_ms
4512 .map(Timestamp::from_millisecond)
4513 .transpose()
4514 .map_err(LockErrorKind::InvalidTimestamp)?;
4515 Ok(Some(Self::Url {
4516 url,
4517 metadata: SourceDistMetadata {
4518 hash,
4519 size,
4520 upload_time,
4521 },
4522 }))
4523 }
4524 IndexUrl::Path(path) => {
4525 let index_path = path
4526 .to_file_path()
4527 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4528 let url = reg_dist
4529 .file
4530 .url
4531 .to_url()
4532 .map_err(LockErrorKind::InvalidUrl)?;
4533
4534 if url.scheme() == "file" {
4535 let reg_dist_path = url
4536 .to_file_path()
4537 .map_err(|()| LockErrorKind::UrlToPath { url })?;
4538 let path =
4539 try_relative_to_if(®_dist_path, index_path, !path.was_given_absolute())
4540 .map_err(LockErrorKind::DistributionRelativePath)?
4541 .into_boxed_path();
4542 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4543 let size = reg_dist.file.size;
4544 let upload_time = reg_dist
4545 .file
4546 .upload_time_utc_ms
4547 .map(Timestamp::from_millisecond)
4548 .transpose()
4549 .map_err(LockErrorKind::InvalidTimestamp)?;
4550 Ok(Some(Self::Path {
4551 path,
4552 metadata: SourceDistMetadata {
4553 hash,
4554 size,
4555 upload_time,
4556 },
4557 }))
4558 } else {
4559 let url = normalize_file_location(®_dist.file.url)
4560 .map_err(LockErrorKind::InvalidUrl)
4561 .map_err(LockError::from)?;
4562 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4563 let size = reg_dist.file.size;
4564 let upload_time = reg_dist
4565 .file
4566 .upload_time_utc_ms
4567 .map(Timestamp::from_millisecond)
4568 .transpose()
4569 .map_err(LockErrorKind::InvalidTimestamp)?;
4570 Ok(Some(Self::Url {
4571 url,
4572 metadata: SourceDistMetadata {
4573 hash,
4574 size,
4575 upload_time,
4576 },
4577 }))
4578 }
4579 }
4580 }
4581 }
4582
4583 fn from_direct_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4584 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4585 let kind = LockErrorKind::Hash {
4586 id: id.clone(),
4587 artifact_type: "direct URL source distribution",
4588 expected: true,
4589 };
4590 return Err(kind.into());
4591 };
4592 Ok(Self::Metadata {
4593 metadata: SourceDistMetadata {
4594 hash: Some(hash),
4595 size: None,
4596 upload_time: None,
4597 },
4598 })
4599 }
4600
4601 fn from_path_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4602 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4603 let kind = LockErrorKind::Hash {
4604 id: id.clone(),
4605 artifact_type: "path source distribution",
4606 expected: true,
4607 };
4608 return Err(kind.into());
4609 };
4610 Ok(Self::Metadata {
4611 metadata: SourceDistMetadata {
4612 hash: Some(hash),
4613 size: None,
4614 upload_time: None,
4615 },
4616 })
4617 }
4618}
4619
4620#[derive(Clone, Debug, serde::Deserialize)]
4621#[serde(untagged, rename_all = "kebab-case")]
4622enum SourceDistWire {
4623 Url {
4624 url: UrlString,
4625 #[serde(flatten)]
4626 metadata: SourceDistMetadata,
4627 },
4628 Path {
4629 path: PortablePathBuf,
4630 #[serde(flatten)]
4631 metadata: SourceDistMetadata,
4632 },
4633 Metadata {
4634 #[serde(flatten)]
4635 metadata: SourceDistMetadata,
4636 },
4637}
4638
4639impl SourceDist {
4640 fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
4642 let mut table = InlineTable::new();
4643 match self {
4644 Self::Metadata { .. } => {}
4645 Self::Url { url, .. } => {
4646 table.insert("url", Value::from(url.as_ref()));
4647 }
4648 Self::Path { path, .. } => {
4649 table.insert("path", Value::from(PortablePath::from(path).to_string()));
4650 }
4651 }
4652 if let Some(hash) = self.hash() {
4653 table.insert("hash", Value::from(hash.to_string()));
4654 }
4655 if let Some(size) = self.size() {
4656 table.insert(
4657 "size",
4658 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4659 );
4660 }
4661 if let Some(upload_time) = self.upload_time() {
4662 table.insert("upload-time", Value::from(upload_time.to_string()));
4663 }
4664 Ok(table)
4665 }
4666}
4667
4668impl From<SourceDistWire> for SourceDist {
4669 fn from(wire: SourceDistWire) -> Self {
4670 match wire {
4671 SourceDistWire::Url { url, metadata } => Self::Url { url, metadata },
4672 SourceDistWire::Path { path, metadata } => Self::Path {
4673 path: path.into(),
4674 metadata,
4675 },
4676 SourceDistWire::Metadata { metadata } => Self::Metadata { metadata },
4677 }
4678 }
4679}
4680
4681impl From<GitReference> for GitSourceKind {
4682 fn from(value: GitReference) -> Self {
4683 match value {
4684 GitReference::Branch(branch) => Self::Branch(branch),
4685 GitReference::Tag(tag) => Self::Tag(tag),
4686 GitReference::BranchOrTag(rev) => Self::Rev(rev),
4687 GitReference::BranchOrTagOrCommit(rev) => Self::Rev(rev),
4688 GitReference::NamedRef(rev) => Self::Rev(rev),
4689 GitReference::DefaultBranch => Self::DefaultBranch,
4690 }
4691 }
4692}
4693
4694impl From<GitSourceKind> for GitReference {
4695 fn from(value: GitSourceKind) -> Self {
4696 match value {
4697 GitSourceKind::Branch(branch) => Self::Branch(branch),
4698 GitSourceKind::Tag(tag) => Self::Tag(tag),
4699 GitSourceKind::Rev(rev) => Self::from_rev(rev),
4700 GitSourceKind::DefaultBranch => Self::DefaultBranch,
4701 }
4702 }
4703}
4704
4705fn locked_git_url(git_dist: &GitSourceDist) -> DisplaySafeUrl {
4707 let mut url = git_dist.git.url().clone();
4708
4709 url.remove_credentials();
4711
4712 url.set_fragment(None);
4714 url.set_query(None);
4715
4716 if let Some(subdirectory) = git_dist
4718 .subdirectory
4719 .as_deref()
4720 .map(PortablePath::from)
4721 .as_ref()
4722 .map(PortablePath::to_string)
4723 {
4724 url.query_pairs_mut()
4725 .append_pair("subdirectory", &subdirectory);
4726 }
4727
4728 if git_dist.git.lfs().enabled() {
4730 url.query_pairs_mut().append_pair("lfs", "true");
4731 }
4732
4733 match git_dist.git.reference() {
4735 GitReference::Branch(branch) => {
4736 url.query_pairs_mut().append_pair("branch", branch.as_str());
4737 }
4738 GitReference::Tag(tag) => {
4739 url.query_pairs_mut().append_pair("tag", tag.as_str());
4740 }
4741 GitReference::BranchOrTag(rev)
4742 | GitReference::BranchOrTagOrCommit(rev)
4743 | GitReference::NamedRef(rev) => {
4744 url.query_pairs_mut().append_pair("rev", rev.as_str());
4745 }
4746 GitReference::DefaultBranch => {}
4747 }
4748
4749 url.set_fragment(
4751 git_dist
4752 .git
4753 .precise()
4754 .as_ref()
4755 .map(GitOid::to_string)
4756 .as_deref(),
4757 );
4758
4759 url
4760}
4761
4762#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4763struct ZstdWheel {
4764 hash: Option<Hash>,
4765 size: Option<u64>,
4766}
4767
4768#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4770#[serde(try_from = "WheelWire")]
4771struct Wheel {
4772 url: WheelWireSource,
4777 hash: Option<Hash>,
4783 size: Option<u64>,
4787 upload_time: Option<Timestamp>,
4791 filename: WheelFilename,
4798 zstd: Option<ZstdWheel>,
4800}
4801
4802impl Wheel {
4803 fn from_annotated_dist(annotated_dist: &AnnotatedDist) -> Result<Vec<Self>, LockError> {
4804 match annotated_dist.dist {
4805 ResolvedDist::Installed { .. } => unreachable!(),
4807 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4808 dist,
4809 annotated_dist.hashes.as_slice(),
4810 annotated_dist.index(),
4811 ),
4812 }
4813 }
4814
4815 fn from_dist(
4816 dist: &Dist,
4817 hashes: &[HashDigest],
4818 index: Option<&IndexUrl>,
4819 ) -> Result<Vec<Self>, LockError> {
4820 match *dist {
4821 Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, hashes, index),
4822 Dist::Source(uv_distribution_types::SourceDist::Registry(ref source_dist)) => {
4823 source_dist
4824 .wheels
4825 .iter()
4826 .filter(|wheel| {
4827 index.is_some_and(|index| *index == wheel.index)
4830 })
4831 .map(Self::from_registry_wheel)
4832 .collect()
4833 }
4834 Dist::Source(_) => Ok(vec![]),
4835 }
4836 }
4837
4838 fn from_built_dist(
4839 built_dist: &BuiltDist,
4840 hashes: &[HashDigest],
4841 index: Option<&IndexUrl>,
4842 ) -> Result<Vec<Self>, LockError> {
4843 match *built_dist {
4844 BuiltDist::Registry(ref reg_dist) => Self::from_registry_dist(reg_dist, index),
4845 BuiltDist::DirectUrl(ref direct_dist) => {
4846 Ok(vec![Self::from_direct_dist(direct_dist, hashes)])
4847 }
4848 BuiltDist::Path(ref path_dist) => Ok(vec![Self::from_path_dist(path_dist, hashes)]),
4849 }
4850 }
4851
4852 fn from_registry_dist(
4853 reg_dist: &RegistryBuiltDist,
4854 index: Option<&IndexUrl>,
4855 ) -> Result<Vec<Self>, LockError> {
4856 reg_dist
4857 .wheels
4858 .iter()
4859 .filter(|wheel| {
4860 index.is_some_and(|index| *index == wheel.index)
4863 })
4864 .map(Self::from_registry_wheel)
4865 .collect()
4866 }
4867
4868 fn from_registry_wheel(wheel: &RegistryBuiltWheel) -> Result<Self, LockError> {
4869 let url = match &wheel.index {
4870 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4871 let url = normalize_file_location(&wheel.file.url)
4872 .map_err(LockErrorKind::InvalidUrl)
4873 .map_err(LockError::from)?;
4874 WheelWireSource::Url { url }
4875 }
4876 IndexUrl::Path(path) => {
4877 let index_path = path
4878 .to_file_path()
4879 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4880 let wheel_url = wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?;
4881
4882 if wheel_url.scheme() == "file" {
4883 let wheel_path = wheel_url
4884 .to_file_path()
4885 .map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?;
4886 let path =
4887 try_relative_to_if(&wheel_path, index_path, !path.was_given_absolute())
4888 .map_err(LockErrorKind::DistributionRelativePath)?
4889 .into_boxed_path();
4890 WheelWireSource::Path { path }
4891 } else {
4892 let url = normalize_file_location(&wheel.file.url)
4893 .map_err(LockErrorKind::InvalidUrl)
4894 .map_err(LockError::from)?;
4895 WheelWireSource::Url { url }
4896 }
4897 }
4898 };
4899 let filename = wheel.filename.clone();
4900 let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from);
4901 let size = wheel.file.size;
4902 let upload_time = wheel
4903 .file
4904 .upload_time_utc_ms
4905 .map(Timestamp::from_millisecond)
4906 .transpose()
4907 .map_err(LockErrorKind::InvalidTimestamp)?;
4908 let zstd = wheel.file.zstd.as_ref().map(|zstd| ZstdWheel {
4909 hash: zstd.hashes.iter().max().cloned().map(Hash::from),
4910 size: zstd.size,
4911 });
4912 Ok(Self {
4913 url,
4914 hash,
4915 size,
4916 upload_time,
4917 filename,
4918 zstd,
4919 })
4920 }
4921
4922 fn from_direct_dist(direct_dist: &DirectUrlBuiltDist, hashes: &[HashDigest]) -> Self {
4923 Self {
4924 url: WheelWireSource::Url {
4925 url: normalize_url(direct_dist.url.to_url()),
4926 },
4927 hash: hashes.iter().max().cloned().map(Hash::from),
4928 size: None,
4929 upload_time: None,
4930 filename: direct_dist.filename.clone(),
4931 zstd: None,
4932 }
4933 }
4934
4935 fn from_path_dist(path_dist: &PathBuiltDist, hashes: &[HashDigest]) -> Self {
4936 Self {
4937 url: WheelWireSource::Filename {
4938 filename: path_dist.filename.clone(),
4939 },
4940 hash: hashes.iter().max().cloned().map(Hash::from),
4941 size: None,
4942 upload_time: None,
4943 filename: path_dist.filename.clone(),
4944 zstd: None,
4945 }
4946 }
4947
4948 pub(crate) fn to_registry_wheel(
4949 &self,
4950 source: &RegistrySource,
4951 root: &Path,
4952 ) -> Result<RegistryBuiltWheel, LockError> {
4953 let filename: WheelFilename = self.filename.clone();
4954
4955 match source {
4956 RegistrySource::Url(url) => {
4957 let file_location = match &self.url {
4958 WheelWireSource::Url { url: file_url } => {
4959 FileLocation::AbsoluteUrl(file_url.clone())
4960 }
4961 WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => {
4962 return Err(LockErrorKind::MissingUrl {
4963 name: filename.name,
4964 version: filename.version,
4965 }
4966 .into());
4967 }
4968 };
4969 let file = Box::new(uv_distribution_types::File {
4970 dist_info_metadata: false,
4971 filename: SmallString::from(filename.to_string()),
4972 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
4973 requires_python: None,
4974 size: self.size,
4975 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
4976 url: file_location,
4977 yanked: None,
4978 zstd: self
4979 .zstd
4980 .as_ref()
4981 .map(|zstd| uv_distribution_types::Zstd {
4982 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
4983 size: zstd.size,
4984 })
4985 .map(Box::new),
4986 });
4987 let index = IndexUrl::from(VerbatimUrl::from_url(
4988 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
4989 ));
4990 Ok(RegistryBuiltWheel {
4991 filename,
4992 file,
4993 index,
4994 })
4995 }
4996 RegistrySource::Path(index_path) => {
4997 let file_location = match &self.url {
4998 WheelWireSource::Url { url: file_url } => {
4999 FileLocation::AbsoluteUrl(file_url.clone())
5000 }
5001 WheelWireSource::Path { path: file_path } => {
5002 let file_path = root.join(index_path).join(file_path);
5003 let file_url =
5004 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
5005 LockErrorKind::PathToUrl {
5006 path: file_path.into_boxed_path(),
5007 }
5008 })?;
5009 FileLocation::AbsoluteUrl(UrlString::from(file_url))
5010 }
5011 WheelWireSource::Filename { .. } => {
5012 return Err(LockErrorKind::MissingPath {
5013 name: filename.name,
5014 version: filename.version,
5015 }
5016 .into());
5017 }
5018 };
5019 let file = Box::new(uv_distribution_types::File {
5020 dist_info_metadata: false,
5021 filename: SmallString::from(filename.to_string()),
5022 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
5023 requires_python: None,
5024 size: self.size,
5025 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
5026 url: file_location,
5027 yanked: None,
5028 zstd: self
5029 .zstd
5030 .as_ref()
5031 .map(|zstd| uv_distribution_types::Zstd {
5032 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
5033 size: zstd.size,
5034 })
5035 .map(Box::new),
5036 });
5037 let index = IndexUrl::from(
5038 VerbatimUrl::from_absolute_path(root.join(index_path))
5039 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
5040 );
5041 Ok(RegistryBuiltWheel {
5042 filename,
5043 file,
5044 index,
5045 })
5046 }
5047 }
5048 }
5049}
5050
5051#[derive(Clone, Debug, serde::Deserialize)]
5052#[serde(rename_all = "kebab-case")]
5053struct WheelWire {
5054 #[serde(flatten)]
5055 url: WheelWireSource,
5056 hash: Option<Hash>,
5062 size: Option<u64>,
5066 #[serde(alias = "upload_time")]
5070 upload_time: Option<Timestamp>,
5071 #[serde(alias = "zstd")]
5073 zstd: Option<ZstdWheel>,
5074}
5075
5076#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
5077#[serde(untagged, rename_all = "kebab-case")]
5078enum WheelWireSource {
5079 Url {
5081 url: UrlString,
5086 },
5087 Path {
5089 path: Box<Path>,
5091 },
5092 Filename {
5096 filename: WheelFilename,
5099 },
5100}
5101
5102impl Wheel {
5103 fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
5105 let mut table = InlineTable::new();
5106 match &self.url {
5107 WheelWireSource::Url { url } => {
5108 table.insert("url", Value::from(url.as_ref()));
5109 }
5110 WheelWireSource::Path { path } => {
5111 table.insert("path", Value::from(PortablePath::from(path).to_string()));
5112 }
5113 WheelWireSource::Filename { filename } => {
5114 table.insert("filename", Value::from(filename.to_string()));
5115 }
5116 }
5117 if let Some(ref hash) = self.hash {
5118 table.insert("hash", Value::from(hash.to_string()));
5119 }
5120 if let Some(size) = self.size {
5121 table.insert(
5122 "size",
5123 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
5124 );
5125 }
5126 if let Some(upload_time) = self.upload_time {
5127 table.insert("upload-time", Value::from(upload_time.to_string()));
5128 }
5129 if let Some(zstd) = &self.zstd {
5130 let mut inner = InlineTable::new();
5131 if let Some(ref hash) = zstd.hash {
5132 inner.insert("hash", Value::from(hash.to_string()));
5133 }
5134 if let Some(size) = zstd.size {
5135 inner.insert(
5136 "size",
5137 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
5138 );
5139 }
5140 table.insert("zstd", Value::from(inner));
5141 }
5142 Ok(table)
5143 }
5144}
5145
5146impl TryFrom<WheelWire> for Wheel {
5147 type Error = String;
5148
5149 fn try_from(wire: WheelWire) -> Result<Self, String> {
5150 let filename = match &wire.url {
5151 WheelWireSource::Url { url } => {
5152 let filename = url.filename().map_err(|err| err.to_string())?;
5153 filename.parse::<WheelFilename>().map_err(|err| {
5154 format!("failed to parse `{filename}` as wheel filename: {err}")
5155 })?
5156 }
5157 WheelWireSource::Path { path } => {
5158 let filename = path
5159 .file_name()
5160 .and_then(|file_name| file_name.to_str())
5161 .ok_or_else(|| {
5162 format!("path `{}` has no filename component", path.display())
5163 })?;
5164 filename.parse::<WheelFilename>().map_err(|err| {
5165 format!("failed to parse `{filename}` as wheel filename: {err}")
5166 })?
5167 }
5168 WheelWireSource::Filename { filename } => filename.clone(),
5169 };
5170
5171 Ok(Self {
5172 url: wire.url,
5173 hash: wire.hash,
5174 size: wire.size,
5175 upload_time: wire.upload_time,
5176 zstd: wire.zstd,
5177 filename,
5178 })
5179 }
5180}
5181
5182#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
5184pub struct Dependency {
5185 package_id: PackageId,
5186 extra: BTreeSet<ExtraName>,
5187 simplified_marker: SimplifiedMarkerTree,
5207 complexified_marker: UniversalMarker,
5211}
5212
5213impl Dependency {
5214 fn new(
5215 requires_python: &RequiresPython,
5216 package_id: PackageId,
5217 extra: BTreeSet<ExtraName>,
5218 complexified_marker: UniversalMarker,
5219 ) -> Self {
5220 let simplified_marker =
5221 SimplifiedMarkerTree::new(requires_python, complexified_marker.combined());
5222 let complexified_marker = simplified_marker.into_marker(requires_python);
5223 Self {
5224 package_id,
5225 extra,
5226 simplified_marker,
5227 complexified_marker: UniversalMarker::from_combined(complexified_marker),
5228 }
5229 }
5230
5231 fn from_annotated_dist(
5232 requires_python: &RequiresPython,
5233 annotated_dist: &AnnotatedDist,
5234 complexified_marker: UniversalMarker,
5235 root: &Path,
5236 ) -> Result<Self, LockError> {
5237 let package_id = PackageId::from_annotated_dist(annotated_dist, root)?;
5238 let extra = annotated_dist.extra.iter().cloned().collect();
5239 Ok(Self::new(
5240 requires_python,
5241 package_id,
5242 extra,
5243 complexified_marker,
5244 ))
5245 }
5246
5247 fn to_toml(
5249 &self,
5250 _requires_python: &RequiresPython,
5251 dist_count_by_name: &FxHashMap<PackageName, u64>,
5252 ) -> Table {
5253 let mut table = Table::new();
5254 self.package_id
5255 .to_toml(Some(dist_count_by_name), &mut table);
5256 if !self.extra.is_empty() {
5257 let extra_array = self
5258 .extra
5259 .iter()
5260 .map(ToString::to_string)
5261 .collect::<Array>();
5262 table.insert("extra", value(extra_array));
5263 }
5264 if let Some(marker) = self.simplified_marker.try_to_string() {
5265 table.insert("marker", value(marker));
5266 }
5267
5268 table
5269 }
5270
5271 pub fn package_name(&self) -> &PackageName {
5273 &self.package_id.name
5274 }
5275
5276 pub fn extra(&self) -> &BTreeSet<ExtraName> {
5278 &self.extra
5279 }
5280}
5281
5282impl Display for Dependency {
5283 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5284 match (self.extra.is_empty(), self.package_id.version.as_ref()) {
5285 (true, Some(version)) => write!(f, "{}=={}", self.package_id.name, version),
5286 (true, None) => write!(f, "{}", self.package_id.name),
5287 (false, Some(version)) => write!(
5288 f,
5289 "{}[{}]=={}",
5290 self.package_id.name,
5291 self.extra.iter().join(","),
5292 version
5293 ),
5294 (false, None) => write!(
5295 f,
5296 "{}[{}]",
5297 self.package_id.name,
5298 self.extra.iter().join(",")
5299 ),
5300 }
5301 }
5302}
5303
5304#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)]
5306#[serde(rename_all = "kebab-case")]
5307struct DependencyWire {
5308 #[serde(flatten)]
5309 package_id: PackageIdForDependency,
5310 #[serde(default)]
5311 extra: BTreeSet<ExtraName>,
5312 #[serde(default)]
5313 marker: SimplifiedMarkerTree,
5314}
5315
5316impl DependencyWire {
5317 fn unwire(
5318 self,
5319 requires_python: &RequiresPython,
5320 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
5321 ) -> Result<Dependency, LockError> {
5322 let complexified_marker = self.marker.into_marker(requires_python);
5323 Ok(Dependency {
5324 package_id: self.package_id.unwire(unambiguous_package_ids)?,
5325 extra: self.extra,
5326 simplified_marker: self.marker,
5327 complexified_marker: UniversalMarker::from_combined(complexified_marker),
5328 })
5329 }
5330}
5331
5332#[derive(Clone, Debug, PartialEq, Eq)]
5337struct Hash(HashDigest);
5338
5339impl From<HashDigest> for Hash {
5340 fn from(hd: HashDigest) -> Self {
5341 Self(hd)
5342 }
5343}
5344
5345impl FromStr for Hash {
5346 type Err = HashParseError;
5347
5348 fn from_str(s: &str) -> Result<Self, HashParseError> {
5349 let (algorithm, digest) = s.split_once(':').ok_or(HashParseError(
5350 "expected '{algorithm}:{digest}', but found no ':' in hash digest",
5351 ))?;
5352 let algorithm = algorithm
5353 .parse()
5354 .map_err(|_| HashParseError("unrecognized hash algorithm"))?;
5355 Ok(Self(HashDigest {
5356 algorithm,
5357 digest: digest.into(),
5358 }))
5359 }
5360}
5361
5362impl Display for Hash {
5363 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5364 write!(f, "{}:{}", self.0.algorithm, self.0.digest)
5365 }
5366}
5367
5368impl<'de> serde::Deserialize<'de> for Hash {
5369 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5370 where
5371 D: serde::de::Deserializer<'de>,
5372 {
5373 struct Visitor;
5374
5375 impl serde::de::Visitor<'_> for Visitor {
5376 type Value = Hash;
5377
5378 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
5379 f.write_str("a string")
5380 }
5381
5382 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
5383 Hash::from_str(v).map_err(serde::de::Error::custom)
5384 }
5385 }
5386
5387 deserializer.deserialize_str(Visitor)
5388 }
5389}
5390
5391impl From<Hash> for Hashes {
5392 fn from(value: Hash) -> Self {
5393 match value.0.algorithm {
5394 HashAlgorithm::Md5 => Self {
5395 md5: Some(value.0.digest),
5396 sha256: None,
5397 sha384: None,
5398 sha512: None,
5399 blake2b: None,
5400 },
5401 HashAlgorithm::Sha256 => Self {
5402 md5: None,
5403 sha256: Some(value.0.digest),
5404 sha384: None,
5405 sha512: None,
5406 blake2b: None,
5407 },
5408 HashAlgorithm::Sha384 => Self {
5409 md5: None,
5410 sha256: None,
5411 sha384: Some(value.0.digest),
5412 sha512: None,
5413 blake2b: None,
5414 },
5415 HashAlgorithm::Sha512 => Self {
5416 md5: None,
5417 sha256: None,
5418 sha384: None,
5419 sha512: Some(value.0.digest),
5420 blake2b: None,
5421 },
5422 HashAlgorithm::Blake2b => Self {
5423 md5: None,
5424 sha256: None,
5425 sha384: None,
5426 sha512: None,
5427 blake2b: Some(value.0.digest),
5428 },
5429 }
5430 }
5431}
5432
5433fn normalize_file_location(location: &FileLocation) -> Result<UrlString, ToUrlError> {
5435 match location {
5436 FileLocation::AbsoluteUrl(absolute) => Ok(absolute.without_fragment().into_owned()),
5437 FileLocation::RelativeUrl(_, _) => Ok(normalize_url(location.to_url()?)),
5438 }
5439}
5440
5441fn normalize_url(mut url: DisplaySafeUrl) -> UrlString {
5443 url.set_fragment(None);
5444 UrlString::from(url)
5445}
5446
5447fn normalize_requirement(
5457 mut requirement: Requirement,
5458 root: &Path,
5459 requires_python: &RequiresPython,
5460) -> Result<Requirement, LockError> {
5461 requirement.extras.sort();
5463 requirement.groups.sort();
5464
5465 match requirement.source {
5467 RequirementSource::Git {
5468 git,
5469 subdirectory,
5470 url: _,
5471 } => {
5472 let git = {
5474 let mut repository = git.url().clone();
5475
5476 repository.remove_credentials();
5478
5479 repository.set_fragment(None);
5481 repository.set_query(None);
5482
5483 GitUrl::from_fields(
5484 repository,
5485 git.reference().clone(),
5486 git.precise(),
5487 git.lfs(),
5488 )?
5489 };
5490
5491 let url = DisplaySafeUrl::from(ParsedGitUrl {
5493 url: git.clone(),
5494 subdirectory: subdirectory.clone(),
5495 });
5496
5497 Ok(Requirement {
5498 name: requirement.name,
5499 extras: requirement.extras,
5500 groups: requirement.groups,
5501 marker: requires_python.simplify_markers(requirement.marker),
5502 source: RequirementSource::Git {
5503 git,
5504 subdirectory,
5505 url: VerbatimUrl::from_url(url),
5506 },
5507 origin: None,
5508 })
5509 }
5510 RequirementSource::Path {
5511 install_path,
5512 ext,
5513 url: _,
5514 } => {
5515 let path = root.join(&install_path);
5516 let install_path = normalize_path(path).into_owned().into_boxed_path();
5517 let url = VerbatimUrl::from_normalized_path(&install_path)
5518 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5519
5520 Ok(Requirement {
5521 name: requirement.name,
5522 extras: requirement.extras,
5523 groups: requirement.groups,
5524 marker: requires_python.simplify_markers(requirement.marker),
5525 source: RequirementSource::Path {
5526 install_path,
5527 ext,
5528 url,
5529 },
5530 origin: None,
5531 })
5532 }
5533 RequirementSource::Directory {
5534 install_path,
5535 editable,
5536 r#virtual,
5537 url: _,
5538 } => {
5539 let path = root.join(&install_path);
5540 let install_path = normalize_path(path).into_owned().into_boxed_path();
5541 let url = VerbatimUrl::from_normalized_path(&install_path)
5542 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5543
5544 Ok(Requirement {
5545 name: requirement.name,
5546 extras: requirement.extras,
5547 groups: requirement.groups,
5548 marker: requires_python.simplify_markers(requirement.marker),
5549 source: RequirementSource::Directory {
5550 install_path,
5551 editable: Some(editable.unwrap_or(false)),
5552 r#virtual: Some(r#virtual.unwrap_or(false)),
5553 url,
5554 },
5555 origin: None,
5556 })
5557 }
5558 RequirementSource::Registry {
5559 specifier,
5560 index,
5561 conflict,
5562 } => {
5563 let index = index
5565 .map(|index| index.url.into_url())
5566 .map(|mut index| {
5567 index.remove_credentials();
5568 index
5569 })
5570 .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index))));
5571 Ok(Requirement {
5572 name: requirement.name,
5573 extras: requirement.extras,
5574 groups: requirement.groups,
5575 marker: requires_python.simplify_markers(requirement.marker),
5576 source: RequirementSource::Registry {
5577 specifier,
5578 index,
5579 conflict,
5580 },
5581 origin: None,
5582 })
5583 }
5584 RequirementSource::Url {
5585 mut location,
5586 subdirectory,
5587 ext,
5588 url: _,
5589 } => {
5590 location.remove_credentials();
5592
5593 location.set_fragment(None);
5595
5596 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
5598 url: location.clone(),
5599 subdirectory: subdirectory.clone(),
5600 ext,
5601 });
5602
5603 Ok(Requirement {
5604 name: requirement.name,
5605 extras: requirement.extras,
5606 groups: requirement.groups,
5607 marker: requires_python.simplify_markers(requirement.marker),
5608 source: RequirementSource::Url {
5609 location,
5610 subdirectory,
5611 ext,
5612 url: VerbatimUrl::from_url(url),
5613 },
5614 origin: None,
5615 })
5616 }
5617 }
5618}
5619
5620#[derive(Debug)]
5621pub struct LockError {
5622 kind: Box<LockErrorKind>,
5623 hint: Option<WheelTagHint>,
5624}
5625
5626impl std::error::Error for LockError {
5627 fn source(&self) -> Option<&(dyn Error + 'static)> {
5628 self.kind.source()
5629 }
5630}
5631
5632impl std::fmt::Display for LockError {
5633 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5634 write!(f, "{}", self.kind)?;
5635 if let Some(hint) = &self.hint {
5636 write!(f, "\n\n{hint}")?;
5637 }
5638 Ok(())
5639 }
5640}
5641
5642impl LockError {
5643 pub fn is_resolution(&self) -> bool {
5645 matches!(&*self.kind, LockErrorKind::Resolution { .. })
5646 }
5647}
5648
5649impl<E> From<E> for LockError
5650where
5651 LockErrorKind: From<E>,
5652{
5653 fn from(err: E) -> Self {
5654 Self {
5655 kind: Box::new(LockErrorKind::from(err)),
5656 hint: None,
5657 }
5658 }
5659}
5660
5661#[derive(Debug, Clone, PartialEq, Eq)]
5662#[expect(clippy::enum_variant_names)]
5663enum WheelTagHint {
5664 LanguageTags {
5667 package: PackageName,
5668 version: Option<Version>,
5669 tags: BTreeSet<LanguageTag>,
5670 best: Option<LanguageTag>,
5671 },
5672 AbiTags {
5675 package: PackageName,
5676 version: Option<Version>,
5677 tags: BTreeSet<AbiTag>,
5678 best: Option<AbiTag>,
5679 },
5680 PlatformTags {
5683 package: PackageName,
5684 version: Option<Version>,
5685 tags: BTreeSet<PlatformTag>,
5686 best: Option<PlatformTag>,
5687 markers: MarkerEnvironment,
5688 },
5689}
5690
5691impl WheelTagHint {
5692 fn from_wheels(
5694 name: &PackageName,
5695 version: Option<&Version>,
5696 filenames: &[&WheelFilename],
5697 tags: &Tags,
5698 markers: &MarkerEnvironment,
5699 ) -> Option<Self> {
5700 let incompatibility = filenames
5701 .iter()
5702 .map(|filename| {
5703 tags.compatibility(
5704 filename.python_tags(),
5705 filename.abi_tags(),
5706 filename.platform_tags(),
5707 )
5708 })
5709 .max()?;
5710 match incompatibility {
5711 TagCompatibility::Incompatible(IncompatibleTag::Python) => {
5712 let best = tags.python_tag();
5713 let tags = Self::python_tags(filenames.iter().copied()).collect::<BTreeSet<_>>();
5714 if tags.is_empty() {
5715 None
5716 } else {
5717 Some(Self::LanguageTags {
5718 package: name.clone(),
5719 version: version.cloned(),
5720 tags,
5721 best,
5722 })
5723 }
5724 }
5725 TagCompatibility::Incompatible(IncompatibleTag::Abi) => {
5726 let best = tags.abi_tag();
5727 let tags = Self::abi_tags(filenames.iter().copied())
5728 .filter(|tag| *tag != AbiTag::None)
5737 .collect::<BTreeSet<_>>();
5738 if tags.is_empty() {
5739 None
5740 } else {
5741 Some(Self::AbiTags {
5742 package: name.clone(),
5743 version: version.cloned(),
5744 tags,
5745 best,
5746 })
5747 }
5748 }
5749 TagCompatibility::Incompatible(IncompatibleTag::Platform) => {
5750 let best = tags.platform_tag().cloned();
5751 let incompatible_tags = Self::platform_tags(filenames.iter().copied(), tags)
5752 .cloned()
5753 .collect::<BTreeSet<_>>();
5754 if incompatible_tags.is_empty() {
5755 None
5756 } else {
5757 Some(Self::PlatformTags {
5758 package: name.clone(),
5759 version: version.cloned(),
5760 tags: incompatible_tags,
5761 best,
5762 markers: markers.clone(),
5763 })
5764 }
5765 }
5766 _ => None,
5767 }
5768 }
5769
5770 fn python_tags<'a>(
5772 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5773 ) -> impl Iterator<Item = LanguageTag> + 'a {
5774 filenames.flat_map(WheelFilename::python_tags).copied()
5775 }
5776
5777 fn abi_tags<'a>(
5779 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5780 ) -> impl Iterator<Item = AbiTag> + 'a {
5781 filenames.flat_map(WheelFilename::abi_tags).copied()
5782 }
5783
5784 fn platform_tags<'a>(
5787 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5788 tags: &'a Tags,
5789 ) -> impl Iterator<Item = &'a PlatformTag> + 'a {
5790 filenames.flat_map(move |filename| {
5791 if filename.python_tags().iter().any(|wheel_py| {
5792 filename
5793 .abi_tags()
5794 .iter()
5795 .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
5796 }) {
5797 filename.platform_tags().iter()
5798 } else {
5799 [].iter()
5800 }
5801 })
5802 }
5803
5804 fn suggest_environment_marker(markers: &MarkerEnvironment) -> String {
5805 let sys_platform = markers.sys_platform();
5806 let platform_machine = markers.platform_machine();
5807
5808 if platform_machine.is_empty() {
5810 format!("sys_platform == '{sys_platform}'")
5811 } else {
5812 format!("sys_platform == '{sys_platform}' and platform_machine == '{platform_machine}'")
5813 }
5814 }
5815}
5816
5817impl std::fmt::Display for WheelTagHint {
5818 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5819 match self {
5820 Self::LanguageTags {
5821 package,
5822 version,
5823 tags,
5824 best,
5825 } => {
5826 if let Some(best) = best {
5827 let s = if tags.len() == 1 { "" } else { "s" };
5828 let best = if let Some(pretty) = best.pretty() {
5829 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5830 } else {
5831 format!("{}", best.cyan())
5832 };
5833 if let Some(version) = version {
5834 write!(
5835 f,
5836 "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python implementation tag{s}: {}",
5837 "hint".bold().cyan(),
5838 ":".bold(),
5839 best,
5840 package.cyan(),
5841 format!("v{version}").cyan(),
5842 tags.iter()
5843 .map(|tag| format!("`{}`", tag.cyan()))
5844 .join(", "),
5845 )
5846 } else {
5847 write!(
5848 f,
5849 "{}{} You're using {}, but `{}` only has wheels with the following Python implementation tag{s}: {}",
5850 "hint".bold().cyan(),
5851 ":".bold(),
5852 best,
5853 package.cyan(),
5854 tags.iter()
5855 .map(|tag| format!("`{}`", tag.cyan()))
5856 .join(", "),
5857 )
5858 }
5859 } else {
5860 let s = if tags.len() == 1 { "" } else { "s" };
5861 if let Some(version) = version {
5862 write!(
5863 f,
5864 "{}{} Wheels are available for `{}` ({}) with the following Python implementation tag{s}: {}",
5865 "hint".bold().cyan(),
5866 ":".bold(),
5867 package.cyan(),
5868 format!("v{version}").cyan(),
5869 tags.iter()
5870 .map(|tag| format!("`{}`", tag.cyan()))
5871 .join(", "),
5872 )
5873 } else {
5874 write!(
5875 f,
5876 "{}{} Wheels are available for `{}` with the following Python implementation tag{s}: {}",
5877 "hint".bold().cyan(),
5878 ":".bold(),
5879 package.cyan(),
5880 tags.iter()
5881 .map(|tag| format!("`{}`", tag.cyan()))
5882 .join(", "),
5883 )
5884 }
5885 }
5886 }
5887 Self::AbiTags {
5888 package,
5889 version,
5890 tags,
5891 best,
5892 } => {
5893 if let Some(best) = best {
5894 let s = if tags.len() == 1 { "" } else { "s" };
5895 let best = if let Some(pretty) = best.pretty() {
5896 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5897 } else {
5898 format!("{}", best.cyan())
5899 };
5900 if let Some(version) = version {
5901 write!(
5902 f,
5903 "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python ABI tag{s}: {}",
5904 "hint".bold().cyan(),
5905 ":".bold(),
5906 best,
5907 package.cyan(),
5908 format!("v{version}").cyan(),
5909 tags.iter()
5910 .map(|tag| format!("`{}`", tag.cyan()))
5911 .join(", "),
5912 )
5913 } else {
5914 write!(
5915 f,
5916 "{}{} You're using {}, but `{}` only has wheels with the following Python ABI tag{s}: {}",
5917 "hint".bold().cyan(),
5918 ":".bold(),
5919 best,
5920 package.cyan(),
5921 tags.iter()
5922 .map(|tag| format!("`{}`", tag.cyan()))
5923 .join(", "),
5924 )
5925 }
5926 } else {
5927 let s = if tags.len() == 1 { "" } else { "s" };
5928 if let Some(version) = version {
5929 write!(
5930 f,
5931 "{}{} Wheels are available for `{}` ({}) with the following Python ABI tag{s}: {}",
5932 "hint".bold().cyan(),
5933 ":".bold(),
5934 package.cyan(),
5935 format!("v{version}").cyan(),
5936 tags.iter()
5937 .map(|tag| format!("`{}`", tag.cyan()))
5938 .join(", "),
5939 )
5940 } else {
5941 write!(
5942 f,
5943 "{}{} Wheels are available for `{}` with the following Python ABI tag{s}: {}",
5944 "hint".bold().cyan(),
5945 ":".bold(),
5946 package.cyan(),
5947 tags.iter()
5948 .map(|tag| format!("`{}`", tag.cyan()))
5949 .join(", "),
5950 )
5951 }
5952 }
5953 }
5954 Self::PlatformTags {
5955 package,
5956 version,
5957 tags,
5958 best,
5959 markers,
5960 } => {
5961 let s = if tags.len() == 1 { "" } else { "s" };
5962 if let Some(best) = best {
5963 let example_marker = Self::suggest_environment_marker(markers);
5964 let best = if let Some(pretty) = best.pretty() {
5965 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5966 } else {
5967 format!("`{}`", best.cyan())
5968 };
5969 let package_ref = if let Some(version) = version {
5970 format!("`{}` ({})", package.cyan(), format!("v{version}").cyan())
5971 } else {
5972 format!("`{}`", package.cyan())
5973 };
5974 write!(
5975 f,
5976 "{}{} 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",
5977 "hint".bold().cyan(),
5978 ":".bold(),
5979 best,
5980 package_ref,
5981 tags.iter()
5982 .map(|tag| format!("`{}`", tag.cyan()))
5983 .join(", "),
5984 format!("\"{example_marker}\"").cyan(),
5985 "tool.uv.required-environments".green()
5986 )
5987 } else {
5988 if let Some(version) = version {
5989 write!(
5990 f,
5991 "{}{} Wheels are available for `{}` ({}) on the following platform{s}: {}",
5992 "hint".bold().cyan(),
5993 ":".bold(),
5994 package.cyan(),
5995 format!("v{version}").cyan(),
5996 tags.iter()
5997 .map(|tag| format!("`{}`", tag.cyan()))
5998 .join(", "),
5999 )
6000 } else {
6001 write!(
6002 f,
6003 "{}{} Wheels are available for `{}` on the following platform{s}: {}",
6004 "hint".bold().cyan(),
6005 ":".bold(),
6006 package.cyan(),
6007 tags.iter()
6008 .map(|tag| format!("`{}`", tag.cyan()))
6009 .join(", "),
6010 )
6011 }
6012 }
6013 }
6014 }
6015 }
6016}
6017
6018#[derive(Debug, thiserror::Error)]
6025enum LockErrorKind {
6026 #[error("Found duplicate package `{id}`", id = id.cyan())]
6029 DuplicatePackage {
6030 id: PackageId,
6032 },
6033 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = id.cyan(), dependency = dependency.cyan())]
6036 DuplicateDependency {
6037 id: PackageId,
6040 dependency: Dependency,
6042 },
6043 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}[{extra}]").cyan(), dependency = dependency.cyan())]
6047 DuplicateOptionalDependency {
6048 id: PackageId,
6051 extra: ExtraName,
6053 dependency: Dependency,
6055 },
6056 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}:{group}").cyan(), dependency = dependency.cyan())]
6060 DuplicateDevDependency {
6061 id: PackageId,
6064 group: GroupName,
6066 dependency: Dependency,
6068 },
6069 #[error(transparent)]
6072 InvalidUrl(
6073 #[from]
6076 ToUrlError,
6077 ),
6078 #[error("Failed to parse file extension for `{id}`; expected one of: {err}", id = id.cyan())]
6081 MissingExtension {
6082 id: PackageId,
6084 err: ExtensionError,
6086 },
6087 #[error("Failed to parse Git URL")]
6089 InvalidGitSourceUrl(
6090 #[source]
6093 SourceParseError,
6094 ),
6095 #[error("Failed to parse timestamp")]
6096 InvalidTimestamp(
6097 #[source]
6100 jiff::Error,
6101 ),
6102 #[error("For package `{id}`, found dependency `{dependency}` with no locked package", id = id.cyan(), dependency = dependency.cyan())]
6106 UnrecognizedDependency {
6107 id: PackageId,
6109 dependency: Dependency,
6112 },
6113 #[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" })]
6116 Hash {
6117 id: PackageId,
6119 artifact_type: &'static str,
6122 expected: bool,
6124 },
6125 #[error("Found package `{id}` with extra `{extra}` but no base package", id = id.cyan(), extra = extra.cyan())]
6128 MissingExtraBase {
6129 id: PackageId,
6131 extra: ExtraName,
6133 },
6134 #[error("Found package `{id}` with development dependency group `{group}` but no base package", id = id.cyan())]
6138 MissingDevBase {
6139 id: PackageId,
6141 group: GroupName,
6143 },
6144 #[error("Wheels cannot come from {source_type} sources")]
6147 InvalidWheelSource {
6148 id: PackageId,
6150 source_type: &'static str,
6152 },
6153 #[error("Found registry distribution `{name}` ({version}) without a valid URL", name = name.cyan(), version = format!("v{version}").cyan())]
6156 MissingUrl {
6157 name: PackageName,
6159 version: Version,
6161 },
6162 #[error("Found registry distribution `{name}` ({version}) without a valid path", name = name.cyan(), version = format!("v{version}").cyan())]
6165 MissingPath {
6166 name: PackageName,
6168 version: Version,
6170 },
6171 #[error("Found registry distribution `{id}` without a valid filename", id = id.cyan())]
6174 MissingFilename {
6175 id: PackageId,
6177 },
6178 #[error("Distribution `{id}` can't be installed because it doesn't have a source distribution or wheel for the current platform", id = id.cyan())]
6181 NeitherSourceDistNorWheel {
6182 id: PackageId,
6184 },
6185 #[error("Distribution `{id}` can't be installed because it is marked as both `--no-binary` and `--no-build`", id = id.cyan())]
6187 NoBinaryNoBuild {
6188 id: PackageId,
6190 },
6191 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but has no source distribution", id = id.cyan())]
6194 NoBinary {
6195 id: PackageId,
6197 },
6198 #[error("Distribution `{id}` can't be installed because it is marked as `--no-build` but has no binary distribution", id = id.cyan())]
6201 NoBuild {
6202 id: PackageId,
6204 },
6205 #[error("Distribution `{id}` can't be installed because the binary distribution is incompatible with the current platform", id = id.cyan())]
6208 IncompatibleWheelOnly {
6209 id: PackageId,
6211 },
6212 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but is itself a binary distribution", id = id.cyan())]
6214 NoBinaryWheelOnly {
6215 id: PackageId,
6217 },
6218 #[error("Found dependency `{id}` with no locked distribution", id = id.cyan())]
6220 VerbatimUrl {
6221 id: PackageId,
6223 #[source]
6225 err: VerbatimUrlError,
6226 },
6227 #[error("Could not compute relative path between workspace and distribution")]
6229 DistributionRelativePath(
6230 #[source]
6232 io::Error,
6233 ),
6234 #[error("Could not compute relative path between workspace and index")]
6236 IndexRelativePath(
6237 #[source]
6239 io::Error,
6240 ),
6241 #[error("Could not compute absolute path from workspace root and lockfile path")]
6243 AbsolutePath(
6244 #[source]
6246 io::Error,
6247 ),
6248 #[error("Dependency `{name}` has missing `version` field but has more than one matching package", name = name.cyan())]
6251 MissingDependencyVersion {
6252 name: PackageName,
6254 },
6255 #[error("Dependency `{name}` has missing `source` field but has more than one matching package", name = name.cyan())]
6258 MissingDependencySource {
6259 name: PackageName,
6261 },
6262 #[error("Could not compute relative path between workspace and requirement")]
6264 RequirementRelativePath(
6265 #[source]
6267 io::Error,
6268 ),
6269 #[error("Could not convert between URL and path")]
6271 RequirementVerbatimUrl(
6272 #[source]
6274 VerbatimUrlError,
6275 ),
6276 #[error("Could not convert between URL and path")]
6278 RegistryVerbatimUrl(
6279 #[source]
6281 VerbatimUrlError,
6282 ),
6283 #[error("Failed to convert path to URL: {path}", path = path.display().cyan())]
6285 PathToUrl { path: Box<Path> },
6286 #[error("Failed to convert URL to path: {url}", url = url.cyan())]
6288 UrlToPath { url: DisplaySafeUrl },
6289 #[error("Found multiple packages matching `{name}`", name = name.cyan())]
6292 MultipleRootPackages {
6293 name: PackageName,
6295 },
6296 #[error("Could not find root package `{name}`", name = name.cyan())]
6298 MissingRootPackage {
6299 name: PackageName,
6301 },
6302 #[error("Failed to generate package metadata for `{id}`", id = id.cyan())]
6304 Resolution {
6305 id: PackageId,
6307 #[source]
6309 err: uv_distribution::Error,
6310 },
6311 #[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())]
6314 InconsistentVersions {
6315 name: PackageName,
6317 version: Version,
6319 wheel: Wheel,
6321 },
6322 #[error(
6323 "Found conflicting extras `{package1}[{extra1}]` \
6324 and `{package2}[{extra2}]` enabled simultaneously"
6325 )]
6326 ConflictingExtra {
6327 package1: PackageName,
6328 extra1: ExtraName,
6329 package2: PackageName,
6330 extra2: ExtraName,
6331 },
6332 #[error(transparent)]
6333 GitUrlParse(#[from] GitUrlParseError),
6334 #[error("Failed to read `{path}`")]
6335 UnreadablePyprojectToml {
6336 path: PathBuf,
6337 #[source]
6338 err: std::io::Error,
6339 },
6340 #[error("Failed to parse `{path}`")]
6341 InvalidPyprojectToml {
6342 path: PathBuf,
6343 #[source]
6344 err: uv_pypi_types::MetadataError,
6345 },
6346 #[error("Workspace member `{id}` has non-local source", id = id.cyan())]
6348 NonLocalWorkspaceMember {
6349 id: PackageId,
6351 },
6352}
6353
6354#[derive(Debug, thiserror::Error)]
6356enum SourceParseError {
6357 #[error("Invalid URL in source `{given}`")]
6359 InvalidUrl {
6360 given: String,
6362 #[source]
6364 err: DisplaySafeUrlError,
6365 },
6366 #[error("Missing SHA in source `{given}`")]
6368 MissingSha {
6369 given: String,
6371 },
6372 #[error("Invalid SHA in source `{given}`")]
6374 InvalidSha {
6375 given: String,
6377 },
6378}
6379
6380#[derive(Clone, Debug, Eq, PartialEq)]
6382struct HashParseError(&'static str);
6383
6384impl std::error::Error for HashParseError {}
6385
6386impl Display for HashParseError {
6387 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
6388 Display::fmt(self.0, f)
6389 }
6390}
6391
6392fn each_element_on_its_line_array(elements: impl Iterator<Item = impl Into<Value>>) -> Array {
6403 let mut array = elements
6404 .map(|item| {
6405 let mut value = item.into();
6406 value.decor_mut().set_prefix("\n ");
6408 value
6409 })
6410 .collect::<Array>();
6411 array.set_trailing_comma(true);
6414 array.set_trailing("\n");
6416 array
6417}
6418
6419fn simplified_universal_markers(
6424 markers: &[UniversalMarker],
6425 requires_python: &RequiresPython,
6426) -> Vec<String> {
6427 canonical_marker_trees(markers, requires_python)
6428 .into_iter()
6429 .filter_map(MarkerTree::try_to_string)
6430 .collect()
6431}
6432
6433fn canonicalize_universal_markers(
6440 markers: &[UniversalMarker],
6441 requires_python: &RequiresPython,
6442) -> Vec<UniversalMarker> {
6443 canonical_marker_trees(markers, requires_python)
6444 .into_iter()
6445 .map(|marker| {
6446 let simplified = SimplifiedMarkerTree::new(requires_python, marker);
6447 UniversalMarker::from_combined(simplified.into_marker(requires_python))
6448 })
6449 .collect()
6450}
6451
6452fn canonical_marker_trees(
6454 markers: &[UniversalMarker],
6455 requires_python: &RequiresPython,
6456) -> Vec<MarkerTree> {
6457 let mut pep508_only = vec![];
6458 let mut seen = FxHashSet::default();
6459 for marker in markers {
6460 let simplified =
6461 SimplifiedMarkerTree::new(requires_python, marker.pep508()).as_simplified_marker_tree();
6462 if seen.insert(simplified) {
6463 pep508_only.push(simplified);
6464 }
6465 }
6466 let any_overlap = pep508_only
6467 .iter()
6468 .tuple_combinations()
6469 .any(|(&marker1, &marker2)| !marker1.is_disjoint(marker2));
6470 let markers = if !any_overlap {
6471 pep508_only
6472 } else {
6473 markers
6474 .iter()
6475 .map(|marker| {
6476 SimplifiedMarkerTree::new(requires_python, marker.combined())
6477 .as_simplified_marker_tree()
6478 })
6479 .collect()
6480 };
6481 markers
6482 .into_iter()
6483 .filter(|marker| !marker.is_true())
6484 .collect()
6485}
6486
6487fn is_wheel_unreachable_for_marker(
6495 filename: &WheelFilename,
6496 requires_python: &RequiresPython,
6497 marker: &UniversalMarker,
6498 tags: Option<&Tags>,
6499) -> bool {
6500 if let Some(tags) = tags
6501 && !filename.compatibility(tags).is_compatible()
6502 {
6503 return true;
6504 }
6505 if !requires_python.matches_wheel_tag(filename) {
6507 return true;
6508 }
6509
6510 let platform_tags = filename.platform_tags();
6519
6520 if platform_tags.iter().all(PlatformTag::is_any) {
6521 return false;
6522 }
6523
6524 if platform_tags.iter().all(PlatformTag::is_linux) {
6525 if platform_tags.iter().all(PlatformTag::is_arm) {
6526 if marker.is_disjoint(*LINUX_ARM_MARKERS) {
6527 return true;
6528 }
6529 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6530 if marker.is_disjoint(*LINUX_X86_64_MARKERS) {
6531 return true;
6532 }
6533 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6534 if marker.is_disjoint(*LINUX_X86_MARKERS) {
6535 return true;
6536 }
6537 } else if platform_tags.iter().all(PlatformTag::is_ppc64le) {
6538 if marker.is_disjoint(*LINUX_PPC64LE_MARKERS) {
6539 return true;
6540 }
6541 } else if platform_tags.iter().all(PlatformTag::is_ppc64) {
6542 if marker.is_disjoint(*LINUX_PPC64_MARKERS) {
6543 return true;
6544 }
6545 } else if platform_tags.iter().all(PlatformTag::is_s390x) {
6546 if marker.is_disjoint(*LINUX_S390X_MARKERS) {
6547 return true;
6548 }
6549 } else if platform_tags.iter().all(PlatformTag::is_riscv64) {
6550 if marker.is_disjoint(*LINUX_RISCV64_MARKERS) {
6551 return true;
6552 }
6553 } else if platform_tags.iter().all(PlatformTag::is_loongarch64) {
6554 if marker.is_disjoint(*LINUX_LOONGARCH64_MARKERS) {
6555 return true;
6556 }
6557 } else if platform_tags.iter().all(PlatformTag::is_armv7l) {
6558 if marker.is_disjoint(*LINUX_ARMV7L_MARKERS) {
6559 return true;
6560 }
6561 } else if platform_tags.iter().all(PlatformTag::is_armv6l) {
6562 if marker.is_disjoint(*LINUX_ARMV6L_MARKERS) {
6563 return true;
6564 }
6565 } else if marker.is_disjoint(*LINUX_MARKERS) {
6566 return true;
6567 }
6568 }
6569
6570 if platform_tags.iter().all(PlatformTag::is_windows) {
6571 if platform_tags.iter().all(PlatformTag::is_arm) {
6572 if marker.is_disjoint(*WINDOWS_ARM_MARKERS) {
6573 return true;
6574 }
6575 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6576 if marker.is_disjoint(*WINDOWS_X86_64_MARKERS) {
6577 return true;
6578 }
6579 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6580 if marker.is_disjoint(*WINDOWS_X86_MARKERS) {
6581 return true;
6582 }
6583 } else if marker.is_disjoint(*WINDOWS_MARKERS) {
6584 return true;
6585 }
6586 }
6587
6588 if platform_tags.iter().all(PlatformTag::is_macos) {
6589 if platform_tags.iter().all(PlatformTag::is_arm) {
6590 if marker.is_disjoint(*MAC_ARM_MARKERS) {
6591 return true;
6592 }
6593 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6594 if marker.is_disjoint(*MAC_X86_64_MARKERS) {
6595 return true;
6596 }
6597 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6598 if marker.is_disjoint(*MAC_X86_MARKERS) {
6599 return true;
6600 }
6601 } else if marker.is_disjoint(*MAC_MARKERS) {
6602 return true;
6603 }
6604 }
6605
6606 if platform_tags.iter().all(PlatformTag::is_android) {
6607 if platform_tags.iter().all(PlatformTag::is_arm) {
6608 if marker.is_disjoint(*ANDROID_ARM_MARKERS) {
6609 return true;
6610 }
6611 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6612 if marker.is_disjoint(*ANDROID_X86_64_MARKERS) {
6613 return true;
6614 }
6615 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6616 if marker.is_disjoint(*ANDROID_X86_MARKERS) {
6617 return true;
6618 }
6619 } else if marker.is_disjoint(*ANDROID_MARKERS) {
6620 return true;
6621 }
6622 }
6623
6624 if platform_tags.iter().all(PlatformTag::is_arm) {
6625 if marker.is_disjoint(*ARM_MARKERS) {
6626 return true;
6627 }
6628 }
6629
6630 if platform_tags.iter().all(PlatformTag::is_x86_64) {
6631 if marker.is_disjoint(*X86_64_MARKERS) {
6632 return true;
6633 }
6634 }
6635
6636 if platform_tags.iter().all(PlatformTag::is_x86) {
6637 if marker.is_disjoint(*X86_MARKERS) {
6638 return true;
6639 }
6640 }
6641
6642 if platform_tags.iter().all(PlatformTag::is_ppc64le) {
6643 if marker.is_disjoint(*PPC64LE_MARKERS) {
6644 return true;
6645 }
6646 }
6647
6648 if platform_tags.iter().all(PlatformTag::is_ppc64) {
6649 if marker.is_disjoint(*PPC64_MARKERS) {
6650 return true;
6651 }
6652 }
6653
6654 if platform_tags.iter().all(PlatformTag::is_s390x) {
6655 if marker.is_disjoint(*S390X_MARKERS) {
6656 return true;
6657 }
6658 }
6659
6660 if platform_tags.iter().all(PlatformTag::is_riscv64) {
6661 if marker.is_disjoint(*RISCV64_MARKERS) {
6662 return true;
6663 }
6664 }
6665
6666 if platform_tags.iter().all(PlatformTag::is_loongarch64) {
6667 if marker.is_disjoint(*LOONGARCH64_MARKERS) {
6668 return true;
6669 }
6670 }
6671
6672 if platform_tags.iter().all(PlatformTag::is_armv7l) {
6673 if marker.is_disjoint(*ARMV7L_MARKERS) {
6674 return true;
6675 }
6676 }
6677
6678 if platform_tags.iter().all(PlatformTag::is_armv6l) {
6679 if marker.is_disjoint(*ARMV6L_MARKERS) {
6680 return true;
6681 }
6682 }
6683
6684 false
6685}
6686
6687pub(crate) fn is_wheel_unreachable(
6688 filename: &WheelFilename,
6689 graph: &ResolverOutput,
6690 requires_python: &RequiresPython,
6691 node_index: NodeIndex,
6692 tags: Option<&Tags>,
6693) -> bool {
6694 is_wheel_unreachable_for_marker(
6695 filename,
6696 requires_python,
6697 graph.graph[node_index].marker(),
6698 tags,
6699 )
6700}
6701
6702#[cfg(test)]
6703mod tests {
6704 use uv_warnings::anstream;
6705
6706 use super::*;
6707
6708 macro_rules! assert_stripped_snapshot {
6710 ($expr:expr, @$snapshot:literal) => {{
6711 let expr = format!("{}", $expr);
6712 let expr = format!("{}", anstream::adapter::strip_str(&expr));
6713 insta::assert_snapshot!(expr, @$snapshot);
6714 }};
6715 }
6716
6717 #[test]
6718 fn missing_dependency_source_unambiguous() {
6719 let data = r#"
6720version = 1
6721requires-python = ">=3.12"
6722
6723[[package]]
6724name = "a"
6725version = "0.1.0"
6726source = { registry = "https://pypi.org/simple" }
6727sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6728
6729[[package]]
6730name = "b"
6731version = "0.1.0"
6732source = { registry = "https://pypi.org/simple" }
6733sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6734
6735[[package.dependencies]]
6736name = "a"
6737version = "0.1.0"
6738"#;
6739 let result: Result<Lock, _> = toml::from_str(data);
6740 insta::assert_debug_snapshot!(result);
6741 }
6742
6743 #[test]
6744 fn missing_dependency_version_unambiguous() {
6745 let data = r#"
6746version = 1
6747requires-python = ">=3.12"
6748
6749[[package]]
6750name = "a"
6751version = "0.1.0"
6752source = { registry = "https://pypi.org/simple" }
6753sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6754
6755[[package]]
6756name = "b"
6757version = "0.1.0"
6758source = { registry = "https://pypi.org/simple" }
6759sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6760
6761[[package.dependencies]]
6762name = "a"
6763source = { registry = "https://pypi.org/simple" }
6764"#;
6765 let result: Result<Lock, _> = toml::from_str(data);
6766 insta::assert_debug_snapshot!(result);
6767 }
6768
6769 #[test]
6770 fn missing_dependency_source_version_unambiguous() {
6771 let data = r#"
6772version = 1
6773requires-python = ">=3.12"
6774
6775[[package]]
6776name = "a"
6777version = "0.1.0"
6778source = { registry = "https://pypi.org/simple" }
6779sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6780
6781[[package]]
6782name = "b"
6783version = "0.1.0"
6784source = { registry = "https://pypi.org/simple" }
6785sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6786
6787[[package.dependencies]]
6788name = "a"
6789"#;
6790 let result: Result<Lock, _> = toml::from_str(data);
6791 insta::assert_debug_snapshot!(result);
6792 }
6793
6794 #[test]
6795 fn missing_dependency_source_ambiguous() {
6796 let data = r#"
6797version = 1
6798requires-python = ">=3.12"
6799
6800[[package]]
6801name = "a"
6802version = "0.1.0"
6803source = { registry = "https://pypi.org/simple" }
6804sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6805
6806[[package]]
6807name = "a"
6808version = "0.1.1"
6809source = { registry = "https://pypi.org/simple" }
6810sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6811
6812[[package]]
6813name = "b"
6814version = "0.1.0"
6815source = { registry = "https://pypi.org/simple" }
6816sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6817
6818[[package.dependencies]]
6819name = "a"
6820version = "0.1.0"
6821"#;
6822 let result = toml::from_str::<Lock>(data).unwrap_err();
6823 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6824 }
6825
6826 #[test]
6827 fn missing_dependency_version_ambiguous() {
6828 let data = r#"
6829version = 1
6830requires-python = ">=3.12"
6831
6832[[package]]
6833name = "a"
6834version = "0.1.0"
6835source = { registry = "https://pypi.org/simple" }
6836sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6837
6838[[package]]
6839name = "a"
6840version = "0.1.1"
6841source = { registry = "https://pypi.org/simple" }
6842sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6843
6844[[package]]
6845name = "b"
6846version = "0.1.0"
6847source = { registry = "https://pypi.org/simple" }
6848sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6849
6850[[package.dependencies]]
6851name = "a"
6852source = { registry = "https://pypi.org/simple" }
6853"#;
6854 let result = toml::from_str::<Lock>(data).unwrap_err();
6855 assert_stripped_snapshot!(result, @"Dependency `a` has missing `version` field but has more than one matching package");
6856 }
6857
6858 #[test]
6859 fn missing_dependency_source_version_ambiguous() {
6860 let data = r#"
6861version = 1
6862requires-python = ">=3.12"
6863
6864[[package]]
6865name = "a"
6866version = "0.1.0"
6867source = { registry = "https://pypi.org/simple" }
6868sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6869
6870[[package]]
6871name = "a"
6872version = "0.1.1"
6873source = { registry = "https://pypi.org/simple" }
6874sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6875
6876[[package]]
6877name = "b"
6878version = "0.1.0"
6879source = { registry = "https://pypi.org/simple" }
6880sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6881
6882[[package.dependencies]]
6883name = "a"
6884"#;
6885 let result = toml::from_str::<Lock>(data).unwrap_err();
6886 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6887 }
6888
6889 #[test]
6890 fn missing_dependency_version_dynamic() {
6891 let data = r#"
6892version = 1
6893requires-python = ">=3.12"
6894
6895[[package]]
6896name = "a"
6897source = { editable = "path/to/a" }
6898
6899[[package]]
6900name = "a"
6901version = "0.1.1"
6902source = { registry = "https://pypi.org/simple" }
6903sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6904
6905[[package]]
6906name = "b"
6907version = "0.1.0"
6908source = { registry = "https://pypi.org/simple" }
6909sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6910
6911[[package.dependencies]]
6912name = "a"
6913source = { editable = "path/to/a" }
6914"#;
6915 let result = toml::from_str::<Lock>(data);
6916 insta::assert_debug_snapshot!(result);
6917 }
6918
6919 #[test]
6920 fn hash_optional_missing() {
6921 let data = r#"
6922version = 1
6923requires-python = ">=3.12"
6924
6925[[package]]
6926name = "anyio"
6927version = "4.3.0"
6928source = { registry = "https://pypi.org/simple" }
6929wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl" }]
6930"#;
6931 let result: Result<Lock, _> = toml::from_str(data);
6932 insta::assert_debug_snapshot!(result);
6933 }
6934
6935 #[test]
6936 fn hash_optional_present() {
6937 let data = r#"
6938version = 1
6939requires-python = ">=3.12"
6940
6941[[package]]
6942name = "anyio"
6943version = "4.3.0"
6944source = { registry = "https://pypi.org/simple" }
6945wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6946"#;
6947 let result: Result<Lock, _> = toml::from_str(data);
6948 insta::assert_debug_snapshot!(result);
6949 }
6950
6951 #[test]
6952 fn hash_required_present() {
6953 let data = r#"
6954version = 1
6955requires-python = ">=3.12"
6956
6957[[package]]
6958name = "anyio"
6959version = "4.3.0"
6960source = { path = "file:///foo/bar" }
6961wheels = [{ url = "file:///foo/bar/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6962"#;
6963 let result: Result<Lock, _> = toml::from_str(data);
6964 insta::assert_debug_snapshot!(result);
6965 }
6966
6967 #[test]
6968 fn source_direct_no_subdir() {
6969 let data = r#"
6970version = 1
6971requires-python = ">=3.12"
6972
6973[[package]]
6974name = "anyio"
6975version = "4.3.0"
6976source = { url = "https://burntsushi.net" }
6977"#;
6978 let result: Result<Lock, _> = toml::from_str(data);
6979 insta::assert_debug_snapshot!(result);
6980 }
6981
6982 #[test]
6983 fn source_direct_has_subdir() {
6984 let data = r#"
6985version = 1
6986requires-python = ">=3.12"
6987
6988[[package]]
6989name = "anyio"
6990version = "4.3.0"
6991source = { url = "https://burntsushi.net", subdirectory = "wat/foo/bar" }
6992"#;
6993 let result: Result<Lock, _> = toml::from_str(data);
6994 insta::assert_debug_snapshot!(result);
6995 }
6996
6997 #[test]
6998 fn source_directory() {
6999 let data = r#"
7000version = 1
7001requires-python = ">=3.12"
7002
7003[[package]]
7004name = "anyio"
7005version = "4.3.0"
7006source = { directory = "path/to/dir" }
7007"#;
7008 let result: Result<Lock, _> = toml::from_str(data);
7009 insta::assert_debug_snapshot!(result);
7010 }
7011
7012 #[test]
7013 fn source_editable() {
7014 let data = r#"
7015version = 1
7016requires-python = ">=3.12"
7017
7018[[package]]
7019name = "anyio"
7020version = "4.3.0"
7021source = { editable = "path/to/dir" }
7022"#;
7023 let result: Result<Lock, _> = toml::from_str(data);
7024 insta::assert_debug_snapshot!(result);
7025 }
7026
7027 #[test]
7030 fn registry_source_windows_drive_letter() {
7031 let data = r#"
7032version = 1
7033requires-python = ">=3.12"
7034
7035[[package]]
7036name = "tqdm"
7037version = "1000.0.0"
7038source = { registry = "C:/Users/user/links" }
7039wheels = [
7040 { path = "C:/Users/user/links/tqdm-1000.0.0-py3-none-any.whl" },
7041]
7042"#;
7043 let lock: Lock = toml::from_str(data).unwrap();
7044 assert_eq!(
7045 lock.packages[0].id.source,
7046 Source::Registry(RegistrySource::Path(
7047 Path::new("C:/Users/user/links").into()
7048 ))
7049 );
7050 }
7051}