Skip to main content

uv_resolver/lock/
mod.rs

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