Skip to main content

uv_resolver/lock/
mod.rs

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