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::iter;
7use std::path::{Path, PathBuf};
8use std::slice;
9use std::str::FromStr;
10use std::sync::{Arc, LazyLock};
11
12use itertools::Itertools;
13use jiff::Timestamp;
14use owo_colors::OwoColorize;
15use petgraph::graph::NodeIndex;
16use petgraph::visit::EdgeRef;
17use rustc_hash::{FxHashMap, FxHashSet};
18use tracing::{debug, instrument, trace};
19use url::Url;
20
21use uv_cache_key::RepositoryUrl;
22use uv_configuration::{
23 BuildOptions, Constraints, DependencyGroupsWithDefaults, ExcludeDependency, Excludes,
24 ExtrasSpecificationWithDefaults, InstallTarget, Override, Overrides, PackageOverride,
25 ScopedOverrideSourceError,
26};
27use uv_distribution::{
28 DistributionDatabase, FlatRequiresDist, Metadata as DistributionMetadata, RequiresDist,
29};
30use uv_distribution_filename::{
31 BuildTag, DistExtension, ExtensionError, SourceDistExtension, WheelFilename,
32};
33use uv_distribution_types::{
34 BuiltDist, DependencyMetadata, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist,
35 Dist, FileLocation, FirstParty, GitDirectorySourceDist, GitPathBuiltDist, GitPathSourceDist,
36 Identifier, IndexLocations, IndexMetadata, IndexUrl, Name, PYPI_URL, PathBuiltDist,
37 PathSourceDist, RegistryBuiltDist, RegistryBuiltWheel, RegistrySourceDist, RemoteSource,
38 Requirement, RequirementSource, RequiresPython, ResolvedDist, SimplifiedMarkerTree,
39 StaticMetadata, ToUrlError, UrlString,
40};
41use uv_fs::{PortablePath, PortablePathBuf, Simplified, normalize_path, try_relative_to_if};
42use uv_git::{RepositoryReference, ResolvedRepositoryReference};
43use uv_git_types::{GitLfs, GitOid, GitReference, GitUrl, GitUrlParseError};
44use uv_normalize::{ExtraName, GroupName, PackageName};
45use uv_pep440::{Version, VersionSpecifiers};
46use uv_pep508::{
47 MarkerEnvironment, MarkerTree, Scheme, VerbatimUrl, VerbatimUrlError, split_scheme,
48};
49use uv_platform_tags::{
50 AbiTag, IncompatibleTag, LanguageTag, PlatformTag, TagCompatibility, TagPriority, Tags,
51};
52use uv_preview::PreviewFeature;
53use uv_pypi_types::{
54 ConflictItem, ConflictKindRef, Conflicts, HashAlgorithm, HashDigest, HashDigests, Hashes,
55 ParsedArchiveUrl, ParsedGitDirectoryUrl, ParsedGitPathUrl, PyProjectToml,
56};
57use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};
58use uv_small_str::SmallString;
59use uv_types::{BuildContext, HashStrategy};
60use uv_warnings::warn_user_once;
61use uv_workspace::{Editability, WorkspaceMember};
62
63use crate::fork_strategy::ForkStrategy;
64pub use crate::lock::deserialize::Error as CanonicalLockError;
65pub(crate) use crate::lock::export::PylockTomlPackage;
66pub use crate::lock::export::RequirementsTxtExport;
67pub use crate::lock::export::{
68 Metadata, PylockToml, PylockTomlError, PylockTomlErrorKind, PythonReport, cyclonedx_json,
69};
70pub use crate::lock::installable::{Installable, InstallableRootKind};
71pub use crate::lock::map::PackageMap;
72pub use crate::lock::tree::{TreeDisplay, TreeJsonTarget};
73use crate::resolution::{AnnotatedDist, ResolutionGraphNode};
74use crate::universal_marker::{ConflictMarker, UniversalMarker};
75use crate::{
76 ExcludeNewer, ExcludeNewerOverride, ExcludeNewerPackage, ExcludeNewerSpan, ExcludeNewerValue,
77 InMemoryIndex, MetadataResponse, Prerelease, PrereleaseMode, PrereleasePackage, ResolutionMode,
78 ResolverOutput,
79};
80
81mod deserialize;
82pub(crate) mod export;
83mod installable;
84mod map;
85mod serialize;
86mod tree;
87
88const VERSION: u32 = 1;
90
91#[derive(Debug, thiserror::Error)]
93pub enum LockParseError {
94 #[error("unsupported lockfile schema version (v{version}, but only v{supported} is supported)")]
96 UnsupportedVersion { supported: u32, version: u32 },
97
98 #[error(
100 "failed to parse lockfile using an unsupported schema version (v{version}, but only v{supported} is supported)"
101 )]
102 UnparsableVersion {
103 supported: u32,
104 version: u32,
105 #[source]
106 source: toml::de::Error,
107 },
108
109 #[error(transparent)]
111 Toml(#[from] toml::de::Error),
112}
113
114const REVISION: u32 = 3;
116
117const METADATA_FREE_REVISION: u32 = 4;
119
120static LINUX_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
121 let pep508 = MarkerTree::from_str("os_name == 'posix' and sys_platform == 'linux'").unwrap();
122 UniversalMarker::new(pep508, ConflictMarker::TRUE)
123});
124static WINDOWS_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
125 let pep508 = MarkerTree::from_str("os_name == 'nt' and sys_platform == 'win32'").unwrap();
126 UniversalMarker::new(pep508, ConflictMarker::TRUE)
127});
128static MAC_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
129 let pep508 = MarkerTree::from_str("os_name == 'posix' and sys_platform == 'darwin'").unwrap();
130 UniversalMarker::new(pep508, ConflictMarker::TRUE)
131});
132static ANDROID_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
133 let pep508 = MarkerTree::from_str("sys_platform == 'android'").unwrap();
134 UniversalMarker::new(pep508, ConflictMarker::TRUE)
135});
136static ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
137 let pep508 =
138 MarkerTree::from_str("platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'ARM64'")
139 .unwrap();
140 UniversalMarker::new(pep508, ConflictMarker::TRUE)
141});
142static X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
143 let pep508 =
144 MarkerTree::from_str("platform_machine == 'x86_64' or platform_machine == 'amd64' or platform_machine == 'AMD64'")
145 .unwrap();
146 UniversalMarker::new(pep508, ConflictMarker::TRUE)
147});
148static X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
149 let pep508 = MarkerTree::from_str(
150 "platform_machine == 'i686' or platform_machine == 'i386' or platform_machine == 'win32' or platform_machine == 'x86'",
151 )
152 .unwrap();
153 UniversalMarker::new(pep508, ConflictMarker::TRUE)
154});
155static PPC64LE_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
156 let pep508 = MarkerTree::from_str("platform_machine == 'ppc64le'").unwrap();
157 UniversalMarker::new(pep508, ConflictMarker::TRUE)
158});
159static PPC64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
160 let pep508 = MarkerTree::from_str("platform_machine == 'ppc64'").unwrap();
161 UniversalMarker::new(pep508, ConflictMarker::TRUE)
162});
163static S390X_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
164 let pep508 = MarkerTree::from_str("platform_machine == 's390x'").unwrap();
165 UniversalMarker::new(pep508, ConflictMarker::TRUE)
166});
167static RISCV64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
168 let pep508 = MarkerTree::from_str("platform_machine == 'riscv64'").unwrap();
169 UniversalMarker::new(pep508, ConflictMarker::TRUE)
170});
171static LOONGARCH64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
172 let pep508 = MarkerTree::from_str("platform_machine == 'loongarch64'").unwrap();
173 UniversalMarker::new(pep508, ConflictMarker::TRUE)
174});
175static ARMV7L_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
176 let pep508 =
177 MarkerTree::from_str("platform_machine == 'armv7l' or platform_machine == 'armv8l'")
178 .unwrap();
179 UniversalMarker::new(pep508, ConflictMarker::TRUE)
180});
181static ARMV6L_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
182 let pep508 = MarkerTree::from_str("platform_machine == 'armv6l'").unwrap();
183 UniversalMarker::new(pep508, ConflictMarker::TRUE)
184});
185static LINUX_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
186 let mut marker = *LINUX_MARKERS;
187 marker.and(*ARM_MARKERS);
188 marker
189});
190static LINUX_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
191 let mut marker = *LINUX_MARKERS;
192 marker.and(*X86_64_MARKERS);
193 marker
194});
195static LINUX_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
196 let mut marker = *LINUX_MARKERS;
197 marker.and(*X86_MARKERS);
198 marker
199});
200static LINUX_PPC64LE_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
201 let mut marker = *LINUX_MARKERS;
202 marker.and(*PPC64LE_MARKERS);
203 marker
204});
205static LINUX_PPC64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
206 let mut marker = *LINUX_MARKERS;
207 marker.and(*PPC64_MARKERS);
208 marker
209});
210static LINUX_S390X_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
211 let mut marker = *LINUX_MARKERS;
212 marker.and(*S390X_MARKERS);
213 marker
214});
215static LINUX_RISCV64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
216 let mut marker = *LINUX_MARKERS;
217 marker.and(*RISCV64_MARKERS);
218 marker
219});
220static LINUX_LOONGARCH64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
221 let mut marker = *LINUX_MARKERS;
222 marker.and(*LOONGARCH64_MARKERS);
223 marker
224});
225static LINUX_ARMV7L_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
226 let mut marker = *LINUX_MARKERS;
227 marker.and(*ARMV7L_MARKERS);
228 marker
229});
230static LINUX_ARMV6L_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
231 let mut marker = *LINUX_MARKERS;
232 marker.and(*ARMV6L_MARKERS);
233 marker
234});
235static WINDOWS_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
236 let mut marker = *WINDOWS_MARKERS;
237 marker.and(*ARM_MARKERS);
238 marker
239});
240static WINDOWS_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
241 let mut marker = *WINDOWS_MARKERS;
242 marker.and(*X86_64_MARKERS);
243 marker
244});
245static WINDOWS_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
246 let mut marker = *WINDOWS_MARKERS;
247 marker.and(*X86_MARKERS);
248 marker
249});
250static MAC_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
251 let mut marker = *MAC_MARKERS;
252 marker.and(*ARM_MARKERS);
253 marker
254});
255static MAC_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
256 let mut marker = *MAC_MARKERS;
257 marker.and(*X86_64_MARKERS);
258 marker
259});
260static MAC_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
261 let mut marker = *MAC_MARKERS;
262 marker.and(*X86_MARKERS);
263 marker
264});
265static ANDROID_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
266 let mut marker = *ANDROID_MARKERS;
267 marker.and(*ARM_MARKERS);
268 marker
269});
270static ANDROID_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
271 let mut marker = *ANDROID_MARKERS;
272 marker.and(*X86_64_MARKERS);
273 marker
274});
275static ANDROID_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
276 let mut marker = *ANDROID_MARKERS;
277 marker.and(*X86_MARKERS);
278 marker
279});
280
281pub(crate) struct HashedDist {
286 dist: Dist,
287 hashes: HashDigests,
288}
289
290#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
291#[serde(try_from = "LockWire")]
292pub struct Lock {
293 version: u32,
302 revision: u32,
308 fork_markers: Vec<UniversalMarker>,
311 conflicts: Conflicts,
313 supported_environments: Vec<MarkerTree>,
315 required_environments: Vec<MarkerTree>,
317 requires_python: RequiresPython,
319 options: ResolverOptions,
321 packages: Vec<Package>,
323 by_id: FxHashMap<PackageId, usize>,
335 manifest: ResolverManifest,
337}
338
339pub fn implicit_constraints_marker(
341 requires_python: MarkerTree,
342 supported_environments: &[MarkerTree],
343) -> MarkerTree {
344 let mut environments_union = if supported_environments.is_empty() {
345 MarkerTree::TRUE
346 } else {
347 let mut environments_union = MarkerTree::FALSE;
348 for environment in supported_environments {
349 environments_union = environments_union.or(*environment);
350 }
351 environments_union
352 };
353 environments_union = environments_union.and(requires_python);
354 environments_union
355}
356
357#[derive(Clone, Debug)]
359pub struct SelectedDependency<'lock> {
360 package: &'lock Package,
361 extras: BTreeSet<&'lock ExtraName>,
362 context: DependencySelectionContext<'lock>,
363}
364
365impl<'lock> SelectedDependency<'lock> {
366 fn from_dependency(
367 package: &'lock Package,
368 dependency: &'lock Dependency,
369 context: DependencySelectionContext<'lock>,
370 ) -> Self {
371 Self {
372 package,
373 extras: dependency.extra.iter().collect(),
374 context,
375 }
376 }
377
378 fn from_requirement(package: &'lock Package, requirement: &'lock Requirement) -> Self {
379 Self {
380 package,
381 extras: requirement.extras.iter().collect(),
382 context: DependencySelectionContext::None,
383 }
384 }
385
386 fn extend_dependency(&mut self, dependency: &'lock Dependency) {
387 self.extras.extend(&dependency.extra);
388 }
389
390 fn extend_requirement(&mut self, requirement: &'lock Requirement) {
391 self.extras.extend(&requirement.extras);
392 }
393
394 fn package(&self) -> &'lock Package {
396 self.package
397 }
398
399 fn extras(&self) -> impl Iterator<Item = &'lock ExtraName> + '_ {
401 self.extras.iter().copied()
402 }
403
404 fn context(&self) -> DependencySelectionContext<'lock> {
405 self.context
406 }
407}
408
409#[derive(Clone, Copy, Debug)]
410pub(super) enum DependencySelectionContext<'lock> {
411 None,
412 Production(&'lock PackageName),
413 Group(&'lock PackageName, &'lock GroupName),
414}
415
416impl<'lock> DependencySelectionContext<'lock> {
417 fn package(self) -> Option<&'lock PackageName> {
418 match self {
419 Self::None => None,
420 Self::Production(package) | Self::Group(package, _) => Some(package),
421 }
422 }
423}
424
425#[derive(Clone, Copy, Debug)]
427enum DependencyContext<'a> {
428 Production,
429 Extra(&'a ExtraName),
430 Group(&'a GroupName),
431}
432
433impl DependencyContext<'_> {
434 fn selected_conflict(
436 self,
437 package: &PackageName,
438 conflicts: &Conflicts,
439 ) -> Option<ConflictItem> {
440 match self {
441 Self::Extra(extra) if conflicts.contains(package, extra) => {
442 Some(ConflictItem::from((package.clone(), extra.clone())))
443 }
444 Self::Group(group) if conflicts.contains(package, group) => {
445 Some(ConflictItem::from((package.clone(), group.clone())))
446 }
447 Self::Production | Self::Extra(_) | Self::Group(_) => None,
448 }
449 }
450
451 fn dependencies(self, package: &Package) -> &[Dependency] {
453 match self {
454 Self::Production => &package.dependencies,
455 Self::Extra(extra) => package
456 .optional_dependencies
457 .get(extra)
458 .map(Vec::as_slice)
459 .unwrap_or_default(),
460 Self::Group(group) => package
461 .dependency_groups
462 .get(group)
463 .map(Vec::as_slice)
464 .unwrap_or_default(),
465 }
466 }
467
468 fn dependencies_mut(self, package: &mut Package) -> &mut Vec<Dependency> {
470 match self {
471 Self::Production => &mut package.dependencies,
472 Self::Extra(extra) => package
473 .optional_dependencies
474 .entry(extra.clone())
475 .or_default(),
476 Self::Group(group) => package.dependency_groups.entry(group.clone()).or_default(),
477 }
478 }
479}
480
481struct LockedDependencyBuilder<'a> {
483 requires_python: &'a RequiresPython,
484 environment: SimplifiedMarkerTree,
485 parent_marker: UniversalMarker,
486}
487
488impl<'a> LockedDependencyBuilder<'a> {
489 fn new(
490 requires_python: &'a RequiresPython,
491 environment: SimplifiedMarkerTree,
492 parent_marker: UniversalMarker,
493 ) -> Self {
494 Self {
495 requires_python,
496 environment,
497 parent_marker,
498 }
499 }
500
501 fn add_requirements(
505 &self,
506 dependencies: &mut Vec<Dependency>,
507 expected: &ExpectedPackageDependencies<'_>,
508 context: DependencyContext<'_>,
509 activated_extras: &mut FxHashMap<PackageId, BTreeSet<ExtraName>>,
510 ) -> Result<bool, LockError> {
511 let empty_requirements = BTreeSet::new();
512 let requirements = match context {
513 DependencyContext::Production | DependencyContext::Extra(_) => &expected.declarations,
514 DependencyContext::Group(group) => expected
515 .dependency_groups
516 .get(group)
517 .unwrap_or(&empty_requirements),
518 };
519 let mut edges: BTreeMap<(PackageId, BTreeSet<ExtraName>), UniversalMarker> =
520 BTreeMap::new();
521 let mut complete = true;
522
523 for requirement in requirements {
524 let production_marker = requirement.marker.simplify_not_extras_with(|_| true);
527 let requirement_marker = match context {
528 DependencyContext::Production | DependencyContext::Group(_) => production_marker,
529 DependencyContext::Extra(extra) => requirement
530 .marker
531 .simplify_extras(slice::from_ref(extra))
532 .simplify_not_extras_with(|candidate| candidate != extra)
533 .and(production_marker.negate()),
534 };
535 let mut required_marker = UniversalMarker::from_combined(requirement_marker);
536 required_marker.and(self.parent_marker);
537 if let Some(conflict_marker) =
538 expected.requirement_conflict_marker(context, requirement)
539 {
540 required_marker.and(conflict_marker);
541 }
542 if required_marker.is_false() {
543 continue;
544 }
545
546 if requirement.name == expected.package.id.name
547 && !matches!(context, DependencyContext::Group(_))
548 {
549 if !expected.package_satisfies_requirement(expected.package, requirement)? {
552 complete = false;
553 }
554 continue;
555 }
556
557 let required_marker = required_marker.combined();
558
559 let mut covered_marker = MarkerTree::FALSE;
560 for dependency in expected.lock.packages_for_name(&requirement.name) {
561 if !expected.package_satisfies_requirement(dependency, requirement)? {
562 continue;
563 }
564
565 let mut marker = UniversalMarker::from_combined(required_marker);
566 if !dependency.fork_markers.is_empty() {
567 let dependency_marker = dependency
568 .fork_markers
569 .iter()
570 .fold(MarkerTree::FALSE, |marker, fork_marker| {
571 marker.or(fork_marker.combined())
572 });
573 marker.and(UniversalMarker::from_combined(dependency_marker));
574 }
575 if marker.is_false() {
576 continue;
577 }
578 covered_marker = covered_marker.or(marker.combined());
579
580 activated_extras
581 .entry(dependency.id.clone())
582 .or_default()
583 .extend(requirement.extras.iter().cloned());
584
585 let extras = requirement
586 .extras
587 .iter()
588 .filter(|extra| dependency.optional_dependencies.contains_key(*extra))
589 .cloned()
590 .collect::<BTreeSet<_>>();
591
592 if !extras.is_empty() {
595 edges
596 .entry((dependency.id.clone(), BTreeSet::new()))
597 .and_modify(|existing| existing.or(marker))
598 .or_insert(marker);
599 }
600 edges
601 .entry((dependency.id.clone(), extras))
602 .and_modify(|existing| existing.or(marker))
603 .or_insert(marker);
604 }
605
606 if !covered_marker.negate().is_disjoint(required_marker) {
608 complete = false;
609 }
610 }
611
612 for ((package_id, extras), marker) in edges {
613 self.add(dependencies, package_id, extras, marker);
614 }
615 Ok(complete)
616 }
617
618 fn add(
619 &self,
620 dependencies: &mut Vec<Dependency>,
621 package_id: PackageId,
622 extras: BTreeSet<ExtraName>,
623 marker: UniversalMarker,
624 ) {
625 let simplified_marker = simplify_dependency_marker(
626 self.requires_python,
627 self.environment,
628 self.parent_marker,
629 marker,
630 );
631 let dependency =
632 Dependency::new(self.requires_python, package_id, extras, simplified_marker);
633
634 let existing = dependencies.iter_mut().find(|existing| {
657 existing.package_id == dependency.package_id
658 && existing.simplified_marker == dependency.simplified_marker
659 });
660 if let Some(existing) = existing {
661 existing.extra.extend(dependency.extra);
662 } else {
663 dependencies.push(dependency);
664 }
665 }
666}
667
668struct ExpectedPackageDependencies<'lock> {
670 lock: &'lock Lock,
671 package: &'lock Package,
672 declarations: BTreeSet<Requirement>,
673 provides_extra: &'lock [ExtraName],
674 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
675 source_requirements: &'lock Constraints,
676 activated_extras: BTreeSet<ExtraName>,
677 package_marker: UniversalMarker,
679 lock_marker: SimplifiedMarkerTree,
681 workspace_root: &'lock Path,
682}
683
684impl<'lock> ExpectedPackageDependencies<'lock> {
685 fn new(
686 lock: &'lock Lock,
687 declarations: &BTreeSet<Requirement>,
688 provides_extra: &'lock [ExtraName],
689 dependency_groups: &BTreeMap<GroupName, BTreeSet<Requirement>>,
690 source_requirements: &'lock Constraints,
691 overrides: &Overrides,
692 excludes: &Excludes,
693 package_requires_python: Option<&VersionSpecifiers>,
694 package: &'lock Package,
695 activated_extras: BTreeSet<ExtraName>,
696 workspace_root: &'lock Path,
697 ) -> Self {
698 let package_context = package
699 .id
700 .version
701 .as_ref()
702 .map(|version| (&package.id.name, version));
703 let declarations = overrides
704 .apply_for_package(package_context, declarations)
705 .filter(|requirement| {
706 !excludes.contains_for_package(package_context, &requirement.name)
707 })
708 .map(Cow::into_owned)
709 .collect::<BTreeSet<_>>();
710 let dependency_groups = dependency_groups
711 .iter()
712 .map(|(group, requirements)| {
713 let requirements = overrides
714 .apply_for_package(None, requirements)
715 .filter(|requirement| {
716 !excludes.contains_for_package(package_context, &requirement.name)
717 })
718 .map(Cow::into_owned)
719 .collect::<BTreeSet<_>>();
720 (group.clone(), requirements)
721 })
722 .collect::<BTreeMap<_, _>>();
723
724 let mut package_marker = UniversalMarker::from_combined(lock.fork_markers_union());
727 if !package.fork_markers.is_empty() {
728 let fork_marker = package
729 .fork_markers
730 .iter()
731 .fold(MarkerTree::FALSE, |fork_marker, marker| {
732 fork_marker.or(marker.combined())
733 });
734 package_marker.and(UniversalMarker::from_combined(fork_marker));
735 }
736 if let Some(requires_python) = package_requires_python {
737 package_marker.and(UniversalMarker::from_combined(
738 RequiresPython::from_specifiers(requires_python.clone()).to_marker_tree(),
739 ));
740 }
741 let lock_marker =
742 SimplifiedMarkerTree::new(&lock.requires_python, lock.fork_markers_union());
743
744 Self {
745 lock,
746 package,
747 declarations,
748 provides_extra,
749 dependency_groups,
750 source_requirements,
751 activated_extras,
752 package_marker,
753 lock_marker,
754 workspace_root,
755 }
756 }
757
758 fn package_satisfies_requirement(
760 &self,
761 package: &Package,
762 requirement: &Requirement,
763 ) -> Result<bool, LockError> {
764 let mut source_matches = package
765 .id
766 .source
767 .satisfies_requirement_source(&requirement.source, self.workspace_root)?;
768
769 if !source_matches
773 && matches!(
774 requirement.source,
775 RequirementSource::Registry { index: None, .. }
776 )
777 && let Some(source_requirements) = self.source_requirements.get(&requirement.name)
778 {
779 for source_requirement in source_requirements {
780 if package
781 .id
782 .source
783 .satisfies_requirement_source(&source_requirement.source, self.workspace_root)?
784 {
785 source_matches = true;
786 break;
787 }
788 }
789 }
790
791 source_matches |= package.id == self.package.id
792 && matches!(
793 requirement.source,
794 RequirementSource::Registry { index: None, .. }
795 );
796 let version_matches = requirement
797 .source
798 .version_specifiers()
799 .zip(package.id.version.as_ref())
800 .is_none_or(|(specifiers, version)| specifiers.contains(version));
802
803 Ok(source_matches && version_matches)
804 }
805
806 fn contexts(&self) -> impl Iterator<Item = DependencyContext<'_>> + '_ {
808 let is_workspace_package = self.lock.members().contains(&self.package.id.name)
809 || self.lock.members().is_empty()
810 && self
811 .lock
812 .root()
813 .is_some_and(|root| root.id == self.package.id);
814 let extras = self
815 .provides_extra
816 .iter()
817 .filter(|extra| is_workspace_package || self.activated_extras.contains(*extra))
818 .chain(self.package.optional_dependencies.keys())
819 .collect::<BTreeSet<_>>();
820 let groups = self
821 .dependency_groups
822 .keys()
823 .filter(|_| is_workspace_package)
824 .chain(self.package.dependency_groups.keys())
825 .collect::<BTreeSet<_>>();
826
827 iter::once(DependencyContext::Production)
828 .chain(extras.into_iter().map(DependencyContext::Extra))
829 .chain(groups.into_iter().map(DependencyContext::Group))
830 }
831
832 fn requirement_conflict_marker(
834 &self,
835 context: DependencyContext<'_>,
836 requirement: &Requirement,
837 ) -> Option<UniversalMarker> {
838 if self.lock.conflicts.is_empty() {
839 return None;
840 }
841
842 let source_conflict = match &requirement.source {
843 RequirementSource::Registry { conflict, .. } => conflict.as_ref(),
844 _ => None,
845 };
846 let requested_conflicts = requirement
847 .extras
848 .iter()
849 .filter(|extra| self.lock.conflicts.contains(&requirement.name, *extra))
850 .map(|extra| ConflictItem::from((requirement.name.clone(), extra.clone())));
851 let requested_project = self
852 .lock
853 .conflicts
854 .contains(&requirement.name, ConflictKindRef::Project)
855 .then(|| ConflictItem::from(requirement.name.clone()));
856 let mut conflicts = source_conflict
857 .cloned()
858 .into_iter()
859 .chain(requested_conflicts)
860 .chain(requested_project)
861 .peekable();
862 conflicts.peek()?;
863 let selected = context.selected_conflict(&self.package.id.name, &self.lock.conflicts);
864 let mut marker = UniversalMarker::TRUE;
865 for conflict in conflicts.chain(selected) {
866 marker.and(UniversalMarker::new(
867 MarkerTree::TRUE,
868 ConflictMarker::from_conflict_item(&conflict),
869 ));
870 }
871 Some(marker)
872 }
873
874 fn context_parent_marker(&self, context: DependencyContext<'_>) -> UniversalMarker {
876 if self.lock.conflicts.is_empty() {
877 return self.package_marker;
878 }
879
880 let project_conflicts = self
881 .lock
882 .conflicts
883 .contains(&self.package.id.name, ConflictKindRef::Project);
884 let project = ConflictItem::from(self.package.id.name.clone());
885 let selected = context.selected_conflict(&self.package.id.name, &self.lock.conflicts);
886
887 let mut world = UniversalMarker::new(
888 MarkerTree::TRUE,
889 ConflictMarker::from_conflicts(&self.lock.conflicts),
890 );
891 if project_conflicts && !matches!(context, DependencyContext::Group(_)) {
892 world.assume_conflict_item(&project);
893 }
894 if let Some(selected) = &selected {
895 world.assume_conflict_item(selected);
896 }
897 if world.is_false() {
899 return UniversalMarker::FALSE;
900 }
901
902 let mut parent_marker = self.package_marker;
903 if project_conflicts && matches!(context, DependencyContext::Production) {
904 let mut activation = UniversalMarker::new(
905 MarkerTree::TRUE,
906 ConflictMarker::from_conflict_item(&project),
907 );
908 for extra in self.provides_extra {
909 if self.lock.conflicts.contains(&self.package.id.name, extra) {
910 activation.or(UniversalMarker::new(
911 MarkerTree::TRUE,
912 ConflictMarker::from_conflict_item(&ConflictItem::from((
913 self.package.id.name.clone(),
914 extra.clone(),
915 ))),
916 ));
917 }
918 }
919 parent_marker.and(activation);
920 }
921 parent_marker
922 }
923
924 fn comparable_dependencies(
926 &self,
927 dependencies: &[Dependency],
928 ) -> Vec<(PackageId, BTreeSet<ExtraName>, SimplifiedMarkerTree)> {
929 let conflicts = ConflictMarker::from_conflicts(&self.lock.conflicts);
930 let mut comparable = dependencies
931 .iter()
932 .map(|dependency| {
933 let mut marker = dependency.complexified_marker;
934 marker.imbibe(conflicts);
935 (
936 dependency.package_id.clone(),
937 dependency.extra.clone(),
938 SimplifiedMarkerTree::new(&self.lock.requires_python, marker.combined()),
939 )
940 })
941 .collect::<Vec<_>>();
942 comparable.sort();
943 comparable
944 }
945}
946
947#[derive(Debug)]
952pub struct DependencySelection<'lock> {
953 root: Option<SelectedDependency<'lock>>,
954 production: Option<SelectedDependency<'lock>>,
955 groups: BTreeMap<&'lock GroupName, SelectedDependency<'lock>>,
956}
957
958impl<'lock> DependencySelection<'lock> {
959 pub fn root(&self) -> Option<&SelectedDependency<'lock>> {
961 self.root.as_ref()
962 }
963
964 pub fn production(&self) -> Option<&SelectedDependency<'lock>> {
966 self.production.as_ref()
967 }
968
969 pub fn group(&self, group: &GroupName) -> Option<&SelectedDependency<'lock>> {
971 self.groups.get(group)
972 }
973}
974
975impl Lock {
976 pub fn from_resolution(
981 resolution: &ResolverOutput,
982 manifest: ResolverManifest,
983 root: &Path,
984 supported_environments: Vec<MarkerTree>,
985 index_locations: &IndexLocations,
986 ) -> Result<Self, LockError> {
987 let mut packages = BTreeMap::new();
988 let requires_python = resolution.requires_python.clone();
989 let supported_environments = supported_environments
990 .into_iter()
991 .map(|marker| requires_python.complexify_markers(marker))
992 .collect::<Vec<_>>();
993 let supported_environments_marker = if supported_environments.is_empty() {
994 None
995 } else {
996 let mut combined = MarkerTree::FALSE;
997 for marker in &supported_environments {
998 combined = combined.or(*marker);
999 }
1000 Some(UniversalMarker::new(combined, ConflictMarker::TRUE))
1001 };
1002 let environment = SimplifiedMarkerTree::new(
1003 &requires_python,
1004 fork_markers_union(&resolution.fork_markers, &requires_python),
1005 );
1006
1007 let mut seen = FxHashSet::default();
1009 let mut duplicates = FxHashSet::default();
1010 for (_, dist) in resolution.base_dists() {
1011 if !seen.insert(dist.name()) {
1012 duplicates.insert(dist.name());
1013 }
1014 }
1015
1016 for (node_index, dist) in resolution.base_dists() {
1018 let fork_markers = if duplicates.contains(dist.name()) {
1024 let fork_markers = resolution
1025 .fork_markers
1026 .iter()
1027 .filter(|fork_markers| !fork_markers.is_disjoint(dist.marker))
1028 .copied()
1029 .collect::<Vec<_>>();
1030 canonicalize_universal_markers(&fork_markers, &requires_python)
1031 } else {
1032 vec![]
1033 };
1034
1035 let mut package =
1036 Package::from_annotated_dist(dist, fork_markers, root, index_locations)?;
1037 let mut wheel_marker = dist.marker;
1038 if let Some(supported_environments_marker) = supported_environments_marker {
1039 wheel_marker.and(supported_environments_marker);
1040 }
1041 let wheels = &mut package.wheels;
1042 wheels.retain(|wheel| {
1043 !is_wheel_unreachable_for_marker(
1044 &wheel.filename,
1045 &requires_python,
1046 &wheel_marker,
1047 None,
1048 )
1049 });
1050
1051 package.add_dependencies(
1052 DependencyContext::Production,
1053 &requires_python,
1054 resolution,
1055 node_index,
1056 environment,
1057 root,
1058 )?;
1059
1060 let id = package.id.clone();
1061 if let Some(locked_dist) = packages.insert(id, package) {
1062 return Err(LockErrorKind::DuplicatePackage {
1063 id: locked_dist.id.clone(),
1064 }
1065 .into());
1066 }
1067 }
1068
1069 for node_index in resolution.graph.node_indices() {
1071 let ResolutionGraphNode::Dist(dist) = &resolution.graph[node_index] else {
1072 continue;
1073 };
1074 if let Some(extra) = dist.extra.as_ref() {
1075 let id = PackageId::from_annotated_dist(dist, root)?;
1076 let Some(package) = packages.get_mut(&id) else {
1077 return Err(LockErrorKind::MissingExtraBase {
1078 id,
1079 extra: extra.clone(),
1080 }
1081 .into());
1082 };
1083 package.add_dependencies(
1084 DependencyContext::Extra(extra),
1085 &requires_python,
1086 resolution,
1087 node_index,
1088 environment,
1089 root,
1090 )?;
1091 }
1092 if let Some(group) = dist.group.as_ref() {
1093 let id = PackageId::from_annotated_dist(dist, root)?;
1094 let Some(package) = packages.get_mut(&id) else {
1095 return Err(LockErrorKind::MissingDevBase {
1096 id,
1097 group: group.clone(),
1098 }
1099 .into());
1100 };
1101 package.add_dependencies(
1102 DependencyContext::Group(group),
1103 &requires_python,
1104 resolution,
1105 node_index,
1106 environment,
1107 root,
1108 )?;
1109 }
1110 }
1111
1112 let packages = packages.into_values().collect();
1113
1114 let options = ResolverOptions {
1115 resolution_mode: resolution.options.resolution_mode,
1116 prerelease: resolution.options.prerelease.clone(),
1117 fork_strategy: resolution.options.fork_strategy,
1118 exclude_newer: resolution.options.exclude_newer.clone(),
1119 };
1120 let fork_markers =
1125 canonicalize_universal_markers(&resolution.fork_markers, &requires_python);
1126 let lock = Self::new(
1127 VERSION,
1128 REVISION,
1129 packages,
1130 requires_python,
1131 options,
1132 manifest,
1133 Conflicts::empty(),
1134 supported_environments,
1135 vec![],
1136 fork_markers,
1137 )?;
1138 Ok(lock)
1139 }
1140
1141 fn new(
1143 version: u32,
1144 revision: u32,
1145 mut packages: Vec<Package>,
1146 requires_python: RequiresPython,
1147 options: ResolverOptions,
1148 manifest: ResolverManifest,
1149 conflicts: Conflicts,
1150 supported_environments: Vec<MarkerTree>,
1151 required_environments: Vec<MarkerTree>,
1152 fork_markers: Vec<UniversalMarker>,
1153 ) -> Result<Self, LockError> {
1154 for package in &mut packages {
1157 package.dependencies.sort();
1158 for [dep1, dep2] in package.dependencies.array_windows() {
1159 if dep1 == dep2 {
1160 return Err(LockErrorKind::DuplicateDependency {
1161 id: package.id.clone(),
1162 dependency: dep1.clone(),
1163 }
1164 .into());
1165 }
1166 }
1167
1168 for (extra, dependencies) in &mut package.optional_dependencies {
1170 dependencies.sort();
1171 for [dep1, dep2] in dependencies.array_windows() {
1172 if dep1 == dep2 {
1173 return Err(LockErrorKind::DuplicateOptionalDependency {
1174 id: package.id.clone(),
1175 extra: extra.clone(),
1176 dependency: dep1.clone(),
1177 }
1178 .into());
1179 }
1180 }
1181 }
1182
1183 for (group, dependencies) in &mut package.dependency_groups {
1185 dependencies.sort();
1186 for [dep1, dep2] in dependencies.array_windows() {
1187 if dep1 == dep2 {
1188 return Err(LockErrorKind::DuplicateDevDependency {
1189 id: package.id.clone(),
1190 group: group.clone(),
1191 dependency: dep1.clone(),
1192 }
1193 .into());
1194 }
1195 }
1196 }
1197 }
1198 packages.sort_by(|dist1, dist2| dist1.id.cmp(&dist2.id));
1199
1200 let mut by_id = FxHashMap::default();
1203 for (i, dist) in packages.iter().enumerate() {
1204 if by_id.insert(dist.id.clone(), i).is_some() {
1205 return Err(LockErrorKind::DuplicatePackage {
1206 id: dist.id.clone(),
1207 }
1208 .into());
1209 }
1210 }
1211
1212 let mut extras_by_id = FxHashMap::default();
1214 for dist in &packages {
1215 for extra in dist.optional_dependencies.keys() {
1216 extras_by_id
1217 .entry(dist.id.clone())
1218 .or_insert_with(FxHashSet::default)
1219 .insert(extra.clone());
1220 }
1221 }
1222
1223 for dist in &mut packages {
1225 for dep in dist
1226 .dependencies
1227 .iter_mut()
1228 .chain(dist.optional_dependencies.values_mut().flatten())
1229 .chain(dist.dependency_groups.values_mut().flatten())
1230 {
1231 dep.extra.retain(|extra| {
1232 extras_by_id
1233 .get(&dep.package_id)
1234 .is_some_and(|extras| extras.contains(extra))
1235 });
1236 }
1237 }
1238
1239 for dist in &packages {
1243 for dependency in dist.all_dependencies() {
1244 if !by_id.contains_key(&dependency.package_id) {
1245 return Err(LockErrorKind::UnrecognizedDependency {
1246 id: dist.id.clone(),
1247 dependency: dependency.clone(),
1248 }
1249 .into());
1250 }
1251 }
1252
1253 if let Some(requires_hash) = dist.id.source.requires_hash() {
1256 for wheel in &dist.wheels {
1257 if requires_hash != wheel.hash.is_some() {
1258 return Err(LockErrorKind::Hash {
1259 id: dist.id.clone(),
1260 artifact_type: "wheel",
1261 expected: requires_hash,
1262 }
1263 .into());
1264 }
1265 }
1266 }
1267 }
1268 let lock = Self {
1269 version,
1270 revision,
1271 fork_markers,
1272 conflicts,
1273 supported_environments,
1274 required_environments,
1275 requires_python,
1276 options,
1277 packages,
1278 by_id,
1279 manifest,
1280 };
1281 Ok(lock)
1282 }
1283
1284 #[must_use]
1286 pub fn with_conflicts(mut self, conflicts: Conflicts) -> Self {
1287 self.conflicts = conflicts;
1288 self
1289 }
1290
1291 #[must_use]
1293 pub fn with_required_environments(mut self, required_environments: Vec<MarkerTree>) -> Self {
1294 self.required_environments = required_environments
1295 .into_iter()
1296 .map(|marker| self.requires_python.complexify_markers(marker))
1297 .collect();
1298 self
1299 }
1300
1301 #[must_use]
1303 pub fn without_package_metadata(mut self) -> Self {
1304 self.revision = METADATA_FREE_REVISION;
1305 for package in &mut self.packages {
1306 package.metadata = PackageMetadata::default();
1307 }
1308 self
1309 }
1310
1311 pub fn supports_provides_extra(&self) -> bool {
1313 (self.version(), self.revision()) >= (1, 1)
1315 }
1316
1317 pub fn supports_missing_package_metadata(&self) -> bool {
1319 (self.version(), self.revision()) >= (VERSION, METADATA_FREE_REVISION)
1320 }
1321
1322 fn includes_empty_groups(&self) -> bool {
1324 (self.version(), self.revision()) >= (1, 1)
1327 }
1328
1329 fn version(&self) -> u32 {
1331 self.version
1332 }
1333
1334 fn revision(&self) -> u32 {
1336 self.revision
1337 }
1338
1339 pub fn len(&self) -> usize {
1341 self.packages.len()
1342 }
1343
1344 pub fn is_empty(&self) -> bool {
1346 self.packages.is_empty()
1347 }
1348
1349 pub fn packages(&self) -> &[Package] {
1351 &self.packages
1352 }
1353
1354 pub fn satisfies_hash_algorithms(
1357 &self,
1358 root: &Path,
1359 index_locations: &IndexLocations,
1360 ) -> Result<bool, LockError> {
1361 for package in &self.packages {
1362 let Some(index) = package.index(root)? else {
1363 continue;
1364 };
1365 let Some(algorithm) = index_locations.hash_algorithm_for(&index) else {
1366 continue;
1367 };
1368 warn_index_hash_algorithm_preview();
1369
1370 let mismatched =
1371 |hash: Option<&Hash>| hash.is_none_or(|hash| hash.0.algorithm != algorithm);
1372
1373 if package.sdist.iter().any(|sdist| mismatched(sdist.hash()))
1374 || package.wheels.iter().any(|wheel| {
1375 mismatched(wheel.hash.as_ref())
1376 || wheel
1377 .zstd
1378 .as_ref()
1379 .is_some_and(|zstd| mismatched(zstd.hash.as_ref()))
1380 })
1381 {
1382 return Ok(false);
1383 }
1384 }
1385
1386 Ok(true)
1387 }
1388
1389 pub fn requires_python(&self) -> &RequiresPython {
1391 &self.requires_python
1392 }
1393
1394 pub fn resolution_mode(&self) -> ResolutionMode {
1396 self.options.resolution_mode
1397 }
1398
1399 pub fn prerelease_mode(&self) -> PrereleaseMode {
1401 self.options.prerelease.global
1402 }
1403
1404 pub fn prerelease(&self) -> &Prerelease {
1406 &self.options.prerelease
1407 }
1408
1409 pub fn fork_strategy(&self) -> ForkStrategy {
1411 self.options.fork_strategy
1412 }
1413
1414 pub fn exclude_newer(&self) -> &ExcludeNewer {
1416 &self.options.exclude_newer
1417 }
1418
1419 pub fn conflicts(&self) -> &Conflicts {
1421 &self.conflicts
1422 }
1423
1424 pub fn supported_environments(&self) -> &[MarkerTree] {
1426 &self.supported_environments
1427 }
1428
1429 fn required_environments(&self) -> &[MarkerTree] {
1431 &self.required_environments
1432 }
1433
1434 pub fn members(&self) -> &BTreeSet<PackageName> {
1436 &self.manifest.members
1437 }
1438
1439 fn is_workspace_member(&self, package: &Package) -> bool {
1441 self.members().contains(&package.id.name)
1442 || self.members().is_empty() && self.root().is_some_and(|root| root.id == package.id)
1443 }
1444
1445 fn requirements(&self) -> &BTreeSet<Requirement> {
1447 &self.manifest.requirements
1448 }
1449
1450 fn root_requirement_marker(
1453 &self,
1454 requirement: &Requirement,
1455 package: &Package,
1456 ) -> Option<MarkerTree> {
1457 let marker = if package.fork_markers.is_empty() {
1458 requirement.marker
1459 } else {
1460 let mut combined = MarkerTree::FALSE;
1461 for fork_marker in &package.fork_markers {
1462 combined = combined.or(fork_marker.pep508());
1463 }
1464 combined = combined.and(requirement.marker);
1465 combined
1466 };
1467
1468 (!marker.is_false()).then(|| self.simplify_environment(marker))
1469 }
1470
1471 pub(crate) fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
1473 &self.manifest.dependency_groups
1474 }
1475
1476 pub fn dependency_selection<'lock>(
1481 &'lock self,
1482 project_name: Option<&PackageName>,
1483 dependency_name: &PackageName,
1484 marker_environment: &MarkerEnvironment,
1485 ) -> Result<DependencySelection<'lock>, String> {
1486 let (root, production, groups) = if let Some(project_name) = project_name {
1487 let Some(project) = self.find_by_name(project_name)? else {
1488 return Ok(DependencySelection {
1489 root: None,
1490 production: None,
1491 groups: BTreeMap::new(),
1492 });
1493 };
1494 let production =
1495 self.find_project_dependency(project, dependency_name, marker_environment)?;
1496 let mut groups = BTreeMap::new();
1497 for group in project.resolved_dependency_groups().keys() {
1498 if let Some(dependency) = self.find_project_dependency_group(
1499 project,
1500 group,
1501 dependency_name,
1502 marker_environment,
1503 )? {
1504 groups.insert(group, dependency);
1505 }
1506 }
1507 (None, production, groups)
1508 } else {
1509 let root_applies = self.manifest.requirements.iter().any(|requirement| {
1510 &requirement.name == dependency_name
1511 && requirement.marker.evaluate(marker_environment, &[])
1512 });
1513 let group_applies =
1514 self.manifest
1515 .dependency_groups
1516 .values()
1517 .flatten()
1518 .any(|requirement| {
1519 &requirement.name == dependency_name
1520 && requirement.marker.evaluate(marker_environment, &[])
1521 });
1522
1523 let package = if root_applies || group_applies {
1527 self.find_by_markers(dependency_name, marker_environment)?
1528 } else {
1529 None
1530 };
1531 let root = package.and_then(|package| {
1532 let mut applicable = self.manifest.requirements.iter().filter(|requirement| {
1533 &requirement.name == dependency_name
1534 && requirement.marker.evaluate(marker_environment, &[])
1535 });
1536 let requirement = applicable.next()?;
1537 let mut selection = SelectedDependency::from_requirement(package, requirement);
1538 for requirement in applicable {
1539 selection.extend_requirement(requirement);
1540 }
1541 Some(selection)
1542 });
1543 let mut groups = BTreeMap::new();
1544 if let Some(package) = package {
1545 for (group, requirements) in &self.manifest.dependency_groups {
1546 let mut applicable = requirements.iter().filter(|requirement| {
1547 &requirement.name == dependency_name
1548 && requirement.marker.evaluate(marker_environment, &[])
1549 });
1550 let Some(requirement) = applicable.next() else {
1551 continue;
1552 };
1553 let mut selection = SelectedDependency::from_requirement(package, requirement);
1554 for requirement in applicable {
1555 selection.extend_requirement(requirement);
1556 }
1557 groups.insert(group, selection);
1558 }
1559 }
1560 (root, None, groups)
1561 };
1562 Ok(DependencySelection {
1563 root,
1564 production,
1565 groups,
1566 })
1567 }
1568
1569 fn find_project_dependency_group<'lock>(
1571 &'lock self,
1572 project: &'lock Package,
1573 group: &'lock GroupName,
1574 dependency_name: &PackageName,
1575 marker_environment: &MarkerEnvironment,
1576 ) -> Result<Option<SelectedDependency<'lock>>, String> {
1577 let Some(dependencies) = project.resolved_dependency_groups().get(group) else {
1578 return Ok(None);
1579 };
1580 let project_name = project.name();
1581
1582 let mut selected: Option<SelectedDependency<'lock>> = None;
1583 for dependency in dependencies
1584 .iter()
1585 .filter(|dependency| &dependency.package_id.name == dependency_name)
1586 {
1587 if !dependency.complexified_marker.evaluate(
1593 marker_environment,
1594 std::iter::empty::<&PackageName>(),
1595 dependency
1596 .extra
1597 .iter()
1598 .map(|extra| (&dependency.package_id.name, extra)),
1599 std::iter::once((project_name, group)),
1600 ) {
1601 continue;
1602 }
1603
1604 let package = self.find_by_id(&dependency.package_id);
1605 if selected
1606 .as_ref()
1607 .is_some_and(|selected| selected.package.id != package.id)
1608 {
1609 return Err(format!(
1610 "found multiple packages matching `{dependency_name}` in dependency group `{group}` for `{project_name}`"
1611 ));
1612 }
1613 if let Some(selected) = selected.as_mut() {
1614 selected.extend_dependency(dependency);
1615 } else {
1616 selected = Some(SelectedDependency::from_dependency(
1617 package,
1618 dependency,
1619 DependencySelectionContext::Group(project_name, group),
1620 ));
1621 }
1622 }
1623 Ok(selected)
1624 }
1625
1626 fn find_project_dependency<'lock>(
1628 &'lock self,
1629 project: &'lock Package,
1630 dependency_name: &PackageName,
1631 marker_environment: &MarkerEnvironment,
1632 ) -> Result<Option<SelectedDependency<'lock>>, String> {
1633 let project_name = project.name();
1634
1635 let mut selected: Option<SelectedDependency<'lock>> = None;
1636 for dependency in project
1637 .dependencies()
1638 .iter()
1639 .filter(|dependency| &dependency.package_id.name == dependency_name)
1640 {
1641 if !dependency.complexified_marker.evaluate(
1642 marker_environment,
1643 std::iter::once(project_name),
1644 dependency
1645 .extra
1646 .iter()
1647 .map(|extra| (&dependency.package_id.name, extra)),
1648 std::iter::empty::<(&PackageName, &GroupName)>(),
1649 ) {
1650 continue;
1651 }
1652
1653 let package = self.find_by_id(&dependency.package_id);
1654 if selected
1655 .as_ref()
1656 .is_some_and(|selected| selected.package.id != package.id)
1657 {
1658 return Err(format!(
1659 "found multiple packages matching production dependency `{dependency_name}` for `{project_name}`"
1660 ));
1661 }
1662 if let Some(selected) = selected.as_mut() {
1663 selected.extend_dependency(dependency);
1664 } else {
1665 selected = Some(SelectedDependency::from_dependency(
1666 package,
1667 dependency,
1668 DependencySelectionContext::Production(project_name),
1669 ));
1670 }
1671 }
1672 Ok(selected)
1673 }
1674
1675 pub fn build_constraints(&self, root: &Path) -> Constraints {
1677 Constraints::from_requirements(
1678 self.manifest
1679 .build_constraints
1680 .iter()
1681 .cloned()
1682 .map(|requirement| requirement.to_absolute(root)),
1683 )
1684 }
1685
1686 pub fn auditable<'lock>(
1693 &'lock self,
1694 extras: &'lock ExtrasSpecificationWithDefaults,
1695 groups: &'lock DependencyGroupsWithDefaults,
1696 collect_filter: impl Fn(&Package) -> bool,
1697 ) -> Auditable<'lock> {
1698 let mut by_name_version: BTreeMap<(&PackageName, &Version), &Package> = BTreeMap::default();
1703 self.walk_auditable(extras, groups, collect_filter, |package, version| {
1704 by_name_version
1705 .entry((package.name(), version))
1706 .or_insert(package);
1707 });
1708 let packages = by_name_version
1709 .into_iter()
1710 .map(|((_, version), package)| (package, version))
1711 .collect();
1712 Auditable { packages }
1713 }
1714
1715 fn walk_auditable<'lock, F>(
1725 &'lock self,
1726 extras: &'lock ExtrasSpecificationWithDefaults,
1727 groups: &'lock DependencyGroupsWithDefaults,
1728 collect_filter: impl Fn(&Package) -> bool,
1729 mut visit: F,
1730 ) where
1731 F: FnMut(&'lock Package, &'lock Version),
1732 {
1733 fn enqueue_dep<'lock>(
1735 lock: &'lock Lock,
1736 seen: &mut FxHashSet<(&'lock PackageId, Option<&'lock ExtraName>)>,
1737 queue: &mut VecDeque<(&'lock Package, Option<&'lock ExtraName>)>,
1738 dep: &'lock Dependency,
1739 ) {
1740 let dep_pkg = lock.find_by_id(&dep.package_id);
1741 for maybe_extra in std::iter::once(None).chain(dep.extra.iter().map(Some)) {
1742 if seen.insert((&dep.package_id, maybe_extra)) {
1743 queue.push_back((dep_pkg, maybe_extra));
1744 }
1745 }
1746 }
1747
1748 let workspace_member_ids: FxHashSet<&PackageId> = if self.members().is_empty() {
1750 self.root().into_iter().map(|package| &package.id).collect()
1751 } else {
1752 self.packages
1753 .iter()
1754 .filter(|package| self.members().contains(&package.id.name))
1755 .map(|package| &package.id)
1756 .collect()
1757 };
1758
1759 let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
1761 let mut seen: FxHashSet<(&PackageId, Option<&ExtraName>)> = FxHashSet::default();
1762
1763 for package in self
1766 .packages
1767 .iter()
1768 .filter(|p| workspace_member_ids.contains(&p.id))
1769 {
1770 if seen.insert((&package.id, None)) {
1771 queue.push_back((package, None));
1772 }
1773 if groups.prod() {
1774 for extra in extras.extra_names(package.optional_dependencies.keys()) {
1775 if seen.insert((&package.id, Some(extra))) {
1776 queue.push_back((package, Some(extra)));
1777 }
1778 }
1779 }
1780 }
1781
1782 for requirement in self.requirements() {
1784 for package in self
1785 .packages
1786 .iter()
1787 .filter(|p| p.id.name == requirement.name)
1788 {
1789 if seen.insert((&package.id, None)) {
1790 queue.push_back((package, None));
1791 }
1792 for extra in &*requirement.extras {
1793 if seen.insert((&package.id, Some(extra))) {
1794 queue.push_back((package, Some(extra)));
1795 }
1796 }
1797 }
1798 }
1799
1800 for (group, requirements) in self.dependency_groups() {
1803 if !groups.contains(group) {
1804 continue;
1805 }
1806 for requirement in requirements {
1807 for package in self
1808 .packages
1809 .iter()
1810 .filter(|p| p.id.name == requirement.name)
1811 {
1812 if seen.insert((&package.id, None)) {
1813 queue.push_back((package, None));
1814 }
1815 for extra in &*requirement.extras {
1816 if seen.insert((&package.id, Some(extra))) {
1817 queue.push_back((package, Some(extra)));
1818 }
1819 }
1820 }
1821 }
1822 }
1823
1824 while let Some((package, extra)) = queue.pop_front() {
1825 let is_member = workspace_member_ids.contains(&package.id);
1826
1827 if !is_member && collect_filter(package) {
1830 if let Some(version) = package.version() {
1831 visit(package, version);
1832 } else {
1833 trace!(
1834 "Skipping audit for `{}` because it has no version information",
1835 package.name()
1836 );
1837 }
1838 }
1839
1840 if is_member && extra.is_none() {
1842 for dep in package
1843 .dependency_groups
1844 .iter()
1845 .filter(|(group, _)| groups.contains(group))
1846 .flat_map(|(_, deps)| deps)
1847 {
1848 enqueue_dep(self, &mut seen, &mut queue, dep);
1849 }
1850 }
1851
1852 let dependencies: &[Dependency] = match extra {
1855 Some(extra) => package
1856 .optional_dependencies
1857 .get(extra)
1858 .map(Vec::as_slice)
1859 .unwrap_or_default(),
1860 None if is_member && !groups.prod() => &[],
1861 None => &package.dependencies,
1862 };
1863
1864 for dep in dependencies {
1865 enqueue_dep(self, &mut seen, &mut queue, dep);
1866 }
1867 }
1868 }
1869
1870 pub fn root(&self) -> Option<&Package> {
1872 self.packages.iter().find(|package| {
1873 let (Source::Editable(path) | Source::Virtual(path)) = &package.id.source else {
1874 return false;
1875 };
1876 path.as_ref() == Path::new("")
1877 })
1878 }
1879
1880 pub fn simplified_supported_environments(&self) -> Vec<MarkerTree> {
1890 self.supported_environments()
1891 .iter()
1892 .copied()
1893 .map(|marker| self.simplify_environment(marker))
1894 .collect()
1895 }
1896
1897 pub fn simplified_required_environments(&self) -> Vec<MarkerTree> {
1900 self.required_environments()
1901 .iter()
1902 .copied()
1903 .map(|marker| self.simplify_environment(marker))
1904 .collect()
1905 }
1906
1907 pub fn simplify_environment(&self, marker: MarkerTree) -> MarkerTree {
1910 self.requires_python.simplify_markers(marker)
1911 }
1912
1913 pub fn fork_markers(&self) -> &[UniversalMarker] {
1916 self.fork_markers.as_slice()
1917 }
1918
1919 fn fork_markers_union(&self) -> MarkerTree {
1921 fork_markers_union(&self.fork_markers, &self.requires_python)
1922 }
1923
1924 pub fn check_marker_coverage(&self) -> Result<(), (MarkerTree, MarkerTree)> {
1928 let fork_markers_union = self.fork_markers_union();
1929 let environments_union = implicit_constraints_marker(
1930 self.requires_python.to_marker_tree(),
1931 &self.supported_environments,
1932 );
1933 if fork_markers_union.negate().is_disjoint(environments_union) {
1934 Ok(())
1935 } else {
1936 Err((fork_markers_union, environments_union))
1937 }
1938 }
1939
1940 pub fn requires_python_coverage(
1950 &self,
1951 new_requires_python: &RequiresPython,
1952 ) -> Result<(), (MarkerTree, MarkerTree)> {
1953 let fork_markers_union = self.fork_markers_union();
1954 let new_requires_python = new_requires_python.to_marker_tree();
1955 if fork_markers_union.is_disjoint(new_requires_python) {
1956 Err((fork_markers_union, new_requires_python))
1957 } else {
1958 Ok(())
1959 }
1960 }
1961
1962 pub fn from_canonical_toml(input: &str) -> Result<Self, CanonicalLockError> {
1967 deserialize::from_str(input)
1968 }
1969
1970 pub fn from_toml(input: &str) -> Result<Self, LockParseError> {
1976 let lock = match Self::from_canonical_toml(input) {
1977 Ok(lock) => lock,
1978 Err(_) => match toml::from_str(input) {
1979 Ok(lock) => lock,
1980 Err(source) => {
1981 if let Ok(lock) = toml::from_str::<LockVersion>(input)
1982 && lock.version() != VERSION
1983 {
1984 return Err(LockParseError::UnparsableVersion {
1985 supported: VERSION,
1986 version: lock.version(),
1987 source,
1988 });
1989 }
1990 return Err(LockParseError::Toml(source));
1991 }
1992 },
1993 };
1994
1995 if lock.version() != VERSION {
1996 return Err(LockParseError::UnsupportedVersion {
1997 supported: VERSION,
1998 version: lock.version(),
1999 });
2000 }
2001
2002 Ok(lock)
2003 }
2004
2005 pub fn to_toml(&self) -> Result<String, toml_edit::ser::Error> {
2007 serialize::to_toml(self)
2008 }
2009
2010 fn packages_for_name(&self, name: &PackageName) -> &[Package] {
2012 let first = self
2013 .packages
2014 .partition_point(|package| &package.id.name < name);
2015 let candidates = &self.packages[first..];
2016 &candidates[..candidates.partition_point(|package| &package.id.name == name)]
2017 }
2018
2019 pub fn find_by_name(&self, name: &PackageName) -> Result<Option<&Package>, String> {
2023 let mut found_dist = None;
2024 for dist in &self.packages {
2025 if &dist.id.name == name {
2026 if found_dist.is_some() {
2027 return Err(format!("found multiple packages matching `{name}`"));
2028 }
2029 found_dist = Some(dist);
2030 }
2031 }
2032 Ok(found_dist)
2033 }
2034
2035 fn find_by_markers(
2045 &self,
2046 name: &PackageName,
2047 marker_env: &MarkerEnvironment,
2048 ) -> Result<Option<&Package>, String> {
2049 let mut found_dist = None;
2050 for dist in &self.packages {
2051 if &dist.id.name == name {
2052 if dist.fork_markers.is_empty()
2053 || dist
2054 .fork_markers
2055 .iter()
2056 .any(|marker| marker.evaluate_no_extras(marker_env))
2057 {
2058 if found_dist.is_some() {
2059 return Err(format!("found multiple packages matching `{name}`"));
2060 }
2061 found_dist = Some(dist);
2062 }
2063 }
2064 }
2065 Ok(found_dist)
2066 }
2067
2068 fn find_by_id(&self, id: &PackageId) -> &Package {
2069 let index = *self.by_id.get(id).expect("locked package for ID");
2070
2071 (self.packages.get(index).expect("valid index for package")) as _
2072 }
2073
2074 fn satisfies_provides_extra<'lock>(
2076 &self,
2077 provides_extra: &[ExtraName],
2078 package: &'lock Package,
2079 allow_missing_package_metadata: bool,
2080 ) -> SatisfiesResult<'lock> {
2081 if !self.supports_provides_extra()
2082 || allow_missing_package_metadata && !package.has_metadata()
2083 {
2084 return SatisfiesResult::Satisfied;
2085 }
2086
2087 let expected: BTreeSet<_> = provides_extra.iter().collect();
2088 let actual: BTreeSet<_> = package.metadata.provides_extra.iter().collect();
2089
2090 if expected != actual {
2091 let expected = provides_extra.iter().cloned().collect();
2092 return SatisfiesResult::MismatchedPackageProvidesExtra(
2093 &package.id.name,
2094 package.id.version.as_ref(),
2095 expected,
2096 actual,
2097 );
2098 }
2099
2100 SatisfiesResult::Satisfied
2101 }
2102
2103 fn satisfies_requires_dist<'lock>(
2105 &self,
2106 requires_dist: Box<[Requirement]>,
2107 provides_extra: &[ExtraName],
2108 dependency_groups: BTreeMap<GroupName, Box<[Requirement]>>,
2109 source_requirements: &Constraints,
2110 overrides: &Overrides,
2111 excludes: &Excludes,
2112 package_requires_python: Option<&VersionSpecifiers>,
2113 package: &'lock Package,
2114 activated_extras: &mut FxHashMap<PackageId, BTreeSet<ExtraName>>,
2115 remotes: &mut Option<BTreeSet<UrlString>>,
2116 locals: &mut Option<BTreeSet<Box<Path>>>,
2117 root: &Path,
2118 allow_missing_package_metadata: bool,
2119 ) -> Result<SatisfiesResult<'lock>, LockError> {
2120 let missing_metadata = allow_missing_package_metadata && !package.has_metadata();
2121 let indexes = requires_dist
2122 .iter()
2123 .chain(dependency_groups.values().flatten())
2124 .filter_map(|requirement| match &requirement.source {
2125 RequirementSource::Registry {
2126 index: Some(index), ..
2127 } => Some(index.clone()),
2128 _ => None,
2129 })
2130 .collect::<Vec<_>>();
2131
2132 let flattened = if package.is_dynamic() || missing_metadata {
2134 Some(
2135 FlatRequiresDist::from_requirements(requires_dist.clone(), &package.id.name)
2136 .into_iter()
2137 .map(|requirement| {
2138 normalize_requirement(requirement, root, &self.requires_python)
2139 })
2140 .collect::<Result<BTreeSet<_>, _>>()?,
2141 )
2142 } else {
2143 None
2144 };
2145
2146 let expected_requirements: BTreeSet<_> = Box::into_iter(requires_dist)
2148 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
2149 .collect::<Result<_, _>>()?;
2150 let actual: BTreeSet<_> = package
2151 .metadata
2152 .requires_dist
2153 .iter()
2154 .cloned()
2155 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
2156 .collect::<Result<_, _>>()?;
2157
2158 if !missing_metadata
2159 && expected_requirements != actual
2160 && flattened
2161 .as_ref()
2162 .is_none_or(|expected| expected != &actual)
2163 {
2164 return Ok(SatisfiesResult::MismatchedPackageRequirements(
2165 &package.id.name,
2166 package.id.version.as_ref(),
2167 expected_requirements,
2168 actual,
2169 ));
2170 }
2171
2172 let expected_groups: BTreeMap<GroupName, BTreeSet<Requirement>> = dependency_groups
2174 .into_iter()
2175 .filter(|(_, requirements)| self.includes_empty_groups() || !requirements.is_empty())
2176 .map(|(group, requirements)| {
2177 Ok::<_, LockError>((
2178 group,
2179 Box::into_iter(requirements)
2180 .map(|requirement| {
2181 normalize_requirement(requirement, root, &self.requires_python)
2182 })
2183 .collect::<Result<_, _>>()?,
2184 ))
2185 })
2186 .collect::<Result<_, _>>()?;
2187 let actual: BTreeMap<GroupName, BTreeSet<Requirement>> = package
2188 .metadata
2189 .dependency_groups
2190 .iter()
2191 .filter(|(_, requirements)| self.includes_empty_groups() || !requirements.is_empty())
2192 .map(|(group, requirements)| {
2193 Ok::<_, LockError>((
2194 group.clone(),
2195 requirements
2196 .iter()
2197 .cloned()
2198 .map(|requirement| {
2199 normalize_requirement(requirement, root, &self.requires_python)
2200 })
2201 .collect::<Result<_, _>>()?,
2202 ))
2203 })
2204 .collect::<Result<_, _>>()?;
2205
2206 if !missing_metadata && expected_groups != actual {
2207 return Ok(SatisfiesResult::MismatchedPackageDependencyGroups(
2208 &package.id.name,
2209 package.id.version.as_ref(),
2210 expected_groups,
2211 actual,
2212 ));
2213 }
2214
2215 if allow_missing_package_metadata {
2216 let declarations = flattened.as_ref().unwrap_or(&expected_requirements);
2217 let package_activated_extras = activated_extras
2218 .get(&package.id)
2219 .cloned()
2220 .unwrap_or_default();
2221 let expected = ExpectedPackageDependencies::new(
2222 self,
2223 declarations,
2224 provides_extra,
2225 &expected_groups,
2226 source_requirements,
2227 overrides,
2228 excludes,
2229 package_requires_python,
2230 package,
2231 package_activated_extras,
2232 root,
2233 );
2234 match self.satisfied_no_metadata(
2235 package,
2236 activated_extras,
2237 missing_metadata,
2238 &expected,
2239 )? {
2240 SatisfiesResult::Satisfied => {}
2241 dissatisfied => return Ok(dissatisfied),
2242 }
2243 }
2244
2245 for index in &indexes {
2251 Self::record_index(index, remotes, locals, root);
2252 }
2253
2254 Ok(SatisfiesResult::Satisfied)
2255 }
2256
2257 fn satisfied_no_metadata<'lock>(
2258 &self,
2259 package: &'lock Package,
2260 activated_extras: &mut FxHashMap<PackageId, BTreeSet<ExtraName>>,
2261 missing_metadata: bool,
2262 expected: &ExpectedPackageDependencies<'_>,
2263 ) -> Result<SatisfiesResult<'lock>, LockError> {
2264 for context in expected.contexts() {
2267 if let DependencyContext::Extra(extra) = context
2269 && !expected.provides_extra.contains(extra)
2270 {
2271 if missing_metadata {
2272 return Ok(SatisfiesResult::MismatchedPackageDependencies(
2273 &package.id.name,
2274 package.id.version.as_ref(),
2275 Vec::new(),
2276 context.dependencies(package),
2277 ));
2278 }
2279 continue;
2280 }
2281
2282 let parent_marker = expected.context_parent_marker(context);
2284
2285 let mut generated = Vec::new();
2286 let builder = LockedDependencyBuilder::new(
2287 &self.requires_python,
2288 expected.lock_marker,
2289 parent_marker,
2290 );
2291 let complete =
2292 builder.add_requirements(&mut generated, expected, context, activated_extras)?;
2293 generated.sort();
2294 if !missing_metadata {
2295 continue;
2296 }
2297 let actual = context.dependencies(package);
2298 if !complete
2299 || expected.comparable_dependencies(&generated)
2300 != expected.comparable_dependencies(actual)
2301 {
2302 return Ok(SatisfiesResult::MismatchedPackageDependencies(
2303 &package.id.name,
2304 package.id.version.as_ref(),
2305 generated,
2306 actual,
2307 ));
2308 }
2309 }
2310
2311 Ok(SatisfiesResult::Satisfied)
2312 }
2313
2314 fn record_index(
2315 index: &IndexMetadata,
2316 remotes: &mut Option<BTreeSet<UrlString>>,
2317 locals: &mut Option<BTreeSet<Box<Path>>>,
2318 root: &Path,
2319 ) {
2320 match &index.url {
2321 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
2322 if let Some(remotes) = remotes.as_mut() {
2323 remotes.insert(UrlString::from(index.url().without_credentials().as_ref()));
2324 }
2325 }
2326 IndexUrl::Path(url) => {
2327 if let Some(locals) = locals.as_mut()
2328 && let Some(path) = url.to_file_path().ok().and_then(|path| {
2329 try_relative_to_if(&path, root, !url.was_given_absolute()).ok()
2330 })
2331 {
2332 locals.insert(path.into_boxed_path());
2333 }
2334 }
2335 }
2336 }
2337
2338 #[instrument(skip_all)]
2340 pub async fn satisfies<Context: BuildContext>(
2341 &self,
2342 root: &Path,
2343 packages: &BTreeMap<PackageName, WorkspaceMember>,
2344 members: &[PackageName],
2345 required_members: &BTreeMap<PackageName, Editability>,
2346 requirements: &[Requirement],
2347 constraints: &[Requirement],
2348 overrides: &[Override<Requirement>],
2349 excludes: &[ExcludeDependency],
2350 build_constraints: &[Requirement],
2351 dependency_groups: &BTreeMap<GroupName, Vec<Requirement>>,
2352 dependency_metadata: &DependencyMetadata,
2353 indexes: Option<&IndexLocations>,
2354 tags: &Tags,
2355 markers: &MarkerEnvironment,
2356 build_options: &BuildOptions,
2357 hasher: &HashStrategy,
2358 index: &InMemoryIndex,
2359 database: &DistributionDatabase<'_, Context>,
2360 allow_missing_package_metadata: bool,
2361 ) -> Result<SatisfiesResult<'_>, LockError> {
2362 let allow_missing_package_metadata =
2363 allow_missing_package_metadata && self.supports_missing_package_metadata();
2364 let mut queue: VecDeque<&Package> = VecDeque::new();
2365 let mut seen = FxHashSet::default();
2366 let mut activated_extras: FxHashMap<PackageId, BTreeSet<ExtraName>> = FxHashMap::default();
2367 let mut validated_extras: FxHashMap<PackageId, BTreeSet<ExtraName>> = FxHashMap::default();
2368
2369 {
2371 let expected = members.iter().cloned().collect::<BTreeSet<_>>();
2372 let actual = &self.manifest.members;
2373 if expected != *actual {
2374 return Ok(SatisfiesResult::MismatchedMembers(expected, actual));
2375 }
2376 }
2377
2378 for (name, member) in packages {
2381 let source = self.find_by_name(name).ok().flatten();
2382
2383 let value = required_members.get(name);
2385 let is_required_member = value.is_some();
2386 let editability = value.copied().flatten();
2387
2388 let expected_virtual = !member.pyproject_toml().is_package(!is_required_member);
2390 let actual_virtual =
2391 source.map(|package| matches!(package.id.source, Source::Virtual(..)));
2392 if actual_virtual != Some(expected_virtual) {
2393 return Ok(SatisfiesResult::MismatchedVirtual(
2394 name.clone(),
2395 expected_virtual,
2396 ));
2397 }
2398
2399 let expected_editable = if expected_virtual {
2401 false
2402 } else {
2403 editability.unwrap_or(true)
2404 };
2405 let actual_editable =
2406 source.map(|package| matches!(package.id.source, Source::Editable(..)));
2407 if actual_editable != Some(expected_editable) {
2408 return Ok(SatisfiesResult::MismatchedEditable(
2409 name.clone(),
2410 expected_editable,
2411 ));
2412 }
2413 }
2414
2415 {
2417 let expected: BTreeSet<_> = requirements
2418 .iter()
2419 .cloned()
2420 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
2421 .collect::<Result<_, _>>()?;
2422 let actual: BTreeSet<_> = self
2423 .manifest
2424 .requirements
2425 .iter()
2426 .cloned()
2427 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
2428 .collect::<Result<_, _>>()?;
2429 if expected != actual {
2430 return Ok(SatisfiesResult::MismatchedRequirements(expected, actual));
2431 }
2432 }
2433
2434 let normalized_constraints = {
2436 let expected: BTreeSet<_> = constraints
2437 .iter()
2438 .cloned()
2439 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
2440 .collect::<Result<_, _>>()?;
2441 let actual: BTreeSet<_> = self
2442 .manifest
2443 .constraints
2444 .iter()
2445 .cloned()
2446 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
2447 .collect::<Result<_, _>>()?;
2448 if expected != actual {
2449 return Ok(SatisfiesResult::MismatchedConstraints(expected, actual));
2450 }
2451 expected
2452 };
2453
2454 let normalized_overrides = {
2456 let normalize = |entry: Override<Requirement>| -> Result<_, LockError> {
2457 match entry {
2458 Override::Requirement(requirement) => Ok(Override::Requirement(
2459 normalize_requirement(requirement, root, &self.requires_python)?,
2460 )),
2461 Override::Package(package) => Ok(Override::Package(PackageOverride {
2462 package: package.package,
2463 dependencies: package
2464 .dependencies
2465 .into_vec()
2466 .into_iter()
2467 .map(|requirement| {
2468 normalize_requirement(requirement, root, &self.requires_python)
2469 })
2470 .collect::<Result<Vec<_>, _>>()?
2471 .into_boxed_slice(),
2472 })),
2473 }
2474 };
2475 let expected: BTreeSet<_> = overrides
2476 .iter()
2477 .cloned()
2478 .map(normalize)
2479 .collect::<Result<_, _>>()?;
2480 let actual: BTreeSet<_> = self
2481 .manifest
2482 .overrides
2483 .iter()
2484 .cloned()
2485 .map(normalize)
2486 .collect::<Result<_, _>>()?;
2487 if expected != actual {
2488 return Ok(SatisfiesResult::MismatchedOverrides(expected, actual));
2489 }
2490 expected
2491 };
2492
2493 {
2495 let expected: BTreeSet<_> = excludes.iter().cloned().collect();
2496 let actual: BTreeSet<_> = self.manifest.excludes.iter().cloned().collect();
2497 if expected != actual {
2498 return Ok(SatisfiesResult::MismatchedExcludes(expected, actual));
2499 }
2500 }
2501
2502 let dependency_overrides = if allow_missing_package_metadata {
2503 Overrides::from_entries(normalized_overrides.into_iter().collect())
2504 .map_err(LockErrorKind::InvalidScopedOverride)?
2505 } else {
2506 Overrides::default()
2507 };
2508 let dependency_excludes = if allow_missing_package_metadata {
2509 Excludes::from_entries(excludes.iter().cloned())
2510 } else {
2511 Excludes::default()
2512 };
2513 let mut source_tree_metadata = FxHashMap::default();
2514 let dependency_sources = if allow_missing_package_metadata {
2515 self.collect_dependency_sources(
2516 normalized_constraints,
2517 requirements,
2518 dependency_groups,
2519 dependency_metadata,
2520 &dependency_overrides,
2521 &dependency_excludes,
2522 root,
2523 tags,
2524 markers,
2525 build_options,
2526 hasher,
2527 index,
2528 database,
2529 &mut source_tree_metadata,
2530 )
2531 .await?
2532 } else {
2533 Constraints::default()
2534 };
2535
2536 {
2538 let expected: BTreeSet<_> = build_constraints
2539 .iter()
2540 .cloned()
2541 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
2542 .collect::<Result<_, _>>()?;
2543 let actual: BTreeSet<_> = self
2544 .manifest
2545 .build_constraints
2546 .iter()
2547 .cloned()
2548 .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
2549 .collect::<Result<_, _>>()?;
2550 if expected != actual {
2551 return Ok(SatisfiesResult::MismatchedBuildConstraints(
2552 expected, actual,
2553 ));
2554 }
2555 }
2556
2557 {
2559 let expected: BTreeMap<GroupName, BTreeSet<Requirement>> = dependency_groups
2560 .iter()
2561 .filter(|(_, requirements)| !requirements.is_empty())
2562 .map(|(group, requirements)| {
2563 Ok::<_, LockError>((
2564 group.clone(),
2565 requirements
2566 .iter()
2567 .cloned()
2568 .map(|requirement| {
2569 normalize_requirement(requirement, root, &self.requires_python)
2570 })
2571 .collect::<Result<_, _>>()?,
2572 ))
2573 })
2574 .collect::<Result<_, _>>()?;
2575 let actual: BTreeMap<GroupName, BTreeSet<Requirement>> = self
2576 .manifest
2577 .dependency_groups
2578 .iter()
2579 .filter(|(_, requirements)| !requirements.is_empty())
2580 .map(|(group, requirements)| {
2581 Ok::<_, LockError>((
2582 group.clone(),
2583 requirements
2584 .iter()
2585 .cloned()
2586 .map(|requirement| {
2587 normalize_requirement(requirement, root, &self.requires_python)
2588 })
2589 .collect::<Result<_, _>>()?,
2590 ))
2591 })
2592 .collect::<Result<_, _>>()?;
2593 if expected != actual {
2594 return Ok(SatisfiesResult::MismatchedDependencyGroups(
2595 expected, actual,
2596 ));
2597 }
2598 }
2599
2600 {
2602 let expected = dependency_metadata
2603 .values()
2604 .cloned()
2605 .collect::<BTreeSet<_>>();
2606 let actual = &self.manifest.dependency_metadata;
2607 if expected != *actual {
2608 return Ok(SatisfiesResult::MismatchedStaticMetadata(expected, actual));
2609 }
2610 }
2611
2612 let mut remotes = indexes.map(|locations| {
2614 locations
2615 .allowed_indexes()
2616 .into_iter()
2617 .filter_map(|index| match index.url() {
2618 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
2619 Some(UrlString::from(index.url().without_credentials().as_ref()))
2620 }
2621 IndexUrl::Path(_) => None,
2622 })
2623 .collect::<BTreeSet<_>>()
2624 });
2625
2626 let mut locals = indexes.map(|locations| {
2627 locations
2628 .allowed_indexes()
2629 .into_iter()
2630 .filter_map(|index| match index.url() {
2631 IndexUrl::Pypi(_) | IndexUrl::Url(_) => None,
2632 IndexUrl::Path(url) => {
2633 let path = url.to_file_path().ok()?;
2634 let path = try_relative_to_if(&path, root, !url.was_given_absolute())
2635 .ok()?
2636 .into_boxed_path();
2637 Some(path)
2638 }
2639 })
2640 .collect::<BTreeSet<_>>()
2641 });
2642
2643 for root_name in packages.keys() {
2645 let root = self
2646 .find_by_name(root_name)
2647 .expect("found too many packages matching root");
2648
2649 let Some(root) = root else {
2650 return Ok(SatisfiesResult::MissingRoot(root_name.clone()));
2652 };
2653
2654 if seen.insert(&root.id) {
2655 queue.push_back(root);
2656 }
2657 }
2658
2659 let root_requirements = requirements
2662 .iter()
2663 .chain(dependency_groups.values().flatten())
2664 .collect::<Vec<_>>();
2665
2666 for requirement in &root_requirements {
2667 if let RequirementSource::Registry {
2668 index: Some(index), ..
2669 } = &requirement.source
2670 {
2671 Self::record_index(index, &mut remotes, &mut locals, root);
2672 }
2673 }
2674
2675 if !root_requirements.is_empty() {
2676 for requirement in root_requirements {
2677 for package in self.packages_for_name(&requirement.name) {
2678 if !package.id.source.is_source_tree() {
2679 continue;
2680 }
2681
2682 let marker = if package.fork_markers.is_empty() {
2683 requirement.marker
2684 } else {
2685 let mut combined = MarkerTree::FALSE;
2686 for fork_marker in &package.fork_markers {
2687 combined = combined.or(fork_marker.pep508());
2688 }
2689 combined = combined.and(requirement.marker);
2690 combined
2691 };
2692 if marker.is_false() {
2693 continue;
2694 }
2695 if !marker.evaluate(markers, &[]) {
2696 continue;
2697 }
2698
2699 activated_extras
2700 .entry(package.id.clone())
2701 .or_default()
2702 .extend(requirement.extras.iter().cloned());
2703
2704 if seen.insert(&package.id) {
2705 queue.push_back(package);
2706 }
2707 }
2708 }
2709 }
2710
2711 while let Some(package) = queue.pop_front() {
2712 if let Source::Registry(index) = &package.id.source {
2714 match index {
2715 RegistrySource::Url(url) => {
2716 if remotes
2717 .as_ref()
2718 .is_some_and(|remotes| !remotes.contains(url))
2719 {
2720 let name = &package.id.name;
2721 let version = &package
2722 .id
2723 .version
2724 .as_ref()
2725 .expect("version for registry source");
2726 return Ok(SatisfiesResult::MissingRemoteIndex(name, version, url));
2727 }
2728 }
2729 RegistrySource::Path(path) => {
2730 if locals.as_ref().is_some_and(|locals| !locals.contains(path)) {
2731 let name = &package.id.name;
2732 let version = &package
2733 .id
2734 .version
2735 .as_ref()
2736 .expect("version for registry source");
2737 return Ok(SatisfiesResult::MissingLocalIndex(name, version, path));
2738 }
2739 }
2740 }
2741 }
2742
2743 if package.id.source.is_immutable() {
2745 continue;
2746 }
2747
2748 if matches!(&package.id.source, Source::Direct(..))
2752 && database.client().unmanaged.connectivity().is_offline()
2753 {
2754 trace!(
2755 "Skipping metadata validation for `{}` because its direct URL cannot be refreshed while offline",
2756 package.id
2757 );
2758 } else if let Some(version) = package.id.version.as_ref() {
2759 let statically_satisfied = if let Some(source_tree) =
2763 package.id.source.as_source_tree()
2764 && let Some(SourceTreeRequiresDist {
2765 version: static_version,
2766 requires_python,
2767 metadata,
2768 }) = Self::source_tree_requires_dist_cached(
2769 source_tree,
2770 root,
2771 package,
2772 database,
2773 &mut source_tree_metadata,
2774 )
2775 .await?
2776 {
2777 if metadata.dynamic {
2780 return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, false));
2781 }
2782
2783 if let Some(static_version) = static_version {
2784 if static_version != *version {
2786 return Ok(SatisfiesResult::MismatchedVersion(
2787 &package.id.name,
2788 version.clone(),
2789 Some(static_version),
2790 ));
2791 }
2792
2793 match self.satisfies_provides_extra(
2795 &metadata.provides_extra,
2796 package,
2797 allow_missing_package_metadata,
2798 ) {
2799 SatisfiesResult::Satisfied => {}
2800 result => return Ok(result),
2801 }
2802
2803 match self.satisfies_requires_dist(
2805 metadata.requires_dist,
2806 &metadata.provides_extra,
2807 metadata.dependency_groups,
2808 &dependency_sources,
2809 &dependency_overrides,
2810 &dependency_excludes,
2811 requires_python.as_ref(),
2812 package,
2813 &mut activated_extras,
2814 &mut remotes,
2815 &mut locals,
2816 root,
2817 allow_missing_package_metadata,
2818 )? {
2819 SatisfiesResult::Satisfied => true,
2820 result => return Ok(result),
2821 }
2822 } else {
2823 false
2824 }
2825 } else {
2826 false
2827 };
2828
2829 if !statically_satisfied {
2830 let metadata = Self::package_metadata(
2833 package,
2834 root,
2835 tags,
2836 markers,
2837 build_options,
2838 hasher,
2839 index,
2840 database,
2841 )
2842 .await?;
2843
2844 if package.id.source.is_source_tree() && metadata.dynamic {
2847 return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, false));
2848 }
2849
2850 if metadata.version != *version {
2852 return Ok(SatisfiesResult::MismatchedVersion(
2853 &package.id.name,
2854 version.clone(),
2855 Some(metadata.version.clone()),
2856 ));
2857 }
2858
2859 match self.satisfies_provides_extra(
2861 &metadata.provides_extra,
2862 package,
2863 allow_missing_package_metadata,
2864 ) {
2865 SatisfiesResult::Satisfied => {}
2866 result => return Ok(result),
2867 }
2868
2869 match self.satisfies_requires_dist(
2871 metadata.requires_dist,
2872 &metadata.provides_extra,
2873 metadata.dependency_groups,
2874 &dependency_sources,
2875 &dependency_overrides,
2876 &dependency_excludes,
2877 metadata.requires_python.as_ref(),
2878 package,
2879 &mut activated_extras,
2880 &mut remotes,
2881 &mut locals,
2882 root,
2883 allow_missing_package_metadata,
2884 )? {
2885 SatisfiesResult::Satisfied => {}
2886 result => return Ok(result),
2887 }
2888 }
2889 } else if let Some(source_tree) = package.id.source.as_source_tree() {
2890 let metadata = Self::source_tree_requires_dist_cached(
2900 source_tree,
2901 root,
2902 package,
2903 database,
2904 &mut source_tree_metadata,
2905 )
2906 .await?;
2907
2908 let satisfied = metadata.is_some_and(|SourceTreeRequiresDist {
2909 requires_python,
2910 metadata,
2911 ..
2912 }| {
2913 if !metadata.dynamic {
2915 debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
2916 return false;
2917 }
2918
2919 if let SatisfiesResult::Satisfied = self.satisfies_provides_extra(
2921 &metadata.provides_extra,
2922 package,
2923 allow_missing_package_metadata,
2924 ) {
2925 debug!("Static `provides-extra` for `{}` is up-to-date", package.id);
2926 } else {
2927 debug!("Static `provides-extra` for `{}` is out-of-date; falling back to distribution database", package.id);
2928 return false;
2929 }
2930
2931 match self.satisfies_requires_dist(
2933 metadata.requires_dist,
2934 &metadata.provides_extra,
2935 metadata.dependency_groups,
2936 &dependency_sources,
2937 &dependency_overrides,
2938 &dependency_excludes,
2939 requires_python.as_ref(),
2940 package,
2941 &mut activated_extras,
2942 &mut remotes,
2943 &mut locals,
2944 root,
2945 allow_missing_package_metadata,
2946 ) {
2947 Ok(SatisfiesResult::Satisfied) => {
2948 debug!("Static `requires-dist` for `{}` is up-to-date", package.id);
2949 },
2950 Ok(..) => {
2951 debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
2952 return false;
2953 },
2954 Err(..) => {
2955 debug!("Static `requires-dist` for `{}` is invalid; falling back to distribution database", package.id);
2956 return false;
2957 },
2958 }
2959
2960 true
2961 });
2962
2963 if !satisfied {
2969 let metadata = Self::package_metadata(
2970 package,
2971 root,
2972 tags,
2973 markers,
2974 build_options,
2975 hasher,
2976 index,
2977 database,
2978 )
2979 .await?;
2980
2981 if !metadata.dynamic {
2983 return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, true));
2984 }
2985
2986 match self.satisfies_provides_extra(
2988 &metadata.provides_extra,
2989 package,
2990 allow_missing_package_metadata,
2991 ) {
2992 SatisfiesResult::Satisfied => {}
2993 result => return Ok(result),
2994 }
2995
2996 match self.satisfies_requires_dist(
2998 metadata.requires_dist,
2999 &metadata.provides_extra,
3000 metadata.dependency_groups,
3001 &dependency_sources,
3002 &dependency_overrides,
3003 &dependency_excludes,
3004 metadata.requires_python.as_ref(),
3005 package,
3006 &mut activated_extras,
3007 &mut remotes,
3008 &mut locals,
3009 root,
3010 allow_missing_package_metadata,
3011 )? {
3012 SatisfiesResult::Satisfied => {}
3013 result => return Ok(result),
3014 }
3015 }
3016 } else {
3017 return Ok(SatisfiesResult::MissingVersion(&package.id.name));
3018 }
3019
3020 validated_extras.insert(
3023 package.id.clone(),
3024 activated_extras
3025 .get(&package.id)
3026 .cloned()
3027 .unwrap_or_default(),
3028 );
3029 for dependency in package.all_dependencies() {
3030 let needs_extra_validation = validated_extras
3031 .get(&dependency.package_id)
3032 .zip(activated_extras.get(&dependency.package_id))
3033 .is_some_and(|(validated, activated)| !activated.is_subset(validated));
3034 if seen.insert(&dependency.package_id) || needs_extra_validation {
3035 let dependency_package = self.find_by_id(&dependency.package_id);
3036 queue.push_back(dependency_package);
3037 }
3038 }
3039 }
3040
3041 Ok(SatisfiesResult::Satisfied)
3042 }
3043
3044 async fn collect_dependency_sources<Context: BuildContext>(
3046 &self,
3047 mut source_requirements: BTreeSet<Requirement>,
3048 requirements: &[Requirement],
3049 dependency_groups: &BTreeMap<GroupName, Vec<Requirement>>,
3050 dependency_metadata: &DependencyMetadata,
3051 dependency_overrides: &Overrides,
3052 dependency_excludes: &Excludes,
3053 root: &Path,
3054 tags: &Tags,
3055 markers: &MarkerEnvironment,
3056 build_options: &BuildOptions,
3057 hasher: &HashStrategy,
3058 index: &InMemoryIndex,
3059 database: &DistributionDatabase<'_, Context>,
3060 source_tree_metadata: &mut FxHashMap<PackageId, Option<SourceTreeRequiresDist>>,
3061 ) -> Result<Constraints, LockError> {
3062 for requirement in dependency_overrides
3063 .apply_for_package(
3064 None,
3065 requirements
3066 .iter()
3067 .chain(dependency_groups.values().flatten()),
3068 )
3069 .filter(|requirement| {
3070 !dependency_excludes.contains_for_package(None, &requirement.name)
3071 })
3072 {
3073 if matches!(requirement.source, RequirementSource::Registry { .. }) {
3074 continue;
3075 }
3076
3077 source_requirements.insert(normalize_requirement(
3078 requirement.into_owned(),
3079 root,
3080 &self.requires_python,
3081 )?);
3082 }
3083
3084 let mut add_source_requirements = |package: &Package,
3085 requirements: Vec<Requirement>|
3086 -> Result<(), LockError> {
3087 let package_context = package
3088 .id
3089 .version
3090 .as_ref()
3091 .map(|version| (&package.id.name, version));
3092
3093 for requirement in dependency_overrides
3094 .apply_for_package(package_context, &requirements)
3095 .filter(|requirement| {
3096 !dependency_excludes.contains_for_package(package_context, &requirement.name)
3097 })
3098 {
3099 if matches!(requirement.source, RequirementSource::Registry { .. }) {
3100 continue;
3101 }
3102
3103 source_requirements.insert(normalize_requirement(
3104 requirement.into_owned(),
3105 root,
3106 &self.requires_python,
3107 )?);
3108 }
3109
3110 Ok(())
3111 };
3112
3113 for package in &self.packages {
3114 if let Some(metadata) =
3115 dependency_metadata.get(&package.id.name, package.id.version.as_ref())
3116 {
3117 add_source_requirements(
3118 package,
3119 Box::into_iter(metadata.requires_dist)
3120 .map(Requirement::from)
3121 .collect(),
3122 )?;
3123 continue;
3124 }
3125
3126 if package
3127 .all_dependencies()
3128 .all(|dependency| matches!(dependency.package_id.source, Source::Registry(..)))
3129 {
3130 continue;
3131 }
3132
3133 let Some(source_tree) = package.id.source.as_source_tree() else {
3134 continue;
3135 };
3136 let (requires_dist, dependency_groups) =
3137 if let Some(SourceTreeRequiresDist { metadata, .. }) =
3138 Self::source_tree_requires_dist_cached(
3139 source_tree,
3140 root,
3141 package,
3142 database,
3143 source_tree_metadata,
3144 )
3145 .await?
3146 {
3147 (metadata.requires_dist, metadata.dependency_groups)
3148 } else {
3149 let metadata = Self::package_metadata(
3150 package,
3151 root,
3152 tags,
3153 markers,
3154 build_options,
3155 hasher,
3156 index,
3157 database,
3158 )
3159 .await?;
3160 (metadata.requires_dist, metadata.dependency_groups)
3161 };
3162 let direct_requirements = requires_dist
3163 .into_vec()
3164 .into_iter()
3165 .chain(
3166 dependency_groups
3167 .into_values()
3168 .flat_map(<[Requirement]>::into_vec),
3169 )
3170 .collect();
3171 add_source_requirements(package, direct_requirements)?;
3172 }
3173
3174 Ok(Constraints::from_requirements(
3175 source_requirements.into_iter(),
3176 ))
3177 }
3178
3179 async fn package_metadata<Context: BuildContext>(
3181 package: &Package,
3182 root: &Path,
3183 tags: &Tags,
3184 markers: &MarkerEnvironment,
3185 build_options: &BuildOptions,
3186 hasher: &HashStrategy,
3187 index: &InMemoryIndex,
3188 database: &DistributionDatabase<'_, Context>,
3189 ) -> Result<DistributionMetadata, LockError> {
3190 let HashedDist { dist, .. } = package.to_dist(
3191 root,
3192 TagPolicy::Preferred(tags),
3193 build_options,
3194 markers,
3195 FirstParty::No,
3196 )?;
3197 let id = dist.distribution_id();
3198 if let Some(archive) = index
3199 .distributions()
3200 .get(&id)
3201 .as_deref()
3202 .and_then(|response| {
3203 if let MetadataResponse::Found(archive, ..) = response {
3204 Some(archive)
3205 } else {
3206 None
3207 }
3208 })
3209 {
3210 return Ok(archive.metadata.clone());
3211 }
3212
3213 let archive = database
3214 .get_or_build_wheel_metadata(&dist, hasher.get(&dist))
3215 .await
3216 .map_err(|err| LockErrorKind::Resolution {
3217 id: package.id.clone(),
3218 err,
3219 })?;
3220 let metadata = archive.metadata.clone();
3221 index
3222 .distributions()
3223 .done(id, Arc::new(MetadataResponse::Found(archive)));
3224 Ok(metadata)
3225 }
3226
3227 async fn source_tree_requires_dist<Context: BuildContext>(
3228 source_tree: &Path,
3229 root: &Path,
3230 package: &Package,
3231 database: &DistributionDatabase<'_, Context>,
3232 ) -> Result<Option<SourceTreeRequiresDist>, LockError> {
3233 let parent = root.join(source_tree);
3234 let path = parent.join("pyproject.toml");
3235 match fs_err::tokio::read_to_string(&path).await {
3236 Ok(contents) => {
3237 let pyproject_toml = PyProjectToml::from_toml(&contents, path.user_display())
3238 .map_err(|err| LockErrorKind::InvalidPyprojectToml {
3239 path: path.clone(),
3240 err,
3241 })?;
3242 let version = pyproject_toml
3243 .project
3244 .as_ref()
3245 .and_then(|project| project.version.clone());
3246 let requires_python = match pyproject_toml.requires_python() {
3247 Ok(requires_python) => requires_python,
3248 Err(
3249 uv_pypi_types::MetadataError::FieldNotFound("project")
3250 | uv_pypi_types::MetadataError::DynamicField("requires-python"),
3251 ) => None,
3252 Err(err) => {
3253 return Err(LockErrorKind::InvalidPyprojectToml {
3254 path: path.clone(),
3255 err,
3256 }
3257 .into());
3258 }
3259 };
3260 let metadata = database
3261 .requires_dist(&parent, &pyproject_toml)
3262 .await
3263 .map_err(|err| LockErrorKind::Resolution {
3264 id: package.id.clone(),
3265 err,
3266 })?;
3267 Ok(metadata.map(|metadata| SourceTreeRequiresDist {
3268 version,
3269 requires_python,
3270 metadata,
3271 }))
3272 }
3273 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
3274 Err(err) => Err(LockErrorKind::UnreadablePyprojectToml { path, err }.into()),
3275 }
3276 }
3277
3278 async fn source_tree_requires_dist_cached<Context: BuildContext>(
3280 source_tree: &Path,
3281 root: &Path,
3282 package: &Package,
3283 database: &DistributionDatabase<'_, Context>,
3284 cache: &mut FxHashMap<PackageId, Option<SourceTreeRequiresDist>>,
3285 ) -> Result<Option<SourceTreeRequiresDist>, LockError> {
3286 if let Some(metadata) = cache.get(&package.id) {
3287 return Ok(metadata.clone());
3288 }
3289
3290 let metadata =
3291 Self::source_tree_requires_dist(source_tree, root, package, database).await?;
3292 cache.insert(package.id.clone(), metadata.clone());
3293 Ok(metadata)
3294 }
3295}
3296
3297#[derive(Debug)]
3305pub struct Auditable<'lock> {
3306 packages: Vec<(&'lock Package, &'lock Version)>,
3308}
3309
3310#[derive(Clone)]
3311struct SourceTreeRequiresDist {
3312 version: Option<Version>,
3313 requires_python: Option<VersionSpecifiers>,
3314 metadata: RequiresDist,
3315}
3316
3317impl<'lock> Auditable<'lock> {
3318 pub fn len(&self) -> usize {
3320 self.packages.len()
3321 }
3322
3323 pub fn is_empty(&self) -> bool {
3325 self.packages.is_empty()
3326 }
3327
3328 pub fn packages(&self) -> impl Iterator<Item = (&'lock PackageName, &'lock Version)> + '_ {
3330 self.packages
3331 .iter()
3332 .map(|(package, version)| (package.name(), *version))
3333 }
3334
3335 pub fn projects(&self, root: &Path) -> Result<Vec<(&'lock PackageName, IndexUrl)>, LockError> {
3339 let mut seen: FxHashSet<(&PackageName, String)> = FxHashSet::default();
3340 let mut projects: Vec<(&PackageName, IndexUrl)> = Vec::with_capacity(self.packages.len());
3341 for (package, _version) in &self.packages {
3342 if let Some(index) = package.index(root)?
3343 && seen.insert((package.name(), index.url().to_string()))
3344 {
3345 projects.push((package.name(), index));
3346 }
3347 }
3348 Ok(projects)
3349 }
3350}
3351
3352#[derive(Debug, Copy, Clone)]
3353enum TagPolicy<'tags> {
3354 Required(&'tags Tags),
3356 Preferred(&'tags Tags),
3359}
3360
3361impl<'tags> TagPolicy<'tags> {
3362 fn tags(&self) -> &'tags Tags {
3364 match self {
3365 Self::Required(tags) | Self::Preferred(tags) => tags,
3366 }
3367 }
3368}
3369
3370#[derive(Debug)]
3372pub enum SatisfiesResult<'lock> {
3373 Satisfied,
3375 MismatchedMembers(BTreeSet<PackageName>, &'lock BTreeSet<PackageName>),
3377 MismatchedVirtual(PackageName, bool),
3379 MismatchedEditable(PackageName, bool),
3381 MismatchedDynamic(&'lock PackageName, bool),
3383 MismatchedVersion(&'lock PackageName, Version, Option<Version>),
3385 MismatchedRequirements(BTreeSet<Requirement>, BTreeSet<Requirement>),
3387 MismatchedConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
3389 MismatchedOverrides(
3391 BTreeSet<Override<Requirement>>,
3392 BTreeSet<Override<Requirement>>,
3393 ),
3394 MismatchedExcludes(BTreeSet<ExcludeDependency>, BTreeSet<ExcludeDependency>),
3396 MismatchedBuildConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
3398 MismatchedDependencyGroups(
3400 BTreeMap<GroupName, BTreeSet<Requirement>>,
3401 BTreeMap<GroupName, BTreeSet<Requirement>>,
3402 ),
3403 MismatchedStaticMetadata(BTreeSet<StaticMetadata>, &'lock BTreeSet<StaticMetadata>),
3405 MissingRoot(PackageName),
3407 MissingRemoteIndex(&'lock PackageName, &'lock Version, &'lock UrlString),
3409 MissingLocalIndex(&'lock PackageName, &'lock Version, &'lock Path),
3411 MismatchedPackageRequirements(
3413 &'lock PackageName,
3414 Option<&'lock Version>,
3415 BTreeSet<Requirement>,
3416 BTreeSet<Requirement>,
3417 ),
3418 MismatchedPackageDependencies(
3420 &'lock PackageName,
3421 Option<&'lock Version>,
3422 Vec<Dependency>,
3423 &'lock [Dependency],
3424 ),
3425 MismatchedPackageProvidesExtra(
3427 &'lock PackageName,
3428 Option<&'lock Version>,
3429 BTreeSet<ExtraName>,
3430 BTreeSet<&'lock ExtraName>,
3431 ),
3432 MismatchedPackageDependencyGroups(
3434 &'lock PackageName,
3435 Option<&'lock Version>,
3436 BTreeMap<GroupName, BTreeSet<Requirement>>,
3437 BTreeMap<GroupName, BTreeSet<Requirement>>,
3438 ),
3439 MissingVersion(&'lock PackageName),
3441}
3442
3443#[derive(Clone, Debug, Default, PartialEq, Eq)]
3445struct ResolverOptions {
3446 resolution_mode: ResolutionMode,
3448 prerelease: Prerelease,
3450 fork_strategy: ForkStrategy,
3452 exclude_newer: ExcludeNewer,
3454}
3455
3456#[derive(Clone, Debug, Default, serde::Deserialize)]
3458#[serde(rename_all = "kebab-case")]
3459struct ResolverOptionsWire {
3460 #[serde(default)]
3462 resolution_mode: ResolutionMode,
3463 #[serde(flatten)]
3465 prerelease: PrereleaseWire,
3466 #[serde(default)]
3468 fork_strategy: ForkStrategy,
3469 #[serde(flatten)]
3471 exclude_newer: ExcludeNewerWire,
3472}
3473
3474#[derive(Clone, Debug, Default, serde::Deserialize)]
3475#[serde(rename_all = "kebab-case")]
3476struct PrereleaseWire {
3477 #[serde(default)]
3478 prerelease_mode: PrereleaseMode,
3479 #[serde(default)]
3480 prerelease_package: PrereleasePackage,
3481}
3482
3483impl From<PrereleaseWire> for Prerelease {
3484 fn from(wire: PrereleaseWire) -> Self {
3485 Self {
3486 global: wire.prerelease_mode,
3487 package: wire.prerelease_package,
3488 }
3489 }
3490}
3491
3492#[expect(clippy::struct_field_names)]
3493#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
3494#[serde(rename_all = "kebab-case")]
3495struct ExcludeNewerWire {
3496 exclude_newer: Option<Timestamp>,
3497 exclude_newer_span: Option<ExcludeNewerSpan>,
3498 #[serde(default, skip_serializing_if = "ExcludeNewerPackage::is_empty")]
3499 exclude_newer_package: ExcludeNewerPackage,
3500}
3501
3502impl From<ExcludeNewerWire> for ExcludeNewer {
3503 fn from(wire: ExcludeNewerWire) -> Self {
3504 let global = match (wire.exclude_newer, wire.exclude_newer_span) {
3505 (Some(timestamp), None) => Some(ExcludeNewerValue::absolute(timestamp)),
3506 (Some(_), Some(span)) => Some(ExcludeNewerValue::relative(span)),
3509 (None, Some(span)) => Some(ExcludeNewerValue::relative(span)),
3512 (None, None) => None,
3513 };
3514 Self {
3515 global,
3516 package: wire.exclude_newer_package,
3517 }
3518 }
3519}
3520
3521impl From<ExcludeNewer> for ExcludeNewerWire {
3522 fn from(exclude_newer: ExcludeNewer) -> Self {
3523 let (timestamp, span) = match exclude_newer.global {
3524 Some(ExcludeNewerValue::Absolute(timestamp)) => (Some(timestamp), None),
3525 Some(ExcludeNewerValue::Relative(span)) => (None, Some(span)),
3526 None => (None, None),
3527 };
3528 Self {
3529 exclude_newer: timestamp,
3530 exclude_newer_span: span,
3531 exclude_newer_package: exclude_newer.package,
3532 }
3533 }
3534}
3535
3536#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
3537#[serde(rename_all = "kebab-case")]
3538pub struct ResolverManifest {
3539 #[serde(default)]
3541 members: BTreeSet<PackageName>,
3542 #[serde(default)]
3547 requirements: BTreeSet<Requirement>,
3548 #[serde(default)]
3554 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
3555 #[serde(default)]
3557 constraints: BTreeSet<Requirement>,
3558 #[serde(default)]
3560 overrides: BTreeSet<Override<Requirement>>,
3561 #[serde(default)]
3563 excludes: BTreeSet<ExcludeDependency>,
3564 #[serde(default)]
3566 build_constraints: BTreeSet<Requirement>,
3567 #[serde(default)]
3569 dependency_metadata: BTreeSet<StaticMetadata>,
3570}
3571
3572impl ResolverManifest {
3573 pub fn new(
3576 members: impl IntoIterator<Item = PackageName>,
3577 requirements: impl IntoIterator<Item = Requirement>,
3578 constraints: impl IntoIterator<Item = Requirement>,
3579 overrides: impl IntoIterator<Item = Override<Requirement>>,
3580 excludes: impl IntoIterator<Item = ExcludeDependency>,
3581 build_constraints: impl IntoIterator<Item = Requirement>,
3582 dependency_groups: impl IntoIterator<Item = (GroupName, Vec<Requirement>)>,
3583 dependency_metadata: impl IntoIterator<Item = StaticMetadata>,
3584 ) -> Self {
3585 Self {
3586 members: members.into_iter().collect(),
3587 requirements: requirements.into_iter().collect(),
3588 constraints: constraints.into_iter().collect(),
3589 overrides: overrides.into_iter().collect(),
3590 excludes: excludes.into_iter().collect(),
3591 build_constraints: build_constraints.into_iter().collect(),
3592 dependency_groups: dependency_groups
3593 .into_iter()
3594 .map(|(group, requirements)| (group, requirements.into_iter().collect()))
3595 .collect(),
3596 dependency_metadata: dependency_metadata.into_iter().collect(),
3597 }
3598 }
3599
3600 pub fn relative_to(self, root: &Path) -> Result<Self, io::Error> {
3602 Ok(Self {
3603 members: self.members,
3604 requirements: self
3605 .requirements
3606 .into_iter()
3607 .map(|requirement| requirement.relative_to(root))
3608 .collect::<Result<BTreeSet<_>, _>>()?,
3609 constraints: self
3610 .constraints
3611 .into_iter()
3612 .map(|requirement| requirement.relative_to(root))
3613 .collect::<Result<BTreeSet<_>, _>>()?,
3614 overrides: self
3615 .overrides
3616 .into_iter()
3617 .map(|entry| match entry {
3618 Override::Requirement(requirement) => {
3619 Ok(Override::Requirement(requirement.relative_to(root)?))
3620 }
3621 Override::Package(package) => Ok(Override::Package(PackageOverride {
3622 package: package.package,
3623 dependencies: package
3624 .dependencies
3625 .into_vec()
3626 .into_iter()
3627 .map(|requirement| requirement.relative_to(root))
3628 .collect::<Result<Vec<_>, _>>()?
3629 .into_boxed_slice(),
3630 })),
3631 })
3632 .collect::<Result<BTreeSet<_>, io::Error>>()?,
3633 excludes: self.excludes,
3634 build_constraints: self
3635 .build_constraints
3636 .into_iter()
3637 .map(|requirement| requirement.relative_to(root))
3638 .collect::<Result<BTreeSet<_>, _>>()?,
3639 dependency_groups: self
3640 .dependency_groups
3641 .into_iter()
3642 .map(|(group, requirements)| {
3643 Ok::<_, io::Error>((
3644 group,
3645 requirements
3646 .into_iter()
3647 .map(|requirement| requirement.relative_to(root))
3648 .collect::<Result<BTreeSet<_>, _>>()?,
3649 ))
3650 })
3651 .collect::<Result<BTreeMap<_, _>, _>>()?,
3652 dependency_metadata: self.dependency_metadata,
3653 })
3654 }
3655}
3656
3657#[derive(Clone, Debug, serde::Deserialize)]
3658#[serde(rename_all = "kebab-case")]
3659struct LockWire {
3660 version: u32,
3661 revision: Option<u32>,
3662 requires_python: RequiresPython,
3663 #[serde(rename = "resolution-markers", default)]
3666 fork_markers: Vec<SimplifiedMarkerTree>,
3667 #[serde(rename = "supported-markers", default)]
3668 supported_environments: Vec<SimplifiedMarkerTree>,
3669 #[serde(rename = "required-markers", default)]
3670 required_environments: Vec<SimplifiedMarkerTree>,
3671 #[serde(rename = "conflicts", default)]
3672 conflicts: Option<Conflicts>,
3673 #[serde(default)]
3675 options: ResolverOptionsWire,
3676 #[serde(default)]
3677 manifest: ResolverManifest,
3678 #[serde(rename = "package", alias = "distribution", default)]
3679 packages: Vec<PackageWire>,
3680}
3681
3682impl TryFrom<LockWire> for Lock {
3683 type Error = LockError;
3684
3685 fn try_from(wire: LockWire) -> Result<Self, LockError> {
3686 let mut unambiguous_package_ids: FxHashMap<PackageName, PackageId> = FxHashMap::default();
3691 let mut ambiguous = FxHashSet::default();
3692 for dist in &wire.packages {
3693 if ambiguous.contains(&dist.id.name) {
3694 continue;
3695 }
3696 if let Some(id) = unambiguous_package_ids.remove(&dist.id.name) {
3697 ambiguous.insert(id.name);
3698 continue;
3699 }
3700 unambiguous_package_ids.insert(dist.id.name.clone(), dist.id.clone());
3701 }
3702
3703 let fork_markers = wire
3704 .fork_markers
3705 .into_iter()
3706 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
3707 .map(UniversalMarker::from_combined)
3708 .collect::<Vec<_>>();
3709 let environment = SimplifiedMarkerTree::new(
3710 &wire.requires_python,
3711 fork_markers_union(&fork_markers, &wire.requires_python),
3712 );
3713 let default =
3716 UniversalMarker::from_combined(environment.into_marker(&wire.requires_python));
3717 let packages = wire
3718 .packages
3719 .into_iter()
3720 .map(|dist| {
3721 dist.unwire(
3722 &wire.requires_python,
3723 environment,
3724 default,
3725 &unambiguous_package_ids,
3726 )
3727 })
3728 .collect::<Result<Vec<_>, _>>()?;
3729 let supported_environments = wire
3730 .supported_environments
3731 .into_iter()
3732 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
3733 .collect();
3734 let required_environments = wire
3735 .required_environments
3736 .into_iter()
3737 .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
3738 .collect();
3739 let mut options_wire = wire.options;
3740 if options_wire.exclude_newer.exclude_newer_span.is_some() {
3741 options_wire.exclude_newer.exclude_newer = None;
3742 }
3743 let options = ResolverOptions {
3744 resolution_mode: options_wire.resolution_mode,
3745 prerelease: options_wire.prerelease.into(),
3746 fork_strategy: options_wire.fork_strategy,
3747 exclude_newer: options_wire.exclude_newer.into(),
3748 };
3749 let lock = Self::new(
3750 wire.version,
3751 wire.revision.unwrap_or(0),
3752 packages,
3753 wire.requires_python,
3754 options,
3755 wire.manifest,
3756 wire.conflicts.unwrap_or_else(Conflicts::empty),
3757 supported_environments,
3758 required_environments,
3759 fork_markers,
3760 )?;
3761
3762 Ok(lock)
3763 }
3764}
3765
3766#[derive(Clone, Debug, serde::Deserialize)]
3770#[serde(rename_all = "kebab-case")]
3771struct LockVersion {
3772 version: u32,
3773}
3774
3775impl LockVersion {
3776 fn version(&self) -> u32 {
3778 self.version
3779 }
3780}
3781
3782#[derive(Clone, Debug, PartialEq, Eq)]
3783pub struct Package {
3784 pub(crate) id: PackageId,
3785 sdist: Option<SourceDist>,
3786 wheels: Vec<Wheel>,
3787 fork_markers: Vec<UniversalMarker>,
3793 dependencies: Vec<Dependency>,
3795 optional_dependencies: BTreeMap<ExtraName, Vec<Dependency>>,
3797 dependency_groups: BTreeMap<GroupName, Vec<Dependency>>,
3799 metadata: PackageMetadata,
3801}
3802
3803impl Package {
3804 pub fn is_from_pypi_registry(&self) -> bool {
3805 self.id.source.is_pypi_registry()
3806 }
3807
3808 fn from_annotated_dist(
3809 annotated_dist: &AnnotatedDist,
3810 fork_markers: Vec<UniversalMarker>,
3811 root: &Path,
3812 index_locations: &IndexLocations,
3813 ) -> Result<Self, LockError> {
3814 let id = PackageId::from_annotated_dist(annotated_dist, root)?;
3815 let sdist = SourceDist::from_annotated_dist(&id, annotated_dist, index_locations)?;
3816 let wheels = Wheel::from_annotated_dist(annotated_dist, index_locations)?;
3817 let metadata = if id.source.is_immutable() {
3818 PackageMetadata::default()
3819 } else {
3820 PackageMetadata::from_distribution(
3821 annotated_dist
3822 .metadata
3823 .as_ref()
3824 .expect("metadata is present"),
3825 root,
3826 )?
3827 };
3828 Ok(Self {
3829 id,
3830 sdist,
3831 wheels,
3832 fork_markers,
3833 dependencies: vec![],
3834 optional_dependencies: BTreeMap::default(),
3835 dependency_groups: BTreeMap::default(),
3836 metadata,
3837 })
3838 }
3839
3840 fn add_dependencies(
3842 &mut self,
3843 context: DependencyContext<'_>,
3844 requires_python: &RequiresPython,
3845 resolution: &ResolverOutput,
3846 node_index: NodeIndex,
3847 environment: SimplifiedMarkerTree,
3848 root: &Path,
3849 ) -> Result<(), LockError> {
3850 let parent_marker = *resolution.graph[node_index].marker();
3851 let builder = LockedDependencyBuilder::new(requires_python, environment, parent_marker);
3852 for edge in resolution.graph.edges(node_index) {
3853 let ResolutionGraphNode::Dist(distribution) = &resolution.graph[edge.target()] else {
3854 continue;
3855 };
3856
3857 let package_id = PackageId::from_annotated_dist(distribution, root)?;
3858 let extras = distribution.extra.iter().cloned().collect();
3859
3860 builder.add(
3862 context.dependencies_mut(self),
3863 package_id,
3864 extras,
3865 *edge.weight(),
3866 );
3867 }
3868
3869 Ok(())
3870 }
3871
3872 fn to_dist(
3874 &self,
3875 workspace_root: &Path,
3876 tag_policy: TagPolicy<'_>,
3877 build_options: &BuildOptions,
3878 markers: &MarkerEnvironment,
3879 first_party: FirstParty,
3880 ) -> Result<HashedDist, LockError> {
3881 let no_binary = build_options.no_binary_package(&self.id.name);
3882 let no_build = build_options.no_build_package(&self.id.name);
3883
3884 if !no_binary {
3885 if let Some(best_wheel_index) = self.find_best_wheel(tag_policy) {
3886 let hashes = {
3887 let wheel = &self.wheels[best_wheel_index];
3888 HashDigests::from(
3889 wheel
3890 .hash
3891 .iter()
3892 .chain(wheel.zstd.iter().flat_map(|z| z.hash.iter()))
3893 .map(|h| h.0.clone())
3894 .collect::<Vec<_>>(),
3895 )
3896 };
3897
3898 let dist = match &self.id.source {
3899 Source::Registry(source) => {
3900 let wheels = self
3901 .wheels
3902 .iter()
3903 .map(|wheel| wheel.to_registry_wheel(source, workspace_root))
3904 .collect::<Result<_, LockError>>()?;
3905 let reg_built_dist = RegistryBuiltDist {
3906 wheels,
3907 best_wheel_index,
3908 sdist: None,
3909 };
3910 Dist::Built(BuiltDist::Registry(reg_built_dist))
3911 }
3912 Source::Path(path) => {
3913 let filename: WheelFilename =
3914 self.wheels[best_wheel_index].filename.clone();
3915 let install_path = absolute_path(workspace_root, path)?;
3916 let path_dist = PathBuiltDist {
3917 filename,
3918 url: verbatim_url(&install_path, &self.id)?,
3919 install_path: absolute_path(workspace_root, path)?.into_boxed_path(),
3920 };
3921 let built_dist = BuiltDist::Path(path_dist);
3922 Dist::Built(built_dist)
3923 }
3924 Source::Direct(url, direct) => {
3925 let filename: WheelFilename =
3926 self.wheels[best_wheel_index].filename.clone();
3927 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
3928 url: url.to_url().map_err(LockErrorKind::InvalidUrl)?,
3929 subdirectory: direct.subdirectory.clone(),
3930 ext: DistExtension::Wheel,
3931 });
3932 let direct_dist = DirectUrlBuiltDist {
3933 filename,
3934 location: Box::new(url.clone()),
3935 url: VerbatimUrl::from_url(url),
3936 size: None,
3937 };
3938 let built_dist = BuiltDist::DirectUrl(direct_dist);
3939 Dist::Built(built_dist)
3940 }
3941 Source::Git(url, git) => {
3942 let Some(install_path) = git.path.as_ref() else {
3943 return Err(LockErrorKind::InvalidWheelSource {
3944 id: self.id.clone(),
3945 source_type: "Git",
3946 }
3947 .into());
3948 };
3949
3950 let mut url = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
3953 url.set_fragment(None);
3954 url.set_query(None);
3955
3956 let git_url = GitUrl::from_commit(
3958 url,
3959 GitReference::from(git.kind.clone()),
3960 git.precise,
3961 git.lfs,
3962 )?;
3963
3964 let url = DisplaySafeUrl::from(ParsedGitPathUrl {
3966 url: git_url.clone(),
3967 install_path: install_path.clone(),
3968 ext: DistExtension::Wheel,
3969 });
3970
3971 let filename: WheelFilename =
3972 self.wheels[best_wheel_index].filename.clone();
3973
3974 let git_dist = GitPathBuiltDist {
3975 filename,
3976 git: Box::new(git_url),
3977 install_path: install_path.clone(),
3978 url: VerbatimUrl::from_url(url),
3979 };
3980 let built_dist = BuiltDist::GitPath(git_dist);
3981 Dist::Built(built_dist)
3982 }
3983 Source::Directory(_) => {
3984 return Err(LockErrorKind::InvalidWheelSource {
3985 id: self.id.clone(),
3986 source_type: "directory",
3987 }
3988 .into());
3989 }
3990 Source::Editable(_) => {
3991 return Err(LockErrorKind::InvalidWheelSource {
3992 id: self.id.clone(),
3993 source_type: "editable",
3994 }
3995 .into());
3996 }
3997 Source::Virtual(_) => {
3998 return Err(LockErrorKind::InvalidWheelSource {
3999 id: self.id.clone(),
4000 source_type: "virtual",
4001 }
4002 .into());
4003 }
4004 };
4005
4006 return Ok(HashedDist { dist, hashes });
4007 }
4008 }
4009
4010 if let Some(sdist) = self.to_source_dist(workspace_root, first_party)? {
4011 if !no_build || sdist.is_virtual() || sdist.is_first_party() {
4015 let hashes = self
4016 .sdist
4017 .as_ref()
4018 .and_then(|s| s.hash())
4019 .map(|hash| HashDigests::from(vec![hash.0.clone()]))
4020 .unwrap_or_else(|| HashDigests::from(vec![]));
4021 return Ok(HashedDist {
4022 dist: Dist::Source(sdist),
4023 hashes,
4024 });
4025 }
4026 }
4027
4028 match (no_binary, no_build) {
4029 (true, true) => Err(LockErrorKind::NoBinaryNoBuild {
4030 id: self.id.clone(),
4031 }
4032 .into()),
4033 (true, false) if self.id.source.is_wheel() => Err(LockErrorKind::NoBinaryWheelOnly {
4034 id: self.id.clone(),
4035 }
4036 .into()),
4037 (true, false) => Err(LockErrorKind::NoBinary {
4038 id: self.id.clone(),
4039 }
4040 .into()),
4041 (false, true) => Err(LockErrorKind::NoBuild {
4042 id: self.id.clone(),
4043 }
4044 .into()),
4045 (false, false) if self.id.source.is_wheel() => Err(LockError {
4046 kind: Box::new(LockErrorKind::IncompatibleWheelOnly {
4047 id: self.id.clone(),
4048 }),
4049 hint: self.tag_hint(tag_policy, markers),
4050 }),
4051 (false, false) => Err(LockError {
4052 kind: Box::new(LockErrorKind::NeitherSourceDistNorWheel {
4053 id: self.id.clone(),
4054 }),
4055 hint: self.tag_hint(tag_policy, markers),
4056 }),
4057 }
4058 }
4059
4060 fn tag_hint(
4062 &self,
4063 tag_policy: TagPolicy<'_>,
4064 markers: &MarkerEnvironment,
4065 ) -> Option<WheelTagHint> {
4066 let filenames = self
4067 .wheels
4068 .iter()
4069 .map(|wheel| &wheel.filename)
4070 .collect::<Vec<_>>();
4071 WheelTagHint::from_wheels(
4072 &self.id.name,
4073 self.id.version.as_ref(),
4074 &filenames,
4075 tag_policy.tags(),
4076 markers,
4077 )
4078 }
4079
4080 fn to_source_dist(
4085 &self,
4086 workspace_root: &Path,
4087 first_party: FirstParty,
4088 ) -> Result<Option<uv_distribution_types::SourceDist>, LockError> {
4089 let sdist = match &self.id.source {
4090 Source::Path(path) => {
4091 let DistExtension::Source(ext) = DistExtension::from_path(path).map_err(|err| {
4093 LockErrorKind::MissingExtension {
4094 id: self.id.clone(),
4095 err,
4096 }
4097 })?
4098 else {
4099 return Ok(None);
4100 };
4101 if !ext.is_pep625_compliant() {
4102 return Err(LockErrorKind::NotPep625Filename {
4103 id: self.id.clone(),
4104 }
4105 .into());
4106 }
4107 let install_path = absolute_path(workspace_root, path)?;
4108 let given = path.to_str().expect("lock file paths must be UTF-8");
4109 let path_dist = PathSourceDist {
4110 name: self.id.name.clone(),
4111 version: self.id.version.clone(),
4112 url: verbatim_url(&install_path, &self.id)?.with_given(given),
4113 install_path: install_path.into_boxed_path(),
4114 ext,
4115 };
4116 uv_distribution_types::SourceDist::Path(path_dist)
4117 }
4118 Source::Directory(path) => {
4119 let install_path = absolute_path(workspace_root, path)?;
4120 let given = path.to_str().expect("lock file paths must be UTF-8");
4121 let dir_dist = DirectorySourceDist {
4122 name: self.id.name.clone(),
4123 url: verbatim_url(&install_path, &self.id)?.with_given(given),
4124 install_path: install_path.into_boxed_path(),
4125 editable: Some(false),
4126 r#virtual: Some(false),
4127 first_party,
4128 };
4129 uv_distribution_types::SourceDist::Directory(dir_dist)
4130 }
4131 Source::Editable(path) => {
4132 let install_path = absolute_path(workspace_root, path)?;
4133 let given = path.to_str().expect("lock file paths must be UTF-8");
4134 let dir_dist = DirectorySourceDist {
4135 name: self.id.name.clone(),
4136 url: verbatim_url(&install_path, &self.id)?.with_given(given),
4137 install_path: install_path.into_boxed_path(),
4138 editable: Some(true),
4139 r#virtual: Some(false),
4140 first_party,
4141 };
4142 uv_distribution_types::SourceDist::Directory(dir_dist)
4143 }
4144 Source::Virtual(path) => {
4145 let install_path = absolute_path(workspace_root, path)?;
4146 let given = path.to_str().expect("lock file paths must be UTF-8");
4147 let dir_dist = DirectorySourceDist {
4148 name: self.id.name.clone(),
4149 url: verbatim_url(&install_path, &self.id)?.with_given(given),
4150 install_path: install_path.into_boxed_path(),
4151 editable: Some(false),
4152 r#virtual: Some(true),
4153 first_party,
4154 };
4155 uv_distribution_types::SourceDist::Directory(dir_dist)
4156 }
4157 Source::Git(url, git) => {
4158 let mut url = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
4161 url.set_fragment(None);
4162 url.set_query(None);
4163
4164 let git_url = GitUrl::from_commit(
4165 url,
4166 GitReference::from(git.kind.clone()),
4167 git.precise,
4168 git.lfs,
4169 )?;
4170
4171 if let Some(install_path) = git.path.as_ref() {
4172 let DistExtension::Source(ext) = DistExtension::from_path(install_path)
4174 .map_err(|err| LockErrorKind::MissingExtension {
4175 id: self.id.clone(),
4176 err,
4177 })?
4178 else {
4179 return Ok(None);
4180 };
4181
4182 let url = DisplaySafeUrl::from(ParsedGitPathUrl {
4184 url: git_url.clone(),
4185 install_path: install_path.clone(),
4186 ext: DistExtension::Source(ext),
4187 });
4188
4189 let git_dist = GitPathSourceDist {
4190 name: self.id.name.clone(),
4191 url: VerbatimUrl::from_url(url),
4192 git: Box::new(git_url),
4193 install_path: install_path.clone(),
4194 ext,
4195 };
4196 uv_distribution_types::SourceDist::GitPath(git_dist)
4197 } else {
4198 let url = DisplaySafeUrl::from(ParsedGitDirectoryUrl {
4200 url: git_url.clone(),
4201 subdirectory: git.subdirectory.clone(),
4202 });
4203
4204 let git_dist = GitDirectorySourceDist {
4205 name: self.id.name.clone(),
4206 url: VerbatimUrl::from_url(url),
4207 git: Box::new(git_url),
4208 subdirectory: git.subdirectory.clone(),
4209 };
4210 uv_distribution_types::SourceDist::GitDirectory(git_dist)
4211 }
4212 }
4213 Source::Direct(url, direct) => {
4214 let DistExtension::Source(ext) =
4216 DistExtension::from_path(url.base_str()).map_err(|err| {
4217 LockErrorKind::MissingExtension {
4218 id: self.id.clone(),
4219 err,
4220 }
4221 })?
4222 else {
4223 return Ok(None);
4224 };
4225 if !ext.is_pep625_compliant() {
4226 return Err(LockErrorKind::NotPep625Filename {
4227 id: self.id.clone(),
4228 }
4229 .into());
4230 }
4231 let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
4232 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
4233 url: location.clone(),
4234 subdirectory: direct.subdirectory.clone(),
4235 ext: DistExtension::Source(ext),
4236 });
4237 let direct_dist = DirectUrlSourceDist {
4238 name: self.id.name.clone(),
4239 location: Box::new(location),
4240 subdirectory: direct.subdirectory.clone(),
4241 ext,
4242 url: VerbatimUrl::from_url(url),
4243 size: None,
4244 };
4245 uv_distribution_types::SourceDist::DirectUrl(direct_dist)
4246 }
4247 Source::Registry(RegistrySource::Url(url)) => {
4248 let Some(ref sdist) = self.sdist else {
4249 return Ok(None);
4250 };
4251
4252 let name = &self.id.name;
4253 let version = self
4254 .id
4255 .version
4256 .as_ref()
4257 .expect("version for registry source");
4258
4259 let file_url = sdist.url().ok_or_else(|| LockErrorKind::MissingUrl {
4260 name: name.clone(),
4261 version: version.clone(),
4262 })?;
4263 let filename = sdist
4264 .filename()
4265 .ok_or_else(|| LockErrorKind::MissingFilename {
4266 id: self.id.clone(),
4267 })?;
4268 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
4269 LockErrorKind::MissingExtension {
4270 id: self.id.clone(),
4271 err,
4272 }
4273 })?;
4274 let file = Box::new(uv_distribution_types::File {
4275 dist_info_metadata: false,
4276 filename: SmallString::from(filename),
4277 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
4278 HashDigests::from(hash.0.clone())
4279 }),
4280 requires_python: None,
4281 size: sdist.size(),
4282 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
4283 url: FileLocation::AbsoluteUrl(file_url.clone()),
4284 yanked: None,
4285 zstd: None,
4286 });
4287
4288 let index = IndexUrl::from(VerbatimUrl::from_url(
4289 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
4290 ));
4291
4292 let reg_dist = RegistrySourceDist {
4293 name: name.clone(),
4294 version: version.clone(),
4295 file,
4296 ext,
4297 index,
4298 wheels: vec![],
4299 size_is_authoritative: false,
4300 };
4301 uv_distribution_types::SourceDist::Registry(reg_dist)
4302 }
4303 Source::Registry(RegistrySource::Path(path)) => {
4304 let Some(ref sdist) = self.sdist else {
4305 return Ok(None);
4306 };
4307
4308 let name = &self.id.name;
4309 let version = self
4310 .id
4311 .version
4312 .as_ref()
4313 .expect("version for registry source");
4314
4315 let file_url = match sdist {
4316 SourceDist::Url { url: file_url, .. } => {
4317 FileLocation::AbsoluteUrl(file_url.clone())
4318 }
4319 SourceDist::Path {
4320 path: file_path, ..
4321 } => {
4322 let file_path = workspace_root.join(path).join(file_path);
4323 let file_url =
4324 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
4325 LockErrorKind::PathToUrl {
4326 path: file_path.into_boxed_path(),
4327 }
4328 })?;
4329 FileLocation::AbsoluteUrl(UrlString::from(file_url))
4330 }
4331 SourceDist::Metadata { .. } => {
4332 return Err(LockErrorKind::MissingPath {
4333 name: name.clone(),
4334 version: version.clone(),
4335 }
4336 .into());
4337 }
4338 };
4339 let filename = sdist
4340 .filename()
4341 .ok_or_else(|| LockErrorKind::MissingFilename {
4342 id: self.id.clone(),
4343 })?;
4344 let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
4345 LockErrorKind::MissingExtension {
4346 id: self.id.clone(),
4347 err,
4348 }
4349 })?;
4350 let file = Box::new(uv_distribution_types::File {
4351 dist_info_metadata: false,
4352 filename: SmallString::from(filename),
4353 hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
4354 HashDigests::from(hash.0.clone())
4355 }),
4356 requires_python: None,
4357 size: sdist.size(),
4358 upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
4359 url: file_url,
4360 yanked: None,
4361 zstd: None,
4362 });
4363
4364 let index = IndexUrl::from(
4365 VerbatimUrl::from_absolute_path(workspace_root.join(path))
4366 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
4367 );
4368
4369 let reg_dist = RegistrySourceDist {
4370 name: name.clone(),
4371 version: version.clone(),
4372 file,
4373 ext,
4374 index,
4375 wheels: vec![],
4376 size_is_authoritative: false,
4377 };
4378 uv_distribution_types::SourceDist::Registry(reg_dist)
4379 }
4380 };
4381
4382 Ok(Some(sdist))
4383 }
4384
4385 fn find_best_wheel(&self, tag_policy: TagPolicy<'_>) -> Option<usize> {
4386 type WheelPriority<'lock> = (TagPriority, Option<&'lock BuildTag>);
4387
4388 let mut best: Option<(WheelPriority, usize)> = None;
4389 for (i, wheel) in self.wheels.iter().enumerate() {
4390 let TagCompatibility::Compatible(tag_priority) =
4391 wheel.filename.compatibility(tag_policy.tags())
4392 else {
4393 continue;
4394 };
4395 let build_tag = wheel.filename.build_tag();
4396 let wheel_priority = (tag_priority, build_tag);
4397 match best {
4398 None => {
4399 best = Some((wheel_priority, i));
4400 }
4401 Some((best_priority, _)) => {
4402 if wheel_priority > best_priority {
4403 best = Some((wheel_priority, i));
4404 }
4405 }
4406 }
4407 }
4408
4409 let best = best.map(|(_, i)| i);
4410 match tag_policy {
4411 TagPolicy::Required(_) => best,
4412 TagPolicy::Preferred(_) => best.or_else(|| self.wheels.first().map(|_| 0)),
4413 }
4414 }
4415
4416 pub fn name(&self) -> &PackageName {
4418 &self.id.name
4419 }
4420
4421 pub fn version(&self) -> Option<&Version> {
4423 self.id.version.as_ref()
4424 }
4425
4426 pub fn git_sha(&self) -> Option<&GitOid> {
4428 match &self.id.source {
4429 Source::Git(_, git) => Some(&git.precise),
4430 _ => None,
4431 }
4432 }
4433
4434 pub(crate) fn fork_markers(&self) -> &[UniversalMarker] {
4436 self.fork_markers.as_slice()
4437 }
4438
4439 pub fn is_included_by_marker(&self, marker: MarkerTree) -> bool {
4441 self.fork_markers.is_empty()
4442 || self
4443 .fork_markers
4444 .iter()
4445 .any(|fork_marker| !fork_marker.pep508().is_disjoint(marker))
4446 }
4447
4448 pub fn index(&self, root: &Path) -> Result<Option<IndexUrl>, LockError> {
4450 match &self.id.source {
4451 Source::Registry(RegistrySource::Url(url)) => {
4452 let index = IndexUrl::from(VerbatimUrl::from_url(
4453 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
4454 ));
4455 Ok(Some(index))
4456 }
4457 Source::Registry(RegistrySource::Path(path)) => {
4458 let index = IndexUrl::from(
4459 VerbatimUrl::from_absolute_path(root.join(path))
4460 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
4461 );
4462 Ok(Some(index))
4463 }
4464 _ => Ok(None),
4465 }
4466 }
4467
4468 fn hashes(&self) -> HashDigests {
4470 let mut hashes = Vec::with_capacity(
4471 usize::from(self.sdist.as_ref().and_then(|sdist| sdist.hash()).is_some())
4472 + self
4473 .wheels
4474 .iter()
4475 .map(|wheel| usize::from(wheel.hash.is_some()))
4476 .sum::<usize>(),
4477 );
4478 if let Some(ref sdist) = self.sdist {
4479 if let Some(hash) = sdist.hash() {
4480 hashes.push(hash.0.clone());
4481 }
4482 }
4483 for wheel in &self.wheels {
4484 hashes.extend(wheel.hash.as_ref().map(|h| h.0.clone()));
4485 if let Some(zstd) = wheel.zstd.as_ref() {
4486 hashes.extend(zstd.hash.as_ref().map(|h| h.0.clone()));
4487 }
4488 }
4489 HashDigests::from(hashes)
4490 }
4491
4492 pub fn as_git_ref(&self) -> Result<Option<ResolvedRepositoryReference>, LockError> {
4494 match &self.id.source {
4495 Source::Git(url, git) => Ok(Some(ResolvedRepositoryReference {
4496 reference: RepositoryReference {
4497 url: RepositoryUrl::new(url.to_url().map_err(LockErrorKind::InvalidUrl)?),
4498 reference: GitReference::from(git.kind.clone()),
4499 },
4500 sha: git.precise,
4501 })),
4502 _ => Ok(None),
4503 }
4504 }
4505
4506 fn is_dynamic(&self) -> bool {
4508 self.id.version.is_none()
4509 }
4510
4511 pub fn has_metadata(&self) -> bool {
4513 self.metadata != PackageMetadata::default()
4514 }
4515
4516 pub fn provides_extras(&self) -> &[ExtraName] {
4518 &self.metadata.provides_extra
4519 }
4520
4521 pub fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
4523 &self.metadata.dependency_groups
4524 }
4525
4526 pub fn dependencies(&self) -> &[Dependency] {
4528 &self.dependencies
4529 }
4530
4531 fn all_dependencies(&self) -> impl Iterator<Item = &Dependency> {
4533 self.dependencies
4534 .iter()
4535 .chain(self.optional_dependencies.values().flatten())
4536 .chain(self.dependency_groups.values().flatten())
4537 }
4538
4539 pub fn optional_dependencies(&self) -> &BTreeMap<ExtraName, Vec<Dependency>> {
4541 &self.optional_dependencies
4542 }
4543
4544 pub fn resolved_dependency_groups(&self) -> &BTreeMap<GroupName, Vec<Dependency>> {
4546 &self.dependency_groups
4547 }
4548
4549 fn as_install_target(&self) -> InstallTarget<'_> {
4551 InstallTarget {
4552 name: self.name(),
4553 is_local: self.id.source.is_local(),
4554 }
4555 }
4556}
4557
4558fn verbatim_url(path: &Path, id: &PackageId) -> Result<VerbatimUrl, LockError> {
4560 let url =
4561 VerbatimUrl::from_normalized_path(path).map_err(|err| LockErrorKind::VerbatimUrl {
4562 id: id.clone(),
4563 err,
4564 })?;
4565 Ok(url)
4566}
4567
4568fn absolute_path(workspace_root: &Path, path: &Path) -> Result<PathBuf, LockError> {
4570 let path = uv_fs::normalize_absolute_path(&workspace_root.join(path))
4571 .map_err(LockErrorKind::AbsolutePath)?;
4572 Ok(path)
4573}
4574
4575#[derive(Clone, Debug, serde::Deserialize)]
4576#[serde(rename_all = "kebab-case")]
4577struct PackageWire {
4578 #[serde(flatten)]
4579 id: PackageId,
4580 #[serde(default)]
4581 metadata: PackageMetadata,
4582 #[serde(default)]
4583 sdist: Option<SourceDist>,
4584 #[serde(default)]
4585 wheels: Vec<Wheel>,
4586 #[serde(default, rename = "resolution-markers")]
4587 fork_markers: Vec<SimplifiedMarkerTree>,
4588 #[serde(default)]
4589 dependencies: Vec<DependencyWire>,
4590 #[serde(default)]
4591 optional_dependencies: BTreeMap<ExtraName, Vec<DependencyWire>>,
4592 #[serde(default, rename = "dev-dependencies", alias = "dependency-groups")]
4593 dependency_groups: BTreeMap<GroupName, Vec<DependencyWire>>,
4594}
4595
4596#[derive(Clone, Default, Debug, Eq, PartialEq, serde::Deserialize)]
4597#[serde(rename_all = "kebab-case")]
4598struct PackageMetadata {
4599 #[serde(default)]
4600 requires_dist: BTreeSet<Requirement>,
4601 #[serde(default, rename = "provides-extras")]
4602 provides_extra: Box<[ExtraName]>,
4603 #[serde(default, rename = "requires-dev", alias = "dependency-groups")]
4604 dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
4605}
4606
4607impl PackageMetadata {
4608 fn from_distribution(metadata: &DistributionMetadata, root: &Path) -> Result<Self, LockError> {
4609 let requires_dist = metadata
4610 .requires_dist
4611 .iter()
4612 .cloned()
4613 .map(|requirement| requirement.relative_to(root))
4614 .collect::<Result<_, _>>()
4615 .map_err(LockErrorKind::RequirementRelativePath)?;
4616 let dependency_groups = metadata
4617 .dependency_groups
4618 .iter()
4619 .map(|(group, requirements)| {
4620 let requirements = requirements
4621 .iter()
4622 .cloned()
4623 .map(|requirement| requirement.relative_to(root))
4624 .collect::<Result<_, _>>()
4625 .map_err(LockErrorKind::RequirementRelativePath)?;
4626 Ok::<_, LockError>((group.clone(), requirements))
4627 })
4628 .collect::<Result<_, _>>()?;
4629
4630 Ok(Self {
4631 requires_dist,
4632 provides_extra: metadata.provides_extra.clone(),
4633 dependency_groups,
4634 })
4635 }
4636}
4637
4638impl PackageWire {
4639 fn unwire(
4640 self,
4641 requires_python: &RequiresPython,
4642 environment: SimplifiedMarkerTree,
4643 default: UniversalMarker,
4644 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
4645 ) -> Result<Package, LockError> {
4646 if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) {
4648 if let Some(version) = &self.id.version {
4649 for wheel in &self.wheels {
4650 if *version != wheel.filename.version
4651 && *version != wheel.filename.version.clone().without_local()
4652 {
4653 return Err(LockError::from(LockErrorKind::InconsistentVersions {
4654 name: self.id.name,
4655 version: version.clone(),
4656 wheel: wheel.clone(),
4657 }));
4658 }
4659 }
4660 }
4663 }
4664
4665 if matches!(self.id.source, Source::Registry(_)) && self.id.version.is_none() {
4668 return Err(LockErrorKind::MissingPackageVersion {
4669 name: self.id.name.clone(),
4670 }
4671 .into());
4672 }
4673
4674 let unwire_deps = |deps: Vec<DependencyWire>| -> Result<Vec<Dependency>, LockError> {
4675 deps.into_iter()
4676 .map(|dep| {
4677 dep.unwire(
4678 requires_python,
4679 environment,
4680 default,
4681 unambiguous_package_ids,
4682 )
4683 })
4684 .collect()
4685 };
4686
4687 Ok(Package {
4688 id: self.id,
4689 metadata: self.metadata,
4690 sdist: self.sdist,
4691 wheels: self.wheels,
4692 fork_markers: self
4693 .fork_markers
4694 .into_iter()
4695 .map(|simplified_marker| simplified_marker.into_marker(requires_python))
4696 .map(UniversalMarker::from_combined)
4697 .collect(),
4698 dependencies: unwire_deps(self.dependencies)?,
4699 optional_dependencies: self
4700 .optional_dependencies
4701 .into_iter()
4702 .map(|(extra, deps)| Ok((extra, unwire_deps(deps)?)))
4703 .collect::<Result<_, LockError>>()?,
4704 dependency_groups: self
4705 .dependency_groups
4706 .into_iter()
4707 .map(|(group, deps)| Ok((group, unwire_deps(deps)?)))
4708 .collect::<Result<_, LockError>>()?,
4709 })
4710 }
4711}
4712
4713#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4716#[serde(rename_all = "kebab-case")]
4717pub(crate) struct PackageId {
4718 pub(crate) name: PackageName,
4719 version: Option<Version>,
4720 source: Source,
4721}
4722
4723impl PackageId {
4724 fn from_annotated_dist(annotated_dist: &AnnotatedDist, root: &Path) -> Result<Self, LockError> {
4725 let source = Source::from_resolved_dist(&annotated_dist.dist, root)?;
4727 let version = if source.is_source_tree()
4729 && annotated_dist
4730 .metadata
4731 .as_ref()
4732 .is_some_and(|metadata| metadata.dynamic)
4733 {
4734 None
4735 } else {
4736 Some(annotated_dist.version.clone())
4737 };
4738 let name = annotated_dist.name.clone();
4739 Ok(Self {
4740 name,
4741 version,
4742 source,
4743 })
4744 }
4745}
4746
4747impl Display for PackageId {
4748 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
4749 if let Some(version) = &self.version {
4750 write!(f, "{}=={} @ {}", self.name, version, self.source)
4751 } else {
4752 write!(f, "{} @ {}", self.name, self.source)
4753 }
4754 }
4755}
4756
4757#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4758#[serde(rename_all = "kebab-case")]
4759struct PackageIdForDependency {
4760 name: PackageName,
4761 version: Option<Version>,
4762 source: Option<Source>,
4763}
4764
4765impl PackageIdForDependency {
4766 fn unwire(
4767 self,
4768 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
4769 ) -> Result<PackageId, LockError> {
4770 let unambiguous_package_id = unambiguous_package_ids.get(&self.name);
4771 let source = self.source.map(Ok::<_, LockError>).unwrap_or_else(|| {
4772 let Some(package_id) = unambiguous_package_id else {
4773 return Err(LockErrorKind::MissingDependencySource {
4774 name: self.name.clone(),
4775 }
4776 .into());
4777 };
4778 Ok(package_id.source.clone())
4779 })?;
4780 let version = if let Some(version) = self.version {
4781 Some(version)
4782 } else {
4783 if let Some(package_id) = unambiguous_package_id {
4784 package_id.version.clone()
4785 } else {
4786 if source.is_source_tree() {
4789 None
4790 } else {
4791 return Err(LockErrorKind::MissingDependencyVersion {
4792 name: self.name.clone(),
4793 }
4794 .into());
4795 }
4796 }
4797 };
4798 Ok(PackageId {
4799 name: self.name,
4800 version,
4801 source,
4802 })
4803 }
4804}
4805
4806impl From<PackageId> for PackageIdForDependency {
4807 fn from(id: PackageId) -> Self {
4808 Self {
4809 name: id.name,
4810 version: id.version,
4811 source: Some(id.source),
4812 }
4813 }
4814}
4815
4816#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4824#[serde(try_from = "SourceWire")]
4825enum Source {
4826 Registry(RegistrySource),
4828 Git(UrlString, GitSource),
4830 Direct(UrlString, DirectSource),
4832 Path(Box<Path>),
4834 Directory(Box<Path>),
4836 Editable(Box<Path>),
4838 Virtual(Box<Path>),
4840}
4841
4842impl Source {
4843 fn from_resolved_dist(resolved_dist: &ResolvedDist, root: &Path) -> Result<Self, LockError> {
4844 match *resolved_dist {
4845 ResolvedDist::Installed { .. } => unreachable!(),
4847 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(dist, root),
4848 }
4849 }
4850
4851 fn from_dist(dist: &Dist, root: &Path) -> Result<Self, LockError> {
4852 match *dist {
4853 Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, root),
4854 Dist::Source(ref source_dist) => Self::from_source_dist(source_dist, root),
4855 }
4856 }
4857
4858 fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result<Self, LockError> {
4859 match *built_dist {
4860 BuiltDist::Registry(ref reg_dist) => Self::from_registry_built_dist(reg_dist, root),
4861 BuiltDist::DirectUrl(ref direct_dist) => Ok(Self::from_direct_built_dist(direct_dist)),
4862 BuiltDist::Path(ref path_dist) => Self::from_path_built_dist(path_dist, root),
4863 BuiltDist::GitPath(ref git_dist) => Ok(Self::from_git_path_built_dist(git_dist)),
4864 }
4865 }
4866
4867 fn from_source_dist(
4868 source_dist: &uv_distribution_types::SourceDist,
4869 root: &Path,
4870 ) -> Result<Self, LockError> {
4871 match *source_dist {
4872 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
4873 Self::from_registry_source_dist(reg_dist, root)
4874 }
4875 uv_distribution_types::SourceDist::DirectUrl(ref direct_dist) => {
4876 Ok(Self::from_direct_source_dist(direct_dist))
4877 }
4878 uv_distribution_types::SourceDist::GitDirectory(ref git_dist) => {
4879 Ok(Self::from_git_directory_source_dist(git_dist))
4880 }
4881 uv_distribution_types::SourceDist::GitPath(ref git_dist) => {
4882 Ok(Self::from_git_path_source_dist(git_dist))
4883 }
4884 uv_distribution_types::SourceDist::Path(ref path_dist) => {
4885 Self::from_path_source_dist(path_dist, root)
4886 }
4887 uv_distribution_types::SourceDist::Directory(ref directory) => {
4888 Self::from_directory_source_dist(directory, root)
4889 }
4890 }
4891 }
4892
4893 fn from_registry_built_dist(
4894 reg_dist: &RegistryBuiltDist,
4895 root: &Path,
4896 ) -> Result<Self, LockError> {
4897 Self::from_index_url(®_dist.best_wheel().index, root)
4898 }
4899
4900 fn from_registry_source_dist(
4901 reg_dist: &RegistrySourceDist,
4902 root: &Path,
4903 ) -> Result<Self, LockError> {
4904 Self::from_index_url(®_dist.index, root)
4905 }
4906
4907 fn from_direct_built_dist(direct_dist: &DirectUrlBuiltDist) -> Self {
4908 Self::Direct(
4909 normalize_url(direct_dist.url.to_url()),
4910 DirectSource { subdirectory: None },
4911 )
4912 }
4913
4914 fn from_direct_source_dist(direct_dist: &DirectUrlSourceDist) -> Self {
4915 Self::Direct(
4916 normalize_url(direct_dist.url.to_url()),
4917 DirectSource {
4918 subdirectory: direct_dist.subdirectory.clone(),
4919 },
4920 )
4921 }
4922
4923 fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result<Self, LockError> {
4924 let path = try_relative_to_if(
4925 &path_dist.install_path,
4926 root,
4927 !path_dist.url.was_given_absolute(),
4928 )
4929 .map_err(LockErrorKind::DistributionRelativePath)?;
4930 Ok(Self::Path(path.into_boxed_path()))
4931 }
4932
4933 fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Self, LockError> {
4934 let path = try_relative_to_if(
4935 &path_dist.install_path,
4936 root,
4937 !path_dist.url.was_given_absolute(),
4938 )
4939 .map_err(LockErrorKind::DistributionRelativePath)?;
4940 Ok(Self::Path(path.into_boxed_path()))
4941 }
4942
4943 fn from_directory_source_dist(
4944 directory_dist: &DirectorySourceDist,
4945 root: &Path,
4946 ) -> Result<Self, LockError> {
4947 let path = try_relative_to_if(
4948 &directory_dist.install_path,
4949 root,
4950 !directory_dist.url.was_given_absolute(),
4951 )
4952 .map_err(LockErrorKind::DistributionRelativePath)?;
4953 if directory_dist.editable.unwrap_or(false) {
4954 Ok(Self::Editable(path.into_boxed_path()))
4955 } else if directory_dist.r#virtual.unwrap_or(false) {
4956 Ok(Self::Virtual(path.into_boxed_path()))
4957 } else {
4958 Ok(Self::Directory(path.into_boxed_path()))
4959 }
4960 }
4961
4962 fn from_index_url(index_url: &IndexUrl, root: &Path) -> Result<Self, LockError> {
4963 match index_url {
4964 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4965 let redacted = index_url.without_credentials();
4967 let source = RegistrySource::Url(UrlString::from(redacted.as_ref()));
4968 Ok(Self::Registry(source))
4969 }
4970 IndexUrl::Path(url) => {
4971 let path = url
4972 .to_file_path()
4973 .map_err(|()| LockErrorKind::UrlToPath { url: url.to_url() })?;
4974 let path = try_relative_to_if(&path, root, !url.was_given_absolute())
4975 .map_err(LockErrorKind::IndexRelativePath)?;
4976 let source = RegistrySource::Path(path.into_boxed_path());
4977 Ok(Self::Registry(source))
4978 }
4979 }
4980 }
4981
4982 fn from_git_path_built_dist(git_dist: &GitPathBuiltDist) -> Self {
4983 let path = git_dist.install_path.clone();
4984 Self::Git(
4985 UrlString::from(locked_git_url(&git_dist.git, None, Some(&path))),
4986 GitSource {
4987 kind: GitSourceKind::from(git_dist.git.reference().clone()),
4988 precise: git_dist.git.precise().unwrap_or_else(|| {
4989 panic!("Git distribution is missing a precise hash: {git_dist}")
4990 }),
4991 subdirectory: None,
4992 path: Some(path),
4993 lfs: git_dist.git.lfs(),
4994 },
4995 )
4996 }
4997
4998 fn from_git_path_source_dist(git_dist: &GitPathSourceDist) -> Self {
4999 let path = git_dist.install_path.clone();
5000 Self::Git(
5001 UrlString::from(locked_git_url(&git_dist.git, None, Some(&path))),
5002 GitSource {
5003 kind: GitSourceKind::from(git_dist.git.reference().clone()),
5004 precise: git_dist.git.precise().unwrap_or_else(|| {
5005 panic!("Git distribution is missing a precise hash: {git_dist}")
5006 }),
5007 subdirectory: None,
5008 path: Some(path),
5009 lfs: git_dist.git.lfs(),
5010 },
5011 )
5012 }
5013
5014 fn from_git_directory_source_dist(git_dist: &GitDirectorySourceDist) -> Self {
5015 Self::Git(
5016 UrlString::from(locked_git_url(
5017 &git_dist.git,
5018 git_dist.subdirectory.as_deref(),
5019 None,
5020 )),
5021 GitSource {
5022 kind: GitSourceKind::from(git_dist.git.reference().clone()),
5023 precise: git_dist.git.precise().unwrap_or_else(|| {
5024 panic!("Git distribution is missing a precise hash: {git_dist}")
5025 }),
5026 subdirectory: git_dist.subdirectory.clone(),
5027 path: None,
5028 lfs: git_dist.git.lfs(),
5029 },
5030 )
5031 }
5032
5033 fn is_pypi_registry(&self) -> bool {
5035 matches!(
5036 self,
5037 Self::Registry(RegistrySource::Url(url)) if url.as_ref() == PYPI_URL.as_str()
5038 )
5039 }
5040
5041 fn satisfies_requirement_source(
5043 &self,
5044 requirement: &RequirementSource,
5045 root: &Path,
5046 ) -> Result<bool, LockError> {
5047 let result = match (self, requirement) {
5048 (Self::Registry(_), RequirementSource::Registry { index: None, .. }) => true,
5049 (
5050 Self::Registry(RegistrySource::Path(actual)),
5051 RequirementSource::Registry {
5052 index:
5053 Some(IndexMetadata {
5054 url: IndexUrl::Path(expected),
5055 ..
5056 }),
5057 ..
5058 },
5059 ) => {
5060 let expected = expected
5061 .to_file_path()
5062 .map_err(|()| LockErrorKind::UrlToPath {
5063 url: expected.to_url(),
5064 })?;
5065 normalize_path(root.join(actual)).as_ref() == normalize_path(expected).as_ref()
5066 }
5067 (
5068 Self::Registry(_),
5069 RequirementSource::Registry {
5070 index: Some(index), ..
5071 },
5072 ) => Self::from_index_url(&index.url, root)? == *self,
5073 (
5074 Self::Direct(url, source),
5075 RequirementSource::Url {
5076 location,
5077 subdirectory,
5078 ..
5079 },
5080 ) => {
5081 let mut actual = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
5082 actual.remove_credentials();
5083 normalize_url(actual) == normalize_url(location.clone())
5084 && source.subdirectory == *subdirectory
5085 }
5086 (Self::Path(path), RequirementSource::Path { install_path, .. }) => {
5087 normalize_path(root.join(path)).as_ref() == install_path.as_ref()
5088 }
5089 (
5090 Self::Directory(path) | Self::Editable(path) | Self::Virtual(path),
5091 RequirementSource::Directory {
5092 install_path,
5093 editable,
5094 r#virtual,
5095 ..
5096 },
5097 ) => {
5098 let actual = normalize_path(root.join(path));
5099 actual.as_ref() == install_path.as_ref()
5100 && matches!(self, Self::Editable(_)) == editable.unwrap_or(false)
5101 && (matches!(self, Self::Virtual(_)) == r#virtual.unwrap_or(false)
5102 || matches!(self, Self::Virtual(_))
5103 && install_path.as_ref() == normalize_path(root).as_ref())
5104 }
5105 (
5106 Self::Git(url, source),
5107 RequirementSource::GitDirectory {
5108 git, subdirectory, ..
5109 },
5110 ) => {
5111 let mut expected = locked_git_url(git, subdirectory.as_deref(), None);
5112 expected.set_fragment(None);
5113 let mut actual = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
5114 actual.set_fragment(None);
5115 expected == actual
5116 && source.path.is_none()
5117 && git
5118 .precise()
5119 .as_ref()
5120 .is_none_or(|precise| precise == &source.precise)
5121 }
5122 (
5123 Self::Git(url, source),
5124 RequirementSource::GitPath {
5125 git, install_path, ..
5126 },
5127 ) => {
5128 let mut expected = locked_git_url(git, None, Some(install_path));
5129 expected.set_fragment(None);
5130 let mut actual = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
5131 actual.set_fragment(None);
5132 expected == actual
5133 && source.path.is_some()
5134 && git
5135 .precise()
5136 .as_ref()
5137 .is_none_or(|precise| precise == &source.precise)
5138 }
5139 _ => false,
5140 };
5141 Ok(result)
5142 }
5143
5144 fn is_immutable(&self) -> bool {
5151 matches!(self, Self::Registry(..) | Self::Git(_, _))
5152 }
5153
5154 fn is_wheel(&self) -> bool {
5156 match self {
5157 Self::Path(path) => {
5158 matches!(
5159 DistExtension::from_path(path).ok(),
5160 Some(DistExtension::Wheel)
5161 )
5162 }
5163 Self::Direct(url, _) => {
5164 matches!(
5165 DistExtension::from_path(url.as_ref()).ok(),
5166 Some(DistExtension::Wheel)
5167 )
5168 }
5169 Self::Directory(..) => false,
5170 Self::Editable(..) => false,
5171 Self::Virtual(..) => false,
5172 Self::Git(..) => false,
5173 Self::Registry(..) => false,
5174 }
5175 }
5176
5177 fn is_source_tree(&self) -> bool {
5179 match self {
5180 Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => true,
5181 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => false,
5182 }
5183 }
5184
5185 fn as_source_tree(&self) -> Option<&Path> {
5187 match self {
5188 Self::Directory(path) | Self::Editable(path) | Self::Virtual(path) => Some(path),
5189 Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => None,
5190 }
5191 }
5192
5193 fn is_local(&self) -> bool {
5195 matches!(
5196 self,
5197 Self::Path(_) | Self::Directory(_) | Self::Editable(_) | Self::Virtual(_)
5198 )
5199 }
5200}
5201
5202impl Display for Source {
5203 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5204 match self {
5205 Self::Registry(RegistrySource::Url(url)) | Self::Git(url, _) | Self::Direct(url, _) => {
5206 write!(f, "{}+{}", self.name(), url)
5207 }
5208 Self::Registry(RegistrySource::Path(path))
5209 | Self::Path(path)
5210 | Self::Directory(path)
5211 | Self::Editable(path)
5212 | Self::Virtual(path) => {
5213 write!(f, "{}+{}", self.name(), PortablePath::from(path))
5214 }
5215 }
5216 }
5217}
5218
5219impl Source {
5220 fn name(&self) -> &str {
5221 match self {
5222 Self::Registry(..) => "registry",
5223 Self::Git(..) => "git",
5224 Self::Direct(..) => "direct",
5225 Self::Path(..) => "path",
5226 Self::Directory(..) => "directory",
5227 Self::Editable(..) => "editable",
5228 Self::Virtual(..) => "virtual",
5229 }
5230 }
5231
5232 fn requires_hash(&self) -> Option<bool> {
5240 match self {
5241 Self::Registry(..) => None,
5242 Self::Direct(..) | Self::Path(..) => Some(true),
5243 Self::Git(.., GitSource { path, .. }) => Some(path.is_some()),
5244 Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => Some(false),
5245 }
5246 }
5247}
5248
5249#[derive(Clone, Debug, serde::Deserialize)]
5250#[serde(untagged, rename_all = "kebab-case")]
5251enum SourceWire {
5252 Registry {
5253 registry: RegistrySourceWire,
5254 },
5255 Git {
5256 git: String,
5257 },
5258 Direct {
5259 url: UrlString,
5260 subdirectory: Option<PortablePathBuf>,
5261 },
5262 Path {
5263 path: PortablePathBuf,
5264 },
5265 Directory {
5266 directory: PortablePathBuf,
5267 },
5268 Editable {
5269 editable: PortablePathBuf,
5270 },
5271 Virtual {
5272 r#virtual: PortablePathBuf,
5273 },
5274}
5275
5276impl TryFrom<SourceWire> for Source {
5277 type Error = LockError;
5278
5279 fn try_from(wire: SourceWire) -> Result<Self, LockError> {
5280 use self::SourceWire::{Direct, Directory, Editable, Git, Path, Registry, Virtual};
5281
5282 match wire {
5283 Registry { registry } => Ok(Self::Registry(registry.into())),
5284 Git { git } => {
5285 let url = DisplaySafeUrl::parse(&git)
5286 .map_err(|err| SourceParseError::InvalidUrl {
5287 given: git.clone(),
5288 err,
5289 })
5290 .map_err(LockErrorKind::InvalidGitSourceUrl)?;
5291
5292 let git_source = GitSource::from_url(&url).map_err(|err| match err {
5293 GitSourceError::InvalidSha => {
5294 LockErrorKind::InvalidGitSourceUrl(SourceParseError::InvalidSha {
5295 given: git,
5296 })
5297 }
5298 GitSourceError::MissingSha => {
5299 LockErrorKind::InvalidGitSourceUrl(SourceParseError::MissingSha {
5300 given: git,
5301 })
5302 }
5303 GitSourceError::RevisionMismatch { revision, precise } => {
5304 let mut repository_url = url.clone();
5305 repository_url.set_query(None);
5306 repository_url.set_fragment(None);
5307 LockErrorKind::GitUrlParse(GitUrlParseError::MismatchedRevision {
5308 revision,
5309 precise,
5310 url: Box::new(repository_url),
5311 })
5312 }
5313 })?;
5314
5315 Ok(Self::Git(UrlString::from(url), git_source))
5316 }
5317 Direct { url, subdirectory } => Ok(Self::Direct(
5318 url,
5319 DirectSource {
5320 subdirectory: subdirectory.map(Box::<std::path::Path>::from),
5321 },
5322 )),
5323 Path { path } => Ok(Self::Path(path.into())),
5324 Directory { directory } => Ok(Self::Directory(directory.into())),
5325 Editable { editable } => Ok(Self::Editable(editable.into())),
5326 Virtual { r#virtual } => Ok(Self::Virtual(r#virtual.into())),
5327 }
5328 }
5329}
5330
5331#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
5333enum RegistrySource {
5334 Url(UrlString),
5336 Path(Box<Path>),
5338}
5339
5340impl Display for RegistrySource {
5341 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5342 match self {
5343 Self::Url(url) => write!(f, "{url}"),
5344 Self::Path(path) => write!(f, "{}", path.display()),
5345 }
5346 }
5347}
5348
5349#[derive(Clone, Debug)]
5350enum RegistrySourceWire {
5351 Url(UrlString),
5353 Path(PortablePathBuf),
5355}
5356
5357impl<'de> serde::de::Deserialize<'de> for RegistrySourceWire {
5358 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5359 where
5360 D: serde::de::Deserializer<'de>,
5361 {
5362 struct Visitor;
5363
5364 impl serde::de::Visitor<'_> for Visitor {
5365 type Value = RegistrySourceWire;
5366
5367 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
5368 formatter.write_str("a valid URL or a file path")
5369 }
5370
5371 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
5372 where
5373 E: serde::de::Error,
5374 {
5375 if split_scheme(value).is_some_and(|(scheme, _)| Scheme::parse(scheme).is_some()) {
5376 Ok(
5377 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
5378 value,
5379 ))
5380 .map(RegistrySourceWire::Url)?,
5381 )
5382 } else {
5383 Ok(
5384 serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
5385 value,
5386 ))
5387 .map(RegistrySourceWire::Path)?,
5388 )
5389 }
5390 }
5391 }
5392
5393 deserializer.deserialize_str(Visitor)
5394 }
5395}
5396
5397impl From<RegistrySourceWire> for RegistrySource {
5398 fn from(wire: RegistrySourceWire) -> Self {
5399 match wire {
5400 RegistrySourceWire::Url(url) => Self::Url(url),
5401 RegistrySourceWire::Path(path) => Self::Path(path.into()),
5402 }
5403 }
5404}
5405
5406#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
5407#[serde(rename_all = "kebab-case")]
5408struct DirectSource {
5409 subdirectory: Option<Box<Path>>,
5410}
5411
5412#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
5417struct GitSource {
5418 precise: GitOid,
5419 subdirectory: Option<Box<Path>>,
5420 path: Option<PathBuf>,
5421 kind: GitSourceKind,
5422 lfs: GitLfs,
5423}
5424
5425#[derive(Clone, Debug, Eq, PartialEq)]
5427enum GitSourceError {
5428 InvalidSha,
5429 MissingSha,
5430 RevisionMismatch { revision: String, precise: GitOid },
5431}
5432
5433impl GitSource {
5434 fn from_url(url: &Url) -> Result<Self, GitSourceError> {
5437 let mut kind = GitSourceKind::DefaultBranch;
5438 let mut subdirectory = None;
5439 let mut lfs = GitLfs::Disabled;
5440 let mut path = None;
5441 for (key, val) in url.query_pairs() {
5442 match &*key {
5443 "tag" => kind = GitSourceKind::Tag(val.into_owned()),
5444 "branch" => kind = GitSourceKind::Branch(val.into_owned()),
5445 "rev" => kind = GitSourceKind::Rev(val.into_owned()),
5446 "subdirectory" => subdirectory = Some(PortablePathBuf::from(val.as_ref()).into()),
5447 "lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
5448 "path" => {
5449 path = Some(PathBuf::from(Box::<Path>::from(PortablePathBuf::from(
5450 val.as_ref(),
5451 ))));
5452 }
5453 _ => {}
5454 }
5455 }
5456
5457 let precise = GitOid::from_str(url.fragment().ok_or(GitSourceError::MissingSha)?)
5458 .map_err(|_| GitSourceError::InvalidSha)?;
5459
5460 if let GitSourceKind::Rev(revision) = &kind
5463 && GitOid::from_str(revision).is_ok()
5464 && !revision.eq_ignore_ascii_case(precise.as_str())
5465 {
5466 return Err(GitSourceError::RevisionMismatch {
5467 revision: revision.clone(),
5468 precise,
5469 });
5470 }
5471
5472 Ok(Self {
5473 precise,
5474 subdirectory,
5475 path,
5476 kind,
5477 lfs,
5478 })
5479 }
5480}
5481
5482#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
5483#[serde(rename_all = "kebab-case")]
5484enum GitSourceKind {
5485 Tag(String),
5486 Branch(String),
5487 Rev(String),
5488 DefaultBranch,
5489}
5490
5491#[derive(Clone, Debug, PartialEq, Eq)]
5493struct SourceDistMetadata {
5494 hash: Option<Hash>,
5496 size: Option<u64>,
5500 upload_time: Option<Timestamp>,
5502}
5503
5504#[derive(Clone, Debug, PartialEq, Eq)]
5509enum SourceDist {
5510 Url {
5511 url: UrlString,
5512 metadata: SourceDistMetadata,
5513 },
5514 Path {
5515 path: Box<Path>,
5516 metadata: SourceDistMetadata,
5517 },
5518 Metadata {
5519 metadata: SourceDistMetadata,
5520 },
5521}
5522
5523impl<'de> serde::Deserialize<'de> for SourceDist {
5524 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5525 where
5526 D: serde::Deserializer<'de>,
5527 {
5528 #[derive(serde::Deserialize)]
5529 #[serde(rename_all = "kebab-case")]
5530 struct Fields {
5531 url: Option<UrlString>,
5532 path: Option<PortablePathBuf>,
5533 hash: Option<Hash>,
5534 size: Option<u64>,
5535 #[serde(alias = "upload_time")]
5536 upload_time: Option<Timestamp>,
5537 }
5538
5539 let Fields {
5540 url,
5541 path,
5542 hash,
5543 size,
5544 upload_time,
5545 } = serde::Deserialize::deserialize(deserializer)?;
5546
5547 let metadata = SourceDistMetadata {
5548 hash,
5549 size,
5550 upload_time,
5551 };
5552
5553 Ok(match (url, path) {
5554 (Some(url), _) => Self::Url { url, metadata },
5555 (None, Some(path)) => Self::Path {
5556 path: path.into(),
5557 metadata,
5558 },
5559 (None, None) => Self::Metadata { metadata },
5560 })
5561 }
5562}
5563
5564impl SourceDist {
5565 fn filename(&self) -> Option<Cow<'_, str>> {
5566 match self {
5567 Self::Metadata { .. } => None,
5568 Self::Url { url, .. } => url.filename().ok(),
5569 Self::Path { path, .. } => path.file_name().map(|filename| filename.to_string_lossy()),
5570 }
5571 }
5572
5573 fn url(&self) -> Option<&UrlString> {
5574 match self {
5575 Self::Metadata { .. } => None,
5576 Self::Url { url, .. } => Some(url),
5577 Self::Path { .. } => None,
5578 }
5579 }
5580
5581 fn hash(&self) -> Option<&Hash> {
5582 match self {
5583 Self::Metadata { metadata } => metadata.hash.as_ref(),
5584 Self::Url { metadata, .. } => metadata.hash.as_ref(),
5585 Self::Path { metadata, .. } => metadata.hash.as_ref(),
5586 }
5587 }
5588
5589 fn size(&self) -> Option<u64> {
5590 match self {
5591 Self::Metadata { metadata } => metadata.size,
5592 Self::Url { metadata, .. } => metadata.size,
5593 Self::Path { metadata, .. } => metadata.size,
5594 }
5595 }
5596
5597 fn upload_time(&self) -> Option<Timestamp> {
5598 match self {
5599 Self::Metadata { metadata } => metadata.upload_time,
5600 Self::Url { metadata, .. } => metadata.upload_time,
5601 Self::Path { metadata, .. } => metadata.upload_time,
5602 }
5603 }
5604}
5605
5606impl SourceDist {
5607 fn from_annotated_dist(
5608 id: &PackageId,
5609 annotated_dist: &AnnotatedDist,
5610 index_locations: &IndexLocations,
5611 ) -> Result<Option<Self>, LockError> {
5612 match annotated_dist.dist {
5613 ResolvedDist::Installed { .. } => unreachable!(),
5615 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
5616 id,
5617 dist,
5618 annotated_dist.hashes.as_slice(),
5619 annotated_dist.index(),
5620 index_locations,
5621 ),
5622 }
5623 }
5624
5625 fn from_dist(
5626 id: &PackageId,
5627 dist: &Dist,
5628 hashes: &[HashDigest],
5629 index: Option<&IndexUrl>,
5630 index_locations: &IndexLocations,
5631 ) -> Result<Option<Self>, LockError> {
5632 match *dist {
5633 Dist::Built(BuiltDist::Registry(ref built_dist)) => {
5634 let Some(sdist) = built_dist.sdist.as_ref() else {
5635 return Ok(None);
5636 };
5637 Self::from_registry_dist(sdist, index, index_locations)
5638 }
5639 Dist::Built(_) => Ok(None),
5640 Dist::Source(ref source_dist) => {
5641 Self::from_source_dist(id, source_dist, hashes, index, index_locations)
5642 }
5643 }
5644 }
5645
5646 fn from_source_dist(
5647 id: &PackageId,
5648 source_dist: &uv_distribution_types::SourceDist,
5649 hashes: &[HashDigest],
5650 index: Option<&IndexUrl>,
5651 index_locations: &IndexLocations,
5652 ) -> Result<Option<Self>, LockError> {
5653 match *source_dist {
5654 uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
5655 Self::from_registry_dist(reg_dist, index, index_locations)
5656 }
5657 uv_distribution_types::SourceDist::DirectUrl(_) => {
5658 Self::from_direct_dist(id, hashes).map(Some)
5659 }
5660 uv_distribution_types::SourceDist::Path(_) => {
5661 Self::from_path_dist(id, hashes).map(Some)
5662 }
5663 uv_distribution_types::SourceDist::GitPath(_) => {
5664 Self::from_git_path_dist(id, hashes).map(Some)
5665 }
5666 uv_distribution_types::SourceDist::GitDirectory(_)
5667 | uv_distribution_types::SourceDist::Directory(_) => Ok(None),
5668 }
5669 }
5670
5671 fn from_registry_dist(
5672 reg_dist: &RegistrySourceDist,
5673 index: Option<&IndexUrl>,
5674 index_locations: &IndexLocations,
5675 ) -> Result<Option<Self>, LockError> {
5676 if index.is_none_or(|index| *index != reg_dist.index) {
5679 return Ok(None);
5680 }
5681
5682 let hash = select_registry_hash(
5683 ®_dist.file.hashes,
5684 ®_dist.index,
5685 index_locations,
5686 reg_dist.file.filename.as_ref(),
5687 )?;
5688
5689 match ®_dist.index {
5690 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
5691 let url = normalize_file_location(®_dist.file.url)
5692 .map_err(LockErrorKind::InvalidUrl)
5693 .map_err(LockError::from)?;
5694 let size = reg_dist.file.size;
5695 let upload_time = reg_dist
5696 .file
5697 .upload_time_utc_ms
5698 .map(Timestamp::from_millisecond)
5699 .transpose()
5700 .map_err(LockErrorKind::InvalidTimestamp)?;
5701 Ok(Some(Self::Url {
5702 url,
5703 metadata: SourceDistMetadata {
5704 hash,
5705 size,
5706 upload_time,
5707 },
5708 }))
5709 }
5710 IndexUrl::Path(path) => {
5711 let index_path = path
5712 .to_file_path()
5713 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
5714 let url = reg_dist
5715 .file
5716 .url
5717 .to_url()
5718 .map_err(LockErrorKind::InvalidUrl)?;
5719
5720 if url.scheme() == "file" {
5721 let reg_dist_path = url
5722 .to_file_path()
5723 .map_err(|()| LockErrorKind::UrlToPath { url })?;
5724 let path =
5725 try_relative_to_if(®_dist_path, index_path, !path.was_given_absolute())
5726 .map_err(LockErrorKind::DistributionRelativePath)?
5727 .into_boxed_path();
5728 let size = reg_dist.file.size;
5729 let upload_time = reg_dist
5730 .file
5731 .upload_time_utc_ms
5732 .map(Timestamp::from_millisecond)
5733 .transpose()
5734 .map_err(LockErrorKind::InvalidTimestamp)?;
5735 Ok(Some(Self::Path {
5736 path,
5737 metadata: SourceDistMetadata {
5738 hash,
5739 size,
5740 upload_time,
5741 },
5742 }))
5743 } else {
5744 let url = normalize_file_location(®_dist.file.url)
5745 .map_err(LockErrorKind::InvalidUrl)
5746 .map_err(LockError::from)?;
5747 let size = reg_dist.file.size;
5748 let upload_time = reg_dist
5749 .file
5750 .upload_time_utc_ms
5751 .map(Timestamp::from_millisecond)
5752 .transpose()
5753 .map_err(LockErrorKind::InvalidTimestamp)?;
5754 Ok(Some(Self::Url {
5755 url,
5756 metadata: SourceDistMetadata {
5757 hash,
5758 size,
5759 upload_time,
5760 },
5761 }))
5762 }
5763 }
5764 }
5765 }
5766
5767 fn from_direct_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
5768 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
5769 let kind = LockErrorKind::Hash {
5770 id: id.clone(),
5771 artifact_type: "direct URL source distribution",
5772 expected: true,
5773 };
5774 return Err(kind.into());
5775 };
5776 Ok(Self::Metadata {
5777 metadata: SourceDistMetadata {
5778 hash: Some(hash),
5779 size: None,
5780 upload_time: None,
5781 },
5782 })
5783 }
5784
5785 fn from_path_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
5786 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
5787 let kind = LockErrorKind::Hash {
5788 id: id.clone(),
5789 artifact_type: "path source distribution",
5790 expected: true,
5791 };
5792 return Err(kind.into());
5793 };
5794 Ok(Self::Metadata {
5795 metadata: SourceDistMetadata {
5796 hash: Some(hash),
5797 size: None,
5798 upload_time: None,
5799 },
5800 })
5801 }
5802
5803 fn from_git_path_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
5804 let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
5805 let kind = LockErrorKind::Hash {
5806 id: id.clone(),
5807 artifact_type: "Git archive source distribution",
5808 expected: true,
5809 };
5810 return Err(kind.into());
5811 };
5812 Ok(Self::Metadata {
5813 metadata: SourceDistMetadata {
5814 hash: Some(hash),
5815 size: None,
5816 upload_time: None,
5817 },
5818 })
5819 }
5820}
5821
5822impl From<GitReference> for GitSourceKind {
5823 fn from(value: GitReference) -> Self {
5824 match value {
5825 GitReference::Branch(branch) => Self::Branch(branch),
5826 GitReference::Tag(tag) => Self::Tag(tag),
5827 GitReference::BranchOrTag(rev) => Self::Rev(rev),
5828 GitReference::BranchOrTagOrCommit(rev) => Self::Rev(rev),
5829 GitReference::NamedRef(rev) => Self::Rev(rev),
5830 GitReference::DefaultBranch => Self::DefaultBranch,
5831 }
5832 }
5833}
5834
5835impl From<GitSourceKind> for GitReference {
5836 fn from(value: GitSourceKind) -> Self {
5837 match value {
5838 GitSourceKind::Branch(branch) => Self::Branch(branch),
5839 GitSourceKind::Tag(tag) => Self::Tag(tag),
5840 GitSourceKind::Rev(rev) => Self::from_rev(rev),
5841 GitSourceKind::DefaultBranch => Self::DefaultBranch,
5842 }
5843 }
5844}
5845
5846fn locked_git_url(
5848 git: &GitUrl,
5849 subdirectory: Option<&Path>,
5850 path: Option<&Path>,
5851) -> DisplaySafeUrl {
5852 let mut url = git.url().clone();
5853
5854 url.remove_credentials();
5856
5857 url.set_fragment(None);
5859 url.set_query(None);
5860
5861 if let Some(subdirectory) = subdirectory
5863 .map(PortablePath::from)
5864 .as_ref()
5865 .map(PortablePath::to_string)
5866 {
5867 url.query_pairs_mut()
5868 .append_pair("subdirectory", &subdirectory);
5869 }
5870
5871 if let Some(path) = path
5873 .map(PortablePath::from)
5874 .as_ref()
5875 .map(PortablePath::to_string)
5876 {
5877 url.query_pairs_mut().append_pair("path", &path);
5878 }
5879
5880 if git.lfs().enabled() {
5882 url.query_pairs_mut().append_pair("lfs", "true");
5883 }
5884
5885 match git.reference() {
5887 GitReference::Branch(branch) => {
5888 url.query_pairs_mut().append_pair("branch", branch.as_str());
5889 }
5890 GitReference::Tag(tag) => {
5891 url.query_pairs_mut().append_pair("tag", tag.as_str());
5892 }
5893 GitReference::BranchOrTag(rev)
5894 | GitReference::BranchOrTagOrCommit(rev)
5895 | GitReference::NamedRef(rev) => {
5896 url.query_pairs_mut().append_pair("rev", rev.as_str());
5897 }
5898 GitReference::DefaultBranch => {}
5899 }
5900
5901 url.set_fragment(git.precise().as_ref().map(GitOid::to_string).as_deref());
5903
5904 url
5905}
5906
5907#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
5908struct ZstdWheel {
5909 hash: Option<Hash>,
5910 size: Option<u64>,
5911}
5912
5913#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
5915#[serde(try_from = "WheelWire")]
5916struct Wheel {
5917 url: WheelWireSource,
5922 hash: Option<Hash>,
5928 size: Option<u64>,
5932 upload_time: Option<Timestamp>,
5936 filename: WheelFilename,
5943 zstd: Option<ZstdWheel>,
5945}
5946
5947impl Wheel {
5948 fn from_annotated_dist(
5949 annotated_dist: &AnnotatedDist,
5950 index_locations: &IndexLocations,
5951 ) -> Result<Vec<Self>, LockError> {
5952 match annotated_dist.dist {
5953 ResolvedDist::Installed { .. } => unreachable!(),
5955 ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
5956 dist,
5957 annotated_dist.hashes.as_slice(),
5958 annotated_dist.index(),
5959 index_locations,
5960 ),
5961 }
5962 }
5963
5964 fn from_dist(
5965 dist: &Dist,
5966 hashes: &[HashDigest],
5967 index: Option<&IndexUrl>,
5968 index_locations: &IndexLocations,
5969 ) -> Result<Vec<Self>, LockError> {
5970 match *dist {
5971 Dist::Built(ref built_dist) => {
5972 Self::from_built_dist(built_dist, hashes, index, index_locations)
5973 }
5974 Dist::Source(uv_distribution_types::SourceDist::Registry(ref source_dist)) => {
5975 source_dist
5976 .wheels
5977 .iter()
5978 .filter(|wheel| {
5979 index.is_some_and(|index| *index == wheel.index)
5982 })
5983 .map(|wheel| Self::from_registry_wheel(wheel, index_locations))
5984 .collect()
5985 }
5986 Dist::Source(_) => Ok(vec![]),
5987 }
5988 }
5989
5990 fn from_built_dist(
5991 built_dist: &BuiltDist,
5992 hashes: &[HashDigest],
5993 index: Option<&IndexUrl>,
5994 index_locations: &IndexLocations,
5995 ) -> Result<Vec<Self>, LockError> {
5996 match *built_dist {
5997 BuiltDist::Registry(ref reg_dist) => {
5998 Self::from_registry_dist(reg_dist, index, index_locations)
5999 }
6000 BuiltDist::DirectUrl(ref direct_dist) => {
6001 Ok(vec![Self::from_direct_dist(direct_dist, hashes)])
6002 }
6003 BuiltDist::Path(ref path_dist) => Ok(vec![Self::from_path_dist(path_dist, hashes)]),
6004 BuiltDist::GitPath(ref git_dist) => {
6005 Ok(vec![Self::from_git_path_dist(git_dist, hashes)])
6006 }
6007 }
6008 }
6009
6010 fn from_registry_dist(
6011 reg_dist: &RegistryBuiltDist,
6012 index: Option<&IndexUrl>,
6013 index_locations: &IndexLocations,
6014 ) -> Result<Vec<Self>, LockError> {
6015 reg_dist
6016 .wheels
6017 .iter()
6018 .filter(|wheel| {
6019 index.is_some_and(|index| *index == wheel.index)
6022 })
6023 .map(|wheel| Self::from_registry_wheel(wheel, index_locations))
6024 .collect()
6025 }
6026
6027 fn from_registry_wheel(
6028 wheel: &RegistryBuiltWheel,
6029 index_locations: &IndexLocations,
6030 ) -> Result<Self, LockError> {
6031 let url = match &wheel.index {
6032 IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
6033 let url = normalize_file_location(&wheel.file.url)
6034 .map_err(LockErrorKind::InvalidUrl)
6035 .map_err(LockError::from)?;
6036 WheelWireSource::Url { url }
6037 }
6038 IndexUrl::Path(path) => {
6039 let index_path = path
6040 .to_file_path()
6041 .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
6042 let wheel_url = wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?;
6043
6044 if wheel_url.scheme() == "file" {
6045 let wheel_path = wheel_url
6046 .to_file_path()
6047 .map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?;
6048 let path =
6049 try_relative_to_if(&wheel_path, index_path, !path.was_given_absolute())
6050 .map_err(LockErrorKind::DistributionRelativePath)?
6051 .into_boxed_path();
6052 WheelWireSource::Path { path }
6053 } else {
6054 let url = normalize_file_location(&wheel.file.url)
6055 .map_err(LockErrorKind::InvalidUrl)
6056 .map_err(LockError::from)?;
6057 WheelWireSource::Url { url }
6058 }
6059 }
6060 };
6061 let filename = wheel.filename.clone();
6062 let hash = select_registry_hash(
6063 &wheel.file.hashes,
6064 &wheel.index,
6065 index_locations,
6066 wheel.file.filename.as_ref(),
6067 )?;
6068 let size = wheel.file.size;
6069 let upload_time = wheel
6070 .file
6071 .upload_time_utc_ms
6072 .map(Timestamp::from_millisecond)
6073 .transpose()
6074 .map_err(LockErrorKind::InvalidTimestamp)?;
6075 let zstd = if let Some(zstd) = wheel.file.zstd.as_ref() {
6076 Some(ZstdWheel {
6077 hash: select_registry_hash(
6078 &zstd.hashes,
6079 &wheel.index,
6080 index_locations,
6081 wheel.file.filename.as_ref(),
6082 )?,
6083 size: zstd.size,
6084 })
6085 } else {
6086 None
6087 };
6088 Ok(Self {
6089 url,
6090 hash,
6091 size,
6092 upload_time,
6093 filename,
6094 zstd,
6095 })
6096 }
6097
6098 fn from_direct_dist(direct_dist: &DirectUrlBuiltDist, hashes: &[HashDigest]) -> Self {
6099 Self {
6100 url: WheelWireSource::Url {
6101 url: normalize_url(direct_dist.url.to_url()),
6102 },
6103 hash: hashes.iter().max().cloned().map(Hash::from),
6104 size: None,
6105 upload_time: None,
6106 filename: direct_dist.filename.clone(),
6107 zstd: None,
6108 }
6109 }
6110
6111 fn from_path_dist(path_dist: &PathBuiltDist, hashes: &[HashDigest]) -> Self {
6112 Self {
6113 url: WheelWireSource::Filename {
6114 filename: path_dist.filename.clone(),
6115 },
6116 hash: hashes.iter().max().cloned().map(Hash::from),
6117 size: None,
6118 upload_time: None,
6119 filename: path_dist.filename.clone(),
6120 zstd: None,
6121 }
6122 }
6123
6124 fn from_git_path_dist(path_dist: &GitPathBuiltDist, hashes: &[HashDigest]) -> Self {
6125 Self {
6126 url: WheelWireSource::Filename {
6127 filename: path_dist.filename.clone(),
6128 },
6129 hash: hashes.iter().max().cloned().map(Hash::from),
6130 size: None,
6131 upload_time: None,
6132 filename: path_dist.filename.clone(),
6133 zstd: None,
6134 }
6135 }
6136
6137 fn to_registry_wheel(
6138 &self,
6139 source: &RegistrySource,
6140 root: &Path,
6141 ) -> Result<RegistryBuiltWheel, LockError> {
6142 let filename: WheelFilename = self.filename.clone();
6143
6144 match source {
6145 RegistrySource::Url(url) => {
6146 let file_location = match &self.url {
6147 WheelWireSource::Url { url: file_url } => {
6148 FileLocation::AbsoluteUrl(file_url.clone())
6149 }
6150 WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => {
6151 return Err(LockErrorKind::MissingUrl {
6152 name: filename.name,
6153 version: filename.version,
6154 }
6155 .into());
6156 }
6157 };
6158 let file = Box::new(uv_distribution_types::File {
6159 dist_info_metadata: false,
6160 filename: SmallString::from(filename.to_string()),
6161 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
6162 requires_python: None,
6163 size: self.size,
6164 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
6165 url: file_location,
6166 yanked: None,
6167 zstd: self
6168 .zstd
6169 .as_ref()
6170 .map(|zstd| uv_distribution_types::Zstd {
6171 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
6172 size: zstd.size,
6173 })
6174 .map(Box::new),
6175 });
6176 let index = IndexUrl::from(VerbatimUrl::from_url(
6177 url.to_url().map_err(LockErrorKind::InvalidUrl)?,
6178 ));
6179 Ok(RegistryBuiltWheel {
6180 filename,
6181 file,
6182 index,
6183 size_is_authoritative: false,
6184 })
6185 }
6186 RegistrySource::Path(index_path) => {
6187 let file_location = match &self.url {
6188 WheelWireSource::Url { url: file_url } => {
6189 FileLocation::AbsoluteUrl(file_url.clone())
6190 }
6191 WheelWireSource::Path { path: file_path } => {
6192 let file_path = root.join(index_path).join(file_path);
6193 let file_url =
6194 DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
6195 LockErrorKind::PathToUrl {
6196 path: file_path.into_boxed_path(),
6197 }
6198 })?;
6199 FileLocation::AbsoluteUrl(UrlString::from(file_url))
6200 }
6201 WheelWireSource::Filename { .. } => {
6202 return Err(LockErrorKind::MissingPath {
6203 name: filename.name,
6204 version: filename.version,
6205 }
6206 .into());
6207 }
6208 };
6209 let file = Box::new(uv_distribution_types::File {
6210 dist_info_metadata: false,
6211 filename: SmallString::from(filename.to_string()),
6212 hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
6213 requires_python: None,
6214 size: self.size,
6215 upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
6216 url: file_location,
6217 yanked: None,
6218 zstd: self
6219 .zstd
6220 .as_ref()
6221 .map(|zstd| uv_distribution_types::Zstd {
6222 hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
6223 size: zstd.size,
6224 })
6225 .map(Box::new),
6226 });
6227 let index = IndexUrl::from(
6228 VerbatimUrl::from_absolute_path(root.join(index_path))
6229 .map_err(LockErrorKind::RegistryVerbatimUrl)?,
6230 );
6231 Ok(RegistryBuiltWheel {
6232 filename,
6233 file,
6234 index,
6235 size_is_authoritative: false,
6236 })
6237 }
6238 }
6239 }
6240}
6241
6242#[derive(Clone, Debug, serde::Deserialize)]
6243#[serde(rename_all = "kebab-case")]
6244struct WheelWire {
6245 url: Option<UrlString>,
6246 path: Option<Box<Path>>,
6247 filename: Option<WheelFilename>,
6248 hash: Option<Hash>,
6254 size: Option<u64>,
6258 #[serde(alias = "upload_time")]
6262 upload_time: Option<Timestamp>,
6263 #[serde(alias = "zstd")]
6265 zstd: Option<ZstdWheel>,
6266}
6267
6268#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
6269#[serde(untagged, rename_all = "kebab-case")]
6270enum WheelWireSource {
6271 Url {
6273 url: UrlString,
6278 },
6279 Path {
6281 path: Box<Path>,
6283 },
6284 Filename {
6288 filename: WheelFilename,
6291 },
6292}
6293
6294impl TryFrom<WheelWire> for Wheel {
6295 type Error = String;
6296
6297 fn try_from(wire: WheelWire) -> Result<Self, String> {
6298 let source = if let Some(url) = wire.url {
6299 WheelWireSource::Url { url }
6300 } else if let Some(path) = wire.path {
6301 WheelWireSource::Path { path }
6302 } else if let Some(filename) = wire.filename {
6303 WheelWireSource::Filename { filename }
6304 } else {
6305 return Err("wheel has no URL, path, or filename".to_string());
6306 };
6307
6308 let filename = match &source {
6309 WheelWireSource::Url { url } => {
6310 let filename = url.filename().map_err(|err| err.to_string())?;
6311 filename.parse::<WheelFilename>().map_err(|err| {
6312 format!("failed to parse `{filename}` as wheel filename: {err}")
6313 })?
6314 }
6315 WheelWireSource::Path { path } => {
6316 let filename = path
6317 .file_name()
6318 .and_then(|file_name| file_name.to_str())
6319 .ok_or_else(|| {
6320 format!("path `{}` has no filename component", path.display())
6321 })?;
6322 filename.parse::<WheelFilename>().map_err(|err| {
6323 format!("failed to parse `{filename}` as wheel filename: {err}")
6324 })?
6325 }
6326 WheelWireSource::Filename { filename } => filename.clone(),
6327 };
6328
6329 Ok(Self {
6330 url: source,
6331 hash: wire.hash,
6332 size: wire.size,
6333 upload_time: wire.upload_time,
6334 zstd: wire.zstd,
6335 filename,
6336 })
6337 }
6338}
6339
6340#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
6342pub struct Dependency {
6343 package_id: PackageId,
6344 extra: BTreeSet<ExtraName>,
6345 simplified_marker: SimplifiedMarkerTree,
6366 complexified_marker: UniversalMarker,
6370}
6371
6372impl Dependency {
6373 fn new(
6374 requires_python: &RequiresPython,
6375 package_id: PackageId,
6376 extra: BTreeSet<ExtraName>,
6377 simplified_marker: SimplifiedMarkerTree,
6378 ) -> Self {
6379 let complexified_marker = simplified_marker.into_marker(requires_python);
6380 Self {
6381 package_id,
6382 extra,
6383 simplified_marker,
6384 complexified_marker: UniversalMarker::from_combined(complexified_marker),
6385 }
6386 }
6387
6388 pub fn package_name(&self) -> &PackageName {
6390 &self.package_id.name
6391 }
6392
6393 pub fn extra(&self) -> &BTreeSet<ExtraName> {
6395 &self.extra
6396 }
6397}
6398
6399impl Display for Dependency {
6400 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
6401 match (self.extra.is_empty(), self.package_id.version.as_ref()) {
6402 (true, Some(version)) => write!(f, "{}=={}", self.package_id.name, version),
6403 (true, None) => write!(f, "{}", self.package_id.name),
6404 (false, Some(version)) => write!(
6405 f,
6406 "{}[{}]=={}",
6407 self.package_id.name,
6408 self.extra.iter().join(","),
6409 version
6410 ),
6411 (false, None) => write!(
6412 f,
6413 "{}[{}]",
6414 self.package_id.name,
6415 self.extra.iter().join(",")
6416 ),
6417 }
6418 }
6419}
6420
6421#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)]
6423#[serde(rename_all = "kebab-case")]
6424struct DependencyWire {
6425 #[serde(flatten)]
6426 package_id: PackageIdForDependency,
6427 #[serde(default)]
6428 extra: BTreeSet<ExtraName>,
6429 #[serde(default)]
6430 marker: SimplifiedMarkerTree,
6431}
6432
6433impl DependencyWire {
6434 fn unwire(
6435 self,
6436 requires_python: &RequiresPython,
6437 environment: SimplifiedMarkerTree,
6438 default: UniversalMarker,
6439 unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
6440 ) -> Result<Dependency, LockError> {
6441 let (simplified_marker, complexified_marker) =
6442 if self.marker.as_simplified_marker_tree().is_true() {
6443 (environment, default)
6444 } else {
6445 let mut simplified_marker = self.marker;
6446 simplified_marker.and(environment);
6447 let complexified_marker =
6448 UniversalMarker::from_combined(simplified_marker.into_marker(requires_python));
6449 (simplified_marker, complexified_marker)
6450 };
6451 Ok(Dependency {
6452 package_id: self.package_id.unwire(unambiguous_package_ids)?,
6453 extra: self.extra,
6454 simplified_marker,
6455 complexified_marker,
6456 })
6457 }
6458}
6459
6460#[derive(Clone, Debug, PartialEq, Eq)]
6465struct Hash(HashDigest);
6466
6467impl From<HashDigest> for Hash {
6468 fn from(hd: HashDigest) -> Self {
6469 Self(hd)
6470 }
6471}
6472
6473fn select_registry_hash(
6478 hashes: &HashDigests,
6479 index: &IndexUrl,
6480 index_locations: &IndexLocations,
6481 filename: &str,
6482) -> Result<Option<Hash>, LockError> {
6483 let Some(algorithm) = index_locations.hash_algorithm_for(index) else {
6484 return Ok(hashes.iter().max().cloned().map(Hash::from));
6485 };
6486 warn_index_hash_algorithm_preview();
6487
6488 hashes
6489 .iter()
6490 .find(|hash| hash.algorithm == algorithm)
6491 .cloned()
6492 .map(Hash::from)
6493 .map(Some)
6494 .ok_or_else(|| {
6495 LockErrorKind::MissingHashAlgorithm {
6496 index: index.clone(),
6497 filename: filename.to_string(),
6498 algorithm,
6499 }
6500 .into()
6501 })
6502}
6503
6504fn warn_index_hash_algorithm_preview() {
6506 if !uv_preview::is_enabled(PreviewFeature::IndexHashAlgorithm) {
6507 warn_user_once!(
6508 "Setting `hash-algorithm` on configured indexes is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.",
6509 PreviewFeature::IndexHashAlgorithm
6510 );
6511 }
6512}
6513
6514impl FromStr for Hash {
6515 type Err = HashParseError;
6516
6517 fn from_str(s: &str) -> Result<Self, HashParseError> {
6518 let (algorithm, digest) = s.split_once(':').ok_or(HashParseError(
6519 "expected '{algorithm}:{digest}', but found no ':' in hash digest",
6520 ))?;
6521 let algorithm = algorithm
6522 .parse()
6523 .map_err(|_| HashParseError("unrecognized hash algorithm"))?;
6524 Ok(Self(HashDigest {
6525 algorithm,
6526 digest: digest.into(),
6527 }))
6528 }
6529}
6530
6531impl Display for Hash {
6532 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
6533 write!(f, "{}:{}", self.0.algorithm, self.0.digest)
6534 }
6535}
6536
6537impl<'de> serde::Deserialize<'de> for Hash {
6538 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
6539 where
6540 D: serde::de::Deserializer<'de>,
6541 {
6542 struct Visitor;
6543
6544 impl serde::de::Visitor<'_> for Visitor {
6545 type Value = Hash;
6546
6547 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
6548 f.write_str("a string")
6549 }
6550
6551 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
6552 Hash::from_str(v).map_err(serde::de::Error::custom)
6553 }
6554 }
6555
6556 deserializer.deserialize_str(Visitor)
6557 }
6558}
6559
6560impl From<Hash> for Hashes {
6561 fn from(value: Hash) -> Self {
6562 match value.0.algorithm {
6563 HashAlgorithm::Md5 => Self {
6564 md5: Some(value.0.digest),
6565 sha256: None,
6566 sha384: None,
6567 sha512: None,
6568 blake2b: None,
6569 },
6570 HashAlgorithm::Sha256 => Self {
6571 md5: None,
6572 sha256: Some(value.0.digest),
6573 sha384: None,
6574 sha512: None,
6575 blake2b: None,
6576 },
6577 HashAlgorithm::Sha384 => Self {
6578 md5: None,
6579 sha256: None,
6580 sha384: Some(value.0.digest),
6581 sha512: None,
6582 blake2b: None,
6583 },
6584 HashAlgorithm::Sha512 => Self {
6585 md5: None,
6586 sha256: None,
6587 sha384: None,
6588 sha512: Some(value.0.digest),
6589 blake2b: None,
6590 },
6591 HashAlgorithm::Blake2b => Self {
6592 md5: None,
6593 sha256: None,
6594 sha384: None,
6595 sha512: None,
6596 blake2b: Some(value.0.digest),
6597 },
6598 }
6599 }
6600}
6601
6602fn normalize_file_location(location: &FileLocation) -> Result<UrlString, ToUrlError> {
6604 match location {
6605 FileLocation::AbsoluteUrl(absolute) => Ok(absolute.without_fragment().into_owned()),
6606 FileLocation::RelativeUrl(_, _) => Ok(normalize_url(location.to_url()?)),
6607 }
6608}
6609
6610fn normalize_url(mut url: DisplaySafeUrl) -> UrlString {
6612 url.set_fragment(None);
6613 UrlString::from(url)
6614}
6615
6616fn normalize_requirement(
6626 mut requirement: Requirement,
6627 root: &Path,
6628 requires_python: &RequiresPython,
6629) -> Result<Requirement, LockError> {
6630 requirement.extras.sort();
6632 requirement.groups.sort();
6633
6634 match requirement.source {
6636 RequirementSource::GitDirectory {
6637 git,
6638 subdirectory,
6639 url: _,
6640 } => {
6641 let git = {
6643 let mut repository = git.url().clone();
6644
6645 repository.remove_credentials();
6647
6648 repository.set_fragment(None);
6650 repository.set_query(None);
6651
6652 GitUrl::from_fields(
6653 repository,
6654 git.reference().clone(),
6655 git.precise(),
6656 git.lfs(),
6657 )?
6658 };
6659
6660 let url = DisplaySafeUrl::from(ParsedGitDirectoryUrl {
6662 url: git.clone(),
6663 subdirectory: subdirectory.clone(),
6664 });
6665
6666 Ok(Requirement {
6667 name: requirement.name,
6668 extras: requirement.extras,
6669 groups: requirement.groups,
6670 marker: requires_python.simplify_markers(requirement.marker),
6671 source: RequirementSource::GitDirectory {
6672 git,
6673 subdirectory,
6674 url: VerbatimUrl::from_url(url),
6675 },
6676 origin: None,
6677 })
6678 }
6679 RequirementSource::GitPath {
6680 git,
6681 install_path,
6682 ext,
6683 url: _,
6684 } => {
6685 let git = {
6687 let mut repository = git.url().clone();
6688
6689 repository.remove_credentials();
6691
6692 repository.set_fragment(None);
6694 repository.set_query(None);
6695
6696 GitUrl::from_fields(
6697 repository,
6698 git.reference().clone(),
6699 git.precise(),
6700 git.lfs(),
6701 )?
6702 };
6703
6704 let url = DisplaySafeUrl::from(ParsedGitPathUrl {
6706 url: git.clone(),
6707 install_path: install_path.clone(),
6708 ext,
6709 });
6710
6711 Ok(Requirement {
6712 name: requirement.name,
6713 extras: requirement.extras,
6714 groups: requirement.groups,
6715 marker: requires_python.simplify_markers(requirement.marker),
6716 source: RequirementSource::GitPath {
6717 git,
6718 install_path,
6719 ext,
6720 url: VerbatimUrl::from_url(url),
6721 },
6722 origin: None,
6723 })
6724 }
6725 RequirementSource::Path {
6726 install_path,
6727 ext,
6728 url: _,
6729 } => {
6730 let path = root.join(&install_path);
6731 let install_path = normalize_path(path).into_owned().into_boxed_path();
6732 let url = VerbatimUrl::from_normalized_path(&install_path)
6733 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
6734
6735 Ok(Requirement {
6736 name: requirement.name,
6737 extras: requirement.extras,
6738 groups: requirement.groups,
6739 marker: requires_python.simplify_markers(requirement.marker),
6740 source: RequirementSource::Path {
6741 install_path,
6742 ext,
6743 url,
6744 },
6745 origin: None,
6746 })
6747 }
6748 RequirementSource::Directory {
6749 install_path,
6750 editable,
6751 r#virtual,
6752 url: _,
6753 } => {
6754 let path = root.join(&install_path);
6755 let install_path = normalize_path(path).into_owned().into_boxed_path();
6756 let url = VerbatimUrl::from_normalized_path(&install_path)
6757 .map_err(LockErrorKind::RequirementVerbatimUrl)?;
6758
6759 Ok(Requirement {
6760 name: requirement.name,
6761 extras: requirement.extras,
6762 groups: requirement.groups,
6763 marker: requires_python.simplify_markers(requirement.marker),
6764 source: RequirementSource::Directory {
6765 install_path,
6766 editable: Some(editable.unwrap_or(false)),
6767 r#virtual: Some(r#virtual.unwrap_or(false)),
6768 url,
6769 },
6770 origin: None,
6771 })
6772 }
6773 RequirementSource::Registry {
6774 specifier,
6775 index,
6776 conflict,
6777 } => {
6778 let index = index
6780 .map(|index| index.url.into_url())
6781 .map(|mut index| {
6782 index.remove_credentials();
6783 index
6784 })
6785 .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index))));
6786 Ok(Requirement {
6787 name: requirement.name,
6788 extras: requirement.extras,
6789 groups: requirement.groups,
6790 marker: requires_python.simplify_markers(requirement.marker),
6791 source: RequirementSource::Registry {
6792 specifier,
6793 index,
6794 conflict,
6795 },
6796 origin: None,
6797 })
6798 }
6799 RequirementSource::Url {
6800 mut location,
6801 subdirectory,
6802 ext,
6803 url: _,
6804 } => {
6805 location.remove_credentials();
6807
6808 location.set_fragment(None);
6810
6811 let url = DisplaySafeUrl::from(ParsedArchiveUrl {
6813 url: location.clone(),
6814 subdirectory: subdirectory.clone(),
6815 ext,
6816 });
6817
6818 Ok(Requirement {
6819 name: requirement.name,
6820 extras: requirement.extras,
6821 groups: requirement.groups,
6822 marker: requires_python.simplify_markers(requirement.marker),
6823 source: RequirementSource::Url {
6824 location,
6825 subdirectory,
6826 ext,
6827 url: VerbatimUrl::from_url(url),
6828 },
6829 origin: None,
6830 })
6831 }
6832 }
6833}
6834
6835#[derive(Debug)]
6836pub struct LockError {
6837 kind: Box<LockErrorKind>,
6838 hint: Option<WheelTagHint>,
6839}
6840
6841impl std::error::Error for LockError {
6842 fn source(&self) -> Option<&(dyn Error + 'static)> {
6843 self.kind.source()
6844 }
6845}
6846
6847impl uv_errors::Hint for LockError {
6848 fn hints(&self) -> uv_errors::Hints<'_> {
6849 if let Some(hint) = &self.hint {
6850 uv_errors::Hints::from(hint.to_string())
6851 } else {
6852 uv_errors::Hints::none()
6853 }
6854 }
6855}
6856
6857impl std::fmt::Display for LockError {
6858 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6859 write!(f, "{}", self.kind)
6860 }
6861}
6862
6863impl LockError {
6864 pub fn is_resolution(&self) -> bool {
6866 matches!(&*self.kind, LockErrorKind::Resolution { .. })
6867 }
6868
6869 pub fn is_no_build(&self) -> bool {
6871 matches!(
6872 &*self.kind,
6873 LockErrorKind::NoBuild { .. } | LockErrorKind::NoBinaryNoBuild { .. }
6874 )
6875 }
6876
6877 pub fn is_not_pep625(&self) -> bool {
6880 matches!(&*self.kind, LockErrorKind::NotPep625Filename { .. })
6881 }
6882}
6883
6884impl<E> From<E> for LockError
6885where
6886 LockErrorKind: From<E>,
6887{
6888 fn from(err: E) -> Self {
6889 Self {
6890 kind: Box::new(LockErrorKind::from(err)),
6891 hint: None,
6892 }
6893 }
6894}
6895
6896#[derive(Debug, Clone, PartialEq, Eq)]
6897#[expect(clippy::enum_variant_names)]
6898enum WheelTagHint {
6899 LanguageTags {
6902 package: PackageName,
6903 version: Option<Version>,
6904 tags: BTreeSet<LanguageTag>,
6905 best: Option<LanguageTag>,
6906 },
6907 AbiTags {
6910 package: PackageName,
6911 version: Option<Version>,
6912 tags: BTreeSet<AbiTag>,
6913 best: Option<AbiTag>,
6914 },
6915 PlatformTags {
6918 package: PackageName,
6919 version: Option<Version>,
6920 tags: BTreeSet<PlatformTag>,
6921 best: Option<PlatformTag>,
6922 markers: MarkerEnvironment,
6923 },
6924}
6925
6926impl WheelTagHint {
6927 fn from_wheels(
6929 name: &PackageName,
6930 version: Option<&Version>,
6931 filenames: &[&WheelFilename],
6932 tags: &Tags,
6933 markers: &MarkerEnvironment,
6934 ) -> Option<Self> {
6935 let incompatibility = filenames
6936 .iter()
6937 .map(|filename| {
6938 tags.compatibility(
6939 filename.python_tags().iter(),
6940 filename.abi_tags().iter(),
6941 filename.platform_tags().iter(),
6942 )
6943 })
6944 .max()?;
6945 match incompatibility {
6946 TagCompatibility::Incompatible(IncompatibleTag::Python) => {
6947 let best = tags.python_tag();
6948 let tags = Self::python_tags(filenames.iter().copied()).collect::<BTreeSet<_>>();
6949 if tags.is_empty() {
6950 None
6951 } else {
6952 Some(Self::LanguageTags {
6953 package: name.clone(),
6954 version: version.cloned(),
6955 tags,
6956 best,
6957 })
6958 }
6959 }
6960 TagCompatibility::Incompatible(IncompatibleTag::Abi) => {
6961 let best = tags.abi_tag();
6962 let tags = Self::abi_tags(filenames.iter().copied())
6963 .filter(|tag| *tag != AbiTag::None)
6972 .collect::<BTreeSet<_>>();
6973 if tags.is_empty() {
6974 None
6975 } else {
6976 Some(Self::AbiTags {
6977 package: name.clone(),
6978 version: version.cloned(),
6979 tags,
6980 best,
6981 })
6982 }
6983 }
6984 TagCompatibility::Incompatible(IncompatibleTag::Platform) => {
6985 let best = tags.platform_tag().cloned();
6986 let incompatible_tags = Self::platform_tags(filenames.iter().copied(), tags)
6987 .cloned()
6988 .collect::<BTreeSet<_>>();
6989 if incompatible_tags.is_empty() {
6990 None
6991 } else {
6992 Some(Self::PlatformTags {
6993 package: name.clone(),
6994 version: version.cloned(),
6995 tags: incompatible_tags,
6996 best,
6997 markers: markers.clone(),
6998 })
6999 }
7000 }
7001 _ => None,
7002 }
7003 }
7004
7005 fn python_tags<'a>(
7007 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
7008 ) -> impl Iterator<Item = LanguageTag> + 'a {
7009 filenames.flat_map(WheelFilename::python_tags).copied()
7010 }
7011
7012 fn abi_tags<'a>(
7014 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
7015 ) -> impl Iterator<Item = AbiTag> + 'a {
7016 filenames.flat_map(WheelFilename::abi_tags).copied()
7017 }
7018
7019 fn platform_tags<'a>(
7022 filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
7023 tags: &'a Tags,
7024 ) -> impl Iterator<Item = &'a PlatformTag> + 'a {
7025 filenames.flat_map(move |filename| {
7026 if filename.python_tags().iter().any(|wheel_py| {
7027 filename
7028 .abi_tags()
7029 .iter()
7030 .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
7031 }) {
7032 filename.platform_tags().iter()
7033 } else {
7034 [].iter()
7035 }
7036 })
7037 }
7038
7039 fn suggest_environment_marker(markers: &MarkerEnvironment) -> String {
7040 let sys_platform = markers.sys_platform();
7041 let platform_machine = markers.platform_machine();
7042
7043 if platform_machine.is_empty() {
7045 format!("sys_platform == '{sys_platform}'")
7046 } else {
7047 format!("sys_platform == '{sys_platform}' and platform_machine == '{platform_machine}'")
7048 }
7049 }
7050}
7051
7052impl std::fmt::Display for WheelTagHint {
7053 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7054 match self {
7055 Self::LanguageTags {
7056 package,
7057 version,
7058 tags,
7059 best,
7060 } => {
7061 if let Some(best) = best {
7062 let s = if tags.len() == 1 { "" } else { "s" };
7063 let best = if let Some(pretty) = best.pretty() {
7064 format!("{} (`{}`)", pretty.cyan(), best.cyan())
7065 } else {
7066 format!("{}", best.cyan())
7067 };
7068 if let Some(version) = version {
7069 write!(
7070 f,
7071 "You're using {}, but `{}` ({}) only has wheels with the following Python implementation tag{s}: {}",
7072 best,
7073 package.cyan(),
7074 format!("v{version}").cyan(),
7075 tags.iter()
7076 .map(|tag| format!("`{}`", tag.cyan()))
7077 .join(", "),
7078 )
7079 } else {
7080 write!(
7081 f,
7082 "You're using {}, but `{}` only has wheels with the following Python implementation tag{s}: {}",
7083 best,
7084 package.cyan(),
7085 tags.iter()
7086 .map(|tag| format!("`{}`", tag.cyan()))
7087 .join(", "),
7088 )
7089 }
7090 } else {
7091 let s = if tags.len() == 1 { "" } else { "s" };
7092 if let Some(version) = version {
7093 write!(
7094 f,
7095 "Wheels are available for `{}` ({}) with the following Python implementation tag{s}: {}",
7096 package.cyan(),
7097 format!("v{version}").cyan(),
7098 tags.iter()
7099 .map(|tag| format!("`{}`", tag.cyan()))
7100 .join(", "),
7101 )
7102 } else {
7103 write!(
7104 f,
7105 "Wheels are available for `{}` with the following Python implementation tag{s}: {}",
7106 package.cyan(),
7107 tags.iter()
7108 .map(|tag| format!("`{}`", tag.cyan()))
7109 .join(", "),
7110 )
7111 }
7112 }
7113 }
7114 Self::AbiTags {
7115 package,
7116 version,
7117 tags,
7118 best,
7119 } => {
7120 if let Some(best) = best {
7121 let s = if tags.len() == 1 { "" } else { "s" };
7122 let best = if let Some(pretty) = best.pretty() {
7123 format!("{} (`{}`)", pretty.cyan(), best.cyan())
7124 } else {
7125 format!("{}", best.cyan())
7126 };
7127 if let Some(version) = version {
7128 write!(
7129 f,
7130 "You're using {}, but `{}` ({}) only has wheels with the following Python ABI tag{s}: {}",
7131 best,
7132 package.cyan(),
7133 format!("v{version}").cyan(),
7134 tags.iter()
7135 .map(|tag| format!("`{}`", tag.cyan()))
7136 .join(", "),
7137 )
7138 } else {
7139 write!(
7140 f,
7141 "You're using {}, but `{}` only has wheels with the following Python ABI tag{s}: {}",
7142 best,
7143 package.cyan(),
7144 tags.iter()
7145 .map(|tag| format!("`{}`", tag.cyan()))
7146 .join(", "),
7147 )
7148 }
7149 } else {
7150 let s = if tags.len() == 1 { "" } else { "s" };
7151 if let Some(version) = version {
7152 write!(
7153 f,
7154 "Wheels are available for `{}` ({}) with the following Python ABI tag{s}: {}",
7155 package.cyan(),
7156 format!("v{version}").cyan(),
7157 tags.iter()
7158 .map(|tag| format!("`{}`", tag.cyan()))
7159 .join(", "),
7160 )
7161 } else {
7162 write!(
7163 f,
7164 "Wheels are available for `{}` with the following Python ABI tag{s}: {}",
7165 package.cyan(),
7166 tags.iter()
7167 .map(|tag| format!("`{}`", tag.cyan()))
7168 .join(", "),
7169 )
7170 }
7171 }
7172 }
7173 Self::PlatformTags {
7174 package,
7175 version,
7176 tags,
7177 best,
7178 markers,
7179 } => {
7180 let s = if tags.len() == 1 { "" } else { "s" };
7181 if let Some(best) = best {
7182 let example_marker = Self::suggest_environment_marker(markers);
7183 let best = if let Some(pretty) = best.pretty() {
7184 format!("{} (`{}`)", pretty.cyan(), best.cyan())
7185 } else {
7186 format!("`{}`", best.cyan())
7187 };
7188 let package_ref = if let Some(version) = version {
7189 format!("`{}` ({})", package.cyan(), format!("v{version}").cyan())
7190 } else {
7191 format!("`{}`", package.cyan())
7192 };
7193 write!(
7194 f,
7195 "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",
7196 best,
7197 package_ref,
7198 tags.iter()
7199 .map(|tag| format!("`{}`", tag.cyan()))
7200 .join(", "),
7201 format!("\"{example_marker}\"").cyan(),
7202 "tool.uv.required-environments".green()
7203 )
7204 } else {
7205 if let Some(version) = version {
7206 write!(
7207 f,
7208 "Wheels are available for `{}` ({}) on the following platform{s}: {}",
7209 package.cyan(),
7210 format!("v{version}").cyan(),
7211 tags.iter()
7212 .map(|tag| format!("`{}`", tag.cyan()))
7213 .join(", "),
7214 )
7215 } else {
7216 write!(
7217 f,
7218 "Wheels are available for `{}` on the following platform{s}: {}",
7219 package.cyan(),
7220 tags.iter()
7221 .map(|tag| format!("`{}`", tag.cyan()))
7222 .join(", "),
7223 )
7224 }
7225 }
7226 }
7227 }
7228 }
7229}
7230
7231#[derive(Debug, thiserror::Error)]
7238enum LockErrorKind {
7239 #[error(transparent)]
7242 InvalidScopedOverride(#[from] ScopedOverrideSourceError),
7243 #[error("Found duplicate package `{id}`", id = id.cyan())]
7246 DuplicatePackage {
7247 id: PackageId,
7249 },
7250 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = id.cyan(), dependency = dependency.cyan())]
7253 DuplicateDependency {
7254 id: PackageId,
7257 dependency: Dependency,
7259 },
7260 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}[{extra}]").cyan(), dependency = dependency.cyan())]
7264 DuplicateOptionalDependency {
7265 id: PackageId,
7268 extra: ExtraName,
7270 dependency: Dependency,
7272 },
7273 #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}:{group}").cyan(), dependency = dependency.cyan())]
7277 DuplicateDevDependency {
7278 id: PackageId,
7281 group: GroupName,
7283 dependency: Dependency,
7285 },
7286 #[error(transparent)]
7289 InvalidUrl(
7290 #[from]
7293 ToUrlError,
7294 ),
7295 #[error("Failed to parse file extension for `{id}`; expected one of: {err}", id = id.cyan())]
7298 MissingExtension {
7299 id: PackageId,
7301 err: ExtensionError,
7303 },
7304 #[error(
7307 "Source distribution for `{id}` has a non-PEP 625-compliant filename; only `.tar.gz` and `.zip` archives are accepted",
7308 id = id.cyan()
7309 )]
7310 NotPep625Filename {
7311 id: PackageId,
7313 },
7314 #[error("Failed to parse Git URL")]
7316 InvalidGitSourceUrl(
7317 #[source]
7320 SourceParseError,
7321 ),
7322 #[error("Failed to parse timestamp")]
7323 InvalidTimestamp(
7324 #[source]
7327 jiff::Error,
7328 ),
7329 #[error("For package `{id}`, found dependency `{dependency}` with no locked package", id = id.cyan(), dependency = dependency.cyan())]
7333 UnrecognizedDependency {
7334 id: PackageId,
7336 dependency: Dependency,
7339 },
7340 #[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" })]
7343 Hash {
7344 id: PackageId,
7346 artifact_type: &'static str,
7349 expected: bool,
7351 },
7352 #[error(
7355 "The index `{index}` requires `{algorithm}` hashes, but `{filename}` does not provide one"
7356 )]
7357 MissingHashAlgorithm {
7358 index: IndexUrl,
7359 filename: String,
7360 algorithm: HashAlgorithm,
7361 },
7362 #[error("Found package `{id}` with extra `{extra}` but no base package", id = id.cyan(), extra = extra.cyan())]
7365 MissingExtraBase {
7366 id: PackageId,
7368 extra: ExtraName,
7370 },
7371 #[error("Found package `{id}` with development dependency group `{group}` but no base package", id = id.cyan())]
7375 MissingDevBase {
7376 id: PackageId,
7378 group: GroupName,
7380 },
7381 #[error("Wheels cannot come from {source_type} sources")]
7384 InvalidWheelSource {
7385 id: PackageId,
7387 source_type: &'static str,
7389 },
7390 #[error("Found registry distribution `{name}` ({version}) without a valid URL", name = name.cyan(), version = format!("v{version}").cyan())]
7393 MissingUrl {
7394 name: PackageName,
7396 version: Version,
7398 },
7399 #[error("Found registry distribution `{name}` ({version}) without a valid path", name = name.cyan(), version = format!("v{version}").cyan())]
7402 MissingPath {
7403 name: PackageName,
7405 version: Version,
7407 },
7408 #[error("Found registry distribution `{id}` without a valid filename", id = id.cyan())]
7411 MissingFilename {
7412 id: PackageId,
7414 },
7415 #[error("Distribution `{id}` can't be installed because it doesn't have a source distribution or wheel for the current platform", id = id.cyan())]
7418 NeitherSourceDistNorWheel {
7419 id: PackageId,
7421 },
7422 #[error("Distribution `{id}` can't be installed because it is marked as both `--no-binary` and `--no-build`", id = id.cyan())]
7424 NoBinaryNoBuild {
7425 id: PackageId,
7427 },
7428 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but has no source distribution", id = id.cyan())]
7431 NoBinary {
7432 id: PackageId,
7434 },
7435 #[error("Distribution `{id}` can't be installed because it is marked as `--no-build` but has no binary distribution", id = id.cyan())]
7438 NoBuild {
7439 id: PackageId,
7441 },
7442 #[error("Distribution `{id}` can't be installed because the binary distribution is incompatible with the current platform", id = id.cyan())]
7445 IncompatibleWheelOnly {
7446 id: PackageId,
7448 },
7449 #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but is itself a binary distribution", id = id.cyan())]
7451 NoBinaryWheelOnly {
7452 id: PackageId,
7454 },
7455 #[error("Found dependency `{id}` with no locked distribution", id = id.cyan())]
7457 VerbatimUrl {
7458 id: PackageId,
7460 #[source]
7462 err: VerbatimUrlError,
7463 },
7464 #[error("Could not compute relative path between workspace and distribution")]
7466 DistributionRelativePath(
7467 #[source]
7469 io::Error,
7470 ),
7471 #[error("Could not compute relative path between workspace and index")]
7473 IndexRelativePath(
7474 #[source]
7476 io::Error,
7477 ),
7478 #[error("Could not compute absolute path from workspace root and lockfile path")]
7480 AbsolutePath(
7481 #[source]
7483 io::Error,
7484 ),
7485 #[error("Dependency `{name}` has missing `version` field but has more than one matching package", name = name.cyan())]
7488 MissingDependencyVersion {
7489 name: PackageName,
7491 },
7492 #[error("Package `{name}` from a registry source has a missing `version` field", name = name.cyan())]
7495 MissingPackageVersion {
7496 name: PackageName,
7498 },
7499 #[error("Dependency `{name}` has missing `source` field but has more than one matching package", name = name.cyan())]
7502 MissingDependencySource {
7503 name: PackageName,
7505 },
7506 #[error("Could not compute relative path between workspace and requirement")]
7508 RequirementRelativePath(
7509 #[source]
7511 io::Error,
7512 ),
7513 #[error("Could not convert between URL and path")]
7515 RequirementVerbatimUrl(
7516 #[source]
7518 VerbatimUrlError,
7519 ),
7520 #[error("Could not convert between URL and path")]
7522 RegistryVerbatimUrl(
7523 #[source]
7525 VerbatimUrlError,
7526 ),
7527 #[error("Failed to convert path to URL: {path}", path = path.display().cyan())]
7529 PathToUrl { path: Box<Path> },
7530 #[error("Failed to convert URL to path: {url}", url = url.cyan())]
7532 UrlToPath { url: DisplaySafeUrl },
7533 #[error("Found multiple packages matching `{name}`", name = name.cyan())]
7536 MultipleRootPackages {
7537 name: PackageName,
7539 },
7540 #[error("Could not find root package `{name}`", name = name.cyan())]
7542 MissingRootPackage {
7543 name: PackageName,
7545 },
7546 #[error("Could not find root package `{id}` in lock", id = id.cyan())]
7548 RootPackageMissingFromLock {
7549 id: PackageId,
7551 },
7552 #[error(
7554 "Cannot materialize dependency `{dependency}` of `{package}` because its conflict marker depends on a package outside the selected subgraph",
7555 package = package.cyan(),
7556 dependency = dependency.cyan()
7557 )]
7558 DependencyConflictOutsideSubgraph {
7559 package: PackageId,
7561 dependency: PackageId,
7563 },
7564 #[error("Failed to generate package metadata for `{id}`", id = id.cyan())]
7566 Resolution {
7567 id: PackageId,
7569 #[source]
7571 err: uv_distribution::Error,
7572 },
7573 #[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())]
7576 InconsistentVersions {
7577 name: PackageName,
7579 version: Version,
7581 wheel: Wheel,
7583 },
7584 #[error(
7585 "Found conflicting extras `{package1}[{extra1}]` \
7586 and `{package2}[{extra2}]` enabled simultaneously"
7587 )]
7588 ConflictingExtra {
7589 package1: PackageName,
7590 extra1: ExtraName,
7591 package2: PackageName,
7592 extra2: ExtraName,
7593 },
7594 #[error(transparent)]
7595 GitUrlParse(#[from] GitUrlParseError),
7596 #[error("Failed to read `{path}`")]
7597 UnreadablePyprojectToml {
7598 path: PathBuf,
7599 #[source]
7600 err: std::io::Error,
7601 },
7602 #[error("Failed to parse `{path}`")]
7603 InvalidPyprojectToml {
7604 path: PathBuf,
7605 #[source]
7606 err: uv_pypi_types::MetadataError,
7607 },
7608 #[error("Workspace member `{id}` has non-local source", id = id.cyan())]
7610 NonLocalWorkspaceMember {
7611 id: PackageId,
7613 },
7614}
7615
7616#[derive(Debug, thiserror::Error)]
7618enum SourceParseError {
7619 #[error("Invalid URL in source `{given}`")]
7621 InvalidUrl {
7622 given: String,
7624 #[source]
7626 err: DisplaySafeUrlError,
7627 },
7628 #[error("Missing SHA in source `{given}`")]
7630 MissingSha {
7631 given: String,
7633 },
7634 #[error("Invalid SHA in source `{given}`")]
7636 InvalidSha {
7637 given: String,
7639 },
7640}
7641
7642#[derive(Clone, Debug, Eq, PartialEq)]
7644struct HashParseError(&'static str);
7645
7646impl std::error::Error for HashParseError {}
7647
7648impl Display for HashParseError {
7649 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
7650 Display::fmt(self.0, f)
7651 }
7652}
7653
7654fn fork_markers_union(
7656 fork_markers: &[UniversalMarker],
7657 requires_python: &RequiresPython,
7658) -> MarkerTree {
7659 if fork_markers.is_empty() {
7660 return requires_python.to_marker_tree();
7661 }
7662 let mut environment = MarkerTree::FALSE;
7663 for fork_marker in fork_markers {
7664 environment = environment.or(fork_marker.pep508());
7665 }
7666 environment
7667}
7668
7669fn simplify_dependency_marker(
7673 requires_python: &RequiresPython,
7674 environment: SimplifiedMarkerTree,
7675 parent: UniversalMarker,
7676 marker: UniversalMarker,
7677) -> SimplifiedMarkerTree {
7678 let parent =
7679 SimplifiedMarkerTree::new(requires_python, parent.pep508()).as_simplified_marker_tree();
7680 let marker =
7681 SimplifiedMarkerTree::new(requires_python, marker.combined()).as_simplified_marker_tree();
7682 let marker = marker.restrict(parent);
7683
7684 let mut marker = SimplifiedMarkerTree::new(requires_python, marker);
7687 marker.and(environment);
7688 marker
7689}
7690
7691fn simplified_universal_markers(
7696 markers: &[UniversalMarker],
7697 requires_python: &RequiresPython,
7698) -> Vec<String> {
7699 canonical_marker_trees(markers, requires_python)
7700 .into_iter()
7701 .filter_map(MarkerTree::try_to_string)
7702 .collect()
7703}
7704
7705fn canonicalize_universal_markers(
7712 markers: &[UniversalMarker],
7713 requires_python: &RequiresPython,
7714) -> Vec<UniversalMarker> {
7715 canonical_marker_trees(markers, requires_python)
7716 .into_iter()
7717 .map(|marker| {
7718 let simplified = SimplifiedMarkerTree::new(requires_python, marker);
7719 UniversalMarker::from_combined(simplified.into_marker(requires_python))
7720 })
7721 .collect()
7722}
7723
7724fn canonical_marker_trees(
7726 markers: &[UniversalMarker],
7727 requires_python: &RequiresPython,
7728) -> Vec<MarkerTree> {
7729 let mut pep508_only = vec![];
7730 let mut seen = FxHashSet::default();
7731 for marker in markers {
7732 let simplified =
7733 SimplifiedMarkerTree::new(requires_python, marker.pep508()).as_simplified_marker_tree();
7734 if seen.insert(simplified) {
7735 pep508_only.push(simplified);
7736 }
7737 }
7738 let any_overlap = pep508_only
7739 .iter()
7740 .tuple_combinations()
7741 .any(|(&marker1, &marker2)| !marker1.is_disjoint(marker2));
7742 let markers = if !any_overlap {
7743 pep508_only
7744 } else {
7745 markers
7746 .iter()
7747 .map(|marker| {
7748 SimplifiedMarkerTree::new(requires_python, marker.combined())
7749 .as_simplified_marker_tree()
7750 })
7751 .collect()
7752 };
7753 markers
7754 .into_iter()
7755 .filter(|marker| !marker.is_true())
7756 .collect()
7757}
7758
7759fn is_wheel_unreachable_for_marker(
7767 filename: &WheelFilename,
7768 requires_python: &RequiresPython,
7769 marker: &UniversalMarker,
7770 tags: Option<&Tags>,
7771) -> bool {
7772 if let Some(tags) = tags
7773 && !filename.compatibility(tags).is_compatible()
7774 {
7775 return true;
7776 }
7777 if !requires_python.matches_wheel_tag(filename) {
7779 return true;
7780 }
7781
7782 let platform_tags = filename.platform_tags();
7791
7792 if platform_tags.iter().all(PlatformTag::is_any) {
7793 return false;
7794 }
7795
7796 if platform_tags.iter().all(PlatformTag::is_linux) {
7797 if platform_tags.iter().all(PlatformTag::is_arm) {
7798 if marker.is_disjoint(*LINUX_ARM_MARKERS) {
7799 return true;
7800 }
7801 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
7802 if marker.is_disjoint(*LINUX_X86_64_MARKERS) {
7803 return true;
7804 }
7805 } else if platform_tags.iter().all(PlatformTag::is_x86) {
7806 if marker.is_disjoint(*LINUX_X86_MARKERS) {
7807 return true;
7808 }
7809 } else if platform_tags.iter().all(PlatformTag::is_ppc64le) {
7810 if marker.is_disjoint(*LINUX_PPC64LE_MARKERS) {
7811 return true;
7812 }
7813 } else if platform_tags.iter().all(PlatformTag::is_ppc64) {
7814 if marker.is_disjoint(*LINUX_PPC64_MARKERS) {
7815 return true;
7816 }
7817 } else if platform_tags.iter().all(PlatformTag::is_s390x) {
7818 if marker.is_disjoint(*LINUX_S390X_MARKERS) {
7819 return true;
7820 }
7821 } else if platform_tags.iter().all(PlatformTag::is_riscv64) {
7822 if marker.is_disjoint(*LINUX_RISCV64_MARKERS) {
7823 return true;
7824 }
7825 } else if platform_tags.iter().all(PlatformTag::is_loongarch64) {
7826 if marker.is_disjoint(*LINUX_LOONGARCH64_MARKERS) {
7827 return true;
7828 }
7829 } else if platform_tags.iter().all(PlatformTag::is_armv7l) {
7830 if marker.is_disjoint(*LINUX_ARMV7L_MARKERS) {
7831 return true;
7832 }
7833 } else if platform_tags.iter().all(PlatformTag::is_armv6l) {
7834 if marker.is_disjoint(*LINUX_ARMV6L_MARKERS) {
7835 return true;
7836 }
7837 } else if marker.is_disjoint(*LINUX_MARKERS) {
7838 return true;
7839 }
7840 }
7841
7842 if platform_tags.iter().all(PlatformTag::is_windows) {
7843 if platform_tags.iter().all(PlatformTag::is_arm) {
7844 if marker.is_disjoint(*WINDOWS_ARM_MARKERS) {
7845 return true;
7846 }
7847 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
7848 if marker.is_disjoint(*WINDOWS_X86_64_MARKERS) {
7849 return true;
7850 }
7851 } else if platform_tags.iter().all(PlatformTag::is_x86) {
7852 if marker.is_disjoint(*WINDOWS_X86_MARKERS) {
7853 return true;
7854 }
7855 } else if marker.is_disjoint(*WINDOWS_MARKERS) {
7856 return true;
7857 }
7858 }
7859
7860 if platform_tags.iter().all(PlatformTag::is_macos) {
7861 if platform_tags.iter().all(PlatformTag::is_arm) {
7862 if marker.is_disjoint(*MAC_ARM_MARKERS) {
7863 return true;
7864 }
7865 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
7866 if marker.is_disjoint(*MAC_X86_64_MARKERS) {
7867 return true;
7868 }
7869 } else if platform_tags.iter().all(PlatformTag::is_x86) {
7870 if marker.is_disjoint(*MAC_X86_MARKERS) {
7871 return true;
7872 }
7873 } else if marker.is_disjoint(*MAC_MARKERS) {
7874 return true;
7875 }
7876 }
7877
7878 if platform_tags.iter().all(PlatformTag::is_android) {
7879 if platform_tags.iter().all(PlatformTag::is_arm) {
7880 if marker.is_disjoint(*ANDROID_ARM_MARKERS) {
7881 return true;
7882 }
7883 } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
7884 if marker.is_disjoint(*ANDROID_X86_64_MARKERS) {
7885 return true;
7886 }
7887 } else if platform_tags.iter().all(PlatformTag::is_x86) {
7888 if marker.is_disjoint(*ANDROID_X86_MARKERS) {
7889 return true;
7890 }
7891 } else if marker.is_disjoint(*ANDROID_MARKERS) {
7892 return true;
7893 }
7894 }
7895
7896 if platform_tags.iter().all(PlatformTag::is_arm) {
7897 if marker.is_disjoint(*ARM_MARKERS) {
7898 return true;
7899 }
7900 }
7901
7902 if platform_tags.iter().all(PlatformTag::is_x86_64) {
7903 if marker.is_disjoint(*X86_64_MARKERS) {
7904 return true;
7905 }
7906 }
7907
7908 if platform_tags.iter().all(PlatformTag::is_x86) {
7909 if marker.is_disjoint(*X86_MARKERS) {
7910 return true;
7911 }
7912 }
7913
7914 if platform_tags.iter().all(PlatformTag::is_ppc64le) {
7915 if marker.is_disjoint(*PPC64LE_MARKERS) {
7916 return true;
7917 }
7918 }
7919
7920 if platform_tags.iter().all(PlatformTag::is_ppc64) {
7921 if marker.is_disjoint(*PPC64_MARKERS) {
7922 return true;
7923 }
7924 }
7925
7926 if platform_tags.iter().all(PlatformTag::is_s390x) {
7927 if marker.is_disjoint(*S390X_MARKERS) {
7928 return true;
7929 }
7930 }
7931
7932 if platform_tags.iter().all(PlatformTag::is_riscv64) {
7933 if marker.is_disjoint(*RISCV64_MARKERS) {
7934 return true;
7935 }
7936 }
7937
7938 if platform_tags.iter().all(PlatformTag::is_loongarch64) {
7939 if marker.is_disjoint(*LOONGARCH64_MARKERS) {
7940 return true;
7941 }
7942 }
7943
7944 if platform_tags.iter().all(PlatformTag::is_armv7l) {
7945 if marker.is_disjoint(*ARMV7L_MARKERS) {
7946 return true;
7947 }
7948 }
7949
7950 if platform_tags.iter().all(PlatformTag::is_armv6l) {
7951 if marker.is_disjoint(*ARMV6L_MARKERS) {
7952 return true;
7953 }
7954 }
7955
7956 false
7957}
7958
7959pub(crate) fn is_wheel_unreachable(
7960 filename: &WheelFilename,
7961 graph: &ResolverOutput,
7962 requires_python: &RequiresPython,
7963 node_index: NodeIndex,
7964 tags: Option<&Tags>,
7965) -> bool {
7966 is_wheel_unreachable_for_marker(
7967 filename,
7968 requires_python,
7969 graph.graph[node_index].marker(),
7970 tags,
7971 )
7972}
7973
7974#[cfg(test)]
7975mod tests {
7976 use uv_pep440::VersionSpecifiers;
7977 use uv_pep508::MarkerEnvironmentBuilder;
7978 use uv_warnings::anstream;
7979
7980 use super::*;
7981
7982 macro_rules! assert_stripped_snapshot {
7984 ($expr:expr, @$snapshot:literal) => {{
7985 let expr = format!("{}", $expr);
7986 let expr = format!("{}", anstream::adapter::strip_str(&expr));
7987 insta::assert_snapshot!(expr, @$snapshot);
7988 }};
7989 }
7990
7991 fn marker_environment() -> MarkerEnvironment {
7992 MarkerEnvironment::try_from(MarkerEnvironmentBuilder {
7993 implementation_name: "cpython",
7994 implementation_version: "3.12.0",
7995 os_name: "posix",
7996 platform_machine: "arm64",
7997 platform_python_implementation: "CPython",
7998 platform_release: "23.0.0",
7999 platform_system: "Darwin",
8000 platform_version: "test",
8001 python_full_version: "3.12.0",
8002 python_version: "3.12",
8003 sys_platform: "darwin",
8004 })
8005 .expect("valid marker environment")
8006 }
8007
8008 #[test]
8009 fn git_source_rejects_mismatched_exact_revision() -> Result<(), Box<dyn Error>> {
8010 let url = Url::parse(
8011 "https://example.com/repository?rev=0dacfd662c64cb4ceb16e6cf65a157a8b715b979#b270df1a2fb5d012294e9aaf05e7e0bab1e6a389",
8012 )?;
8013 assert_eq!(
8014 GitSource::from_url(&url),
8015 Err(GitSourceError::RevisionMismatch {
8016 revision: "0dacfd662c64cb4ceb16e6cf65a157a8b715b979".to_string(),
8017 precise: GitOid::from_str("b270df1a2fb5d012294e9aaf05e7e0bab1e6a389")?,
8018 })
8019 );
8020
8021 let url = Url::parse(
8022 "https://example.com/repository?rev=0DACFD662C64CB4CEB16E6CF65A157A8B715B979#0dacfd662c64cb4ceb16e6cf65a157a8b715b979",
8023 )?;
8024 assert!(GitSource::from_url(&url).is_ok());
8025
8026 let url = Url::parse(
8027 "https://example.com/repository?rev=0dacfd6#b270df1a2fb5d012294e9aaf05e7e0bab1e6a389",
8028 )?;
8029 assert!(GitSource::from_url(&url).is_ok());
8030
8031 let url = Url::parse(
8032 "https://example.com/repository?branch=0dacfd662c64cb4ceb16e6cf65a157a8b715b979#b270df1a2fb5d012294e9aaf05e7e0bab1e6a389",
8033 )?;
8034 assert!(GitSource::from_url(&url).is_ok());
8035
8036 Ok(())
8037 }
8038
8039 #[test]
8040 fn dependency_marker_preserves_parent_conflicts() {
8041 let requires_python = RequiresPython::from_specifiers(
8042 VersionSpecifiers::from_str(">=3.12").expect("valid version specifier"),
8043 );
8044 let parent = UniversalMarker::from_combined(
8045 MarkerTree::from_str(
8046 "python_full_version >= '3.12' and sys_platform == 'darwin' and extra != 'extra-1-x-foo'",
8047 )
8048 .expect("valid parent marker"),
8049 );
8050 let environment = SimplifiedMarkerTree::new(&requires_python, MarkerTree::TRUE);
8051
8052 let simplified_marker =
8053 simplify_dependency_marker(&requires_python, environment, parent, parent);
8054 assert_eq!(
8055 simplified_marker.try_to_string().as_deref(),
8056 Some("extra != 'extra-1-x-foo'")
8057 );
8058
8059 let marker = simplified_marker.into_marker(&requires_python);
8060 assert_eq!(
8061 marker.try_to_string().as_deref(),
8062 Some("python_full_version >= '3.12' and extra != 'extra-1-x-foo'")
8063 );
8064 }
8065
8066 #[test]
8067 fn dependency_selection_resolves_included_groups_to_same_package() {
8068 let lock: Lock = toml::from_str(
8069 r#"
8070version = 1
8071revision = 3
8072requires-python = ">=3.12"
8073
8074[[package]]
8075name = "project"
8076version = "0.1.0"
8077source = { virtual = "." }
8078dependencies = [{ name = "ty" }]
8079
8080[package.dependency-groups]
8081dev = [{ name = "ty" }]
8082typing = [{ name = "ty" }]
8083
8084[[package]]
8085name = "ty"
8086version = "1.0.0"
8087source = { registry = "https://example.com/simple" }
8088"#,
8089 )
8090 .expect("valid lock");
8091 let project_name = PackageName::from_str("project").expect("valid package name");
8092 let dependency_name = PackageName::from_str("ty").expect("valid package name");
8093 let dev = GroupName::from_str("dev").expect("valid group name");
8094 let typing = GroupName::from_str("typing").expect("valid group name");
8095 let marker_environment = marker_environment();
8096
8097 let selection = lock
8098 .dependency_selection(Some(&project_name), &dependency_name, &marker_environment)
8099 .expect("unique project package");
8100 let preferred = selection.group(&dev).expect("dev dependency").package();
8101 let included = selection
8102 .group(&typing)
8103 .expect("typing dependency")
8104 .package();
8105 let production = selection
8106 .production()
8107 .expect("production dependency")
8108 .package();
8109
8110 assert!(std::ptr::eq(preferred, included));
8111 assert!(std::ptr::eq(preferred, production));
8112 }
8113
8114 #[test]
8115 fn dependency_selection_resolves_lock_manifest_requirement() {
8116 let lock: Lock = toml::from_str(
8117 r#"
8118version = 1
8119revision = 3
8120requires-python = ">=3.12"
8121
8122[manifest]
8123requirements = [{ name = "ty" }]
8124
8125[[package]]
8126name = "ty"
8127version = "1.0.0"
8128source = { registry = "https://example.com/simple" }
8129"#,
8130 )
8131 .expect("valid lock");
8132 let dependency_name = PackageName::from_str("ty").expect("valid package name");
8133 let marker_environment = marker_environment();
8134
8135 let selection = lock
8136 .dependency_selection(None, &dependency_name, &marker_environment)
8137 .expect("unique root package");
8138 let root = selection.root().expect("root dependency");
8139
8140 assert_eq!(root.package().name(), &dependency_name);
8141 assert!(selection.production().is_none());
8142 }
8143
8144 #[test]
8145 fn dependency_selection_returns_any_selection_error() {
8146 let lock: Lock = toml::from_str(
8147 r#"
8148version = 1
8149revision = 3
8150requires-python = ">=3.12"
8151
8152[[package]]
8153name = "project"
8154version = "0.1.0"
8155source = { virtual = "." }
8156dependencies = [
8157 { name = "ty", version = "1.0.0", source = { registry = "https://example.com/simple" } },
8158 { name = "ty", version = "2.0.0", source = { registry = "https://example.com/simple" } },
8159]
8160
8161[package.dependency-groups]
8162dev = [
8163 { name = "ty", version = "1.0.0", source = { registry = "https://example.com/simple" } },
8164]
8165
8166[[package]]
8167name = "ty"
8168version = "1.0.0"
8169source = { registry = "https://example.com/simple" }
8170
8171[[package]]
8172name = "ty"
8173version = "2.0.0"
8174source = { registry = "https://example.com/simple" }
8175"#,
8176 )
8177 .expect("valid lock");
8178 let project_name = PackageName::from_str("project").expect("valid package name");
8179 let dependency_name = PackageName::from_str("ty").expect("valid package name");
8180 let marker_environment = marker_environment();
8181
8182 let error = lock
8183 .dependency_selection(Some(&project_name), &dependency_name, &marker_environment)
8184 .expect_err("ambiguous production selection");
8185 insta::assert_snapshot!(error, @"found multiple packages matching production dependency `ty` for `project`");
8186 }
8187
8188 #[test]
8189 fn missing_dependency_source_unambiguous() {
8190 let data = r#"
8191version = 1
8192requires-python = ">=3.12"
8193
8194[[package]]
8195name = "a"
8196version = "0.1.0"
8197source = { registry = "https://pypi.org/simple" }
8198sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8199
8200[[package]]
8201name = "b"
8202version = "0.1.0"
8203source = { registry = "https://pypi.org/simple" }
8204sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8205
8206[[package.dependencies]]
8207name = "a"
8208version = "0.1.0"
8209"#;
8210 let result: Result<Lock, _> = toml::from_str(data);
8211 insta::assert_debug_snapshot!(result);
8212 }
8213
8214 #[test]
8215 fn missing_dependency_version_unambiguous() {
8216 let data = r#"
8217version = 1
8218requires-python = ">=3.12"
8219
8220[[package]]
8221name = "a"
8222version = "0.1.0"
8223source = { registry = "https://pypi.org/simple" }
8224sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8225
8226[[package]]
8227name = "b"
8228version = "0.1.0"
8229source = { registry = "https://pypi.org/simple" }
8230sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8231
8232[[package.dependencies]]
8233name = "a"
8234source = { registry = "https://pypi.org/simple" }
8235"#;
8236 let result: Result<Lock, _> = toml::from_str(data);
8237 insta::assert_debug_snapshot!(result);
8238 }
8239
8240 #[test]
8241 fn missing_dependency_source_version_unambiguous() {
8242 let data = r#"
8243version = 1
8244requires-python = ">=3.12"
8245
8246[[package]]
8247name = "a"
8248version = "0.1.0"
8249source = { registry = "https://pypi.org/simple" }
8250sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8251
8252[[package]]
8253name = "b"
8254version = "0.1.0"
8255source = { registry = "https://pypi.org/simple" }
8256sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8257
8258[[package.dependencies]]
8259name = "a"
8260"#;
8261 let result: Result<Lock, _> = toml::from_str(data);
8262 insta::assert_debug_snapshot!(result);
8263 }
8264
8265 #[test]
8266 fn missing_dependency_source_ambiguous() {
8267 let data = r#"
8268version = 1
8269requires-python = ">=3.12"
8270
8271[[package]]
8272name = "a"
8273version = "0.1.0"
8274source = { registry = "https://pypi.org/simple" }
8275sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8276
8277[[package]]
8278name = "a"
8279version = "0.1.1"
8280source = { registry = "https://pypi.org/simple" }
8281sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8282
8283[[package]]
8284name = "b"
8285version = "0.1.0"
8286source = { registry = "https://pypi.org/simple" }
8287sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8288
8289[[package.dependencies]]
8290name = "a"
8291version = "0.1.0"
8292"#;
8293 let result = toml::from_str::<Lock>(data).unwrap_err();
8294 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
8295 }
8296
8297 #[test]
8298 fn missing_dependency_version_ambiguous() {
8299 let data = r#"
8300version = 1
8301requires-python = ">=3.12"
8302
8303[[package]]
8304name = "a"
8305version = "0.1.0"
8306source = { registry = "https://pypi.org/simple" }
8307sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8308
8309[[package]]
8310name = "a"
8311version = "0.1.1"
8312source = { registry = "https://pypi.org/simple" }
8313sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8314
8315[[package]]
8316name = "b"
8317version = "0.1.0"
8318source = { registry = "https://pypi.org/simple" }
8319sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8320
8321[[package.dependencies]]
8322name = "a"
8323source = { registry = "https://pypi.org/simple" }
8324"#;
8325 let result = toml::from_str::<Lock>(data).unwrap_err();
8326 assert_stripped_snapshot!(result, @"Dependency `a` has missing `version` field but has more than one matching package");
8327 }
8328
8329 #[test]
8330 fn missing_package_version_registry() {
8331 let data = r#"
8332version = 1
8333requires-python = ">=3.12"
8334
8335[[package]]
8336name = "a"
8337source = { registry = "https://pypi.org/simple" }
8338sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8339"#;
8340 let result = toml::from_str::<Lock>(data).unwrap_err();
8341 assert_stripped_snapshot!(result, @"Package `a` from a registry source has a missing `version` field");
8342 }
8343
8344 #[test]
8345 fn missing_dependency_source_version_ambiguous() {
8346 let data = r#"
8347version = 1
8348requires-python = ">=3.12"
8349
8350[[package]]
8351name = "a"
8352version = "0.1.0"
8353source = { registry = "https://pypi.org/simple" }
8354sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8355
8356[[package]]
8357name = "a"
8358version = "0.1.1"
8359source = { registry = "https://pypi.org/simple" }
8360sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8361
8362[[package]]
8363name = "b"
8364version = "0.1.0"
8365source = { registry = "https://pypi.org/simple" }
8366sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8367
8368[[package.dependencies]]
8369name = "a"
8370"#;
8371 let result = toml::from_str::<Lock>(data).unwrap_err();
8372 assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
8373 }
8374
8375 #[test]
8376 fn missing_dependency_version_dynamic() {
8377 let data = r#"
8378version = 1
8379requires-python = ">=3.12"
8380
8381[[package]]
8382name = "a"
8383source = { editable = "path/to/a" }
8384
8385[[package]]
8386name = "a"
8387version = "0.1.1"
8388source = { registry = "https://pypi.org/simple" }
8389sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8390
8391[[package]]
8392name = "b"
8393version = "0.1.0"
8394source = { registry = "https://pypi.org/simple" }
8395sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
8396
8397[[package.dependencies]]
8398name = "a"
8399source = { editable = "path/to/a" }
8400"#;
8401 let result = toml::from_str::<Lock>(data);
8402 insta::assert_debug_snapshot!(result);
8403 }
8404
8405 #[test]
8406 fn wheel_sources_deserialize() {
8407 for source in [
8408 r#"url = "https://example.com/dependency-1.0.0-py3-none-any.whl""#,
8409 r#"path = "dependency-1.0.0-py3-none-any.whl""#,
8410 r#"filename = "dependency-1.0.0-py3-none-any.whl""#,
8411 ] {
8412 let wheel: Wheel = toml::from_str(source).expect("valid wheel source");
8413 assert_eq!(
8414 wheel.filename.to_string(),
8415 "dependency-1.0.0-py3-none-any.whl"
8416 );
8417 }
8418 }
8419
8420 #[test]
8421 fn hash_optional_missing() {
8422 let data = r#"
8423version = 1
8424requires-python = ">=3.12"
8425
8426[[package]]
8427name = "anyio"
8428version = "4.3.0"
8429source = { registry = "https://pypi.org/simple" }
8430wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl" }]
8431"#;
8432 let result: Result<Lock, _> = toml::from_str(data);
8433 insta::assert_debug_snapshot!(result);
8434 }
8435
8436 #[test]
8437 fn hash_optional_present() {
8438 let data = r#"
8439version = 1
8440requires-python = ">=3.12"
8441
8442[[package]]
8443name = "anyio"
8444version = "4.3.0"
8445source = { registry = "https://pypi.org/simple" }
8446wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
8447"#;
8448 let result: Result<Lock, _> = toml::from_str(data);
8449 insta::assert_debug_snapshot!(result);
8450 }
8451
8452 #[test]
8453 fn hash_required_present() {
8454 let data = r#"
8455version = 1
8456requires-python = ">=3.12"
8457
8458[[package]]
8459name = "anyio"
8460version = "4.3.0"
8461source = { path = "file:///foo/bar" }
8462wheels = [{ url = "file:///foo/bar/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
8463"#;
8464 let result: Result<Lock, _> = toml::from_str(data);
8465 insta::assert_debug_snapshot!(result);
8466 }
8467
8468 #[test]
8469 fn source_direct_no_subdir() {
8470 let data = r#"
8471version = 1
8472requires-python = ">=3.12"
8473
8474[[package]]
8475name = "anyio"
8476version = "4.3.0"
8477source = { url = "https://burntsushi.net" }
8478"#;
8479 let result: Result<Lock, _> = toml::from_str(data);
8480 insta::assert_debug_snapshot!(result);
8481 }
8482
8483 #[test]
8484 fn source_direct_has_subdir() {
8485 let data = r#"
8486version = 1
8487requires-python = ">=3.12"
8488
8489[[package]]
8490name = "anyio"
8491version = "4.3.0"
8492source = { url = "https://burntsushi.net", subdirectory = "wat/foo/bar" }
8493"#;
8494 let result: Result<Lock, _> = toml::from_str(data);
8495 insta::assert_debug_snapshot!(result);
8496 }
8497
8498 #[test]
8499 fn source_directory() {
8500 let data = r#"
8501version = 1
8502requires-python = ">=3.12"
8503
8504[[package]]
8505name = "anyio"
8506version = "4.3.0"
8507source = { directory = "path/to/dir" }
8508"#;
8509 let result: Result<Lock, _> = toml::from_str(data);
8510 insta::assert_debug_snapshot!(result);
8511 }
8512
8513 #[test]
8514 fn source_editable() {
8515 let data = r#"
8516version = 1
8517requires-python = ">=3.12"
8518
8519[[package]]
8520name = "anyio"
8521version = "4.3.0"
8522source = { editable = "path/to/dir" }
8523"#;
8524 let result: Result<Lock, _> = toml::from_str(data);
8525 insta::assert_debug_snapshot!(result);
8526 }
8527
8528 #[test]
8531 fn registry_source_windows_drive_letter() {
8532 let data = r#"
8533version = 1
8534requires-python = ">=3.12"
8535
8536[[package]]
8537name = "tqdm"
8538version = "1000.0.0"
8539source = { registry = "C:/Users/user/links" }
8540wheels = [
8541 { path = "C:/Users/user/links/tqdm-1000.0.0-py3-none-any.whl" },
8542]
8543"#;
8544 let lock: Lock = toml::from_str(data).unwrap();
8545 assert_eq!(
8546 lock.packages[0].id.source,
8547 Source::Registry(RegistrySource::Path(
8548 Path::new("C:/Users/user/links").into()
8549 ))
8550 );
8551 }
8552}