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