Skip to main content

uv_resolver/lock/
mod.rs

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