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