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