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