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