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