Skip to main content

uv_resolver/lock/
mod.rs

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