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