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