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 packages_for_audit<'lock>(
813 &'lock self,
814 extras: &'lock ExtrasSpecificationWithDefaults,
815 groups: &'lock DependencyGroupsWithDefaults,
816 ) -> Vec<(&'lock PackageName, &'lock Version)> {
817 fn enqueue_dep<'lock>(
819 lock: &'lock Lock,
820 seen: &mut FxHashSet<(&'lock PackageId, Option<&'lock ExtraName>)>,
821 queue: &mut VecDeque<(&'lock Package, Option<&'lock ExtraName>)>,
822 dep: &'lock Dependency,
823 ) {
824 let dep_pkg = lock.find_by_id(&dep.package_id);
825 for maybe_extra in std::iter::once(None).chain(dep.extra.iter().map(Some)) {
826 if seen.insert((&dep.package_id, maybe_extra)) {
827 queue.push_back((dep_pkg, maybe_extra));
828 }
829 }
830 }
831
832 let workspace_member_ids: FxHashSet<&PackageId> = if self.members().is_empty() {
834 self.root().into_iter().map(|package| &package.id).collect()
835 } else {
836 self.packages
837 .iter()
838 .filter(|package| self.members().contains(&package.id.name))
839 .map(|package| &package.id)
840 .collect()
841 };
842
843 let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
845 let mut seen: FxHashSet<(&PackageId, Option<&ExtraName>)> = FxHashSet::default();
846
847 for package in self
850 .packages
851 .iter()
852 .filter(|p| workspace_member_ids.contains(&p.id))
853 {
854 if seen.insert((&package.id, None)) {
855 queue.push_back((package, None));
856 }
857 if groups.prod() {
858 for extra in extras.extra_names(package.optional_dependencies.keys()) {
859 if seen.insert((&package.id, Some(extra))) {
860 queue.push_back((package, Some(extra)));
861 }
862 }
863 }
864 }
865
866 for requirement in self.requirements() {
868 for package in self
869 .packages
870 .iter()
871 .filter(|p| p.id.name == requirement.name)
872 {
873 if seen.insert((&package.id, None)) {
874 queue.push_back((package, None));
875 }
876 for extra in &*requirement.extras {
877 if seen.insert((&package.id, Some(extra))) {
878 queue.push_back((package, Some(extra)));
879 }
880 }
881 }
882 }
883
884 for (group, requirements) in self.dependency_groups() {
887 if !groups.contains(group) {
888 continue;
889 }
890 for requirement in requirements {
891 for package in self
892 .packages
893 .iter()
894 .filter(|p| p.id.name == requirement.name)
895 {
896 if seen.insert((&package.id, None)) {
897 queue.push_back((package, None));
898 }
899 for extra in &*requirement.extras {
900 if seen.insert((&package.id, Some(extra))) {
901 queue.push_back((package, Some(extra)));
902 }
903 }
904 }
905 }
906 }
907
908 let mut auditable: BTreeSet<(&PackageName, &Version)> = BTreeSet::default();
909
910 while let Some((package, extra)) = queue.pop_front() {
911 let is_member = workspace_member_ids.contains(&package.id);
912
913 if !is_member {
915 if let Some(version) = package.version() {
916 auditable.insert((package.name(), version));
917 } else {
918 trace!(
919 "Skipping audit for `{}` because it has no version information",
920 package.name()
921 );
922 }
923 }
924
925 if is_member && extra.is_none() {
927 for dep in package
928 .dependency_groups
929 .iter()
930 .filter(|(group, _)| groups.contains(group))
931 .flat_map(|(_, deps)| deps)
932 {
933 enqueue_dep(self, &mut seen, &mut queue, dep);
934 }
935 }
936
937 let dependencies: &[Dependency] = match extra {
940 Some(extra) => package
941 .optional_dependencies
942 .get(extra)
943 .map(Vec::as_slice)
944 .unwrap_or_default(),
945 None if is_member && !groups.prod() => &[],
946 None => &package.dependencies,
947 };
948
949 for dep in dependencies {
950 enqueue_dep(self, &mut seen, &mut queue, dep);
951 }
952 }
953
954 auditable.into_iter().collect()
955 }
956
957 pub fn root(&self) -> Option<&Package> {
959 self.packages.iter().find(|package| {
960 let (Source::Editable(path) | Source::Virtual(path)) = &package.id.source else {
961 return false;
962 };
963 path.as_ref() == Path::new("")
964 })
965 }
966
967 pub fn simplified_supported_environments(&self) -> Vec<MarkerTree> {
977 self.supported_environments()
978 .iter()
979 .copied()
980 .map(|marker| self.simplify_environment(marker))
981 .collect()
982 }
983
984 pub fn simplified_required_environments(&self) -> Vec<MarkerTree> {
987 self.required_environments()
988 .iter()
989 .copied()
990 .map(|marker| self.simplify_environment(marker))
991 .collect()
992 }
993
994 pub fn simplify_environment(&self, marker: MarkerTree) -> MarkerTree {
997 self.requires_python.simplify_markers(marker)
998 }
999
1000 pub fn fork_markers(&self) -> &[UniversalMarker] {
1003 self.fork_markers.as_slice()
1004 }
1005
1006 pub fn check_marker_coverage(&self) -> Result<(), (MarkerTree, MarkerTree)> {
1010 let fork_markers_union = if self.fork_markers().is_empty() {
1011 self.requires_python.to_marker_tree()
1012 } else {
1013 let mut fork_markers_union = MarkerTree::FALSE;
1014 for fork_marker in self.fork_markers() {
1015 fork_markers_union.or(fork_marker.pep508());
1016 }
1017 fork_markers_union
1018 };
1019 let mut environments_union = if !self.supported_environments.is_empty() {
1020 let mut environments_union = MarkerTree::FALSE;
1021 for fork_marker in &self.supported_environments {
1022 environments_union.or(*fork_marker);
1023 }
1024 environments_union
1025 } else {
1026 MarkerTree::TRUE
1027 };
1028 environments_union.and(self.requires_python.to_marker_tree());
1030 if fork_markers_union.negate().is_disjoint(environments_union) {
1031 Ok(())
1032 } else {
1033 Err((fork_markers_union, environments_union))
1034 }
1035 }
1036
1037 pub fn requires_python_coverage(
1047 &self,
1048 new_requires_python: &RequiresPython,
1049 ) -> Result<(), (MarkerTree, MarkerTree)> {
1050 let fork_markers_union = if self.fork_markers().is_empty() {
1051 self.requires_python.to_marker_tree()
1052 } else {
1053 let mut fork_markers_union = MarkerTree::FALSE;
1054 for fork_marker in self.fork_markers() {
1055 fork_markers_union.or(fork_marker.pep508());
1056 }
1057 fork_markers_union
1058 };
1059 let new_requires_python = new_requires_python.to_marker_tree();
1060 if fork_markers_union.is_disjoint(new_requires_python) {
1061 Err((fork_markers_union, new_requires_python))
1062 } else {
1063 Ok(())
1064 }
1065 }
1066
1067 pub fn to_toml(&self) -> Result<String, toml_edit::ser::Error> {
1069 debug_assert!(self.check_marker_coverage().is_ok());
1072
1073 let mut doc = toml_edit::DocumentMut::new();
1076 doc.insert("version", value(i64::from(self.version)));
1077
1078 if self.revision > 0 {
1079 doc.insert("revision", value(i64::from(self.revision)));
1080 }
1081
1082 doc.insert("requires-python", value(self.requires_python.to_string()));
1083
1084 if !self.fork_markers.is_empty() {
1085 let fork_markers = each_element_on_its_line_array(
1086 simplified_universal_markers(&self.fork_markers, &self.requires_python).into_iter(),
1087 );
1088 if !fork_markers.is_empty() {
1089 doc.insert("resolution-markers", value(fork_markers));
1090 }
1091 }
1092
1093 if !self.supported_environments.is_empty() {
1094 let supported_environments = each_element_on_its_line_array(
1095 self.supported_environments
1096 .iter()
1097 .copied()
1098 .map(|marker| SimplifiedMarkerTree::new(&self.requires_python, marker))
1099 .filter_map(SimplifiedMarkerTree::try_to_string),
1100 );
1101 doc.insert("supported-markers", value(supported_environments));
1102 }
1103
1104 if !self.required_environments.is_empty() {
1105 let required_environments = each_element_on_its_line_array(
1106 self.required_environments
1107 .iter()
1108 .copied()
1109 .map(|marker| SimplifiedMarkerTree::new(&self.requires_python, marker))
1110 .filter_map(SimplifiedMarkerTree::try_to_string),
1111 );
1112 doc.insert("required-markers", value(required_environments));
1113 }
1114
1115 if !self.conflicts.is_empty() {
1116 let mut list = Array::new();
1117 for set in self.conflicts.iter() {
1118 list.push(each_element_on_its_line_array(set.iter().map(|item| {
1119 let mut table = InlineTable::new();
1120 table.insert("package", Value::from(item.package().to_string()));
1121 match item.kind() {
1122 ConflictKind::Project => {}
1123 ConflictKind::Extra(extra) => {
1124 table.insert("extra", Value::from(extra.to_string()));
1125 }
1126 ConflictKind::Group(group) => {
1127 table.insert("group", Value::from(group.to_string()));
1128 }
1129 }
1130 table
1131 })));
1132 }
1133 doc.insert("conflicts", value(list));
1134 }
1135
1136 {
1140 let mut options_table = Table::new();
1141
1142 if self.options.resolution_mode != ResolutionMode::default() {
1143 options_table.insert(
1144 "resolution-mode",
1145 value(self.options.resolution_mode.to_string()),
1146 );
1147 }
1148 if self.options.prerelease_mode != PrereleaseMode::default() {
1149 options_table.insert(
1150 "prerelease-mode",
1151 value(self.options.prerelease_mode.to_string()),
1152 );
1153 }
1154 if self.options.fork_strategy != ForkStrategy::default() {
1155 options_table.insert(
1156 "fork-strategy",
1157 value(self.options.fork_strategy.to_string()),
1158 );
1159 }
1160 let exclude_newer = ExcludeNewer::from(self.options.exclude_newer.clone());
1161 if !exclude_newer.is_empty() {
1162 if let Some(global) = &exclude_newer.global {
1164 options_table.insert("exclude-newer", value(global.to_string()));
1165 if let Some(span) = global.span() {
1167 options_table.insert("exclude-newer-span", value(span.to_string()));
1168 }
1169 }
1170
1171 if !exclude_newer.package.is_empty() {
1173 let mut package_table = toml_edit::Table::new();
1174 for (name, setting) in &exclude_newer.package {
1175 match setting {
1176 ExcludeNewerOverride::Enabled(exclude_newer_value) => {
1177 if let Some(span) = exclude_newer_value.span() {
1178 let mut inline = toml_edit::InlineTable::new();
1180 inline.insert(
1181 "timestamp",
1182 exclude_newer_value.timestamp().to_string().into(),
1183 );
1184 inline.insert("span", span.to_string().into());
1185 package_table.insert(name.as_ref(), Item::Value(inline.into()));
1186 } else {
1187 package_table.insert(
1189 name.as_ref(),
1190 value(exclude_newer_value.to_string()),
1191 );
1192 }
1193 }
1194 ExcludeNewerOverride::Disabled => {
1195 package_table.insert(name.as_ref(), value(false));
1196 }
1197 }
1198 }
1199 options_table.insert("exclude-newer-package", Item::Table(package_table));
1200 }
1201 }
1202
1203 if !options_table.is_empty() {
1204 doc.insert("options", Item::Table(options_table));
1205 }
1206 }
1207
1208 {
1210 let mut manifest_table = Table::new();
1211
1212 if !self.manifest.members.is_empty() {
1213 manifest_table.insert(
1214 "members",
1215 value(each_element_on_its_line_array(
1216 self.manifest
1217 .members
1218 .iter()
1219 .map(std::string::ToString::to_string),
1220 )),
1221 );
1222 }
1223
1224 if !self.manifest.requirements.is_empty() {
1225 let requirements = self
1226 .manifest
1227 .requirements
1228 .iter()
1229 .map(|requirement| {
1230 serde::Serialize::serialize(
1231 &requirement,
1232 toml_edit::ser::ValueSerializer::new(),
1233 )
1234 })
1235 .collect::<Result<Vec<_>, _>>()?;
1236 let requirements = match requirements.as_slice() {
1237 [] => Array::new(),
1238 [requirement] => Array::from_iter([requirement]),
1239 requirements => each_element_on_its_line_array(requirements.iter()),
1240 };
1241 manifest_table.insert("requirements", value(requirements));
1242 }
1243
1244 if !self.manifest.constraints.is_empty() {
1245 let constraints = self
1246 .manifest
1247 .constraints
1248 .iter()
1249 .map(|requirement| {
1250 serde::Serialize::serialize(
1251 &requirement,
1252 toml_edit::ser::ValueSerializer::new(),
1253 )
1254 })
1255 .collect::<Result<Vec<_>, _>>()?;
1256 let constraints = match constraints.as_slice() {
1257 [] => Array::new(),
1258 [requirement] => Array::from_iter([requirement]),
1259 constraints => each_element_on_its_line_array(constraints.iter()),
1260 };
1261 manifest_table.insert("constraints", value(constraints));
1262 }
1263
1264 if !self.manifest.overrides.is_empty() {
1265 let overrides = self
1266 .manifest
1267 .overrides
1268 .iter()
1269 .map(|requirement| {
1270 serde::Serialize::serialize(
1271 &requirement,
1272 toml_edit::ser::ValueSerializer::new(),
1273 )
1274 })
1275 .collect::<Result<Vec<_>, _>>()?;
1276 let overrides = match overrides.as_slice() {
1277 [] => Array::new(),
1278 [requirement] => Array::from_iter([requirement]),
1279 overrides => each_element_on_its_line_array(overrides.iter()),
1280 };
1281 manifest_table.insert("overrides", value(overrides));
1282 }
1283
1284 if !self.manifest.excludes.is_empty() {
1285 let excludes = self
1286 .manifest
1287 .excludes
1288 .iter()
1289 .map(|name| {
1290 serde::Serialize::serialize(&name, toml_edit::ser::ValueSerializer::new())
1291 })
1292 .collect::<Result<Vec<_>, _>>()?;
1293 let excludes = match excludes.as_slice() {
1294 [] => Array::new(),
1295 [name] => Array::from_iter([name]),
1296 excludes => each_element_on_its_line_array(excludes.iter()),
1297 };
1298 manifest_table.insert("excludes", value(excludes));
1299 }
1300
1301 if !self.manifest.build_constraints.is_empty() {
1302 let build_constraints = self
1303 .manifest
1304 .build_constraints
1305 .iter()
1306 .map(|requirement| {
1307 serde::Serialize::serialize(
1308 &requirement,
1309 toml_edit::ser::ValueSerializer::new(),
1310 )
1311 })
1312 .collect::<Result<Vec<_>, _>>()?;
1313 let build_constraints = match build_constraints.as_slice() {
1314 [] => Array::new(),
1315 [requirement] => Array::from_iter([requirement]),
1316 build_constraints => each_element_on_its_line_array(build_constraints.iter()),
1317 };
1318 manifest_table.insert("build-constraints", value(build_constraints));
1319 }
1320
1321 if !self.manifest.dependency_groups.is_empty() {
1322 let mut dependency_groups = Table::new();
1323 for (extra, requirements) in &self.manifest.dependency_groups {
1324 let requirements = requirements
1325 .iter()
1326 .map(|requirement| {
1327 serde::Serialize::serialize(
1328 &requirement,
1329 toml_edit::ser::ValueSerializer::new(),
1330 )
1331 })
1332 .collect::<Result<Vec<_>, _>>()?;
1333 let requirements = match requirements.as_slice() {
1334 [] => Array::new(),
1335 [requirement] => Array::from_iter([requirement]),
1336 requirements => each_element_on_its_line_array(requirements.iter()),
1337 };
1338 if !requirements.is_empty() {
1339 dependency_groups.insert(extra.as_ref(), value(requirements));
1340 }
1341 }
1342 if !dependency_groups.is_empty() {
1343 manifest_table.insert("dependency-groups", Item::Table(dependency_groups));
1344 }
1345 }
1346
1347 if !self.manifest.dependency_metadata.is_empty() {
1348 let mut tables = ArrayOfTables::new();
1349 for metadata in &self.manifest.dependency_metadata {
1350 let mut table = Table::new();
1351 table.insert("name", value(metadata.name.to_string()));
1352 if let Some(version) = metadata.version.as_ref() {
1353 table.insert("version", value(version.to_string()));
1354 }
1355 if !metadata.requires_dist.is_empty() {
1356 table.insert(
1357 "requires-dist",
1358 value(serde::Serialize::serialize(
1359 &metadata.requires_dist,
1360 toml_edit::ser::ValueSerializer::new(),
1361 )?),
1362 );
1363 }
1364 if let Some(requires_python) = metadata.requires_python.as_ref() {
1365 table.insert("requires-python", value(requires_python.to_string()));
1366 }
1367 if !metadata.provides_extra.is_empty() {
1368 table.insert(
1369 "provides-extras",
1370 value(serde::Serialize::serialize(
1371 &metadata.provides_extra,
1372 toml_edit::ser::ValueSerializer::new(),
1373 )?),
1374 );
1375 }
1376 tables.push(table);
1377 }
1378 manifest_table.insert("dependency-metadata", Item::ArrayOfTables(tables));
1379 }
1380
1381 if !manifest_table.is_empty() {
1382 doc.insert("manifest", Item::Table(manifest_table));
1383 }
1384 }
1385
1386 let mut dist_count_by_name: FxHashMap<PackageName, u64> = FxHashMap::default();
1391 for dist in &self.packages {
1392 *dist_count_by_name.entry(dist.id.name.clone()).or_default() += 1;
1393 }
1394
1395 let mut packages = ArrayOfTables::new();
1396 for dist in &self.packages {
1397 packages.push(dist.to_toml(&self.requires_python, &dist_count_by_name)?);
1398 }
1399
1400 doc.insert("package", Item::ArrayOfTables(packages));
1401 Ok(doc.to_string())
1402 }
1403
1404 pub fn find_by_name(&self, name: &PackageName) -> Result<Option<&Package>, String> {
1408 let mut found_dist = None;
1409 for dist in &self.packages {
1410 if &dist.id.name == name {
1411 if found_dist.is_some() {
1412 return Err(format!("found multiple packages matching `{name}`"));
1413 }
1414 found_dist = Some(dist);
1415 }
1416 }
1417 Ok(found_dist)
1418 }
1419
1420 fn find_by_markers(
1430 &self,
1431 name: &PackageName,
1432 marker_env: &MarkerEnvironment,
1433 ) -> Result<Option<&Package>, String> {
1434 let mut found_dist = None;
1435 for dist in &self.packages {
1436 if &dist.id.name == name {
1437 if dist.fork_markers.is_empty()
1438 || dist
1439 .fork_markers
1440 .iter()
1441 .any(|marker| marker.evaluate_no_extras(marker_env))
1442 {
1443 if found_dist.is_some() {
1444 return Err(format!("found multiple packages matching `{name}`"));
1445 }
1446 found_dist = Some(dist);
1447 }
1448 }
1449 }
1450 Ok(found_dist)
1451 }
1452
1453 fn find_by_id(&self, id: &PackageId) -> &Package {
1454 let index = *self.by_id.get(id).expect("locked package for ID");
1455
1456 (self.packages.get(index).expect("valid index for package")) as _
1457 }
1458
1459 fn satisfies_provides_extra<'lock>(
1461 &self,
1462 provides_extra: Box<[ExtraName]>,
1463 package: &'lock Package,
1464 ) -> SatisfiesResult<'lock> {
1465 if !self.supports_provides_extra() {
1466 return SatisfiesResult::Satisfied;
1467 }
1468
1469 let expected: BTreeSet<_> = provides_extra.iter().collect();
1470 let actual: BTreeSet<_> = package.metadata.provides_extra.iter().collect();
1471
1472 if expected != actual {
1473 let expected = Box::into_iter(provides_extra).collect();
1474 return SatisfiesResult::MismatchedPackageProvidesExtra(
1475 &package.id.name,
1476 package.id.version.as_ref(),
1477 expected,
1478 actual,
1479 );
1480 }
1481
1482 SatisfiesResult::Satisfied
1483 }
1484
1485 fn satisfies_requires_dist<'lock>(
1487 &self,
1488 requires_dist: Box<[Requirement]>,
1489 dependency_groups: BTreeMap<GroupName, Box<[Requirement]>>,
1490 package: &'lock Package,
1491 root: &Path,
1492 ) -> Result<SatisfiesResult<'lock>, LockError> {
1493 let flattened = if package.is_dynamic() {
1495 Some(
1496 FlatRequiresDist::from_requirements(requires_dist.clone(), &package.id.name)
1497 .into_iter()
1498 .map(|requirement| {
1499 normalize_requirement(requirement, root, &self.requires_python)
1500 })
1501 .collect::<Result<BTreeSet<_>, _>>()?,
1502 )
1503 } else {
1504 None
1505 };
1506
1507 let expected: BTreeSet<_> = Box::into_iter(requires_dist)
1509 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1510 .collect::<Result<_, _>>()?;
1511 let actual: BTreeSet<_> = package
1512 .metadata
1513 .requires_dist
1514 .iter()
1515 .cloned()
1516 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1517 .collect::<Result<_, _>>()?;
1518
1519 if expected != actual && flattened.is_none_or(|expected| expected != actual) {
1520 return Ok(SatisfiesResult::MismatchedPackageRequirements(
1521 &package.id.name,
1522 package.id.version.as_ref(),
1523 expected,
1524 actual,
1525 ));
1526 }
1527
1528 let expected: BTreeMap<GroupName, BTreeSet<Requirement>> = dependency_groups
1530 .into_iter()
1531 .filter(|(_, requirements)| self.includes_empty_groups() || !requirements.is_empty())
1532 .map(|(group, requirements)| {
1533 Ok::<_, LockError>((
1534 group,
1535 Box::into_iter(requirements)
1536 .map(|requirement| {
1537 normalize_requirement(requirement, root, &self.requires_python)
1538 })
1539 .collect::<Result<_, _>>()?,
1540 ))
1541 })
1542 .collect::<Result<_, _>>()?;
1543 let actual: BTreeMap<GroupName, BTreeSet<Requirement>> = package
1544 .metadata
1545 .dependency_groups
1546 .iter()
1547 .filter(|(_, requirements)| self.includes_empty_groups() || !requirements.is_empty())
1548 .map(|(group, requirements)| {
1549 Ok::<_, LockError>((
1550 group.clone(),
1551 requirements
1552 .iter()
1553 .cloned()
1554 .map(|requirement| {
1555 normalize_requirement(requirement, root, &self.requires_python)
1556 })
1557 .collect::<Result<_, _>>()?,
1558 ))
1559 })
1560 .collect::<Result<_, _>>()?;
1561
1562 if expected != actual {
1563 return Ok(SatisfiesResult::MismatchedPackageDependencyGroups(
1564 &package.id.name,
1565 package.id.version.as_ref(),
1566 expected,
1567 actual,
1568 ));
1569 }
1570
1571 Ok(SatisfiesResult::Satisfied)
1572 }
1573
1574 #[instrument(skip_all)]
1576 pub async fn satisfies<Context: BuildContext>(
1577 &self,
1578 root: &Path,
1579 packages: &BTreeMap<PackageName, WorkspaceMember>,
1580 members: &[PackageName],
1581 required_members: &BTreeMap<PackageName, Editability>,
1582 requirements: &[Requirement],
1583 constraints: &[Requirement],
1584 overrides: &[Requirement],
1585 excludes: &[PackageName],
1586 build_constraints: &[Requirement],
1587 dependency_groups: &BTreeMap<GroupName, Vec<Requirement>>,
1588 dependency_metadata: &DependencyMetadata,
1589 indexes: Option<&IndexLocations>,
1590 tags: &Tags,
1591 markers: &MarkerEnvironment,
1592 hasher: &HashStrategy,
1593 index: &InMemoryIndex,
1594 database: &DistributionDatabase<'_, Context>,
1595 ) -> Result<SatisfiesResult<'_>, LockError> {
1596 let mut queue: VecDeque<&Package> = VecDeque::new();
1597 let mut seen = FxHashSet::default();
1598
1599 {
1601 let expected = members.iter().cloned().collect::<BTreeSet<_>>();
1602 let actual = &self.manifest.members;
1603 if expected != *actual {
1604 return Ok(SatisfiesResult::MismatchedMembers(expected, actual));
1605 }
1606 }
1607
1608 for (name, member) in packages {
1611 let source = self.find_by_name(name).ok().flatten();
1612
1613 let value = required_members.get(name);
1615 let is_required_member = value.is_some();
1616 let editability = value.copied().flatten();
1617
1618 let expected_virtual = !member.pyproject_toml().is_package(!is_required_member);
1620 let actual_virtual =
1621 source.map(|package| matches!(package.id.source, Source::Virtual(..)));
1622 if actual_virtual != Some(expected_virtual) {
1623 return Ok(SatisfiesResult::MismatchedVirtual(
1624 name.clone(),
1625 expected_virtual,
1626 ));
1627 }
1628
1629 let expected_editable = if expected_virtual {
1631 false
1632 } else {
1633 editability.unwrap_or(true)
1634 };
1635 let actual_editable =
1636 source.map(|package| matches!(package.id.source, Source::Editable(..)));
1637 if actual_editable != Some(expected_editable) {
1638 return Ok(SatisfiesResult::MismatchedEditable(
1639 name.clone(),
1640 expected_editable,
1641 ));
1642 }
1643 }
1644
1645 {
1647 let expected: BTreeSet<_> = requirements
1648 .iter()
1649 .cloned()
1650 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1651 .collect::<Result<_, _>>()?;
1652 let actual: BTreeSet<_> = self
1653 .manifest
1654 .requirements
1655 .iter()
1656 .cloned()
1657 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1658 .collect::<Result<_, _>>()?;
1659 if expected != actual {
1660 return Ok(SatisfiesResult::MismatchedRequirements(expected, actual));
1661 }
1662 }
1663
1664 {
1666 let expected: BTreeSet<_> = constraints
1667 .iter()
1668 .cloned()
1669 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1670 .collect::<Result<_, _>>()?;
1671 let actual: BTreeSet<_> = self
1672 .manifest
1673 .constraints
1674 .iter()
1675 .cloned()
1676 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1677 .collect::<Result<_, _>>()?;
1678 if expected != actual {
1679 return Ok(SatisfiesResult::MismatchedConstraints(expected, actual));
1680 }
1681 }
1682
1683 {
1685 let expected: BTreeSet<_> = overrides
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 .overrides
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::MismatchedOverrides(expected, actual));
1699 }
1700 }
1701
1702 {
1704 let expected: BTreeSet<_> = excludes.iter().cloned().collect();
1705 let actual: BTreeSet<_> = self.manifest.excludes.iter().cloned().collect();
1706 if expected != actual {
1707 return Ok(SatisfiesResult::MismatchedExcludes(expected, actual));
1708 }
1709 }
1710
1711 {
1713 let expected: BTreeSet<_> = build_constraints
1714 .iter()
1715 .cloned()
1716 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1717 .collect::<Result<_, _>>()?;
1718 let actual: BTreeSet<_> = self
1719 .manifest
1720 .build_constraints
1721 .iter()
1722 .cloned()
1723 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1724 .collect::<Result<_, _>>()?;
1725 if expected != actual {
1726 return Ok(SatisfiesResult::MismatchedBuildConstraints(
1727 expected, actual,
1728 ));
1729 }
1730 }
1731
1732 {
1734 let expected: BTreeMap<GroupName, BTreeSet<Requirement>> = dependency_groups
1735 .iter()
1736 .filter(|(_, requirements)| !requirements.is_empty())
1737 .map(|(group, requirements)| {
1738 Ok::<_, LockError>((
1739 group.clone(),
1740 requirements
1741 .iter()
1742 .cloned()
1743 .map(|requirement| {
1744 normalize_requirement(requirement, root, &self.requires_python)
1745 })
1746 .collect::<Result<_, _>>()?,
1747 ))
1748 })
1749 .collect::<Result<_, _>>()?;
1750 let actual: BTreeMap<GroupName, BTreeSet<Requirement>> = self
1751 .manifest
1752 .dependency_groups
1753 .iter()
1754 .filter(|(_, requirements)| !requirements.is_empty())
1755 .map(|(group, requirements)| {
1756 Ok::<_, LockError>((
1757 group.clone(),
1758 requirements
1759 .iter()
1760 .cloned()
1761 .map(|requirement| {
1762 normalize_requirement(requirement, root, &self.requires_python)
1763 })
1764 .collect::<Result<_, _>>()?,
1765 ))
1766 })
1767 .collect::<Result<_, _>>()?;
1768 if expected != actual {
1769 return Ok(SatisfiesResult::MismatchedDependencyGroups(
1770 expected, actual,
1771 ));
1772 }
1773 }
1774
1775 {
1777 let expected = dependency_metadata
1778 .values()
1779 .cloned()
1780 .collect::<BTreeSet<_>>();
1781 let actual = &self.manifest.dependency_metadata;
1782 if expected != *actual {
1783 return Ok(SatisfiesResult::MismatchedStaticMetadata(expected, actual));
1784 }
1785 }
1786
1787 let mut remotes = indexes.map(|locations| {
1789 locations
1790 .allowed_indexes()
1791 .into_iter()
1792 .filter_map(|index| match index.url() {
1793 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
1794 Some(UrlString::from(index.url().without_credentials().as_ref()))
1795 }
1796 IndexUrl::Path(_) => None,
1797 })
1798 .collect::<BTreeSet<_>>()
1799 });
1800
1801 let mut locals = indexes.map(|locations| {
1802 locations
1803 .allowed_indexes()
1804 .into_iter()
1805 .filter_map(|index| match index.url() {
1806 IndexUrl::Pypi(_) | IndexUrl::Url(_) => None,
1807 IndexUrl::Path(url) => {
1808 let path = url.to_file_path().ok()?;
1809 let path = try_relative_to_if(&path, root, !url.was_given_absolute())
1810 .ok()?
1811 .into_boxed_path();
1812 Some(path)
1813 }
1814 })
1815 .collect::<BTreeSet<_>>()
1816 });
1817
1818 for root_name in packages.keys() {
1820 let root = self
1821 .find_by_name(root_name)
1822 .expect("found too many packages matching root");
1823
1824 let Some(root) = root else {
1825 return Ok(SatisfiesResult::MissingRoot(root_name.clone()));
1827 };
1828
1829 if seen.insert(&root.id) {
1830 queue.push_back(root);
1831 }
1832 }
1833
1834 let root_requirements = requirements
1837 .iter()
1838 .chain(dependency_groups.values().flatten())
1839 .collect::<Vec<_>>();
1840
1841 for requirement in &root_requirements {
1842 if let RequirementSource::Registry {
1843 index: Some(index), ..
1844 } = &requirement.source
1845 {
1846 match &index.url {
1847 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
1848 if let Some(remotes) = remotes.as_mut() {
1849 remotes.insert(UrlString::from(
1850 index.url().without_credentials().as_ref(),
1851 ));
1852 }
1853 }
1854 IndexUrl::Path(url) => {
1855 if let Some(locals) = locals.as_mut() {
1856 if let Some(path) = url.to_file_path().ok().and_then(|path| {
1857 try_relative_to_if(&path, root, !url.was_given_absolute()).ok()
1858 }) {
1859 locals.insert(path.into_boxed_path());
1860 }
1861 }
1862 }
1863 }
1864 }
1865 }
1866
1867 if !root_requirements.is_empty() {
1868 let names = root_requirements
1869 .iter()
1870 .map(|requirement| &requirement.name)
1871 .collect::<FxHashSet<_>>();
1872
1873 let by_name: FxHashMap<_, Vec<_>> = self.packages.iter().fold(
1874 FxHashMap::with_capacity_and_hasher(self.packages.len(), FxBuildHasher),
1875 |mut by_name, package| {
1876 if names.contains(&package.id.name) {
1877 by_name.entry(&package.id.name).or_default().push(package);
1878 }
1879 by_name
1880 },
1881 );
1882
1883 for requirement in root_requirements {
1884 for package in by_name.get(&requirement.name).into_iter().flatten() {
1885 if !package.id.source.is_source_tree() {
1886 continue;
1887 }
1888
1889 let marker = if package.fork_markers.is_empty() {
1890 requirement.marker
1891 } else {
1892 let mut combined = MarkerTree::FALSE;
1893 for fork_marker in &package.fork_markers {
1894 combined.or(fork_marker.pep508());
1895 }
1896 combined.and(requirement.marker);
1897 combined
1898 };
1899 if marker.is_false() {
1900 continue;
1901 }
1902 if !marker.evaluate(markers, &[]) {
1903 continue;
1904 }
1905
1906 if seen.insert(&package.id) {
1907 queue.push_back(package);
1908 }
1909 }
1910 }
1911 }
1912
1913 while let Some(package) = queue.pop_front() {
1914 if let Source::Registry(index) = &package.id.source {
1916 match index {
1917 RegistrySource::Url(url) => {
1918 if remotes
1919 .as_ref()
1920 .is_some_and(|remotes| !remotes.contains(url))
1921 {
1922 let name = &package.id.name;
1923 let version = &package
1924 .id
1925 .version
1926 .as_ref()
1927 .expect("version for registry source");
1928 return Ok(SatisfiesResult::MissingRemoteIndex(name, version, url));
1929 }
1930 }
1931 RegistrySource::Path(path) => {
1932 if locals.as_ref().is_some_and(|locals| !locals.contains(path)) {
1933 let name = &package.id.name;
1934 let version = &package
1935 .id
1936 .version
1937 .as_ref()
1938 .expect("version for registry source");
1939 return Ok(SatisfiesResult::MissingLocalIndex(name, version, path));
1940 }
1941 }
1942 }
1943 }
1944
1945 if package.id.source.is_immutable() {
1947 continue;
1948 }
1949
1950 if let Some(version) = package.id.version.as_ref() {
1951 let HashedDist { dist, .. } = package.to_dist(
1953 root,
1954 TagPolicy::Preferred(tags),
1955 &BuildOptions::default(),
1956 markers,
1957 )?;
1958
1959 let metadata = {
1960 let id = dist.distribution_id();
1961 if let Some(archive) =
1962 index
1963 .distributions()
1964 .get(&id)
1965 .as_deref()
1966 .and_then(|response| {
1967 if let MetadataResponse::Found(archive, ..) = response {
1968 Some(archive)
1969 } else {
1970 None
1971 }
1972 })
1973 {
1974 archive.metadata.clone()
1976 } else {
1977 let archive = database
1979 .get_or_build_wheel_metadata(&dist, hasher.get(&dist))
1980 .await
1981 .map_err(|err| LockErrorKind::Resolution {
1982 id: package.id.clone(),
1983 err,
1984 })?;
1985
1986 let metadata = archive.metadata.clone();
1987
1988 index
1990 .distributions()
1991 .done(id, Arc::new(MetadataResponse::Found(archive)));
1992
1993 metadata
1994 }
1995 };
1996
1997 if package.id.source.is_source_tree() {
2000 if metadata.dynamic {
2001 return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, false));
2002 }
2003 }
2004
2005 if metadata.version != *version {
2007 return Ok(SatisfiesResult::MismatchedVersion(
2008 &package.id.name,
2009 version.clone(),
2010 Some(metadata.version.clone()),
2011 ));
2012 }
2013
2014 match self.satisfies_provides_extra(metadata.provides_extra, package) {
2016 SatisfiesResult::Satisfied => {}
2017 result => return Ok(result),
2018 }
2019
2020 match self.satisfies_requires_dist(
2022 metadata.requires_dist,
2023 metadata.dependency_groups,
2024 package,
2025 root,
2026 )? {
2027 SatisfiesResult::Satisfied => {}
2028 result => return Ok(result),
2029 }
2030 } else if let Some(source_tree) = package.id.source.as_source_tree() {
2031 let parent = root.join(source_tree);
2041 let path = parent.join("pyproject.toml");
2042 let metadata = match fs_err::tokio::read_to_string(&path).await {
2043 Ok(contents) => {
2044 let pyproject_toml =
2045 PyProjectToml::from_toml(&contents, path.user_display()).map_err(
2046 |err| LockErrorKind::InvalidPyprojectToml {
2047 path: path.clone(),
2048 err,
2049 },
2050 )?;
2051 database
2052 .requires_dist(&parent, &pyproject_toml)
2053 .await
2054 .map_err(|err| LockErrorKind::Resolution {
2055 id: package.id.clone(),
2056 err,
2057 })?
2058 }
2059 Err(err) if err.kind() == io::ErrorKind::NotFound => None,
2060 Err(err) => {
2061 return Err(LockErrorKind::UnreadablePyprojectToml { path, err }.into());
2062 }
2063 };
2064
2065 let satisfied = metadata.is_some_and(|metadata| {
2066 if !metadata.dynamic {
2068 debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
2069 return false;
2070 }
2071
2072 if let SatisfiesResult::Satisfied = self.satisfies_provides_extra(metadata.provides_extra, package, ) {
2074 debug!("Static `provides-extra` for `{}` is up-to-date", package.id);
2075 } else {
2076 debug!("Static `provides-extra` for `{}` is out-of-date; falling back to distribution database", package.id);
2077 return false;
2078 }
2079
2080 match self.satisfies_requires_dist(metadata.requires_dist, metadata.dependency_groups, package, root) {
2082 Ok(SatisfiesResult::Satisfied) => {
2083 debug!("Static `requires-dist` for `{}` is up-to-date", package.id);
2084 },
2085 Ok(..) => {
2086 debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
2087 return false;
2088 },
2089 Err(..) => {
2090 debug!("Static `requires-dist` for `{}` is invalid; falling back to distribution database", package.id);
2091 return false;
2092 },
2093 }
2094
2095 true
2096 });
2097
2098 if !satisfied {
2104 let HashedDist { dist, .. } = package.to_dist(
2105 root,
2106 TagPolicy::Preferred(tags),
2107 &BuildOptions::default(),
2108 markers,
2109 )?;
2110
2111 let metadata = {
2112 let id = dist.distribution_id();
2113 if let Some(archive) =
2114 index
2115 .distributions()
2116 .get(&id)
2117 .as_deref()
2118 .and_then(|response| {
2119 if let MetadataResponse::Found(archive, ..) = response {
2120 Some(archive)
2121 } else {
2122 None
2123 }
2124 })
2125 {
2126 archive.metadata.clone()
2128 } else {
2129 let archive = database
2131 .get_or_build_wheel_metadata(&dist, hasher.get(&dist))
2132 .await
2133 .map_err(|err| LockErrorKind::Resolution {
2134 id: package.id.clone(),
2135 err,
2136 })?;
2137
2138 let metadata = archive.metadata.clone();
2139
2140 index
2142 .distributions()
2143 .done(id, Arc::new(MetadataResponse::Found(archive)));
2144
2145 metadata
2146 }
2147 };
2148
2149 if !metadata.dynamic {
2151 return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, true));
2152 }
2153
2154 match self.satisfies_provides_extra(metadata.provides_extra, package) {
2156 SatisfiesResult::Satisfied => {}
2157 result => return Ok(result),
2158 }
2159
2160 match self.satisfies_requires_dist(
2162 metadata.requires_dist,
2163 metadata.dependency_groups,
2164 package,
2165 root,
2166 )? {
2167 SatisfiesResult::Satisfied => {}
2168 result => return Ok(result),
2169 }
2170 }
2171 } else {
2172 return Ok(SatisfiesResult::MissingVersion(&package.id.name));
2173 }
2174
2175 for requirement in package
2180 .metadata
2181 .requires_dist
2182 .iter()
2183 .chain(package.metadata.dependency_groups.values().flatten())
2184 {
2185 if let RequirementSource::Registry {
2186 index: Some(index), ..
2187 } = &requirement.source
2188 {
2189 match &index.url {
2190 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
2191 if let Some(remotes) = remotes.as_mut() {
2192 remotes.insert(UrlString::from(
2193 index.url().without_credentials().as_ref(),
2194 ));
2195 }
2196 }
2197 IndexUrl::Path(url) => {
2198 if let Some(locals) = locals.as_mut() {
2199 if let Some(path) = url.to_file_path().ok().and_then(|path| {
2200 try_relative_to_if(&path, root, !url.was_given_absolute()).ok()
2201 }) {
2202 locals.insert(path.into_boxed_path());
2203 }
2204 }
2205 }
2206 }
2207 }
2208 }
2209
2210 for dep in &package.dependencies {
2212 if seen.insert(&dep.package_id) {
2213 let dep_dist = self.find_by_id(&dep.package_id);
2214 queue.push_back(dep_dist);
2215 }
2216 }
2217
2218 for dependencies in package.optional_dependencies.values() {
2219 for dep in dependencies {
2220 if seen.insert(&dep.package_id) {
2221 let dep_dist = self.find_by_id(&dep.package_id);
2222 queue.push_back(dep_dist);
2223 }
2224 }
2225 }
2226
2227 for dependencies in package.dependency_groups.values() {
2228 for dep in dependencies {
2229 if seen.insert(&dep.package_id) {
2230 let dep_dist = self.find_by_id(&dep.package_id);
2231 queue.push_back(dep_dist);
2232 }
2233 }
2234 }
2235 }
2236
2237 Ok(SatisfiesResult::Satisfied)
2238 }
2239}
2240
2241#[derive(Debug, Copy, Clone)]
2242enum TagPolicy<'tags> {
2243 Required(&'tags Tags),
2245 Preferred(&'tags Tags),
2248}
2249
2250impl<'tags> TagPolicy<'tags> {
2251 fn tags(&self) -> &'tags Tags {
2253 match self {
2254 Self::Required(tags) | Self::Preferred(tags) => tags,
2255 }
2256 }
2257}
2258
2259#[derive(Debug)]
2261pub enum SatisfiesResult<'lock> {
2262 Satisfied,
2264 MismatchedMembers(BTreeSet<PackageName>, &'lock BTreeSet<PackageName>),
2266 MismatchedVirtual(PackageName, bool),
2268 MismatchedEditable(PackageName, bool),
2270 MismatchedDynamic(&'lock PackageName, bool),
2272 MismatchedVersion(&'lock PackageName, Version, Option<Version>),
2274 MismatchedRequirements(BTreeSet<Requirement>, BTreeSet<Requirement>),
2276 MismatchedConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
2278 MismatchedOverrides(BTreeSet<Requirement>, BTreeSet<Requirement>),
2280 MismatchedExcludes(BTreeSet<PackageName>, BTreeSet<PackageName>),
2282 MismatchedBuildConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
2284 MismatchedDependencyGroups(
2286 BTreeMap<GroupName, BTreeSet<Requirement>>,
2287 BTreeMap<GroupName, BTreeSet<Requirement>>,
2288 ),
2289 MismatchedStaticMetadata(BTreeSet<StaticMetadata>, &'lock BTreeSet<StaticMetadata>),
2291 MissingRoot(PackageName),
2293 MissingRemoteIndex(&'lock PackageName, &'lock Version, &'lock UrlString),
2295 MissingLocalIndex(&'lock PackageName, &'lock Version, &'lock Path),
2297 MismatchedPackageRequirements(
2299 &'lock PackageName,
2300 Option<&'lock Version>,
2301 BTreeSet<Requirement>,
2302 BTreeSet<Requirement>,
2303 ),
2304 MismatchedPackageProvidesExtra(
2306 &'lock PackageName,
2307 Option<&'lock Version>,
2308 BTreeSet<ExtraName>,
2309 BTreeSet<&'lock ExtraName>,
2310 ),
2311 MismatchedPackageDependencyGroups(
2313 &'lock PackageName,
2314 Option<&'lock Version>,
2315 BTreeMap<GroupName, BTreeSet<Requirement>>,
2316 BTreeMap<GroupName, BTreeSet<Requirement>>,
2317 ),
2318 MissingVersion(&'lock PackageName),
2320}
2321
2322#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2324#[serde(rename_all = "kebab-case")]
2325struct ResolverOptions {
2326 #[serde(default)]
2328 resolution_mode: ResolutionMode,
2329 #[serde(default)]
2331 prerelease_mode: PrereleaseMode,
2332 #[serde(default)]
2334 fork_strategy: ForkStrategy,
2335 #[serde(flatten)]
2337 exclude_newer: ExcludeNewerWire,
2338}
2339
2340#[expect(clippy::struct_field_names)]
2341#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2342#[serde(rename_all = "kebab-case")]
2343struct ExcludeNewerWire {
2344 exclude_newer: Option<Timestamp>,
2345 exclude_newer_span: Option<ExcludeNewerSpan>,
2346 #[serde(default, skip_serializing_if = "ExcludeNewerPackage::is_empty")]
2347 exclude_newer_package: ExcludeNewerPackage,
2348}
2349
2350impl From<ExcludeNewerWire> for ExcludeNewer {
2351 fn from(wire: ExcludeNewerWire) -> Self {
2352 Self {
2353 global: wire
2354 .exclude_newer
2355 .map(|timestamp| ExcludeNewerValue::new(timestamp, wire.exclude_newer_span)),
2356 package: wire.exclude_newer_package,
2357 }
2358 }
2359}
2360
2361impl From<ExcludeNewer> for ExcludeNewerWire {
2362 fn from(exclude_newer: ExcludeNewer) -> Self {
2363 let (timestamp, span) = exclude_newer
2364 .global
2365 .map(ExcludeNewerValue::into_parts)
2366 .map_or((None, None), |(t, s)| (Some(t), s));
2367 Self {
2368 exclude_newer: timestamp,
2369 exclude_newer_span: span,
2370 exclude_newer_package: exclude_newer.package,
2371 }
2372 }
2373}
2374
2375#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2376#[serde(rename_all = "kebab-case")]
2377pub struct ResolverManifest {
2378 #[serde(default)]
2380 members: BTreeSet<PackageName>,
2381 #[serde(default)]
2386 requirements: BTreeSet<Requirement>,
2387 #[serde(default)]
2393 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
2394 #[serde(default)]
2396 constraints: BTreeSet<Requirement>,
2397 #[serde(default)]
2399 overrides: BTreeSet<Requirement>,
2400 #[serde(default)]
2402 excludes: BTreeSet<PackageName>,
2403 #[serde(default)]
2405 build_constraints: BTreeSet<Requirement>,
2406 #[serde(default)]
2408 dependency_metadata: BTreeSet<StaticMetadata>,
2409}
2410
2411impl ResolverManifest {
2412 pub fn new(
2415 members: impl IntoIterator<Item = PackageName>,
2416 requirements: impl IntoIterator<Item = Requirement>,
2417 constraints: impl IntoIterator<Item = Requirement>,
2418 overrides: impl IntoIterator<Item = Requirement>,
2419 excludes: impl IntoIterator<Item = PackageName>,
2420 build_constraints: impl IntoIterator<Item = Requirement>,
2421 dependency_groups: impl IntoIterator<Item = (GroupName, Vec<Requirement>)>,
2422 dependency_metadata: impl IntoIterator<Item = StaticMetadata>,
2423 ) -> Self {
2424 Self {
2425 members: members.into_iter().collect(),
2426 requirements: requirements.into_iter().collect(),
2427 constraints: constraints.into_iter().collect(),
2428 overrides: overrides.into_iter().collect(),
2429 excludes: excludes.into_iter().collect(),
2430 build_constraints: build_constraints.into_iter().collect(),
2431 dependency_groups: dependency_groups
2432 .into_iter()
2433 .map(|(group, requirements)| (group, requirements.into_iter().collect()))
2434 .collect(),
2435 dependency_metadata: dependency_metadata.into_iter().collect(),
2436 }
2437 }
2438
2439 pub fn relative_to(self, root: &Path) -> Result<Self, io::Error> {
2441 Ok(Self {
2442 members: self.members,
2443 requirements: self
2444 .requirements
2445 .into_iter()
2446 .map(|requirement| requirement.relative_to(root))
2447 .collect::<Result<BTreeSet<_>, _>>()?,
2448 constraints: self
2449 .constraints
2450 .into_iter()
2451 .map(|requirement| requirement.relative_to(root))
2452 .collect::<Result<BTreeSet<_>, _>>()?,
2453 overrides: self
2454 .overrides
2455 .into_iter()
2456 .map(|requirement| requirement.relative_to(root))
2457 .collect::<Result<BTreeSet<_>, _>>()?,
2458 excludes: self.excludes,
2459 build_constraints: self
2460 .build_constraints
2461 .into_iter()
2462 .map(|requirement| requirement.relative_to(root))
2463 .collect::<Result<BTreeSet<_>, _>>()?,
2464 dependency_groups: self
2465 .dependency_groups
2466 .into_iter()
2467 .map(|(group, requirements)| {
2468 Ok::<_, io::Error>((
2469 group,
2470 requirements
2471 .into_iter()
2472 .map(|requirement| requirement.relative_to(root))
2473 .collect::<Result<BTreeSet<_>, _>>()?,
2474 ))
2475 })
2476 .collect::<Result<BTreeMap<_, _>, _>>()?,
2477 dependency_metadata: self.dependency_metadata,
2478 })
2479 }
2480}
2481
2482#[derive(Clone, Debug, serde::Deserialize)]
2483#[serde(rename_all = "kebab-case")]
2484struct LockWire {
2485 version: u32,
2486 revision: Option<u32>,
2487 requires_python: RequiresPython,
2488 #[serde(rename = "resolution-markers", default)]
2491 fork_markers: Vec<SimplifiedMarkerTree>,
2492 #[serde(rename = "supported-markers", default)]
2493 supported_environments: Vec<SimplifiedMarkerTree>,
2494 #[serde(rename = "required-markers", default)]
2495 required_environments: Vec<SimplifiedMarkerTree>,
2496 #[serde(rename = "conflicts", default)]
2497 conflicts: Option<Conflicts>,
2498 #[serde(default)]
2500 options: ResolverOptions,
2501 #[serde(default)]
2502 manifest: ResolverManifest,
2503 #[serde(rename = "package", alias = "distribution", default)]
2504 packages: Vec<PackageWire>,
2505}
2506
2507impl TryFrom<LockWire> for Lock {
2508 type Error = LockError;
2509
2510 fn try_from(wire: LockWire) -> Result<Self, LockError> {
2511 let mut unambiguous_package_ids: FxHashMap<PackageName, PackageId> = FxHashMap::default();
2516 let mut ambiguous = FxHashSet::default();
2517 for dist in &wire.packages {
2518 if ambiguous.contains(&dist.id.name) {
2519 continue;
2520 }
2521 if let Some(id) = unambiguous_package_ids.remove(&dist.id.name) {
2522 ambiguous.insert(id.name);
2523 continue;
2524 }
2525 unambiguous_package_ids.insert(dist.id.name.clone(), dist.id.clone());
2526 }
2527
2528 let packages = wire
2529 .packages
2530 .into_iter()
2531 .map(|dist| dist.unwire(&wire.requires_python, &unambiguous_package_ids))
2532 .collect::<Result<Vec<_>, _>>()?;
2533 let supported_environments = wire
2534 .supported_environments
2535 .into_iter()
2536 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2537 .collect();
2538 let required_environments = wire
2539 .required_environments
2540 .into_iter()
2541 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2542 .collect();
2543 let fork_markers = wire
2544 .fork_markers
2545 .into_iter()
2546 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2547 .map(UniversalMarker::from_combined)
2548 .collect();
2549 let lock = Self::new(
2550 wire.version,
2551 wire.revision.unwrap_or(0),
2552 packages,
2553 wire.requires_python,
2554 wire.options,
2555 wire.manifest,
2556 wire.conflicts.unwrap_or_else(Conflicts::empty),
2557 supported_environments,
2558 required_environments,
2559 fork_markers,
2560 )?;
2561
2562 Ok(lock)
2563 }
2564}
2565
2566#[derive(Clone, Debug, serde::Deserialize)]
2570#[serde(rename_all = "kebab-case")]
2571pub struct LockVersion {
2572 version: u32,
2573}
2574
2575impl LockVersion {
2576 pub fn version(&self) -> u32 {
2578 self.version
2579 }
2580}
2581
2582#[derive(Clone, Debug, PartialEq, Eq)]
2583pub struct Package {
2584 pub(crate) id: PackageId,
2585 sdist: Option<SourceDist>,
2586 wheels: Vec<Wheel>,
2587 fork_markers: Vec<UniversalMarker>,
2593 dependencies: Vec<Dependency>,
2595 optional_dependencies: BTreeMap<ExtraName, Vec<Dependency>>,
2597 dependency_groups: BTreeMap<GroupName, Vec<Dependency>>,
2599 metadata: PackageMetadata,
2601}
2602
2603impl Package {
2604 fn from_annotated_dist(
2605 annotated_dist: &AnnotatedDist,
2606 fork_markers: Vec<UniversalMarker>,
2607 root: &Path,
2608 ) -> Result<Self, LockError> {
2609 let id = PackageId::from_annotated_dist(annotated_dist, root)?;
2610 let sdist = SourceDist::from_annotated_dist(&id, annotated_dist)?;
2611 let wheels = Wheel::from_annotated_dist(annotated_dist)?;
2612 let requires_dist = if id.source.is_immutable() {
2613 BTreeSet::default()
2614 } else {
2615 annotated_dist
2616 .metadata
2617 .as_ref()
2618 .expect("metadata is present")
2619 .requires_dist
2620 .iter()
2621 .cloned()
2622 .map(|requirement| requirement.relative_to(root))
2623 .collect::<Result<_, _>>()
2624 .map_err(LockErrorKind::RequirementRelativePath)?
2625 };
2626 let provides_extra = if id.source.is_immutable() {
2627 Box::default()
2628 } else {
2629 annotated_dist
2630 .metadata
2631 .as_ref()
2632 .expect("metadata is present")
2633 .provides_extra
2634 .clone()
2635 };
2636 let dependency_groups = if id.source.is_immutable() {
2637 BTreeMap::default()
2638 } else {
2639 annotated_dist
2640 .metadata
2641 .as_ref()
2642 .expect("metadata is present")
2643 .dependency_groups
2644 .iter()
2645 .map(|(group, requirements)| {
2646 let requirements = requirements
2647 .iter()
2648 .cloned()
2649 .map(|requirement| requirement.relative_to(root))
2650 .collect::<Result<_, _>>()
2651 .map_err(LockErrorKind::RequirementRelativePath)?;
2652 Ok::<_, LockError>((group.clone(), requirements))
2653 })
2654 .collect::<Result<_, _>>()?
2655 };
2656 Ok(Self {
2657 id,
2658 sdist,
2659 wheels,
2660 fork_markers,
2661 dependencies: vec![],
2662 optional_dependencies: BTreeMap::default(),
2663 dependency_groups: BTreeMap::default(),
2664 metadata: PackageMetadata {
2665 requires_dist,
2666 provides_extra,
2667 dependency_groups,
2668 },
2669 })
2670 }
2671
2672 fn add_dependency(
2674 &mut self,
2675 requires_python: &RequiresPython,
2676 annotated_dist: &AnnotatedDist,
2677 marker: UniversalMarker,
2678 root: &Path,
2679 ) -> Result<(), LockError> {
2680 let new_dep =
2681 Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2682 for existing_dep in &mut self.dependencies {
2683 if existing_dep.package_id == new_dep.package_id
2684 && existing_dep.simplified_marker == new_dep.simplified_marker
2707 {
2708 existing_dep.extra.extend(new_dep.extra);
2709 return Ok(());
2710 }
2711 }
2712
2713 self.dependencies.push(new_dep);
2714 Ok(())
2715 }
2716
2717 fn add_optional_dependency(
2719 &mut self,
2720 requires_python: &RequiresPython,
2721 extra: ExtraName,
2722 annotated_dist: &AnnotatedDist,
2723 marker: UniversalMarker,
2724 root: &Path,
2725 ) -> Result<(), LockError> {
2726 let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2727 let optional_deps = self.optional_dependencies.entry(extra).or_default();
2728 for existing_dep in &mut *optional_deps {
2729 if existing_dep.package_id == dep.package_id
2730 && existing_dep.simplified_marker == dep.simplified_marker
2733 {
2734 existing_dep.extra.extend(dep.extra);
2735 return Ok(());
2736 }
2737 }
2738
2739 optional_deps.push(dep);
2740 Ok(())
2741 }
2742
2743 fn add_group_dependency(
2745 &mut self,
2746 requires_python: &RequiresPython,
2747 group: GroupName,
2748 annotated_dist: &AnnotatedDist,
2749 marker: UniversalMarker,
2750 root: &Path,
2751 ) -> Result<(), LockError> {
2752 let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2753 let deps = self.dependency_groups.entry(group).or_default();
2754 for existing_dep in &mut *deps {
2755 if existing_dep.package_id == dep.package_id
2756 && existing_dep.simplified_marker == dep.simplified_marker
2759 {
2760 existing_dep.extra.extend(dep.extra);
2761 return Ok(());
2762 }
2763 }
2764
2765 deps.push(dep);
2766 Ok(())
2767 }
2768
2769 fn to_dist(
2771 &self,
2772 workspace_root: &Path,
2773 tag_policy: TagPolicy<'_>,
2774 build_options: &BuildOptions,
2775 markers: &MarkerEnvironment,
2776 ) -> Result<HashedDist, LockError> {
2777 let no_binary = build_options.no_binary_package(&self.id.name);
2778 let no_build = build_options.no_build_package(&self.id.name);
2779
2780 if !no_binary {
2781 if let Some(best_wheel_index) = self.find_best_wheel(tag_policy) {
2782 let hashes = {
2783 let wheel = &self.wheels[best_wheel_index];
2784 HashDigests::from(
2785 wheel
2786 .hash
2787 .iter()
2788 .chain(wheel.zstd.iter().flat_map(|z| z.hash.iter()))
2789 .map(|h| h.0.clone())
2790 .collect::<Vec<_>>(),
2791 )
2792 };
2793
2794 let dist = match &self.id.source {
2795 Source::Registry(source) => {
2796 let wheels = self
2797 .wheels
2798 .iter()
2799 .map(|wheel| wheel.to_registry_wheel(source, workspace_root))
2800 .collect::<Result<_, LockError>>()?;
2801 let reg_built_dist = RegistryBuiltDist {
2802 wheels,
2803 best_wheel_index,
2804 sdist: None,
2805 };
2806 Dist::Built(BuiltDist::Registry(reg_built_dist))
2807 }
2808 Source::Path(path) => {
2809 let filename: WheelFilename =
2810 self.wheels[best_wheel_index].filename.clone();
2811 let install_path = absolute_path(workspace_root, path)?;
2812 let path_dist = PathBuiltDist {
2813 filename,
2814 url: verbatim_url(&install_path, &self.id)?,
2815 install_path: absolute_path(workspace_root, path)?.into_boxed_path(),
2816 };
2817 let built_dist = BuiltDist::Path(path_dist);
2818 Dist::Built(built_dist)
2819 }
2820 Source::Direct(url, direct) => {
2821 let filename: WheelFilename =
2822 self.wheels[best_wheel_index].filename.clone();
2823 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
2824 url: url.to_url().map_err(LockErrorKind::InvalidUrl)?,
2825 subdirectory: direct.subdirectory.clone(),
2826 ext: DistExtension::Wheel,
2827 });
2828 let direct_dist = DirectUrlBuiltDist {
2829 filename,
2830 location: Box::new(url.clone()),
2831 url: VerbatimUrl::from_url(url),
2832 };
2833 let built_dist = BuiltDist::DirectUrl(direct_dist);
2834 Dist::Built(built_dist)
2835 }
2836 Source::Git(_, _) => {
2837 return Err(LockErrorKind::InvalidWheelSource {
2838 id: self.id.clone(),
2839 source_type: "Git",
2840 }
2841 .into());
2842 }
2843 Source::Directory(_) => {
2844 return Err(LockErrorKind::InvalidWheelSource {
2845 id: self.id.clone(),
2846 source_type: "directory",
2847 }
2848 .into());
2849 }
2850 Source::Editable(_) => {
2851 return Err(LockErrorKind::InvalidWheelSource {
2852 id: self.id.clone(),
2853 source_type: "editable",
2854 }
2855 .into());
2856 }
2857 Source::Virtual(_) => {
2858 return Err(LockErrorKind::InvalidWheelSource {
2859 id: self.id.clone(),
2860 source_type: "virtual",
2861 }
2862 .into());
2863 }
2864 };
2865
2866 return Ok(HashedDist { dist, hashes });
2867 }
2868 }
2869
2870 if let Some(sdist) = self.to_source_dist(workspace_root)? {
2871 if !no_build || sdist.is_virtual() {
2875 let hashes = self
2876 .sdist
2877 .as_ref()
2878 .and_then(|s| s.hash())
2879 .map(|hash| HashDigests::from(vec![hash.0.clone()]))
2880 .unwrap_or_else(|| HashDigests::from(vec![]));
2881 return Ok(HashedDist {
2882 dist: Dist::Source(sdist),
2883 hashes,
2884 });
2885 }
2886 }
2887
2888 match (no_binary, no_build) {
2889 (true, true) => Err(LockErrorKind::NoBinaryNoBuild {
2890 id: self.id.clone(),
2891 }
2892 .into()),
2893 (true, false) if self.id.source.is_wheel() => Err(LockErrorKind::NoBinaryWheelOnly {
2894 id: self.id.clone(),
2895 }
2896 .into()),
2897 (true, false) => Err(LockErrorKind::NoBinary {
2898 id: self.id.clone(),
2899 }
2900 .into()),
2901 (false, true) => Err(LockErrorKind::NoBuild {
2902 id: self.id.clone(),
2903 }
2904 .into()),
2905 (false, false) if self.id.source.is_wheel() => Err(LockError {
2906 kind: Box::new(LockErrorKind::IncompatibleWheelOnly {
2907 id: self.id.clone(),
2908 }),
2909 hint: self.tag_hint(tag_policy, markers),
2910 }),
2911 (false, false) => Err(LockError {
2912 kind: Box::new(LockErrorKind::NeitherSourceDistNorWheel {
2913 id: self.id.clone(),
2914 }),
2915 hint: self.tag_hint(tag_policy, markers),
2916 }),
2917 }
2918 }
2919
2920 fn tag_hint(
2922 &self,
2923 tag_policy: TagPolicy<'_>,
2924 markers: &MarkerEnvironment,
2925 ) -> Option<WheelTagHint> {
2926 let filenames = self
2927 .wheels
2928 .iter()
2929 .map(|wheel| &wheel.filename)
2930 .collect::<Vec<_>>();
2931 WheelTagHint::from_wheels(
2932 &self.id.name,
2933 self.id.version.as_ref(),
2934 &filenames,
2935 tag_policy.tags(),
2936 markers,
2937 )
2938 }
2939
2940 fn to_source_dist(
2945 &self,
2946 workspace_root: &Path,
2947 ) -> Result<Option<uv_distribution_types::SourceDist>, LockError> {
2948 let sdist = match &self.id.source {
2949 Source::Path(path) => {
2950 let DistExtension::Source(ext) = DistExtension::from_path(path).map_err(|err| {
2952 LockErrorKind::MissingExtension {
2953 id: self.id.clone(),
2954 err,
2955 }
2956 })?
2957 else {
2958 return Ok(None);
2959 };
2960 let install_path = absolute_path(workspace_root, path)?;
2961 let given = path.to_str().expect("lock file paths must be UTF-8");
2962 let path_dist = PathSourceDist {
2963 name: self.id.name.clone(),
2964 version: self.id.version.clone(),
2965 url: verbatim_url(&install_path, &self.id)?.with_given(given),
2966 install_path: install_path.into_boxed_path(),
2967 ext,
2968 };
2969 uv_distribution_types::SourceDist::Path(path_dist)
2970 }
2971 Source::Directory(path) => {
2972 let install_path = absolute_path(workspace_root, path)?;
2973 let given = path.to_str().expect("lock file paths must be UTF-8");
2974 let dir_dist = DirectorySourceDist {
2975 name: self.id.name.clone(),
2976 url: verbatim_url(&install_path, &self.id)?.with_given(given),
2977 install_path: install_path.into_boxed_path(),
2978 editable: Some(false),
2979 r#virtual: Some(false),
2980 };
2981 uv_distribution_types::SourceDist::Directory(dir_dist)
2982 }
2983 Source::Editable(path) => {
2984 let install_path = absolute_path(workspace_root, path)?;
2985 let given = path.to_str().expect("lock file paths must be UTF-8");
2986 let dir_dist = DirectorySourceDist {
2987 name: self.id.name.clone(),
2988 url: verbatim_url(&install_path, &self.id)?.with_given(given),
2989 install_path: install_path.into_boxed_path(),
2990 editable: Some(true),
2991 r#virtual: Some(false),
2992 };
2993 uv_distribution_types::SourceDist::Directory(dir_dist)
2994 }
2995 Source::Virtual(path) => {
2996 let install_path = absolute_path(workspace_root, path)?;
2997 let given = path.to_str().expect("lock file paths must be UTF-8");
2998 let dir_dist = DirectorySourceDist {
2999 name: self.id.name.clone(),
3000 url: verbatim_url(&install_path, &self.id)?.with_given(given),
3001 install_path: install_path.into_boxed_path(),
3002 editable: Some(false),
3003 r#virtual: Some(true),
3004 };
3005 uv_distribution_types::SourceDist::Directory(dir_dist)
3006 }
3007 Source::Git(url, git) => {
3008 let mut url = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
3011 url.set_fragment(None);
3012 url.set_query(None);
3013
3014 let git_url = GitUrl::from_commit(
3016 url,
3017 GitReference::from(git.kind.clone()),
3018 git.precise,
3019 git.lfs,
3020 )?;
3021
3022 let url = DisplaySafeUrl::from(ParsedGitUrl {
3024 url: git_url.clone(),
3025 subdirectory: git.subdirectory.clone(),
3026 });
3027
3028 let git_dist = GitSourceDist {
3029 name: self.id.name.clone(),
3030 url: VerbatimUrl::from_url(url),
3031 git: Box::new(git_url),
3032 subdirectory: git.subdirectory.clone(),
3033 };
3034 uv_distribution_types::SourceDist::Git(git_dist)
3035 }
3036 Source::Direct(url, direct) => {
3037 let DistExtension::Source(ext) =
3039 DistExtension::from_path(url.base_str()).map_err(|err| {
3040 LockErrorKind::MissingExtension {
3041 id: self.id.clone(),
3042 err,
3043 }
3044 })?
3045 else {
3046 return Ok(None);
3047 };
3048 let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
3049 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
3050 url: location.clone(),
3051 subdirectory: direct.subdirectory.clone(),
3052 ext: DistExtension::Source(ext),
3053 });
3054 let direct_dist = DirectUrlSourceDist {
3055 name: self.id.name.clone(),
3056 location: Box::new(location),
3057 subdirectory: direct.subdirectory.clone(),
3058 ext,
3059 url: VerbatimUrl::from_url(url),
3060 };
3061 uv_distribution_types::SourceDist::DirectUrl(direct_dist)
3062 }
3063 Source::Registry(RegistrySource::Url(url)) => {
3064 let Some(ref sdist) = self.sdist else {
3065 return Ok(None);
3066 };
3067
3068 let name = &self.id.name;
3069 let version = self
3070 .id
3071 .version
3072 .as_ref()
3073 .expect("version for registry source");
3074
3075 let file_url = sdist.url().ok_or_else(|| LockErrorKind::MissingUrl {
3076 name: name.clone(),
3077 version: version.clone(),
3078 })?;
3079 let filename = sdist
3080 .filename()
3081 .ok_or_else(|| LockErrorKind::MissingFilename {
3082 id: self.id.clone(),
3083 })?;
3084 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
3085 LockErrorKind::MissingExtension {
3086 id: self.id.clone(),
3087 err,
3088 }
3089 })?;
3090 let file = Box::new(uv_distribution_types::File {
3091 dist_info_metadata: false,
3092 filename: SmallString::from(filename),
3093 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
3094 HashDigests::from(hash.0.clone())
3095 }),
3096 requires_python: None,
3097 size: sdist.size(),
3098 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
3099 url: FileLocation::AbsoluteUrl(file_url.clone()),
3100 yanked: None,
3101 zstd: None,
3102 });
3103
3104 let index = IndexUrl::from(VerbatimUrl::from_url(
3105 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
3106 ));
3107
3108 let reg_dist = RegistrySourceDist {
3109 name: name.clone(),
3110 version: version.clone(),
3111 file,
3112 ext,
3113 index,
3114 wheels: vec![],
3115 };
3116 uv_distribution_types::SourceDist::Registry(reg_dist)
3117 }
3118 Source::Registry(RegistrySource::Path(path)) => {
3119 let Some(ref sdist) = self.sdist else {
3120 return Ok(None);
3121 };
3122
3123 let name = &self.id.name;
3124 let version = self
3125 .id
3126 .version
3127 .as_ref()
3128 .expect("version for registry source");
3129
3130 let file_url = match sdist {
3131 SourceDist::Url { url: file_url, .. } => {
3132 FileLocation::AbsoluteUrl(file_url.clone())
3133 }
3134 SourceDist::Path {
3135 path: file_path, ..
3136 } => {
3137 let file_path = workspace_root.join(path).join(file_path);
3138 let file_url =
3139 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
3140 LockErrorKind::PathToUrl {
3141 path: file_path.into_boxed_path(),
3142 }
3143 })?;
3144 FileLocation::AbsoluteUrl(UrlString::from(file_url))
3145 }
3146 SourceDist::Metadata { .. } => {
3147 return Err(LockErrorKind::MissingPath {
3148 name: name.clone(),
3149 version: version.clone(),
3150 }
3151 .into());
3152 }
3153 };
3154 let filename = sdist
3155 .filename()
3156 .ok_or_else(|| LockErrorKind::MissingFilename {
3157 id: self.id.clone(),
3158 })?;
3159 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
3160 LockErrorKind::MissingExtension {
3161 id: self.id.clone(),
3162 err,
3163 }
3164 })?;
3165 let file = Box::new(uv_distribution_types::File {
3166 dist_info_metadata: false,
3167 filename: SmallString::from(filename),
3168 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
3169 HashDigests::from(hash.0.clone())
3170 }),
3171 requires_python: None,
3172 size: sdist.size(),
3173 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
3174 url: file_url,
3175 yanked: None,
3176 zstd: None,
3177 });
3178
3179 let index = IndexUrl::from(
3180 VerbatimUrl::from_absolute_path(workspace_root.join(path))
3181 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
3182 );
3183
3184 let reg_dist = RegistrySourceDist {
3185 name: name.clone(),
3186 version: version.clone(),
3187 file,
3188 ext,
3189 index,
3190 wheels: vec![],
3191 };
3192 uv_distribution_types::SourceDist::Registry(reg_dist)
3193 }
3194 };
3195
3196 Ok(Some(sdist))
3197 }
3198
3199 fn to_toml(
3200 &self,
3201 requires_python: &RequiresPython,
3202 dist_count_by_name: &FxHashMap<PackageName, u64>,
3203 ) -> Result<Table, toml_edit::ser::Error> {
3204 let mut table = Table::new();
3205
3206 self.id.to_toml(None, &mut table);
3207
3208 if !self.fork_markers.is_empty() {
3209 let fork_markers = each_element_on_its_line_array(
3210 simplified_universal_markers(&self.fork_markers, requires_python).into_iter(),
3211 );
3212 if !fork_markers.is_empty() {
3213 table.insert("resolution-markers", value(fork_markers));
3214 }
3215 }
3216
3217 if !self.dependencies.is_empty() {
3218 let deps = each_element_on_its_line_array(self.dependencies.iter().map(|dep| {
3219 dep.to_toml(requires_python, dist_count_by_name)
3220 .into_inline_table()
3221 }));
3222 table.insert("dependencies", value(deps));
3223 }
3224
3225 if !self.optional_dependencies.is_empty() {
3226 let mut optional_deps = Table::new();
3227 for (extra, deps) in &self.optional_dependencies {
3228 let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3229 dep.to_toml(requires_python, dist_count_by_name)
3230 .into_inline_table()
3231 }));
3232 if !deps.is_empty() {
3233 optional_deps.insert(extra.as_ref(), value(deps));
3234 }
3235 }
3236 if !optional_deps.is_empty() {
3237 table.insert("optional-dependencies", Item::Table(optional_deps));
3238 }
3239 }
3240
3241 if !self.dependency_groups.is_empty() {
3242 let mut dependency_groups = Table::new();
3243 for (extra, deps) in &self.dependency_groups {
3244 let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3245 dep.to_toml(requires_python, dist_count_by_name)
3246 .into_inline_table()
3247 }));
3248 if !deps.is_empty() {
3249 dependency_groups.insert(extra.as_ref(), value(deps));
3250 }
3251 }
3252 if !dependency_groups.is_empty() {
3253 table.insert("dev-dependencies", Item::Table(dependency_groups));
3254 }
3255 }
3256
3257 if let Some(ref sdist) = self.sdist {
3258 table.insert("sdist", value(sdist.to_toml()?));
3259 }
3260
3261 if !self.wheels.is_empty() {
3262 let wheels = each_element_on_its_line_array(
3263 self.wheels
3264 .iter()
3265 .map(Wheel::to_toml)
3266 .collect::<Result<Vec<_>, _>>()?
3267 .into_iter(),
3268 );
3269 table.insert("wheels", value(wheels));
3270 }
3271
3272 {
3274 let mut metadata_table = Table::new();
3275
3276 if !self.metadata.requires_dist.is_empty() {
3277 let requires_dist = self
3278 .metadata
3279 .requires_dist
3280 .iter()
3281 .map(|requirement| {
3282 serde::Serialize::serialize(
3283 &requirement,
3284 toml_edit::ser::ValueSerializer::new(),
3285 )
3286 })
3287 .collect::<Result<Vec<_>, _>>()?;
3288 let requires_dist = match requires_dist.as_slice() {
3289 [] => Array::new(),
3290 [requirement] => Array::from_iter([requirement]),
3291 requires_dist => each_element_on_its_line_array(requires_dist.iter()),
3292 };
3293 metadata_table.insert("requires-dist", value(requires_dist));
3294 }
3295
3296 if !self.metadata.dependency_groups.is_empty() {
3297 let mut dependency_groups = Table::new();
3298 for (extra, deps) in &self.metadata.dependency_groups {
3299 let deps = deps
3300 .iter()
3301 .map(|requirement| {
3302 serde::Serialize::serialize(
3303 &requirement,
3304 toml_edit::ser::ValueSerializer::new(),
3305 )
3306 })
3307 .collect::<Result<Vec<_>, _>>()?;
3308 let deps = match deps.as_slice() {
3309 [] => Array::new(),
3310 [requirement] => Array::from_iter([requirement]),
3311 deps => each_element_on_its_line_array(deps.iter()),
3312 };
3313 dependency_groups.insert(extra.as_ref(), value(deps));
3314 }
3315 if !dependency_groups.is_empty() {
3316 metadata_table.insert("requires-dev", Item::Table(dependency_groups));
3317 }
3318 }
3319
3320 if !self.metadata.provides_extra.is_empty() {
3321 let provides_extras = self
3322 .metadata
3323 .provides_extra
3324 .iter()
3325 .map(|extra| {
3326 serde::Serialize::serialize(&extra, toml_edit::ser::ValueSerializer::new())
3327 })
3328 .collect::<Result<Vec<_>, _>>()?;
3329 let provides_extras = Array::from_iter(provides_extras);
3331 metadata_table.insert("provides-extras", value(provides_extras));
3332 }
3333
3334 if !metadata_table.is_empty() {
3335 table.insert("metadata", Item::Table(metadata_table));
3336 }
3337 }
3338
3339 Ok(table)
3340 }
3341
3342 fn find_best_wheel(&self, tag_policy: TagPolicy<'_>) -> Option<usize> {
3343 type WheelPriority<'lock> = (TagPriority, Option<&'lock BuildTag>);
3344
3345 let mut best: Option<(WheelPriority, usize)> = None;
3346 for (i, wheel) in self.wheels.iter().enumerate() {
3347 let TagCompatibility::Compatible(tag_priority) =
3348 wheel.filename.compatibility(tag_policy.tags())
3349 else {
3350 continue;
3351 };
3352 let build_tag = wheel.filename.build_tag();
3353 let wheel_priority = (tag_priority, build_tag);
3354 match best {
3355 None => {
3356 best = Some((wheel_priority, i));
3357 }
3358 Some((best_priority, _)) => {
3359 if wheel_priority > best_priority {
3360 best = Some((wheel_priority, i));
3361 }
3362 }
3363 }
3364 }
3365
3366 let best = best.map(|(_, i)| i);
3367 match tag_policy {
3368 TagPolicy::Required(_) => best,
3369 TagPolicy::Preferred(_) => best.or_else(|| self.wheels.first().map(|_| 0)),
3370 }
3371 }
3372
3373 pub fn name(&self) -> &PackageName {
3375 &self.id.name
3376 }
3377
3378 pub fn version(&self) -> Option<&Version> {
3380 self.id.version.as_ref()
3381 }
3382
3383 pub fn git_sha(&self) -> Option<&GitOid> {
3385 match &self.id.source {
3386 Source::Git(_, git) => Some(&git.precise),
3387 _ => None,
3388 }
3389 }
3390
3391 pub fn fork_markers(&self) -> &[UniversalMarker] {
3393 self.fork_markers.as_slice()
3394 }
3395
3396 pub fn index(&self, root: &Path) -> Result<Option<IndexUrl>, LockError> {
3398 match &self.id.source {
3399 Source::Registry(RegistrySource::Url(url)) => {
3400 let index = IndexUrl::from(VerbatimUrl::from_url(
3401 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
3402 ));
3403 Ok(Some(index))
3404 }
3405 Source::Registry(RegistrySource::Path(path)) => {
3406 let index = IndexUrl::from(
3407 VerbatimUrl::from_absolute_path(root.join(path))
3408 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
3409 );
3410 Ok(Some(index))
3411 }
3412 _ => Ok(None),
3413 }
3414 }
3415
3416 fn hashes(&self) -> HashDigests {
3418 let mut hashes = Vec::with_capacity(
3419 usize::from(self.sdist.as_ref().and_then(|sdist| sdist.hash()).is_some())
3420 + self
3421 .wheels
3422 .iter()
3423 .map(|wheel| usize::from(wheel.hash.is_some()))
3424 .sum::<usize>(),
3425 );
3426 if let Some(ref sdist) = self.sdist {
3427 if let Some(hash) = sdist.hash() {
3428 hashes.push(hash.0.clone());
3429 }
3430 }
3431 for wheel in &self.wheels {
3432 hashes.extend(wheel.hash.as_ref().map(|h| h.0.clone()));
3433 if let Some(zstd) = wheel.zstd.as_ref() {
3434 hashes.extend(zstd.hash.as_ref().map(|h| h.0.clone()));
3435 }
3436 }
3437 HashDigests::from(hashes)
3438 }
3439
3440 pub fn as_git_ref(&self) -> Result<Option<ResolvedRepositoryReference>, LockError> {
3442 match &self.id.source {
3443 Source::Git(url, git) => Ok(Some(ResolvedRepositoryReference {
3444 reference: RepositoryReference {
3445 url: RepositoryUrl::new(&url.to_url().map_err(LockErrorKind::InvalidUrl)?),
3446 reference: GitReference::from(git.kind.clone()),
3447 },
3448 sha: git.precise,
3449 })),
3450 _ => Ok(None),
3451 }
3452 }
3453
3454 fn is_dynamic(&self) -> bool {
3456 self.id.version.is_none()
3457 }
3458
3459 pub fn provides_extras(&self) -> &[ExtraName] {
3461 &self.metadata.provides_extra
3462 }
3463
3464 pub fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
3466 &self.metadata.dependency_groups
3467 }
3468
3469 pub fn dependencies(&self) -> &[Dependency] {
3471 &self.dependencies
3472 }
3473
3474 pub fn optional_dependencies(&self) -> &BTreeMap<ExtraName, Vec<Dependency>> {
3476 &self.optional_dependencies
3477 }
3478
3479 pub fn resolved_dependency_groups(&self) -> &BTreeMap<GroupName, Vec<Dependency>> {
3481 &self.dependency_groups
3482 }
3483
3484 pub fn as_install_target(&self) -> InstallTarget<'_> {
3486 InstallTarget {
3487 name: self.name(),
3488 is_local: self.id.source.is_local(),
3489 }
3490 }
3491}
3492
3493fn verbatim_url(path: &Path, id: &PackageId) -> Result<VerbatimUrl, LockError> {
3495 let url =
3496 VerbatimUrl::from_normalized_path(path).map_err(|err| LockErrorKind::VerbatimUrl {
3497 id: id.clone(),
3498 err,
3499 })?;
3500 Ok(url)
3501}
3502
3503fn absolute_path(workspace_root: &Path, path: &Path) -> Result<PathBuf, LockError> {
3505 let path = uv_fs::normalize_absolute_path(&workspace_root.join(path))
3506 .map_err(LockErrorKind::AbsolutePath)?;
3507 Ok(path)
3508}
3509
3510#[derive(Clone, Debug, serde::Deserialize)]
3511#[serde(rename_all = "kebab-case")]
3512struct PackageWire {
3513 #[serde(flatten)]
3514 id: PackageId,
3515 #[serde(default)]
3516 metadata: PackageMetadata,
3517 #[serde(default)]
3518 sdist: Option<SourceDist>,
3519 #[serde(default)]
3520 wheels: Vec<Wheel>,
3521 #[serde(default, rename = "resolution-markers")]
3522 fork_markers: Vec<SimplifiedMarkerTree>,
3523 #[serde(default)]
3524 dependencies: Vec<DependencyWire>,
3525 #[serde(default)]
3526 optional_dependencies: BTreeMap<ExtraName, Vec<DependencyWire>>,
3527 #[serde(default, rename = "dev-dependencies", alias = "dependency-groups")]
3528 dependency_groups: BTreeMap<GroupName, Vec<DependencyWire>>,
3529}
3530
3531#[derive(Clone, Default, Debug, Eq, PartialEq, serde::Deserialize)]
3532#[serde(rename_all = "kebab-case")]
3533struct PackageMetadata {
3534 #[serde(default)]
3535 requires_dist: BTreeSet<Requirement>,
3536 #[serde(default, rename = "provides-extras")]
3537 provides_extra: Box<[ExtraName]>,
3538 #[serde(default, rename = "requires-dev", alias = "dependency-groups")]
3539 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
3540}
3541
3542impl PackageWire {
3543 fn unwire(
3544 self,
3545 requires_python: &RequiresPython,
3546 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3547 ) -> Result<Package, LockError> {
3548 if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) {
3550 if let Some(version) = &self.id.version {
3551 for wheel in &self.wheels {
3552 if *version != wheel.filename.version
3553 && *version != wheel.filename.version.clone().without_local()
3554 {
3555 return Err(LockError::from(LockErrorKind::InconsistentVersions {
3556 name: self.id.name,
3557 version: version.clone(),
3558 wheel: wheel.clone(),
3559 }));
3560 }
3561 }
3562 }
3565 }
3566
3567 let unwire_deps = |deps: Vec<DependencyWire>| -> Result<Vec<Dependency>, LockError> {
3568 deps.into_iter()
3569 .map(|dep| dep.unwire(requires_python, unambiguous_package_ids))
3570 .collect()
3571 };
3572
3573 Ok(Package {
3574 id: self.id,
3575 metadata: self.metadata,
3576 sdist: self.sdist,
3577 wheels: self.wheels,
3578 fork_markers: self
3579 .fork_markers
3580 .into_iter()
3581 .map(|simplified_marker| simplified_marker.into_marker(requires_python))
3582 .map(UniversalMarker::from_combined)
3583 .collect(),
3584 dependencies: unwire_deps(self.dependencies)?,
3585 optional_dependencies: self
3586 .optional_dependencies
3587 .into_iter()
3588 .map(|(extra, deps)| Ok((extra, unwire_deps(deps)?)))
3589 .collect::<Result<_, LockError>>()?,
3590 dependency_groups: self
3591 .dependency_groups
3592 .into_iter()
3593 .map(|(group, deps)| Ok((group, unwire_deps(deps)?)))
3594 .collect::<Result<_, LockError>>()?,
3595 })
3596 }
3597}
3598
3599#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3602#[serde(rename_all = "kebab-case")]
3603pub(crate) struct PackageId {
3604 pub(crate) name: PackageName,
3605 pub(crate) version: Option<Version>,
3606 source: Source,
3607}
3608
3609impl PackageId {
3610 fn from_annotated_dist(annotated_dist: &AnnotatedDist, root: &Path) -> Result<Self, LockError> {
3611 let source = Source::from_resolved_dist(&annotated_dist.dist, root)?;
3613 let version = if source.is_source_tree()
3615 && annotated_dist
3616 .metadata
3617 .as_ref()
3618 .is_some_and(|metadata| metadata.dynamic)
3619 {
3620 None
3621 } else {
3622 Some(annotated_dist.version.clone())
3623 };
3624 let name = annotated_dist.name.clone();
3625 Ok(Self {
3626 name,
3627 version,
3628 source,
3629 })
3630 }
3631
3632 fn to_toml(&self, dist_count_by_name: Option<&FxHashMap<PackageName, u64>>, table: &mut Table) {
3639 let count = dist_count_by_name.and_then(|map| map.get(&self.name).copied());
3640 table.insert("name", value(self.name.to_string()));
3641 if count.map(|count| count > 1).unwrap_or(true) {
3642 if let Some(version) = &self.version {
3643 table.insert("version", value(version.to_string()));
3644 }
3645 self.source.to_toml(table);
3646 }
3647 }
3648}
3649
3650impl Display for PackageId {
3651 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3652 if let Some(version) = &self.version {
3653 write!(f, "{}=={} @ {}", self.name, version, self.source)
3654 } else {
3655 write!(f, "{} @ {}", self.name, self.source)
3656 }
3657 }
3658}
3659
3660#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3661#[serde(rename_all = "kebab-case")]
3662struct PackageIdForDependency {
3663 name: PackageName,
3664 version: Option<Version>,
3665 source: Option<Source>,
3666}
3667
3668impl PackageIdForDependency {
3669 fn unwire(
3670 self,
3671 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3672 ) -> Result<PackageId, LockError> {
3673 let unambiguous_package_id = unambiguous_package_ids.get(&self.name);
3674 let source = self.source.map(Ok::<_, LockError>).unwrap_or_else(|| {
3675 let Some(package_id) = unambiguous_package_id else {
3676 return Err(LockErrorKind::MissingDependencySource {
3677 name: self.name.clone(),
3678 }
3679 .into());
3680 };
3681 Ok(package_id.source.clone())
3682 })?;
3683 let version = if let Some(version) = self.version {
3684 Some(version)
3685 } else {
3686 if let Some(package_id) = unambiguous_package_id {
3687 package_id.version.clone()
3688 } else {
3689 if source.is_source_tree() {
3692 None
3693 } else {
3694 return Err(LockErrorKind::MissingDependencyVersion {
3695 name: self.name.clone(),
3696 }
3697 .into());
3698 }
3699 }
3700 };
3701 Ok(PackageId {
3702 name: self.name,
3703 version,
3704 source,
3705 })
3706 }
3707}
3708
3709impl From<PackageId> for PackageIdForDependency {
3710 fn from(id: PackageId) -> Self {
3711 Self {
3712 name: id.name,
3713 version: id.version,
3714 source: Some(id.source),
3715 }
3716 }
3717}
3718
3719#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3727#[serde(try_from = "SourceWire")]
3728enum Source {
3729 Registry(RegistrySource),
3731 Git(UrlString, GitSource),
3733 Direct(UrlString, DirectSource),
3735 Path(Box<Path>),
3737 Directory(Box<Path>),
3739 Editable(Box<Path>),
3741 Virtual(Box<Path>),
3743}
3744
3745impl Source {
3746 fn from_resolved_dist(resolved_dist: &ResolvedDist, root: &Path) -> Result<Self, LockError> {
3747 match *resolved_dist {
3748 ResolvedDist::Installed { .. } => unreachable!(),
3750 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(dist, root),
3751 }
3752 }
3753
3754 fn from_dist(dist: &Dist, root: &Path) -> Result<Self, LockError> {
3755 match *dist {
3756 Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, root),
3757 Dist::Source(ref source_dist) => Self::from_source_dist(source_dist, root),
3758 }
3759 }
3760
3761 fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result<Self, LockError> {
3762 match *built_dist {
3763 BuiltDist::Registry(ref reg_dist) => Self::from_registry_built_dist(reg_dist, root),
3764 BuiltDist::DirectUrl(ref direct_dist) => Ok(Self::from_direct_built_dist(direct_dist)),
3765 BuiltDist::Path(ref path_dist) => Self::from_path_built_dist(path_dist, root),
3766 }
3767 }
3768
3769 fn from_source_dist(
3770 source_dist: &uv_distribution_types::SourceDist,
3771 root: &Path,
3772 ) -> Result<Self, LockError> {
3773 match *source_dist {
3774 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
3775 Self::from_registry_source_dist(reg_dist, root)
3776 }
3777 uv_distribution_types::SourceDist::DirectUrl(ref direct_dist) => {
3778 Ok(Self::from_direct_source_dist(direct_dist))
3779 }
3780 uv_distribution_types::SourceDist::Git(ref git_dist) => {
3781 Ok(Self::from_git_dist(git_dist))
3782 }
3783 uv_distribution_types::SourceDist::Path(ref path_dist) => {
3784 Self::from_path_source_dist(path_dist, root)
3785 }
3786 uv_distribution_types::SourceDist::Directory(ref directory) => {
3787 Self::from_directory_source_dist(directory, root)
3788 }
3789 }
3790 }
3791
3792 fn from_registry_built_dist(
3793 reg_dist: &RegistryBuiltDist,
3794 root: &Path,
3795 ) -> Result<Self, LockError> {
3796 Self::from_index_url(®_dist.best_wheel().index, root)
3797 }
3798
3799 fn from_registry_source_dist(
3800 reg_dist: &RegistrySourceDist,
3801 root: &Path,
3802 ) -> Result<Self, LockError> {
3803 Self::from_index_url(®_dist.index, root)
3804 }
3805
3806 fn from_direct_built_dist(direct_dist: &DirectUrlBuiltDist) -> Self {
3807 Self::Direct(
3808 normalize_url(direct_dist.url.to_url()),
3809 DirectSource { subdirectory: None },
3810 )
3811 }
3812
3813 fn from_direct_source_dist(direct_dist: &DirectUrlSourceDist) -> Self {
3814 Self::Direct(
3815 normalize_url(direct_dist.url.to_url()),
3816 DirectSource {
3817 subdirectory: direct_dist.subdirectory.clone(),
3818 },
3819 )
3820 }
3821
3822 fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result<Self, LockError> {
3823 let path = try_relative_to_if(
3824 &path_dist.install_path,
3825 root,
3826 !path_dist.url.was_given_absolute(),
3827 )
3828 .map_err(LockErrorKind::DistributionRelativePath)?;
3829 Ok(Self::Path(path.into_boxed_path()))
3830 }
3831
3832 fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Self, LockError> {
3833 let path = try_relative_to_if(
3834 &path_dist.install_path,
3835 root,
3836 !path_dist.url.was_given_absolute(),
3837 )
3838 .map_err(LockErrorKind::DistributionRelativePath)?;
3839 Ok(Self::Path(path.into_boxed_path()))
3840 }
3841
3842 fn from_directory_source_dist(
3843 directory_dist: &DirectorySourceDist,
3844 root: &Path,
3845 ) -> Result<Self, LockError> {
3846 let path = try_relative_to_if(
3847 &directory_dist.install_path,
3848 root,
3849 !directory_dist.url.was_given_absolute(),
3850 )
3851 .map_err(LockErrorKind::DistributionRelativePath)?;
3852 if directory_dist.editable.unwrap_or(false) {
3853 Ok(Self::Editable(path.into_boxed_path()))
3854 } else if directory_dist.r#virtual.unwrap_or(false) {
3855 Ok(Self::Virtual(path.into_boxed_path()))
3856 } else {
3857 Ok(Self::Directory(path.into_boxed_path()))
3858 }
3859 }
3860
3861 fn from_index_url(index_url: &IndexUrl, root: &Path) -> Result<Self, LockError> {
3862 match index_url {
3863 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
3864 let redacted = index_url.without_credentials();
3866 let source = RegistrySource::Url(UrlString::from(redacted.as_ref()));
3867 Ok(Self::Registry(source))
3868 }
3869 IndexUrl::Path(url) => {
3870 let path = url
3871 .to_file_path()
3872 .map_err(|()| LockErrorKind::UrlToPath { url: url.to_url() })?;
3873 let path = try_relative_to_if(&path, root, !url.was_given_absolute())
3874 .map_err(LockErrorKind::IndexRelativePath)?;
3875 let source = RegistrySource::Path(path.into_boxed_path());
3876 Ok(Self::Registry(source))
3877 }
3878 }
3879 }
3880
3881 fn from_git_dist(git_dist: &GitSourceDist) -> Self {
3882 Self::Git(
3883 UrlString::from(locked_git_url(git_dist)),
3884 GitSource {
3885 kind: GitSourceKind::from(git_dist.git.reference().clone()),
3886 precise: git_dist.git.precise().unwrap_or_else(|| {
3887 panic!("Git distribution is missing a precise hash: {git_dist}")
3888 }),
3889 subdirectory: git_dist.subdirectory.clone(),
3890 lfs: git_dist.git.lfs(),
3891 },
3892 )
3893 }
3894
3895 fn is_immutable(&self) -> bool {
3902 matches!(self, Self::Registry(..) | Self::Git(_, _))
3903 }
3904
3905 fn is_wheel(&self) -> bool {
3907 match self {
3908 Self::Path(path) => {
3909 matches!(
3910 DistExtension::from_path(path).ok(),
3911 Some(DistExtension::Wheel)
3912 )
3913 }
3914 Self::Direct(url, _) => {
3915 matches!(
3916 DistExtension::from_path(url.as_ref()).ok(),
3917 Some(DistExtension::Wheel)
3918 )
3919 }
3920 Self::Directory(..) => false,
3921 Self::Editable(..) => false,
3922 Self::Virtual(..) => false,
3923 Self::Git(..) => false,
3924 Self::Registry(..) => false,
3925 }
3926 }
3927
3928 fn is_source_tree(&self) -> bool {
3930 match self {
3931 Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => true,
3932 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => false,
3933 }
3934 }
3935
3936 fn as_source_tree(&self) -> Option<&Path> {
3938 match self {
3939 Self::Directory(path) | Self::Editable(path) | Self::Virtual(path) => Some(path),
3940 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => None,
3941 }
3942 }
3943
3944 fn to_toml(&self, table: &mut Table) {
3945 let mut source_table = InlineTable::new();
3946 match self {
3947 Self::Registry(source) => match source {
3948 RegistrySource::Url(url) => {
3949 source_table.insert("registry", Value::from(url.as_ref()));
3950 }
3951 RegistrySource::Path(path) => {
3952 source_table.insert(
3953 "registry",
3954 Value::from(PortablePath::from(path).to_string()),
3955 );
3956 }
3957 },
3958 Self::Git(url, _) => {
3959 source_table.insert("git", Value::from(url.as_ref()));
3960 }
3961 Self::Direct(url, DirectSource { subdirectory }) => {
3962 source_table.insert("url", Value::from(url.as_ref()));
3963 if let Some(ref subdirectory) = *subdirectory {
3964 source_table.insert(
3965 "subdirectory",
3966 Value::from(PortablePath::from(subdirectory).to_string()),
3967 );
3968 }
3969 }
3970 Self::Path(path) => {
3971 source_table.insert("path", Value::from(PortablePath::from(path).to_string()));
3972 }
3973 Self::Directory(path) => {
3974 source_table.insert(
3975 "directory",
3976 Value::from(PortablePath::from(path).to_string()),
3977 );
3978 }
3979 Self::Editable(path) => {
3980 source_table.insert(
3981 "editable",
3982 Value::from(PortablePath::from(path).to_string()),
3983 );
3984 }
3985 Self::Virtual(path) => {
3986 source_table.insert("virtual", Value::from(PortablePath::from(path).to_string()));
3987 }
3988 }
3989 table.insert("source", value(source_table));
3990 }
3991
3992 pub(crate) fn is_local(&self) -> bool {
3994 matches!(
3995 self,
3996 Self::Path(_) | Self::Directory(_) | Self::Editable(_) | Self::Virtual(_)
3997 )
3998 }
3999}
4000
4001impl Display for Source {
4002 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
4003 match self {
4004 Self::Registry(RegistrySource::Url(url)) | Self::Git(url, _) | Self::Direct(url, _) => {
4005 write!(f, "{}+{}", self.name(), url)
4006 }
4007 Self::Registry(RegistrySource::Path(path))
4008 | Self::Path(path)
4009 | Self::Directory(path)
4010 | Self::Editable(path)
4011 | Self::Virtual(path) => {
4012 write!(f, "{}+{}", self.name(), PortablePath::from(path))
4013 }
4014 }
4015 }
4016}
4017
4018impl Source {
4019 fn name(&self) -> &str {
4020 match self {
4021 Self::Registry(..) => "registry",
4022 Self::Git(..) => "git",
4023 Self::Direct(..) => "direct",
4024 Self::Path(..) => "path",
4025 Self::Directory(..) => "directory",
4026 Self::Editable(..) => "editable",
4027 Self::Virtual(..) => "virtual",
4028 }
4029 }
4030
4031 fn requires_hash(&self) -> Option<bool> {
4039 match self {
4040 Self::Registry(..) => None,
4041 Self::Direct(..) | Self::Path(..) => Some(true),
4042 Self::Git(..) | Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => {
4043 Some(false)
4044 }
4045 }
4046 }
4047}
4048
4049#[derive(Clone, Debug, serde::Deserialize)]
4050#[serde(untagged, rename_all = "kebab-case")]
4051enum SourceWire {
4052 Registry {
4053 registry: RegistrySourceWire,
4054 },
4055 Git {
4056 git: String,
4057 },
4058 Direct {
4059 url: UrlString,
4060 subdirectory: Option<PortablePathBuf>,
4061 },
4062 Path {
4063 path: PortablePathBuf,
4064 },
4065 Directory {
4066 directory: PortablePathBuf,
4067 },
4068 Editable {
4069 editable: PortablePathBuf,
4070 },
4071 Virtual {
4072 r#virtual: PortablePathBuf,
4073 },
4074}
4075
4076impl TryFrom<SourceWire> for Source {
4077 type Error = LockError;
4078
4079 fn try_from(wire: SourceWire) -> Result<Self, LockError> {
4080 use self::SourceWire::{Direct, Directory, Editable, Git, Path, Registry, Virtual};
4081
4082 match wire {
4083 Registry { registry } => Ok(Self::Registry(registry.into())),
4084 Git { git } => {
4085 let url = DisplaySafeUrl::parse(&git)
4086 .map_err(|err| SourceParseError::InvalidUrl {
4087 given: git.clone(),
4088 err,
4089 })
4090 .map_err(LockErrorKind::InvalidGitSourceUrl)?;
4091
4092 let git_source = GitSource::from_url(&url)
4093 .map_err(|err| match err {
4094 GitSourceError::InvalidSha => SourceParseError::InvalidSha { given: git },
4095 GitSourceError::MissingSha => SourceParseError::MissingSha { given: git },
4096 })
4097 .map_err(LockErrorKind::InvalidGitSourceUrl)?;
4098
4099 Ok(Self::Git(UrlString::from(url), git_source))
4100 }
4101 Direct { url, subdirectory } => Ok(Self::Direct(
4102 url,
4103 DirectSource {
4104 subdirectory: subdirectory.map(Box::<std::path::Path>::from),
4105 },
4106 )),
4107 Path { path } => Ok(Self::Path(path.into())),
4108 Directory { directory } => Ok(Self::Directory(directory.into())),
4109 Editable { editable } => Ok(Self::Editable(editable.into())),
4110 Virtual { r#virtual } => Ok(Self::Virtual(r#virtual.into())),
4111 }
4112 }
4113}
4114
4115#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
4117enum RegistrySource {
4118 Url(UrlString),
4120 Path(Box<Path>),
4122}
4123
4124impl Display for RegistrySource {
4125 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
4126 match self {
4127 Self::Url(url) => write!(f, "{url}"),
4128 Self::Path(path) => write!(f, "{}", path.display()),
4129 }
4130 }
4131}
4132
4133#[derive(Clone, Debug)]
4134enum RegistrySourceWire {
4135 Url(UrlString),
4137 Path(PortablePathBuf),
4139}
4140
4141impl<'de> serde::de::Deserialize<'de> for RegistrySourceWire {
4142 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4143 where
4144 D: serde::de::Deserializer<'de>,
4145 {
4146 struct Visitor;
4147
4148 impl serde::de::Visitor<'_> for Visitor {
4149 type Value = RegistrySourceWire;
4150
4151 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
4152 formatter.write_str("a valid URL or a file path")
4153 }
4154
4155 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
4156 where
4157 E: serde::de::Error,
4158 {
4159 if split_scheme(value).is_some_and(|(scheme, _)| Scheme::parse(scheme).is_some()) {
4160 Ok(
4161 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
4162 value,
4163 ))
4164 .map(RegistrySourceWire::Url)?,
4165 )
4166 } else {
4167 Ok(
4168 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
4169 value,
4170 ))
4171 .map(RegistrySourceWire::Path)?,
4172 )
4173 }
4174 }
4175 }
4176
4177 deserializer.deserialize_str(Visitor)
4178 }
4179}
4180
4181impl From<RegistrySourceWire> for RegistrySource {
4182 fn from(wire: RegistrySourceWire) -> Self {
4183 match wire {
4184 RegistrySourceWire::Url(url) => Self::Url(url),
4185 RegistrySourceWire::Path(path) => Self::Path(path.into()),
4186 }
4187 }
4188}
4189
4190#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4191#[serde(rename_all = "kebab-case")]
4192struct DirectSource {
4193 subdirectory: Option<Box<Path>>,
4194}
4195
4196#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
4201struct GitSource {
4202 precise: GitOid,
4203 subdirectory: Option<Box<Path>>,
4204 kind: GitSourceKind,
4205 lfs: GitLfs,
4206}
4207
4208#[derive(Clone, Debug, Eq, PartialEq)]
4210enum GitSourceError {
4211 InvalidSha,
4212 MissingSha,
4213}
4214
4215impl GitSource {
4216 fn from_url(url: &Url) -> Result<Self, GitSourceError> {
4219 let mut kind = GitSourceKind::DefaultBranch;
4220 let mut subdirectory = None;
4221 let mut lfs = GitLfs::Disabled;
4222 for (key, val) in url.query_pairs() {
4223 match &*key {
4224 "tag" => kind = GitSourceKind::Tag(val.into_owned()),
4225 "branch" => kind = GitSourceKind::Branch(val.into_owned()),
4226 "rev" => kind = GitSourceKind::Rev(val.into_owned()),
4227 "subdirectory" => subdirectory = Some(PortablePathBuf::from(val.as_ref()).into()),
4228 "lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
4229 _ => {}
4230 }
4231 }
4232
4233 let precise = GitOid::from_str(url.fragment().ok_or(GitSourceError::MissingSha)?)
4234 .map_err(|_| GitSourceError::InvalidSha)?;
4235
4236 Ok(Self {
4237 precise,
4238 subdirectory,
4239 kind,
4240 lfs,
4241 })
4242 }
4243}
4244
4245#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4246#[serde(rename_all = "kebab-case")]
4247enum GitSourceKind {
4248 Tag(String),
4249 Branch(String),
4250 Rev(String),
4251 DefaultBranch,
4252}
4253
4254#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4256#[serde(rename_all = "kebab-case")]
4257struct SourceDistMetadata {
4258 hash: Option<Hash>,
4260 size: Option<u64>,
4264 #[serde(alias = "upload_time")]
4266 upload_time: Option<Timestamp>,
4267}
4268
4269#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4274#[serde(from = "SourceDistWire")]
4275enum SourceDist {
4276 Url {
4277 url: UrlString,
4278 #[serde(flatten)]
4279 metadata: SourceDistMetadata,
4280 },
4281 Path {
4282 path: Box<Path>,
4283 #[serde(flatten)]
4284 metadata: SourceDistMetadata,
4285 },
4286 Metadata {
4287 #[serde(flatten)]
4288 metadata: SourceDistMetadata,
4289 },
4290}
4291
4292impl SourceDist {
4293 fn filename(&self) -> Option<Cow<'_, str>> {
4294 match self {
4295 Self::Metadata { .. } => None,
4296 Self::Url { url, .. } => url.filename().ok(),
4297 Self::Path { path, .. } => path.file_name().map(|filename| filename.to_string_lossy()),
4298 }
4299 }
4300
4301 fn url(&self) -> Option<&UrlString> {
4302 match self {
4303 Self::Metadata { .. } => None,
4304 Self::Url { url, .. } => Some(url),
4305 Self::Path { .. } => None,
4306 }
4307 }
4308
4309 pub(crate) fn hash(&self) -> Option<&Hash> {
4310 match self {
4311 Self::Metadata { metadata } => metadata.hash.as_ref(),
4312 Self::Url { metadata, .. } => metadata.hash.as_ref(),
4313 Self::Path { metadata, .. } => metadata.hash.as_ref(),
4314 }
4315 }
4316
4317 pub(crate) fn size(&self) -> Option<u64> {
4318 match self {
4319 Self::Metadata { metadata } => metadata.size,
4320 Self::Url { metadata, .. } => metadata.size,
4321 Self::Path { metadata, .. } => metadata.size,
4322 }
4323 }
4324
4325 pub(crate) fn upload_time(&self) -> Option<Timestamp> {
4326 match self {
4327 Self::Metadata { metadata } => metadata.upload_time,
4328 Self::Url { metadata, .. } => metadata.upload_time,
4329 Self::Path { metadata, .. } => metadata.upload_time,
4330 }
4331 }
4332}
4333
4334impl SourceDist {
4335 fn from_annotated_dist(
4336 id: &PackageId,
4337 annotated_dist: &AnnotatedDist,
4338 ) -> Result<Option<Self>, LockError> {
4339 match annotated_dist.dist {
4340 ResolvedDist::Installed { .. } => unreachable!(),
4342 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4343 id,
4344 dist,
4345 annotated_dist.hashes.as_slice(),
4346 annotated_dist.index(),
4347 ),
4348 }
4349 }
4350
4351 fn from_dist(
4352 id: &PackageId,
4353 dist: &Dist,
4354 hashes: &[HashDigest],
4355 index: Option<&IndexUrl>,
4356 ) -> Result<Option<Self>, LockError> {
4357 match *dist {
4358 Dist::Built(BuiltDist::Registry(ref built_dist)) => {
4359 let Some(sdist) = built_dist.sdist.as_ref() else {
4360 return Ok(None);
4361 };
4362 Self::from_registry_dist(sdist, index)
4363 }
4364 Dist::Built(_) => Ok(None),
4365 Dist::Source(ref source_dist) => Self::from_source_dist(id, source_dist, hashes, index),
4366 }
4367 }
4368
4369 fn from_source_dist(
4370 id: &PackageId,
4371 source_dist: &uv_distribution_types::SourceDist,
4372 hashes: &[HashDigest],
4373 index: Option<&IndexUrl>,
4374 ) -> Result<Option<Self>, LockError> {
4375 match *source_dist {
4376 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
4377 Self::from_registry_dist(reg_dist, index)
4378 }
4379 uv_distribution_types::SourceDist::DirectUrl(_) => {
4380 Self::from_direct_dist(id, hashes).map(Some)
4381 }
4382 uv_distribution_types::SourceDist::Path(_) => {
4383 Self::from_path_dist(id, hashes).map(Some)
4384 }
4385 uv_distribution_types::SourceDist::Git(_)
4389 | uv_distribution_types::SourceDist::Directory(_) => Ok(None),
4390 }
4391 }
4392
4393 fn from_registry_dist(
4394 reg_dist: &RegistrySourceDist,
4395 index: Option<&IndexUrl>,
4396 ) -> Result<Option<Self>, LockError> {
4397 if index.is_none_or(|index| *index != reg_dist.index) {
4400 return Ok(None);
4401 }
4402
4403 match ®_dist.index {
4404 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4405 let url = normalize_file_location(®_dist.file.url)
4406 .map_err(LockErrorKind::InvalidUrl)
4407 .map_err(LockError::from)?;
4408 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4409 let size = reg_dist.file.size;
4410 let upload_time = reg_dist
4411 .file
4412 .upload_time_utc_ms
4413 .map(Timestamp::from_millisecond)
4414 .transpose()
4415 .map_err(LockErrorKind::InvalidTimestamp)?;
4416 Ok(Some(Self::Url {
4417 url,
4418 metadata: SourceDistMetadata {
4419 hash,
4420 size,
4421 upload_time,
4422 },
4423 }))
4424 }
4425 IndexUrl::Path(path) => {
4426 let index_path = path
4427 .to_file_path()
4428 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4429 let url = reg_dist
4430 .file
4431 .url
4432 .to_url()
4433 .map_err(LockErrorKind::InvalidUrl)?;
4434
4435 if url.scheme() == "file" {
4436 let reg_dist_path = url
4437 .to_file_path()
4438 .map_err(|()| LockErrorKind::UrlToPath { url })?;
4439 let path =
4440 try_relative_to_if(®_dist_path, index_path, !path.was_given_absolute())
4441 .map_err(LockErrorKind::DistributionRelativePath)?
4442 .into_boxed_path();
4443 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4444 let size = reg_dist.file.size;
4445 let upload_time = reg_dist
4446 .file
4447 .upload_time_utc_ms
4448 .map(Timestamp::from_millisecond)
4449 .transpose()
4450 .map_err(LockErrorKind::InvalidTimestamp)?;
4451 Ok(Some(Self::Path {
4452 path,
4453 metadata: SourceDistMetadata {
4454 hash,
4455 size,
4456 upload_time,
4457 },
4458 }))
4459 } else {
4460 let url = normalize_file_location(®_dist.file.url)
4461 .map_err(LockErrorKind::InvalidUrl)
4462 .map_err(LockError::from)?;
4463 let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4464 let size = reg_dist.file.size;
4465 let upload_time = reg_dist
4466 .file
4467 .upload_time_utc_ms
4468 .map(Timestamp::from_millisecond)
4469 .transpose()
4470 .map_err(LockErrorKind::InvalidTimestamp)?;
4471 Ok(Some(Self::Url {
4472 url,
4473 metadata: SourceDistMetadata {
4474 hash,
4475 size,
4476 upload_time,
4477 },
4478 }))
4479 }
4480 }
4481 }
4482 }
4483
4484 fn from_direct_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4485 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4486 let kind = LockErrorKind::Hash {
4487 id: id.clone(),
4488 artifact_type: "direct URL source distribution",
4489 expected: true,
4490 };
4491 return Err(kind.into());
4492 };
4493 Ok(Self::Metadata {
4494 metadata: SourceDistMetadata {
4495 hash: Some(hash),
4496 size: None,
4497 upload_time: None,
4498 },
4499 })
4500 }
4501
4502 fn from_path_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4503 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4504 let kind = LockErrorKind::Hash {
4505 id: id.clone(),
4506 artifact_type: "path source distribution",
4507 expected: true,
4508 };
4509 return Err(kind.into());
4510 };
4511 Ok(Self::Metadata {
4512 metadata: SourceDistMetadata {
4513 hash: Some(hash),
4514 size: None,
4515 upload_time: None,
4516 },
4517 })
4518 }
4519}
4520
4521#[derive(Clone, Debug, serde::Deserialize)]
4522#[serde(untagged, rename_all = "kebab-case")]
4523enum SourceDistWire {
4524 Url {
4525 url: UrlString,
4526 #[serde(flatten)]
4527 metadata: SourceDistMetadata,
4528 },
4529 Path {
4530 path: PortablePathBuf,
4531 #[serde(flatten)]
4532 metadata: SourceDistMetadata,
4533 },
4534 Metadata {
4535 #[serde(flatten)]
4536 metadata: SourceDistMetadata,
4537 },
4538}
4539
4540impl SourceDist {
4541 fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
4543 let mut table = InlineTable::new();
4544 match self {
4545 Self::Metadata { .. } => {}
4546 Self::Url { url, .. } => {
4547 table.insert("url", Value::from(url.as_ref()));
4548 }
4549 Self::Path { path, .. } => {
4550 table.insert("path", Value::from(PortablePath::from(path).to_string()));
4551 }
4552 }
4553 if let Some(hash) = self.hash() {
4554 table.insert("hash", Value::from(hash.to_string()));
4555 }
4556 if let Some(size) = self.size() {
4557 table.insert(
4558 "size",
4559 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4560 );
4561 }
4562 if let Some(upload_time) = self.upload_time() {
4563 table.insert("upload-time", Value::from(upload_time.to_string()));
4564 }
4565 Ok(table)
4566 }
4567}
4568
4569impl From<SourceDistWire> for SourceDist {
4570 fn from(wire: SourceDistWire) -> Self {
4571 match wire {
4572 SourceDistWire::Url { url, metadata } => Self::Url { url, metadata },
4573 SourceDistWire::Path { path, metadata } => Self::Path {
4574 path: path.into(),
4575 metadata,
4576 },
4577 SourceDistWire::Metadata { metadata } => Self::Metadata { metadata },
4578 }
4579 }
4580}
4581
4582impl From<GitReference> for GitSourceKind {
4583 fn from(value: GitReference) -> Self {
4584 match value {
4585 GitReference::Branch(branch) => Self::Branch(branch),
4586 GitReference::Tag(tag) => Self::Tag(tag),
4587 GitReference::BranchOrTag(rev) => Self::Rev(rev),
4588 GitReference::BranchOrTagOrCommit(rev) => Self::Rev(rev),
4589 GitReference::NamedRef(rev) => Self::Rev(rev),
4590 GitReference::DefaultBranch => Self::DefaultBranch,
4591 }
4592 }
4593}
4594
4595impl From<GitSourceKind> for GitReference {
4596 fn from(value: GitSourceKind) -> Self {
4597 match value {
4598 GitSourceKind::Branch(branch) => Self::Branch(branch),
4599 GitSourceKind::Tag(tag) => Self::Tag(tag),
4600 GitSourceKind::Rev(rev) => Self::from_rev(rev),
4601 GitSourceKind::DefaultBranch => Self::DefaultBranch,
4602 }
4603 }
4604}
4605
4606fn locked_git_url(git_dist: &GitSourceDist) -> DisplaySafeUrl {
4608 let mut url = git_dist.git.url().clone();
4609
4610 url.remove_credentials();
4612
4613 url.set_fragment(None);
4615 url.set_query(None);
4616
4617 if let Some(subdirectory) = git_dist
4619 .subdirectory
4620 .as_deref()
4621 .map(PortablePath::from)
4622 .as_ref()
4623 .map(PortablePath::to_string)
4624 {
4625 url.query_pairs_mut()
4626 .append_pair("subdirectory", &subdirectory);
4627 }
4628
4629 if git_dist.git.lfs().enabled() {
4631 url.query_pairs_mut().append_pair("lfs", "true");
4632 }
4633
4634 match git_dist.git.reference() {
4636 GitReference::Branch(branch) => {
4637 url.query_pairs_mut().append_pair("branch", branch.as_str());
4638 }
4639 GitReference::Tag(tag) => {
4640 url.query_pairs_mut().append_pair("tag", tag.as_str());
4641 }
4642 GitReference::BranchOrTag(rev)
4643 | GitReference::BranchOrTagOrCommit(rev)
4644 | GitReference::NamedRef(rev) => {
4645 url.query_pairs_mut().append_pair("rev", rev.as_str());
4646 }
4647 GitReference::DefaultBranch => {}
4648 }
4649
4650 url.set_fragment(
4652 git_dist
4653 .git
4654 .precise()
4655 .as_ref()
4656 .map(GitOid::to_string)
4657 .as_deref(),
4658 );
4659
4660 url
4661}
4662
4663#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4664struct ZstdWheel {
4665 hash: Option<Hash>,
4666 size: Option<u64>,
4667}
4668
4669#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4671#[serde(try_from = "WheelWire")]
4672struct Wheel {
4673 url: WheelWireSource,
4678 hash: Option<Hash>,
4684 size: Option<u64>,
4688 upload_time: Option<Timestamp>,
4692 filename: WheelFilename,
4699 zstd: Option<ZstdWheel>,
4701}
4702
4703impl Wheel {
4704 fn from_annotated_dist(annotated_dist: &AnnotatedDist) -> Result<Vec<Self>, LockError> {
4705 match annotated_dist.dist {
4706 ResolvedDist::Installed { .. } => unreachable!(),
4708 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4709 dist,
4710 annotated_dist.hashes.as_slice(),
4711 annotated_dist.index(),
4712 ),
4713 }
4714 }
4715
4716 fn from_dist(
4717 dist: &Dist,
4718 hashes: &[HashDigest],
4719 index: Option<&IndexUrl>,
4720 ) -> Result<Vec<Self>, LockError> {
4721 match *dist {
4722 Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, hashes, index),
4723 Dist::Source(uv_distribution_types::SourceDist::Registry(ref source_dist)) => {
4724 source_dist
4725 .wheels
4726 .iter()
4727 .filter(|wheel| {
4728 index.is_some_and(|index| *index == wheel.index)
4731 })
4732 .map(Self::from_registry_wheel)
4733 .collect()
4734 }
4735 Dist::Source(_) => Ok(vec![]),
4736 }
4737 }
4738
4739 fn from_built_dist(
4740 built_dist: &BuiltDist,
4741 hashes: &[HashDigest],
4742 index: Option<&IndexUrl>,
4743 ) -> Result<Vec<Self>, LockError> {
4744 match *built_dist {
4745 BuiltDist::Registry(ref reg_dist) => Self::from_registry_dist(reg_dist, index),
4746 BuiltDist::DirectUrl(ref direct_dist) => {
4747 Ok(vec![Self::from_direct_dist(direct_dist, hashes)])
4748 }
4749 BuiltDist::Path(ref path_dist) => Ok(vec![Self::from_path_dist(path_dist, hashes)]),
4750 }
4751 }
4752
4753 fn from_registry_dist(
4754 reg_dist: &RegistryBuiltDist,
4755 index: Option<&IndexUrl>,
4756 ) -> Result<Vec<Self>, LockError> {
4757 reg_dist
4758 .wheels
4759 .iter()
4760 .filter(|wheel| {
4761 index.is_some_and(|index| *index == wheel.index)
4764 })
4765 .map(Self::from_registry_wheel)
4766 .collect()
4767 }
4768
4769 fn from_registry_wheel(wheel: &RegistryBuiltWheel) -> Result<Self, LockError> {
4770 let url = match &wheel.index {
4771 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4772 let url = normalize_file_location(&wheel.file.url)
4773 .map_err(LockErrorKind::InvalidUrl)
4774 .map_err(LockError::from)?;
4775 WheelWireSource::Url { url }
4776 }
4777 IndexUrl::Path(path) => {
4778 let index_path = path
4779 .to_file_path()
4780 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4781 let wheel_url = wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?;
4782
4783 if wheel_url.scheme() == "file" {
4784 let wheel_path = wheel_url
4785 .to_file_path()
4786 .map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?;
4787 let path =
4788 try_relative_to_if(&wheel_path, index_path, !path.was_given_absolute())
4789 .map_err(LockErrorKind::DistributionRelativePath)?
4790 .into_boxed_path();
4791 WheelWireSource::Path { path }
4792 } else {
4793 let url = normalize_file_location(&wheel.file.url)
4794 .map_err(LockErrorKind::InvalidUrl)
4795 .map_err(LockError::from)?;
4796 WheelWireSource::Url { url }
4797 }
4798 }
4799 };
4800 let filename = wheel.filename.clone();
4801 let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from);
4802 let size = wheel.file.size;
4803 let upload_time = wheel
4804 .file
4805 .upload_time_utc_ms
4806 .map(Timestamp::from_millisecond)
4807 .transpose()
4808 .map_err(LockErrorKind::InvalidTimestamp)?;
4809 let zstd = wheel.file.zstd.as_ref().map(|zstd| ZstdWheel {
4810 hash: zstd.hashes.iter().max().cloned().map(Hash::from),
4811 size: zstd.size,
4812 });
4813 Ok(Self {
4814 url,
4815 hash,
4816 size,
4817 upload_time,
4818 filename,
4819 zstd,
4820 })
4821 }
4822
4823 fn from_direct_dist(direct_dist: &DirectUrlBuiltDist, hashes: &[HashDigest]) -> Self {
4824 Self {
4825 url: WheelWireSource::Url {
4826 url: normalize_url(direct_dist.url.to_url()),
4827 },
4828 hash: hashes.iter().max().cloned().map(Hash::from),
4829 size: None,
4830 upload_time: None,
4831 filename: direct_dist.filename.clone(),
4832 zstd: None,
4833 }
4834 }
4835
4836 fn from_path_dist(path_dist: &PathBuiltDist, hashes: &[HashDigest]) -> Self {
4837 Self {
4838 url: WheelWireSource::Filename {
4839 filename: path_dist.filename.clone(),
4840 },
4841 hash: hashes.iter().max().cloned().map(Hash::from),
4842 size: None,
4843 upload_time: None,
4844 filename: path_dist.filename.clone(),
4845 zstd: None,
4846 }
4847 }
4848
4849 pub(crate) fn to_registry_wheel(
4850 &self,
4851 source: &RegistrySource,
4852 root: &Path,
4853 ) -> Result<RegistryBuiltWheel, LockError> {
4854 let filename: WheelFilename = self.filename.clone();
4855
4856 match source {
4857 RegistrySource::Url(url) => {
4858 let file_location = match &self.url {
4859 WheelWireSource::Url { url: file_url } => {
4860 FileLocation::AbsoluteUrl(file_url.clone())
4861 }
4862 WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => {
4863 return Err(LockErrorKind::MissingUrl {
4864 name: filename.name,
4865 version: filename.version,
4866 }
4867 .into());
4868 }
4869 };
4870 let file = Box::new(uv_distribution_types::File {
4871 dist_info_metadata: false,
4872 filename: SmallString::from(filename.to_string()),
4873 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
4874 requires_python: None,
4875 size: self.size,
4876 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
4877 url: file_location,
4878 yanked: None,
4879 zstd: self
4880 .zstd
4881 .as_ref()
4882 .map(|zstd| uv_distribution_types::Zstd {
4883 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
4884 size: zstd.size,
4885 })
4886 .map(Box::new),
4887 });
4888 let index = IndexUrl::from(VerbatimUrl::from_url(
4889 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
4890 ));
4891 Ok(RegistryBuiltWheel {
4892 filename,
4893 file,
4894 index,
4895 })
4896 }
4897 RegistrySource::Path(index_path) => {
4898 let file_location = match &self.url {
4899 WheelWireSource::Url { url: file_url } => {
4900 FileLocation::AbsoluteUrl(file_url.clone())
4901 }
4902 WheelWireSource::Path { path: file_path } => {
4903 let file_path = root.join(index_path).join(file_path);
4904 let file_url =
4905 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
4906 LockErrorKind::PathToUrl {
4907 path: file_path.into_boxed_path(),
4908 }
4909 })?;
4910 FileLocation::AbsoluteUrl(UrlString::from(file_url))
4911 }
4912 WheelWireSource::Filename { .. } => {
4913 return Err(LockErrorKind::MissingPath {
4914 name: filename.name,
4915 version: filename.version,
4916 }
4917 .into());
4918 }
4919 };
4920 let file = Box::new(uv_distribution_types::File {
4921 dist_info_metadata: false,
4922 filename: SmallString::from(filename.to_string()),
4923 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
4924 requires_python: None,
4925 size: self.size,
4926 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
4927 url: file_location,
4928 yanked: None,
4929 zstd: self
4930 .zstd
4931 .as_ref()
4932 .map(|zstd| uv_distribution_types::Zstd {
4933 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
4934 size: zstd.size,
4935 })
4936 .map(Box::new),
4937 });
4938 let index = IndexUrl::from(
4939 VerbatimUrl::from_absolute_path(root.join(index_path))
4940 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
4941 );
4942 Ok(RegistryBuiltWheel {
4943 filename,
4944 file,
4945 index,
4946 })
4947 }
4948 }
4949 }
4950}
4951
4952#[derive(Clone, Debug, serde::Deserialize)]
4953#[serde(rename_all = "kebab-case")]
4954struct WheelWire {
4955 #[serde(flatten)]
4956 url: WheelWireSource,
4957 hash: Option<Hash>,
4963 size: Option<u64>,
4967 #[serde(alias = "upload_time")]
4971 upload_time: Option<Timestamp>,
4972 #[serde(alias = "zstd")]
4974 zstd: Option<ZstdWheel>,
4975}
4976
4977#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4978#[serde(untagged, rename_all = "kebab-case")]
4979enum WheelWireSource {
4980 Url {
4982 url: UrlString,
4987 },
4988 Path {
4990 path: Box<Path>,
4992 },
4993 Filename {
4997 filename: WheelFilename,
5000 },
5001}
5002
5003impl Wheel {
5004 fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
5006 let mut table = InlineTable::new();
5007 match &self.url {
5008 WheelWireSource::Url { url } => {
5009 table.insert("url", Value::from(url.as_ref()));
5010 }
5011 WheelWireSource::Path { path } => {
5012 table.insert("path", Value::from(PortablePath::from(path).to_string()));
5013 }
5014 WheelWireSource::Filename { filename } => {
5015 table.insert("filename", Value::from(filename.to_string()));
5016 }
5017 }
5018 if let Some(ref hash) = self.hash {
5019 table.insert("hash", Value::from(hash.to_string()));
5020 }
5021 if let Some(size) = self.size {
5022 table.insert(
5023 "size",
5024 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
5025 );
5026 }
5027 if let Some(upload_time) = self.upload_time {
5028 table.insert("upload-time", Value::from(upload_time.to_string()));
5029 }
5030 if let Some(zstd) = &self.zstd {
5031 let mut inner = InlineTable::new();
5032 if let Some(ref hash) = zstd.hash {
5033 inner.insert("hash", Value::from(hash.to_string()));
5034 }
5035 if let Some(size) = zstd.size {
5036 inner.insert(
5037 "size",
5038 toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
5039 );
5040 }
5041 table.insert("zstd", Value::from(inner));
5042 }
5043 Ok(table)
5044 }
5045}
5046
5047impl TryFrom<WheelWire> for Wheel {
5048 type Error = String;
5049
5050 fn try_from(wire: WheelWire) -> Result<Self, String> {
5051 let filename = match &wire.url {
5052 WheelWireSource::Url { url } => {
5053 let filename = url.filename().map_err(|err| err.to_string())?;
5054 filename.parse::<WheelFilename>().map_err(|err| {
5055 format!("failed to parse `{filename}` as wheel filename: {err}")
5056 })?
5057 }
5058 WheelWireSource::Path { path } => {
5059 let filename = path
5060 .file_name()
5061 .and_then(|file_name| file_name.to_str())
5062 .ok_or_else(|| {
5063 format!("path `{}` has no filename component", path.display())
5064 })?;
5065 filename.parse::<WheelFilename>().map_err(|err| {
5066 format!("failed to parse `{filename}` as wheel filename: {err}")
5067 })?
5068 }
5069 WheelWireSource::Filename { filename } => filename.clone(),
5070 };
5071
5072 Ok(Self {
5073 url: wire.url,
5074 hash: wire.hash,
5075 size: wire.size,
5076 upload_time: wire.upload_time,
5077 zstd: wire.zstd,
5078 filename,
5079 })
5080 }
5081}
5082
5083#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
5085pub struct Dependency {
5086 package_id: PackageId,
5087 extra: BTreeSet<ExtraName>,
5088 simplified_marker: SimplifiedMarkerTree,
5108 complexified_marker: UniversalMarker,
5112}
5113
5114impl Dependency {
5115 fn new(
5116 requires_python: &RequiresPython,
5117 package_id: PackageId,
5118 extra: BTreeSet<ExtraName>,
5119 complexified_marker: UniversalMarker,
5120 ) -> Self {
5121 let simplified_marker =
5122 SimplifiedMarkerTree::new(requires_python, complexified_marker.combined());
5123 let complexified_marker = simplified_marker.into_marker(requires_python);
5124 Self {
5125 package_id,
5126 extra,
5127 simplified_marker,
5128 complexified_marker: UniversalMarker::from_combined(complexified_marker),
5129 }
5130 }
5131
5132 fn from_annotated_dist(
5133 requires_python: &RequiresPython,
5134 annotated_dist: &AnnotatedDist,
5135 complexified_marker: UniversalMarker,
5136 root: &Path,
5137 ) -> Result<Self, LockError> {
5138 let package_id = PackageId::from_annotated_dist(annotated_dist, root)?;
5139 let extra = annotated_dist.extra.iter().cloned().collect();
5140 Ok(Self::new(
5141 requires_python,
5142 package_id,
5143 extra,
5144 complexified_marker,
5145 ))
5146 }
5147
5148 fn to_toml(
5150 &self,
5151 _requires_python: &RequiresPython,
5152 dist_count_by_name: &FxHashMap<PackageName, u64>,
5153 ) -> Table {
5154 let mut table = Table::new();
5155 self.package_id
5156 .to_toml(Some(dist_count_by_name), &mut table);
5157 if !self.extra.is_empty() {
5158 let extra_array = self
5159 .extra
5160 .iter()
5161 .map(ToString::to_string)
5162 .collect::<Array>();
5163 table.insert("extra", value(extra_array));
5164 }
5165 if let Some(marker) = self.simplified_marker.try_to_string() {
5166 table.insert("marker", value(marker));
5167 }
5168
5169 table
5170 }
5171
5172 pub fn package_name(&self) -> &PackageName {
5174 &self.package_id.name
5175 }
5176
5177 pub fn extra(&self) -> &BTreeSet<ExtraName> {
5179 &self.extra
5180 }
5181}
5182
5183impl Display for Dependency {
5184 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5185 match (self.extra.is_empty(), self.package_id.version.as_ref()) {
5186 (true, Some(version)) => write!(f, "{}=={}", self.package_id.name, version),
5187 (true, None) => write!(f, "{}", self.package_id.name),
5188 (false, Some(version)) => write!(
5189 f,
5190 "{}[{}]=={}",
5191 self.package_id.name,
5192 self.extra.iter().join(","),
5193 version
5194 ),
5195 (false, None) => write!(
5196 f,
5197 "{}[{}]",
5198 self.package_id.name,
5199 self.extra.iter().join(",")
5200 ),
5201 }
5202 }
5203}
5204
5205#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)]
5207#[serde(rename_all = "kebab-case")]
5208struct DependencyWire {
5209 #[serde(flatten)]
5210 package_id: PackageIdForDependency,
5211 #[serde(default)]
5212 extra: BTreeSet<ExtraName>,
5213 #[serde(default)]
5214 marker: SimplifiedMarkerTree,
5215}
5216
5217impl DependencyWire {
5218 fn unwire(
5219 self,
5220 requires_python: &RequiresPython,
5221 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
5222 ) -> Result<Dependency, LockError> {
5223 let complexified_marker = self.marker.into_marker(requires_python);
5224 Ok(Dependency {
5225 package_id: self.package_id.unwire(unambiguous_package_ids)?,
5226 extra: self.extra,
5227 simplified_marker: self.marker,
5228 complexified_marker: UniversalMarker::from_combined(complexified_marker),
5229 })
5230 }
5231}
5232
5233#[derive(Clone, Debug, PartialEq, Eq)]
5238struct Hash(HashDigest);
5239
5240impl From<HashDigest> for Hash {
5241 fn from(hd: HashDigest) -> Self {
5242 Self(hd)
5243 }
5244}
5245
5246impl FromStr for Hash {
5247 type Err = HashParseError;
5248
5249 fn from_str(s: &str) -> Result<Self, HashParseError> {
5250 let (algorithm, digest) = s.split_once(':').ok_or(HashParseError(
5251 "expected '{algorithm}:{digest}', but found no ':' in hash digest",
5252 ))?;
5253 let algorithm = algorithm
5254 .parse()
5255 .map_err(|_| HashParseError("unrecognized hash algorithm"))?;
5256 Ok(Self(HashDigest {
5257 algorithm,
5258 digest: digest.into(),
5259 }))
5260 }
5261}
5262
5263impl Display for Hash {
5264 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5265 write!(f, "{}:{}", self.0.algorithm, self.0.digest)
5266 }
5267}
5268
5269impl<'de> serde::Deserialize<'de> for Hash {
5270 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5271 where
5272 D: serde::de::Deserializer<'de>,
5273 {
5274 struct Visitor;
5275
5276 impl serde::de::Visitor<'_> for Visitor {
5277 type Value = Hash;
5278
5279 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
5280 f.write_str("a string")
5281 }
5282
5283 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
5284 Hash::from_str(v).map_err(serde::de::Error::custom)
5285 }
5286 }
5287
5288 deserializer.deserialize_str(Visitor)
5289 }
5290}
5291
5292impl From<Hash> for Hashes {
5293 fn from(value: Hash) -> Self {
5294 match value.0.algorithm {
5295 HashAlgorithm::Md5 => Self {
5296 md5: Some(value.0.digest),
5297 sha256: None,
5298 sha384: None,
5299 sha512: None,
5300 blake2b: None,
5301 },
5302 HashAlgorithm::Sha256 => Self {
5303 md5: None,
5304 sha256: Some(value.0.digest),
5305 sha384: None,
5306 sha512: None,
5307 blake2b: None,
5308 },
5309 HashAlgorithm::Sha384 => Self {
5310 md5: None,
5311 sha256: None,
5312 sha384: Some(value.0.digest),
5313 sha512: None,
5314 blake2b: None,
5315 },
5316 HashAlgorithm::Sha512 => Self {
5317 md5: None,
5318 sha256: None,
5319 sha384: None,
5320 sha512: Some(value.0.digest),
5321 blake2b: None,
5322 },
5323 HashAlgorithm::Blake2b => Self {
5324 md5: None,
5325 sha256: None,
5326 sha384: None,
5327 sha512: None,
5328 blake2b: Some(value.0.digest),
5329 },
5330 }
5331 }
5332}
5333
5334fn normalize_file_location(location: &FileLocation) -> Result<UrlString, ToUrlError> {
5336 match location {
5337 FileLocation::AbsoluteUrl(absolute) => Ok(absolute.without_fragment().into_owned()),
5338 FileLocation::RelativeUrl(_, _) => Ok(normalize_url(location.to_url()?)),
5339 }
5340}
5341
5342fn normalize_url(mut url: DisplaySafeUrl) -> UrlString {
5344 url.set_fragment(None);
5345 UrlString::from(url)
5346}
5347
5348fn normalize_requirement(
5358 mut requirement: Requirement,
5359 root: &Path,
5360 requires_python: &RequiresPython,
5361) -> Result<Requirement, LockError> {
5362 requirement.extras.sort();
5364 requirement.groups.sort();
5365
5366 match requirement.source {
5368 RequirementSource::Git {
5369 git,
5370 subdirectory,
5371 url: _,
5372 } => {
5373 let git = {
5375 let mut repository = git.url().clone();
5376
5377 repository.remove_credentials();
5379
5380 repository.set_fragment(None);
5382 repository.set_query(None);
5383
5384 GitUrl::from_fields(
5385 repository,
5386 git.reference().clone(),
5387 git.precise(),
5388 git.lfs(),
5389 )?
5390 };
5391
5392 let url = DisplaySafeUrl::from(ParsedGitUrl {
5394 url: git.clone(),
5395 subdirectory: subdirectory.clone(),
5396 });
5397
5398 Ok(Requirement {
5399 name: requirement.name,
5400 extras: requirement.extras,
5401 groups: requirement.groups,
5402 marker: requires_python.simplify_markers(requirement.marker),
5403 source: RequirementSource::Git {
5404 git,
5405 subdirectory,
5406 url: VerbatimUrl::from_url(url),
5407 },
5408 origin: None,
5409 })
5410 }
5411 RequirementSource::Path {
5412 install_path,
5413 ext,
5414 url: _,
5415 } => {
5416 let path = root.join(&install_path);
5417 let install_path = normalize_path(path).into_owned().into_boxed_path();
5418 let url = VerbatimUrl::from_normalized_path(&install_path)
5419 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5420
5421 Ok(Requirement {
5422 name: requirement.name,
5423 extras: requirement.extras,
5424 groups: requirement.groups,
5425 marker: requires_python.simplify_markers(requirement.marker),
5426 source: RequirementSource::Path {
5427 install_path,
5428 ext,
5429 url,
5430 },
5431 origin: None,
5432 })
5433 }
5434 RequirementSource::Directory {
5435 install_path,
5436 editable,
5437 r#virtual,
5438 url: _,
5439 } => {
5440 let path = root.join(&install_path);
5441 let install_path = normalize_path(path).into_owned().into_boxed_path();
5442 let url = VerbatimUrl::from_normalized_path(&install_path)
5443 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5444
5445 Ok(Requirement {
5446 name: requirement.name,
5447 extras: requirement.extras,
5448 groups: requirement.groups,
5449 marker: requires_python.simplify_markers(requirement.marker),
5450 source: RequirementSource::Directory {
5451 install_path,
5452 editable: Some(editable.unwrap_or(false)),
5453 r#virtual: Some(r#virtual.unwrap_or(false)),
5454 url,
5455 },
5456 origin: None,
5457 })
5458 }
5459 RequirementSource::Registry {
5460 specifier,
5461 index,
5462 conflict,
5463 } => {
5464 let index = index
5466 .map(|index| index.url.into_url())
5467 .map(|mut index| {
5468 index.remove_credentials();
5469 index
5470 })
5471 .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index))));
5472 Ok(Requirement {
5473 name: requirement.name,
5474 extras: requirement.extras,
5475 groups: requirement.groups,
5476 marker: requires_python.simplify_markers(requirement.marker),
5477 source: RequirementSource::Registry {
5478 specifier,
5479 index,
5480 conflict,
5481 },
5482 origin: None,
5483 })
5484 }
5485 RequirementSource::Url {
5486 mut location,
5487 subdirectory,
5488 ext,
5489 url: _,
5490 } => {
5491 location.remove_credentials();
5493
5494 location.set_fragment(None);
5496
5497 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
5499 url: location.clone(),
5500 subdirectory: subdirectory.clone(),
5501 ext,
5502 });
5503
5504 Ok(Requirement {
5505 name: requirement.name,
5506 extras: requirement.extras,
5507 groups: requirement.groups,
5508 marker: requires_python.simplify_markers(requirement.marker),
5509 source: RequirementSource::Url {
5510 location,
5511 subdirectory,
5512 ext,
5513 url: VerbatimUrl::from_url(url),
5514 },
5515 origin: None,
5516 })
5517 }
5518 }
5519}
5520
5521#[derive(Debug)]
5522pub struct LockError {
5523 kind: Box<LockErrorKind>,
5524 hint: Option<WheelTagHint>,
5525}
5526
5527impl std::error::Error for LockError {
5528 fn source(&self) -> Option<&(dyn Error + 'static)> {
5529 self.kind.source()
5530 }
5531}
5532
5533impl std::fmt::Display for LockError {
5534 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5535 write!(f, "{}", self.kind)?;
5536 if let Some(hint) = &self.hint {
5537 write!(f, "\n\n{hint}")?;
5538 }
5539 Ok(())
5540 }
5541}
5542
5543impl LockError {
5544 pub fn is_resolution(&self) -> bool {
5546 matches!(&*self.kind, LockErrorKind::Resolution { .. })
5547 }
5548}
5549
5550impl<E> From<E> for LockError
5551where
5552 LockErrorKind: From<E>,
5553{
5554 fn from(err: E) -> Self {
5555 Self {
5556 kind: Box::new(LockErrorKind::from(err)),
5557 hint: None,
5558 }
5559 }
5560}
5561
5562#[derive(Debug, Clone, PartialEq, Eq)]
5563#[expect(clippy::enum_variant_names)]
5564enum WheelTagHint {
5565 LanguageTags {
5568 package: PackageName,
5569 version: Option<Version>,
5570 tags: BTreeSet<LanguageTag>,
5571 best: Option<LanguageTag>,
5572 },
5573 AbiTags {
5576 package: PackageName,
5577 version: Option<Version>,
5578 tags: BTreeSet<AbiTag>,
5579 best: Option<AbiTag>,
5580 },
5581 PlatformTags {
5584 package: PackageName,
5585 version: Option<Version>,
5586 tags: BTreeSet<PlatformTag>,
5587 best: Option<PlatformTag>,
5588 markers: MarkerEnvironment,
5589 },
5590}
5591
5592impl WheelTagHint {
5593 fn from_wheels(
5595 name: &PackageName,
5596 version: Option<&Version>,
5597 filenames: &[&WheelFilename],
5598 tags: &Tags,
5599 markers: &MarkerEnvironment,
5600 ) -> Option<Self> {
5601 let incompatibility = filenames
5602 .iter()
5603 .map(|filename| {
5604 tags.compatibility(
5605 filename.python_tags(),
5606 filename.abi_tags(),
5607 filename.platform_tags(),
5608 )
5609 })
5610 .max()?;
5611 match incompatibility {
5612 TagCompatibility::Incompatible(IncompatibleTag::Python) => {
5613 let best = tags.python_tag();
5614 let tags = Self::python_tags(filenames.iter().copied()).collect::<BTreeSet<_>>();
5615 if tags.is_empty() {
5616 None
5617 } else {
5618 Some(Self::LanguageTags {
5619 package: name.clone(),
5620 version: version.cloned(),
5621 tags,
5622 best,
5623 })
5624 }
5625 }
5626 TagCompatibility::Incompatible(IncompatibleTag::Abi) => {
5627 let best = tags.abi_tag();
5628 let tags = Self::abi_tags(filenames.iter().copied())
5629 .filter(|tag| *tag != AbiTag::None)
5638 .collect::<BTreeSet<_>>();
5639 if tags.is_empty() {
5640 None
5641 } else {
5642 Some(Self::AbiTags {
5643 package: name.clone(),
5644 version: version.cloned(),
5645 tags,
5646 best,
5647 })
5648 }
5649 }
5650 TagCompatibility::Incompatible(IncompatibleTag::Platform) => {
5651 let best = tags.platform_tag().cloned();
5652 let incompatible_tags = Self::platform_tags(filenames.iter().copied(), tags)
5653 .cloned()
5654 .collect::<BTreeSet<_>>();
5655 if incompatible_tags.is_empty() {
5656 None
5657 } else {
5658 Some(Self::PlatformTags {
5659 package: name.clone(),
5660 version: version.cloned(),
5661 tags: incompatible_tags,
5662 best,
5663 markers: markers.clone(),
5664 })
5665 }
5666 }
5667 _ => None,
5668 }
5669 }
5670
5671 fn python_tags<'a>(
5673 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5674 ) -> impl Iterator<Item = LanguageTag> + 'a {
5675 filenames.flat_map(WheelFilename::python_tags).copied()
5676 }
5677
5678 fn abi_tags<'a>(
5680 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5681 ) -> impl Iterator<Item = AbiTag> + 'a {
5682 filenames.flat_map(WheelFilename::abi_tags).copied()
5683 }
5684
5685 fn platform_tags<'a>(
5688 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5689 tags: &'a Tags,
5690 ) -> impl Iterator<Item = &'a PlatformTag> + 'a {
5691 filenames.flat_map(move |filename| {
5692 if filename.python_tags().iter().any(|wheel_py| {
5693 filename
5694 .abi_tags()
5695 .iter()
5696 .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
5697 }) {
5698 filename.platform_tags().iter()
5699 } else {
5700 [].iter()
5701 }
5702 })
5703 }
5704
5705 fn suggest_environment_marker(markers: &MarkerEnvironment) -> String {
5706 let sys_platform = markers.sys_platform();
5707 let platform_machine = markers.platform_machine();
5708
5709 if platform_machine.is_empty() {
5711 format!("sys_platform == '{sys_platform}'")
5712 } else {
5713 format!("sys_platform == '{sys_platform}' and platform_machine == '{platform_machine}'")
5714 }
5715 }
5716}
5717
5718impl std::fmt::Display for WheelTagHint {
5719 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5720 match self {
5721 Self::LanguageTags {
5722 package,
5723 version,
5724 tags,
5725 best,
5726 } => {
5727 if let Some(best) = best {
5728 let s = if tags.len() == 1 { "" } else { "s" };
5729 let best = if let Some(pretty) = best.pretty() {
5730 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5731 } else {
5732 format!("{}", best.cyan())
5733 };
5734 if let Some(version) = version {
5735 write!(
5736 f,
5737 "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python implementation tag{s}: {}",
5738 "hint".bold().cyan(),
5739 ":".bold(),
5740 best,
5741 package.cyan(),
5742 format!("v{version}").cyan(),
5743 tags.iter()
5744 .map(|tag| format!("`{}`", tag.cyan()))
5745 .join(", "),
5746 )
5747 } else {
5748 write!(
5749 f,
5750 "{}{} You're using {}, but `{}` only has wheels with the following Python implementation tag{s}: {}",
5751 "hint".bold().cyan(),
5752 ":".bold(),
5753 best,
5754 package.cyan(),
5755 tags.iter()
5756 .map(|tag| format!("`{}`", tag.cyan()))
5757 .join(", "),
5758 )
5759 }
5760 } else {
5761 let s = if tags.len() == 1 { "" } else { "s" };
5762 if let Some(version) = version {
5763 write!(
5764 f,
5765 "{}{} Wheels are available for `{}` ({}) with the following Python implementation tag{s}: {}",
5766 "hint".bold().cyan(),
5767 ":".bold(),
5768 package.cyan(),
5769 format!("v{version}").cyan(),
5770 tags.iter()
5771 .map(|tag| format!("`{}`", tag.cyan()))
5772 .join(", "),
5773 )
5774 } else {
5775 write!(
5776 f,
5777 "{}{} Wheels are available for `{}` with the following Python implementation tag{s}: {}",
5778 "hint".bold().cyan(),
5779 ":".bold(),
5780 package.cyan(),
5781 tags.iter()
5782 .map(|tag| format!("`{}`", tag.cyan()))
5783 .join(", "),
5784 )
5785 }
5786 }
5787 }
5788 Self::AbiTags {
5789 package,
5790 version,
5791 tags,
5792 best,
5793 } => {
5794 if let Some(best) = best {
5795 let s = if tags.len() == 1 { "" } else { "s" };
5796 let best = if let Some(pretty) = best.pretty() {
5797 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5798 } else {
5799 format!("{}", best.cyan())
5800 };
5801 if let Some(version) = version {
5802 write!(
5803 f,
5804 "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python ABI tag{s}: {}",
5805 "hint".bold().cyan(),
5806 ":".bold(),
5807 best,
5808 package.cyan(),
5809 format!("v{version}").cyan(),
5810 tags.iter()
5811 .map(|tag| format!("`{}`", tag.cyan()))
5812 .join(", "),
5813 )
5814 } else {
5815 write!(
5816 f,
5817 "{}{} You're using {}, but `{}` only has wheels with the following Python ABI tag{s}: {}",
5818 "hint".bold().cyan(),
5819 ":".bold(),
5820 best,
5821 package.cyan(),
5822 tags.iter()
5823 .map(|tag| format!("`{}`", tag.cyan()))
5824 .join(", "),
5825 )
5826 }
5827 } else {
5828 let s = if tags.len() == 1 { "" } else { "s" };
5829 if let Some(version) = version {
5830 write!(
5831 f,
5832 "{}{} Wheels are available for `{}` ({}) with the following Python ABI tag{s}: {}",
5833 "hint".bold().cyan(),
5834 ":".bold(),
5835 package.cyan(),
5836 format!("v{version}").cyan(),
5837 tags.iter()
5838 .map(|tag| format!("`{}`", tag.cyan()))
5839 .join(", "),
5840 )
5841 } else {
5842 write!(
5843 f,
5844 "{}{} Wheels are available for `{}` with the following Python ABI tag{s}: {}",
5845 "hint".bold().cyan(),
5846 ":".bold(),
5847 package.cyan(),
5848 tags.iter()
5849 .map(|tag| format!("`{}`", tag.cyan()))
5850 .join(", "),
5851 )
5852 }
5853 }
5854 }
5855 Self::PlatformTags {
5856 package,
5857 version,
5858 tags,
5859 best,
5860 markers,
5861 } => {
5862 let s = if tags.len() == 1 { "" } else { "s" };
5863 if let Some(best) = best {
5864 let example_marker = Self::suggest_environment_marker(markers);
5865 let best = if let Some(pretty) = best.pretty() {
5866 format!("{} (`{}`)", pretty.cyan(), best.cyan())
5867 } else {
5868 format!("`{}`", best.cyan())
5869 };
5870 let package_ref = if let Some(version) = version {
5871 format!("`{}` ({})", package.cyan(), format!("v{version}").cyan())
5872 } else {
5873 format!("`{}`", package.cyan())
5874 };
5875 write!(
5876 f,
5877 "{}{} 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",
5878 "hint".bold().cyan(),
5879 ":".bold(),
5880 best,
5881 package_ref,
5882 tags.iter()
5883 .map(|tag| format!("`{}`", tag.cyan()))
5884 .join(", "),
5885 format!("\"{example_marker}\"").cyan(),
5886 "tool.uv.required-environments".green()
5887 )
5888 } else {
5889 if let Some(version) = version {
5890 write!(
5891 f,
5892 "{}{} Wheels are available for `{}` ({}) on the following platform{s}: {}",
5893 "hint".bold().cyan(),
5894 ":".bold(),
5895 package.cyan(),
5896 format!("v{version}").cyan(),
5897 tags.iter()
5898 .map(|tag| format!("`{}`", tag.cyan()))
5899 .join(", "),
5900 )
5901 } else {
5902 write!(
5903 f,
5904 "{}{} Wheels are available for `{}` on the following platform{s}: {}",
5905 "hint".bold().cyan(),
5906 ":".bold(),
5907 package.cyan(),
5908 tags.iter()
5909 .map(|tag| format!("`{}`", tag.cyan()))
5910 .join(", "),
5911 )
5912 }
5913 }
5914 }
5915 }
5916 }
5917}
5918
5919#[derive(Debug, thiserror::Error)]
5926enum LockErrorKind {
5927 #[error("Found duplicate package `{id}`", id = id.cyan())]
5930 DuplicatePackage {
5931 id: PackageId,
5933 },
5934 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = id.cyan(), dependency = dependency.cyan())]
5937 DuplicateDependency {
5938 id: PackageId,
5941 dependency: Dependency,
5943 },
5944 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}[{extra}]").cyan(), dependency = dependency.cyan())]
5948 DuplicateOptionalDependency {
5949 id: PackageId,
5952 extra: ExtraName,
5954 dependency: Dependency,
5956 },
5957 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}:{group}").cyan(), dependency = dependency.cyan())]
5961 DuplicateDevDependency {
5962 id: PackageId,
5965 group: GroupName,
5967 dependency: Dependency,
5969 },
5970 #[error(transparent)]
5973 InvalidUrl(
5974 #[from]
5977 ToUrlError,
5978 ),
5979 #[error("Failed to parse file extension for `{id}`; expected one of: {err}", id = id.cyan())]
5982 MissingExtension {
5983 id: PackageId,
5985 err: ExtensionError,
5987 },
5988 #[error("Failed to parse Git URL")]
5990 InvalidGitSourceUrl(
5991 #[source]
5994 SourceParseError,
5995 ),
5996 #[error("Failed to parse timestamp")]
5997 InvalidTimestamp(
5998 #[source]
6001 jiff::Error,
6002 ),
6003 #[error("For package `{id}`, found dependency `{dependency}` with no locked package", id = id.cyan(), dependency = dependency.cyan())]
6007 UnrecognizedDependency {
6008 id: PackageId,
6010 dependency: Dependency,
6013 },
6014 #[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" })]
6017 Hash {
6018 id: PackageId,
6020 artifact_type: &'static str,
6023 expected: bool,
6025 },
6026 #[error("Found package `{id}` with extra `{extra}` but no base package", id = id.cyan(), extra = extra.cyan())]
6029 MissingExtraBase {
6030 id: PackageId,
6032 extra: ExtraName,
6034 },
6035 #[error("Found package `{id}` with development dependency group `{group}` but no base package", id = id.cyan())]
6039 MissingDevBase {
6040 id: PackageId,
6042 group: GroupName,
6044 },
6045 #[error("Wheels cannot come from {source_type} sources")]
6048 InvalidWheelSource {
6049 id: PackageId,
6051 source_type: &'static str,
6053 },
6054 #[error("Found registry distribution `{name}` ({version}) without a valid URL", name = name.cyan(), version = format!("v{version}").cyan())]
6057 MissingUrl {
6058 name: PackageName,
6060 version: Version,
6062 },
6063 #[error("Found registry distribution `{name}` ({version}) without a valid path", name = name.cyan(), version = format!("v{version}").cyan())]
6066 MissingPath {
6067 name: PackageName,
6069 version: Version,
6071 },
6072 #[error("Found registry distribution `{id}` without a valid filename", id = id.cyan())]
6075 MissingFilename {
6076 id: PackageId,
6078 },
6079 #[error("Distribution `{id}` can't be installed because it doesn't have a source distribution or wheel for the current platform", id = id.cyan())]
6082 NeitherSourceDistNorWheel {
6083 id: PackageId,
6085 },
6086 #[error("Distribution `{id}` can't be installed because it is marked as both `--no-binary` and `--no-build`", id = id.cyan())]
6088 NoBinaryNoBuild {
6089 id: PackageId,
6091 },
6092 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but has no source distribution", id = id.cyan())]
6095 NoBinary {
6096 id: PackageId,
6098 },
6099 #[error("Distribution `{id}` can't be installed because it is marked as `--no-build` but has no binary distribution", id = id.cyan())]
6102 NoBuild {
6103 id: PackageId,
6105 },
6106 #[error("Distribution `{id}` can't be installed because the binary distribution is incompatible with the current platform", id = id.cyan())]
6109 IncompatibleWheelOnly {
6110 id: PackageId,
6112 },
6113 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but is itself a binary distribution", id = id.cyan())]
6115 NoBinaryWheelOnly {
6116 id: PackageId,
6118 },
6119 #[error("Found dependency `{id}` with no locked distribution", id = id.cyan())]
6121 VerbatimUrl {
6122 id: PackageId,
6124 #[source]
6126 err: VerbatimUrlError,
6127 },
6128 #[error("Could not compute relative path between workspace and distribution")]
6130 DistributionRelativePath(
6131 #[source]
6133 io::Error,
6134 ),
6135 #[error("Could not compute relative path between workspace and index")]
6137 IndexRelativePath(
6138 #[source]
6140 io::Error,
6141 ),
6142 #[error("Could not compute absolute path from workspace root and lockfile path")]
6144 AbsolutePath(
6145 #[source]
6147 io::Error,
6148 ),
6149 #[error("Dependency `{name}` has missing `version` field but has more than one matching package", name = name.cyan())]
6152 MissingDependencyVersion {
6153 name: PackageName,
6155 },
6156 #[error("Dependency `{name}` has missing `source` field but has more than one matching package", name = name.cyan())]
6159 MissingDependencySource {
6160 name: PackageName,
6162 },
6163 #[error("Could not compute relative path between workspace and requirement")]
6165 RequirementRelativePath(
6166 #[source]
6168 io::Error,
6169 ),
6170 #[error("Could not convert between URL and path")]
6172 RequirementVerbatimUrl(
6173 #[source]
6175 VerbatimUrlError,
6176 ),
6177 #[error("Could not convert between URL and path")]
6179 RegistryVerbatimUrl(
6180 #[source]
6182 VerbatimUrlError,
6183 ),
6184 #[error("Failed to convert path to URL: {path}", path = path.display().cyan())]
6186 PathToUrl { path: Box<Path> },
6187 #[error("Failed to convert URL to path: {url}", url = url.cyan())]
6189 UrlToPath { url: DisplaySafeUrl },
6190 #[error("Found multiple packages matching `{name}`", name = name.cyan())]
6193 MultipleRootPackages {
6194 name: PackageName,
6196 },
6197 #[error("Could not find root package `{name}`", name = name.cyan())]
6199 MissingRootPackage {
6200 name: PackageName,
6202 },
6203 #[error("Failed to generate package metadata for `{id}`", id = id.cyan())]
6205 Resolution {
6206 id: PackageId,
6208 #[source]
6210 err: uv_distribution::Error,
6211 },
6212 #[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())]
6215 InconsistentVersions {
6216 name: PackageName,
6218 version: Version,
6220 wheel: Wheel,
6222 },
6223 #[error(
6224 "Found conflicting extras `{package1}[{extra1}]` \
6225 and `{package2}[{extra2}]` enabled simultaneously"
6226 )]
6227 ConflictingExtra {
6228 package1: PackageName,
6229 extra1: ExtraName,
6230 package2: PackageName,
6231 extra2: ExtraName,
6232 },
6233 #[error(transparent)]
6234 GitUrlParse(#[from] GitUrlParseError),
6235 #[error("Failed to read `{path}`")]
6236 UnreadablePyprojectToml {
6237 path: PathBuf,
6238 #[source]
6239 err: std::io::Error,
6240 },
6241 #[error("Failed to parse `{path}`")]
6242 InvalidPyprojectToml {
6243 path: PathBuf,
6244 #[source]
6245 err: uv_pypi_types::MetadataError,
6246 },
6247 #[error("Workspace member `{id}` has non-local source", id = id.cyan())]
6249 NonLocalWorkspaceMember {
6250 id: PackageId,
6252 },
6253}
6254
6255#[derive(Debug, thiserror::Error)]
6257enum SourceParseError {
6258 #[error("Invalid URL in source `{given}`")]
6260 InvalidUrl {
6261 given: String,
6263 #[source]
6265 err: DisplaySafeUrlError,
6266 },
6267 #[error("Missing SHA in source `{given}`")]
6269 MissingSha {
6270 given: String,
6272 },
6273 #[error("Invalid SHA in source `{given}`")]
6275 InvalidSha {
6276 given: String,
6278 },
6279}
6280
6281#[derive(Clone, Debug, Eq, PartialEq)]
6283struct HashParseError(&'static str);
6284
6285impl std::error::Error for HashParseError {}
6286
6287impl Display for HashParseError {
6288 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
6289 Display::fmt(self.0, f)
6290 }
6291}
6292
6293fn each_element_on_its_line_array(elements: impl Iterator<Item = impl Into<Value>>) -> Array {
6304 let mut array = elements
6305 .map(|item| {
6306 let mut value = item.into();
6307 value.decor_mut().set_prefix("\n ");
6309 value
6310 })
6311 .collect::<Array>();
6312 array.set_trailing_comma(true);
6315 array.set_trailing("\n");
6317 array
6318}
6319
6320fn simplified_universal_markers(
6325 markers: &[UniversalMarker],
6326 requires_python: &RequiresPython,
6327) -> Vec<String> {
6328 canonical_marker_trees(markers, requires_python)
6329 .into_iter()
6330 .filter_map(MarkerTree::try_to_string)
6331 .collect()
6332}
6333
6334fn canonicalize_universal_markers(
6341 markers: &[UniversalMarker],
6342 requires_python: &RequiresPython,
6343) -> Vec<UniversalMarker> {
6344 canonical_marker_trees(markers, requires_python)
6345 .into_iter()
6346 .map(|marker| {
6347 let simplified = SimplifiedMarkerTree::new(requires_python, marker);
6348 UniversalMarker::from_combined(simplified.into_marker(requires_python))
6349 })
6350 .collect()
6351}
6352
6353fn canonical_marker_trees(
6355 markers: &[UniversalMarker],
6356 requires_python: &RequiresPython,
6357) -> Vec<MarkerTree> {
6358 let mut pep508_only = vec![];
6359 let mut seen = FxHashSet::default();
6360 for marker in markers {
6361 let simplified =
6362 SimplifiedMarkerTree::new(requires_python, marker.pep508()).as_simplified_marker_tree();
6363 if seen.insert(simplified) {
6364 pep508_only.push(simplified);
6365 }
6366 }
6367 let any_overlap = pep508_only
6368 .iter()
6369 .tuple_combinations()
6370 .any(|(&marker1, &marker2)| !marker1.is_disjoint(marker2));
6371 let markers = if !any_overlap {
6372 pep508_only
6373 } else {
6374 markers
6375 .iter()
6376 .map(|marker| {
6377 SimplifiedMarkerTree::new(requires_python, marker.combined())
6378 .as_simplified_marker_tree()
6379 })
6380 .collect()
6381 };
6382 markers
6383 .into_iter()
6384 .filter(|marker| !marker.is_true())
6385 .collect()
6386}
6387
6388fn is_wheel_unreachable_for_marker(
6396 filename: &WheelFilename,
6397 requires_python: &RequiresPython,
6398 marker: &UniversalMarker,
6399 tags: Option<&Tags>,
6400) -> bool {
6401 if let Some(tags) = tags
6402 && !filename.compatibility(tags).is_compatible()
6403 {
6404 return true;
6405 }
6406 if !requires_python.matches_wheel_tag(filename) {
6408 return true;
6409 }
6410
6411 let platform_tags = filename.platform_tags();
6420
6421 if platform_tags.iter().all(PlatformTag::is_any) {
6422 return false;
6423 }
6424
6425 if platform_tags.iter().all(PlatformTag::is_linux) {
6426 if platform_tags.iter().all(PlatformTag::is_arm) {
6427 if marker.is_disjoint(*LINUX_ARM_MARKERS) {
6428 return true;
6429 }
6430 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6431 if marker.is_disjoint(*LINUX_X86_64_MARKERS) {
6432 return true;
6433 }
6434 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6435 if marker.is_disjoint(*LINUX_X86_MARKERS) {
6436 return true;
6437 }
6438 } else if platform_tags.iter().all(PlatformTag::is_ppc64le) {
6439 if marker.is_disjoint(*LINUX_PPC64LE_MARKERS) {
6440 return true;
6441 }
6442 } else if platform_tags.iter().all(PlatformTag::is_ppc64) {
6443 if marker.is_disjoint(*LINUX_PPC64_MARKERS) {
6444 return true;
6445 }
6446 } else if platform_tags.iter().all(PlatformTag::is_s390x) {
6447 if marker.is_disjoint(*LINUX_S390X_MARKERS) {
6448 return true;
6449 }
6450 } else if platform_tags.iter().all(PlatformTag::is_riscv64) {
6451 if marker.is_disjoint(*LINUX_RISCV64_MARKERS) {
6452 return true;
6453 }
6454 } else if platform_tags.iter().all(PlatformTag::is_loongarch64) {
6455 if marker.is_disjoint(*LINUX_LOONGARCH64_MARKERS) {
6456 return true;
6457 }
6458 } else if platform_tags.iter().all(PlatformTag::is_armv7l) {
6459 if marker.is_disjoint(*LINUX_ARMV7L_MARKERS) {
6460 return true;
6461 }
6462 } else if platform_tags.iter().all(PlatformTag::is_armv6l) {
6463 if marker.is_disjoint(*LINUX_ARMV6L_MARKERS) {
6464 return true;
6465 }
6466 } else if marker.is_disjoint(*LINUX_MARKERS) {
6467 return true;
6468 }
6469 }
6470
6471 if platform_tags.iter().all(PlatformTag::is_windows) {
6472 if platform_tags.iter().all(PlatformTag::is_arm) {
6473 if marker.is_disjoint(*WINDOWS_ARM_MARKERS) {
6474 return true;
6475 }
6476 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6477 if marker.is_disjoint(*WINDOWS_X86_64_MARKERS) {
6478 return true;
6479 }
6480 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6481 if marker.is_disjoint(*WINDOWS_X86_MARKERS) {
6482 return true;
6483 }
6484 } else if marker.is_disjoint(*WINDOWS_MARKERS) {
6485 return true;
6486 }
6487 }
6488
6489 if platform_tags.iter().all(PlatformTag::is_macos) {
6490 if platform_tags.iter().all(PlatformTag::is_arm) {
6491 if marker.is_disjoint(*MAC_ARM_MARKERS) {
6492 return true;
6493 }
6494 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6495 if marker.is_disjoint(*MAC_X86_64_MARKERS) {
6496 return true;
6497 }
6498 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6499 if marker.is_disjoint(*MAC_X86_MARKERS) {
6500 return true;
6501 }
6502 } else if marker.is_disjoint(*MAC_MARKERS) {
6503 return true;
6504 }
6505 }
6506
6507 if platform_tags.iter().all(PlatformTag::is_android) {
6508 if platform_tags.iter().all(PlatformTag::is_arm) {
6509 if marker.is_disjoint(*ANDROID_ARM_MARKERS) {
6510 return true;
6511 }
6512 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6513 if marker.is_disjoint(*ANDROID_X86_64_MARKERS) {
6514 return true;
6515 }
6516 } else if platform_tags.iter().all(PlatformTag::is_x86) {
6517 if marker.is_disjoint(*ANDROID_X86_MARKERS) {
6518 return true;
6519 }
6520 } else if marker.is_disjoint(*ANDROID_MARKERS) {
6521 return true;
6522 }
6523 }
6524
6525 if platform_tags.iter().all(PlatformTag::is_arm) {
6526 if marker.is_disjoint(*ARM_MARKERS) {
6527 return true;
6528 }
6529 }
6530
6531 if platform_tags.iter().all(PlatformTag::is_x86_64) {
6532 if marker.is_disjoint(*X86_64_MARKERS) {
6533 return true;
6534 }
6535 }
6536
6537 if platform_tags.iter().all(PlatformTag::is_x86) {
6538 if marker.is_disjoint(*X86_MARKERS) {
6539 return true;
6540 }
6541 }
6542
6543 if platform_tags.iter().all(PlatformTag::is_ppc64le) {
6544 if marker.is_disjoint(*PPC64LE_MARKERS) {
6545 return true;
6546 }
6547 }
6548
6549 if platform_tags.iter().all(PlatformTag::is_ppc64) {
6550 if marker.is_disjoint(*PPC64_MARKERS) {
6551 return true;
6552 }
6553 }
6554
6555 if platform_tags.iter().all(PlatformTag::is_s390x) {
6556 if marker.is_disjoint(*S390X_MARKERS) {
6557 return true;
6558 }
6559 }
6560
6561 if platform_tags.iter().all(PlatformTag::is_riscv64) {
6562 if marker.is_disjoint(*RISCV64_MARKERS) {
6563 return true;
6564 }
6565 }
6566
6567 if platform_tags.iter().all(PlatformTag::is_loongarch64) {
6568 if marker.is_disjoint(*LOONGARCH64_MARKERS) {
6569 return true;
6570 }
6571 }
6572
6573 if platform_tags.iter().all(PlatformTag::is_armv7l) {
6574 if marker.is_disjoint(*ARMV7L_MARKERS) {
6575 return true;
6576 }
6577 }
6578
6579 if platform_tags.iter().all(PlatformTag::is_armv6l) {
6580 if marker.is_disjoint(*ARMV6L_MARKERS) {
6581 return true;
6582 }
6583 }
6584
6585 false
6586}
6587
6588pub(crate) fn is_wheel_unreachable(
6589 filename: &WheelFilename,
6590 graph: &ResolverOutput,
6591 requires_python: &RequiresPython,
6592 node_index: NodeIndex,
6593 tags: Option<&Tags>,
6594) -> bool {
6595 is_wheel_unreachable_for_marker(
6596 filename,
6597 requires_python,
6598 graph.graph[node_index].marker(),
6599 tags,
6600 )
6601}
6602
6603#[cfg(test)]
6604mod tests {
6605 use uv_warnings::anstream;
6606
6607 use super::*;
6608
6609 macro_rules! assert_stripped_snapshot {
6611 ($expr:expr, @$snapshot:literal) => {{
6612 let expr = format!("{}", $expr);
6613 let expr = format!("{}", anstream::adapter::strip_str(&expr));
6614 insta::assert_snapshot!(expr, @$snapshot);
6615 }};
6616 }
6617
6618 #[test]
6619 fn missing_dependency_source_unambiguous() {
6620 let data = r#"
6621version = 1
6622requires-python = ">=3.12"
6623
6624[[package]]
6625name = "a"
6626version = "0.1.0"
6627source = { registry = "https://pypi.org/simple" }
6628sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6629
6630[[package]]
6631name = "b"
6632version = "0.1.0"
6633source = { registry = "https://pypi.org/simple" }
6634sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6635
6636[[package.dependencies]]
6637name = "a"
6638version = "0.1.0"
6639"#;
6640 let result: Result<Lock, _> = toml::from_str(data);
6641 insta::assert_debug_snapshot!(result);
6642 }
6643
6644 #[test]
6645 fn missing_dependency_version_unambiguous() {
6646 let data = r#"
6647version = 1
6648requires-python = ">=3.12"
6649
6650[[package]]
6651name = "a"
6652version = "0.1.0"
6653source = { registry = "https://pypi.org/simple" }
6654sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6655
6656[[package]]
6657name = "b"
6658version = "0.1.0"
6659source = { registry = "https://pypi.org/simple" }
6660sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6661
6662[[package.dependencies]]
6663name = "a"
6664source = { registry = "https://pypi.org/simple" }
6665"#;
6666 let result: Result<Lock, _> = toml::from_str(data);
6667 insta::assert_debug_snapshot!(result);
6668 }
6669
6670 #[test]
6671 fn missing_dependency_source_version_unambiguous() {
6672 let data = r#"
6673version = 1
6674requires-python = ">=3.12"
6675
6676[[package]]
6677name = "a"
6678version = "0.1.0"
6679source = { registry = "https://pypi.org/simple" }
6680sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6681
6682[[package]]
6683name = "b"
6684version = "0.1.0"
6685source = { registry = "https://pypi.org/simple" }
6686sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6687
6688[[package.dependencies]]
6689name = "a"
6690"#;
6691 let result: Result<Lock, _> = toml::from_str(data);
6692 insta::assert_debug_snapshot!(result);
6693 }
6694
6695 #[test]
6696 fn missing_dependency_source_ambiguous() {
6697 let data = r#"
6698version = 1
6699requires-python = ">=3.12"
6700
6701[[package]]
6702name = "a"
6703version = "0.1.0"
6704source = { registry = "https://pypi.org/simple" }
6705sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6706
6707[[package]]
6708name = "a"
6709version = "0.1.1"
6710source = { registry = "https://pypi.org/simple" }
6711sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6712
6713[[package]]
6714name = "b"
6715version = "0.1.0"
6716source = { registry = "https://pypi.org/simple" }
6717sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6718
6719[[package.dependencies]]
6720name = "a"
6721version = "0.1.0"
6722"#;
6723 let result = toml::from_str::<Lock>(data).unwrap_err();
6724 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6725 }
6726
6727 #[test]
6728 fn missing_dependency_version_ambiguous() {
6729 let data = r#"
6730version = 1
6731requires-python = ">=3.12"
6732
6733[[package]]
6734name = "a"
6735version = "0.1.0"
6736source = { registry = "https://pypi.org/simple" }
6737sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6738
6739[[package]]
6740name = "a"
6741version = "0.1.1"
6742source = { registry = "https://pypi.org/simple" }
6743sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6744
6745[[package]]
6746name = "b"
6747version = "0.1.0"
6748source = { registry = "https://pypi.org/simple" }
6749sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6750
6751[[package.dependencies]]
6752name = "a"
6753source = { registry = "https://pypi.org/simple" }
6754"#;
6755 let result = toml::from_str::<Lock>(data).unwrap_err();
6756 assert_stripped_snapshot!(result, @"Dependency `a` has missing `version` field but has more than one matching package");
6757 }
6758
6759 #[test]
6760 fn missing_dependency_source_version_ambiguous() {
6761 let data = r#"
6762version = 1
6763requires-python = ">=3.12"
6764
6765[[package]]
6766name = "a"
6767version = "0.1.0"
6768source = { registry = "https://pypi.org/simple" }
6769sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6770
6771[[package]]
6772name = "a"
6773version = "0.1.1"
6774source = { registry = "https://pypi.org/simple" }
6775sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6776
6777[[package]]
6778name = "b"
6779version = "0.1.0"
6780source = { registry = "https://pypi.org/simple" }
6781sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6782
6783[[package.dependencies]]
6784name = "a"
6785"#;
6786 let result = toml::from_str::<Lock>(data).unwrap_err();
6787 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6788 }
6789
6790 #[test]
6791 fn missing_dependency_version_dynamic() {
6792 let data = r#"
6793version = 1
6794requires-python = ">=3.12"
6795
6796[[package]]
6797name = "a"
6798source = { editable = "path/to/a" }
6799
6800[[package]]
6801name = "a"
6802version = "0.1.1"
6803source = { registry = "https://pypi.org/simple" }
6804sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6805
6806[[package]]
6807name = "b"
6808version = "0.1.0"
6809source = { registry = "https://pypi.org/simple" }
6810sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6811
6812[[package.dependencies]]
6813name = "a"
6814source = { editable = "path/to/a" }
6815"#;
6816 let result = toml::from_str::<Lock>(data);
6817 insta::assert_debug_snapshot!(result);
6818 }
6819
6820 #[test]
6821 fn hash_optional_missing() {
6822 let data = r#"
6823version = 1
6824requires-python = ">=3.12"
6825
6826[[package]]
6827name = "anyio"
6828version = "4.3.0"
6829source = { registry = "https://pypi.org/simple" }
6830wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl" }]
6831"#;
6832 let result: Result<Lock, _> = toml::from_str(data);
6833 insta::assert_debug_snapshot!(result);
6834 }
6835
6836 #[test]
6837 fn hash_optional_present() {
6838 let data = r#"
6839version = 1
6840requires-python = ">=3.12"
6841
6842[[package]]
6843name = "anyio"
6844version = "4.3.0"
6845source = { registry = "https://pypi.org/simple" }
6846wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6847"#;
6848 let result: Result<Lock, _> = toml::from_str(data);
6849 insta::assert_debug_snapshot!(result);
6850 }
6851
6852 #[test]
6853 fn hash_required_present() {
6854 let data = r#"
6855version = 1
6856requires-python = ">=3.12"
6857
6858[[package]]
6859name = "anyio"
6860version = "4.3.0"
6861source = { path = "file:///foo/bar" }
6862wheels = [{ url = "file:///foo/bar/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6863"#;
6864 let result: Result<Lock, _> = toml::from_str(data);
6865 insta::assert_debug_snapshot!(result);
6866 }
6867
6868 #[test]
6869 fn source_direct_no_subdir() {
6870 let data = r#"
6871version = 1
6872requires-python = ">=3.12"
6873
6874[[package]]
6875name = "anyio"
6876version = "4.3.0"
6877source = { url = "https://burntsushi.net" }
6878"#;
6879 let result: Result<Lock, _> = toml::from_str(data);
6880 insta::assert_debug_snapshot!(result);
6881 }
6882
6883 #[test]
6884 fn source_direct_has_subdir() {
6885 let data = r#"
6886version = 1
6887requires-python = ">=3.12"
6888
6889[[package]]
6890name = "anyio"
6891version = "4.3.0"
6892source = { url = "https://burntsushi.net", subdirectory = "wat/foo/bar" }
6893"#;
6894 let result: Result<Lock, _> = toml::from_str(data);
6895 insta::assert_debug_snapshot!(result);
6896 }
6897
6898 #[test]
6899 fn source_directory() {
6900 let data = r#"
6901version = 1
6902requires-python = ">=3.12"
6903
6904[[package]]
6905name = "anyio"
6906version = "4.3.0"
6907source = { directory = "path/to/dir" }
6908"#;
6909 let result: Result<Lock, _> = toml::from_str(data);
6910 insta::assert_debug_snapshot!(result);
6911 }
6912
6913 #[test]
6914 fn source_editable() {
6915 let data = r#"
6916version = 1
6917requires-python = ">=3.12"
6918
6919[[package]]
6920name = "anyio"
6921version = "4.3.0"
6922source = { editable = "path/to/dir" }
6923"#;
6924 let result: Result<Lock, _> = toml::from_str(data);
6925 insta::assert_debug_snapshot!(result);
6926 }
6927
6928 #[test]
6931 fn registry_source_windows_drive_letter() {
6932 let data = r#"
6933version = 1
6934requires-python = ">=3.12"
6935
6936[[package]]
6937name = "tqdm"
6938version = "1000.0.0"
6939source = { registry = "C:/Users/user/links" }
6940wheels = [
6941 { path = "C:/Users/user/links/tqdm-1000.0.0-py3-none-any.whl" },
6942]
6943"#;
6944 let lock: Lock = toml::from_str(data).unwrap();
6945 assert_eq!(
6946 lock.packages[0].id.source,
6947 Source::Registry(RegistrySource::Path(
6948 Path::new("C:/Users/user/links").into()
6949 ))
6950 );
6951 }
6952}