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