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, RequiresDist};
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        build_options: &BuildOptions,
1631        hasher: &HashStrategy,
1632        index: &InMemoryIndex,
1633        database: &DistributionDatabase<'_, Context>,
1634    ) -> Result<SatisfiesResult<'_>, LockError> {
1635        let mut queue: VecDeque<&Package> = VecDeque::new();
1636        let mut seen = FxHashSet::default();
1637
1638        // Validate that the lockfile was generated with the same root members.
1639        {
1640            let expected = members.iter().cloned().collect::<BTreeSet<_>>();
1641            let actual = &self.manifest.members;
1642            if expected != *actual {
1643                return Ok(SatisfiesResult::MismatchedMembers(expected, actual));
1644            }
1645        }
1646
1647        // Validate that the member sources have not changed (e.g., that they've switched from
1648        // virtual to non-virtual or vice versa).
1649        for (name, member) in packages {
1650            let source = self.find_by_name(name).ok().flatten();
1651
1652            // Determine whether the member was required by any other member.
1653            let value = required_members.get(name);
1654            let is_required_member = value.is_some();
1655            let editability = value.copied().flatten();
1656
1657            // Verify that the member is virtual (or not).
1658            let expected_virtual = !member.pyproject_toml().is_package(!is_required_member);
1659            let actual_virtual =
1660                source.map(|package| matches!(package.id.source, Source::Virtual(..)));
1661            if actual_virtual != Some(expected_virtual) {
1662                return Ok(SatisfiesResult::MismatchedVirtual(
1663                    name.clone(),
1664                    expected_virtual,
1665                ));
1666            }
1667
1668            // Verify that the member is editable (or not).
1669            let expected_editable = if expected_virtual {
1670                false
1671            } else {
1672                editability.unwrap_or(true)
1673            };
1674            let actual_editable =
1675                source.map(|package| matches!(package.id.source, Source::Editable(..)));
1676            if actual_editable != Some(expected_editable) {
1677                return Ok(SatisfiesResult::MismatchedEditable(
1678                    name.clone(),
1679                    expected_editable,
1680                ));
1681            }
1682        }
1683
1684        // Validate that the lockfile was generated with the same requirements.
1685        {
1686            let expected: BTreeSet<_> = requirements
1687                .iter()
1688                .cloned()
1689                .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1690                .collect::<Result<_, _>>()?;
1691            let actual: BTreeSet<_> = self
1692                .manifest
1693                .requirements
1694                .iter()
1695                .cloned()
1696                .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1697                .collect::<Result<_, _>>()?;
1698            if expected != actual {
1699                return Ok(SatisfiesResult::MismatchedRequirements(expected, actual));
1700            }
1701        }
1702
1703        // Validate that the lockfile was generated with the same constraints.
1704        {
1705            let expected: BTreeSet<_> = constraints
1706                .iter()
1707                .cloned()
1708                .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1709                .collect::<Result<_, _>>()?;
1710            let actual: BTreeSet<_> = self
1711                .manifest
1712                .constraints
1713                .iter()
1714                .cloned()
1715                .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1716                .collect::<Result<_, _>>()?;
1717            if expected != actual {
1718                return Ok(SatisfiesResult::MismatchedConstraints(expected, actual));
1719            }
1720        }
1721
1722        // Validate that the lockfile was generated with the same overrides.
1723        {
1724            let expected: BTreeSet<_> = overrides
1725                .iter()
1726                .cloned()
1727                .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1728                .collect::<Result<_, _>>()?;
1729            let actual: BTreeSet<_> = self
1730                .manifest
1731                .overrides
1732                .iter()
1733                .cloned()
1734                .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1735                .collect::<Result<_, _>>()?;
1736            if expected != actual {
1737                return Ok(SatisfiesResult::MismatchedOverrides(expected, actual));
1738            }
1739        }
1740
1741        // Validate that the lockfile was generated with the same excludes.
1742        {
1743            let expected: BTreeSet<_> = excludes.iter().cloned().collect();
1744            let actual: BTreeSet<_> = self.manifest.excludes.iter().cloned().collect();
1745            if expected != actual {
1746                return Ok(SatisfiesResult::MismatchedExcludes(expected, actual));
1747            }
1748        }
1749
1750        // Validate that the lockfile was generated with the same build constraints.
1751        {
1752            let expected: BTreeSet<_> = build_constraints
1753                .iter()
1754                .cloned()
1755                .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1756                .collect::<Result<_, _>>()?;
1757            let actual: BTreeSet<_> = self
1758                .manifest
1759                .build_constraints
1760                .iter()
1761                .cloned()
1762                .map(|requirement| normalize_requirement(requirement, root, &self.requires_python))
1763                .collect::<Result<_, _>>()?;
1764            if expected != actual {
1765                return Ok(SatisfiesResult::MismatchedBuildConstraints(
1766                    expected, actual,
1767                ));
1768            }
1769        }
1770
1771        // Validate that the lockfile was generated with the dependency groups.
1772        {
1773            let expected: BTreeMap<GroupName, BTreeSet<Requirement>> = dependency_groups
1774                .iter()
1775                .filter(|(_, requirements)| !requirements.is_empty())
1776                .map(|(group, requirements)| {
1777                    Ok::<_, LockError>((
1778                        group.clone(),
1779                        requirements
1780                            .iter()
1781                            .cloned()
1782                            .map(|requirement| {
1783                                normalize_requirement(requirement, root, &self.requires_python)
1784                            })
1785                            .collect::<Result<_, _>>()?,
1786                    ))
1787                })
1788                .collect::<Result<_, _>>()?;
1789            let actual: BTreeMap<GroupName, BTreeSet<Requirement>> = self
1790                .manifest
1791                .dependency_groups
1792                .iter()
1793                .filter(|(_, requirements)| !requirements.is_empty())
1794                .map(|(group, requirements)| {
1795                    Ok::<_, LockError>((
1796                        group.clone(),
1797                        requirements
1798                            .iter()
1799                            .cloned()
1800                            .map(|requirement| {
1801                                normalize_requirement(requirement, root, &self.requires_python)
1802                            })
1803                            .collect::<Result<_, _>>()?,
1804                    ))
1805                })
1806                .collect::<Result<_, _>>()?;
1807            if expected != actual {
1808                return Ok(SatisfiesResult::MismatchedDependencyGroups(
1809                    expected, actual,
1810                ));
1811            }
1812        }
1813
1814        // Validate that the lockfile was generated with the same static metadata.
1815        {
1816            let expected = dependency_metadata
1817                .values()
1818                .cloned()
1819                .collect::<BTreeSet<_>>();
1820            let actual = &self.manifest.dependency_metadata;
1821            if expected != *actual {
1822                return Ok(SatisfiesResult::MismatchedStaticMetadata(expected, actual));
1823            }
1824        }
1825
1826        // Collect the set of available indexes (both `--index-url` and `--find-links` entries).
1827        let mut remotes = indexes.map(|locations| {
1828            locations
1829                .allowed_indexes()
1830                .into_iter()
1831                .filter_map(|index| match index.url() {
1832                    IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
1833                        Some(UrlString::from(index.url().without_credentials().as_ref()))
1834                    }
1835                    IndexUrl::Path(_) => None,
1836                })
1837                .collect::<BTreeSet<_>>()
1838        });
1839
1840        let mut locals = indexes.map(|locations| {
1841            locations
1842                .allowed_indexes()
1843                .into_iter()
1844                .filter_map(|index| match index.url() {
1845                    IndexUrl::Pypi(_) | IndexUrl::Url(_) => None,
1846                    IndexUrl::Path(url) => {
1847                        let path = url.to_file_path().ok()?;
1848                        let path = try_relative_to_if(&path, root, !url.was_given_absolute())
1849                            .ok()?
1850                            .into_boxed_path();
1851                        Some(path)
1852                    }
1853                })
1854                .collect::<BTreeSet<_>>()
1855        });
1856
1857        // Add the workspace packages to the queue.
1858        for root_name in packages.keys() {
1859            let root = self
1860                .find_by_name(root_name)
1861                .expect("found too many packages matching root");
1862
1863            let Some(root) = root else {
1864                // The package is not in the lockfile, so it can't be satisfied.
1865                return Ok(SatisfiesResult::MissingRoot(root_name.clone()));
1866            };
1867
1868            if seen.insert(&root.id) {
1869                queue.push_back(root);
1870            }
1871        }
1872
1873        // Add requirements attached directly to the target root (e.g., PEP 723 requirements or
1874        // dependency groups in workspaces without a `[project]` table).
1875        let root_requirements = requirements
1876            .iter()
1877            .chain(dependency_groups.values().flatten())
1878            .collect::<Vec<_>>();
1879
1880        for requirement in &root_requirements {
1881            if let RequirementSource::Registry {
1882                index: Some(index), ..
1883            } = &requirement.source
1884            {
1885                match &index.url {
1886                    IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
1887                        if let Some(remotes) = remotes.as_mut() {
1888                            remotes.insert(UrlString::from(
1889                                index.url().without_credentials().as_ref(),
1890                            ));
1891                        }
1892                    }
1893                    IndexUrl::Path(url) => {
1894                        if let Some(locals) = locals.as_mut() {
1895                            if let Some(path) = url.to_file_path().ok().and_then(|path| {
1896                                try_relative_to_if(&path, root, !url.was_given_absolute()).ok()
1897                            }) {
1898                                locals.insert(path.into_boxed_path());
1899                            }
1900                        }
1901                    }
1902                }
1903            }
1904        }
1905
1906        if !root_requirements.is_empty() {
1907            let names = root_requirements
1908                .iter()
1909                .map(|requirement| &requirement.name)
1910                .collect::<FxHashSet<_>>();
1911
1912            let by_name: FxHashMap<_, Vec<_>> = self.packages.iter().fold(
1913                FxHashMap::with_capacity_and_hasher(self.packages.len(), FxBuildHasher),
1914                |mut by_name, package| {
1915                    if names.contains(&package.id.name) {
1916                        by_name.entry(&package.id.name).or_default().push(package);
1917                    }
1918                    by_name
1919                },
1920            );
1921
1922            for requirement in root_requirements {
1923                for package in by_name.get(&requirement.name).into_iter().flatten() {
1924                    if !package.id.source.is_source_tree() {
1925                        continue;
1926                    }
1927
1928                    let marker = if package.fork_markers.is_empty() {
1929                        requirement.marker
1930                    } else {
1931                        let mut combined = MarkerTree::FALSE;
1932                        for fork_marker in &package.fork_markers {
1933                            combined.or(fork_marker.pep508());
1934                        }
1935                        combined.and(requirement.marker);
1936                        combined
1937                    };
1938                    if marker.is_false() {
1939                        continue;
1940                    }
1941                    if !marker.evaluate(markers, &[]) {
1942                        continue;
1943                    }
1944
1945                    if seen.insert(&package.id) {
1946                        queue.push_back(package);
1947                    }
1948                }
1949            }
1950        }
1951
1952        while let Some(package) = queue.pop_front() {
1953            // If the lockfile references an index that was not provided, we can't validate it.
1954            if let Source::Registry(index) = &package.id.source {
1955                match index {
1956                    RegistrySource::Url(url) => {
1957                        if remotes
1958                            .as_ref()
1959                            .is_some_and(|remotes| !remotes.contains(url))
1960                        {
1961                            let name = &package.id.name;
1962                            let version = &package
1963                                .id
1964                                .version
1965                                .as_ref()
1966                                .expect("version for registry source");
1967                            return Ok(SatisfiesResult::MissingRemoteIndex(name, version, url));
1968                        }
1969                    }
1970                    RegistrySource::Path(path) => {
1971                        if locals.as_ref().is_some_and(|locals| !locals.contains(path)) {
1972                            let name = &package.id.name;
1973                            let version = &package
1974                                .id
1975                                .version
1976                                .as_ref()
1977                                .expect("version for registry source");
1978                            return Ok(SatisfiesResult::MissingLocalIndex(name, version, path));
1979                        }
1980                    }
1981                }
1982            }
1983
1984            // If the package is immutable, we don't need to validate it (or its dependencies).
1985            if package.id.source.is_immutable() {
1986                continue;
1987            }
1988
1989            if let Some(version) = package.id.version.as_ref() {
1990                // If the distribution is a source tree, attempt to validate it from statically
1991                // available `pyproject.toml` metadata before converting it to an installable
1992                // distribution. This avoids requiring build permission for static local packages.
1993                let statically_satisfied = if let Some(source_tree) =
1994                    package.id.source.as_source_tree()
1995                    && let Some(SourceTreeRequiresDist {
1996                        version: static_version,
1997                        metadata,
1998                    }) = Self::source_tree_requires_dist(source_tree, root, package, database)
1999                        .await?
2000                {
2001                    // If this local package has become dynamic, the locked package should
2002                    // no longer contain a version.
2003                    if metadata.dynamic {
2004                        return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, false));
2005                    }
2006
2007                    if let Some(static_version) = static_version {
2008                        // Validate the static `version` metadata.
2009                        if static_version != *version {
2010                            return Ok(SatisfiesResult::MismatchedVersion(
2011                                &package.id.name,
2012                                version.clone(),
2013                                Some(static_version),
2014                            ));
2015                        }
2016
2017                        // Validate the static `provides-extras` metadata.
2018                        match self.satisfies_provides_extra(metadata.provides_extra, package) {
2019                            SatisfiesResult::Satisfied => {}
2020                            result => return Ok(result),
2021                        }
2022
2023                        // Validate that the static requirements are unchanged.
2024                        match self.satisfies_requires_dist(
2025                            metadata.requires_dist,
2026                            metadata.dependency_groups,
2027                            package,
2028                            root,
2029                        )? {
2030                            SatisfiesResult::Satisfied => true,
2031                            result => return Ok(result),
2032                        }
2033                    } else {
2034                        false
2035                    }
2036                } else {
2037                    false
2038                };
2039
2040                if !statically_satisfied {
2041                    // For a non-dynamic package without usable static metadata, fetch the metadata
2042                    // from the distribution database.
2043                    let HashedDist { dist, .. } = package.to_dist(
2044                        root,
2045                        TagPolicy::Preferred(tags),
2046                        build_options,
2047                        markers,
2048                    )?;
2049
2050                    let metadata = {
2051                        let id = dist.distribution_id();
2052                        if let Some(archive) =
2053                            index
2054                                .distributions()
2055                                .get(&id)
2056                                .as_deref()
2057                                .and_then(|response| {
2058                                    if let MetadataResponse::Found(archive, ..) = response {
2059                                        Some(archive)
2060                                    } else {
2061                                        None
2062                                    }
2063                                })
2064                        {
2065                            // If the metadata is already in the index, return it.
2066                            archive.metadata.clone()
2067                        } else {
2068                            // Run the PEP 517 build process to extract metadata from the source distribution.
2069                            let archive = database
2070                                .get_or_build_wheel_metadata(&dist, hasher.get(&dist))
2071                                .await
2072                                .map_err(|err| LockErrorKind::Resolution {
2073                                    id: package.id.clone(),
2074                                    err,
2075                                })?;
2076
2077                            let metadata = archive.metadata.clone();
2078
2079                            // Insert the metadata into the index.
2080                            index
2081                                .distributions()
2082                                .done(id, Arc::new(MetadataResponse::Found(archive)));
2083
2084                            metadata
2085                        }
2086                    };
2087
2088                    // If this is a local package, validate that it hasn't become dynamic (in which
2089                    // case, we'd expect the version to be omitted).
2090                    if package.id.source.is_source_tree() && metadata.dynamic {
2091                        return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, false));
2092                    }
2093
2094                    // Validate the `version` metadata.
2095                    if metadata.version != *version {
2096                        return Ok(SatisfiesResult::MismatchedVersion(
2097                            &package.id.name,
2098                            version.clone(),
2099                            Some(metadata.version.clone()),
2100                        ));
2101                    }
2102
2103                    // Validate the `provides-extras` metadata.
2104                    match self.satisfies_provides_extra(metadata.provides_extra, package) {
2105                        SatisfiesResult::Satisfied => {}
2106                        result => return Ok(result),
2107                    }
2108
2109                    // Validate that the requirements are unchanged.
2110                    match self.satisfies_requires_dist(
2111                        metadata.requires_dist,
2112                        metadata.dependency_groups,
2113                        package,
2114                        root,
2115                    )? {
2116                        SatisfiesResult::Satisfied => {}
2117                        result => return Ok(result),
2118                    }
2119                }
2120            } else if let Some(source_tree) = package.id.source.as_source_tree() {
2121                // For dynamic packages, we don't need the version. We only need to know that the
2122                // package is still dynamic, and that the requirements are unchanged.
2123                //
2124                // If the distribution is a source tree, attempt to extract the requirements from the
2125                // `pyproject.toml` directly. The distribution database will do this too, but we can be
2126                // even more aggressive here since we _only_ need the requirements. So, for example,
2127                // even if the version is dynamic, we can still extract the requirements without
2128                // performing a build, unlike in the database where we typically construct a "complete"
2129                // metadata object.
2130                let metadata =
2131                    Self::source_tree_requires_dist(source_tree, root, package, database)
2132                        .await?
2133                        .map(|metadata| metadata.metadata);
2134
2135                let satisfied = metadata.is_some_and(|metadata| {
2136                    // Validate that the package is still dynamic.
2137                    if !metadata.dynamic {
2138                        debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
2139                        return false;
2140                    }
2141
2142                    // Validate that the extras are unchanged.
2143                    if let SatisfiesResult::Satisfied = self.satisfies_provides_extra(metadata.provides_extra, package, ) {
2144                        debug!("Static `provides-extra` for `{}` is up-to-date", package.id);
2145                    } else {
2146                        debug!("Static `provides-extra` for `{}` is out-of-date; falling back to distribution database", package.id);
2147                        return false;
2148                    }
2149
2150                    // Validate that the requirements are unchanged.
2151                    match self.satisfies_requires_dist(metadata.requires_dist, metadata.dependency_groups, package, root) {
2152                        Ok(SatisfiesResult::Satisfied) => {
2153                            debug!("Static `requires-dist` for `{}` is up-to-date", package.id);
2154                        },
2155                        Ok(..) => {
2156                            debug!("Static `requires-dist` for `{}` is out-of-date; falling back to distribution database", package.id);
2157                            return false;
2158                        },
2159                        Err(..) => {
2160                            debug!("Static `requires-dist` for `{}` is invalid; falling back to distribution database", package.id);
2161                            return false;
2162                        },
2163                    }
2164
2165                    true
2166                });
2167
2168                // If the `requires-dist` metadata matches the requirements, we're done; otherwise,
2169                // fetch the "full" metadata, which may involve invoking the build system. In some
2170                // cases, build backends return metadata that does _not_ match the `pyproject.toml`
2171                // exactly. For example, `hatchling` will flatten any recursive (or self-referential)
2172                // extras, while `setuptools` will not.
2173                if !satisfied {
2174                    let HashedDist { dist, .. } = package.to_dist(
2175                        root,
2176                        TagPolicy::Preferred(tags),
2177                        build_options,
2178                        markers,
2179                    )?;
2180
2181                    let metadata = {
2182                        let id = dist.distribution_id();
2183                        if let Some(archive) =
2184                            index
2185                                .distributions()
2186                                .get(&id)
2187                                .as_deref()
2188                                .and_then(|response| {
2189                                    if let MetadataResponse::Found(archive, ..) = response {
2190                                        Some(archive)
2191                                    } else {
2192                                        None
2193                                    }
2194                                })
2195                        {
2196                            // If the metadata is already in the index, return it.
2197                            archive.metadata.clone()
2198                        } else {
2199                            // Run the PEP 517 build process to extract metadata from the source distribution.
2200                            let archive = database
2201                                .get_or_build_wheel_metadata(&dist, hasher.get(&dist))
2202                                .await
2203                                .map_err(|err| LockErrorKind::Resolution {
2204                                    id: package.id.clone(),
2205                                    err,
2206                                })?;
2207
2208                            let metadata = archive.metadata.clone();
2209
2210                            // Insert the metadata into the index.
2211                            index
2212                                .distributions()
2213                                .done(id, Arc::new(MetadataResponse::Found(archive)));
2214
2215                            metadata
2216                        }
2217                    };
2218
2219                    // Validate that the package is still dynamic.
2220                    if !metadata.dynamic {
2221                        return Ok(SatisfiesResult::MismatchedDynamic(&package.id.name, true));
2222                    }
2223
2224                    // Validate that the extras are unchanged.
2225                    match self.satisfies_provides_extra(metadata.provides_extra, package) {
2226                        SatisfiesResult::Satisfied => {}
2227                        result => return Ok(result),
2228                    }
2229
2230                    // Validate that the requirements are unchanged.
2231                    match self.satisfies_requires_dist(
2232                        metadata.requires_dist,
2233                        metadata.dependency_groups,
2234                        package,
2235                        root,
2236                    )? {
2237                        SatisfiesResult::Satisfied => {}
2238                        result => return Ok(result),
2239                    }
2240                }
2241            } else {
2242                return Ok(SatisfiesResult::MissingVersion(&package.id.name));
2243            }
2244
2245            // Add any explicit indexes to the list of known locals or remotes. These indexes may
2246            // not be available as top-level configuration (i.e., if they're defined within a
2247            // workspace member), but we already validated that the dependencies are up-to-date, so
2248            // we can consider them "available".
2249            for requirement in package
2250                .metadata
2251                .requires_dist
2252                .iter()
2253                .chain(package.metadata.dependency_groups.values().flatten())
2254            {
2255                if let RequirementSource::Registry {
2256                    index: Some(index), ..
2257                } = &requirement.source
2258                {
2259                    match &index.url {
2260                        IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
2261                            if let Some(remotes) = remotes.as_mut() {
2262                                remotes.insert(UrlString::from(
2263                                    index.url().without_credentials().as_ref(),
2264                                ));
2265                            }
2266                        }
2267                        IndexUrl::Path(url) => {
2268                            if let Some(locals) = locals.as_mut() {
2269                                if let Some(path) = url.to_file_path().ok().and_then(|path| {
2270                                    try_relative_to_if(&path, root, !url.was_given_absolute()).ok()
2271                                }) {
2272                                    locals.insert(path.into_boxed_path());
2273                                }
2274                            }
2275                        }
2276                    }
2277                }
2278            }
2279
2280            // Recurse.
2281            for dep in &package.dependencies {
2282                if seen.insert(&dep.package_id) {
2283                    let dep_dist = self.find_by_id(&dep.package_id);
2284                    queue.push_back(dep_dist);
2285                }
2286            }
2287
2288            for dependencies in package.optional_dependencies.values() {
2289                for dep in dependencies {
2290                    if seen.insert(&dep.package_id) {
2291                        let dep_dist = self.find_by_id(&dep.package_id);
2292                        queue.push_back(dep_dist);
2293                    }
2294                }
2295            }
2296
2297            for dependencies in package.dependency_groups.values() {
2298                for dep in dependencies {
2299                    if seen.insert(&dep.package_id) {
2300                        let dep_dist = self.find_by_id(&dep.package_id);
2301                        queue.push_back(dep_dist);
2302                    }
2303                }
2304            }
2305        }
2306
2307        Ok(SatisfiesResult::Satisfied)
2308    }
2309
2310    async fn source_tree_requires_dist<Context: BuildContext>(
2311        source_tree: &Path,
2312        root: &Path,
2313        package: &Package,
2314        database: &DistributionDatabase<'_, Context>,
2315    ) -> Result<Option<SourceTreeRequiresDist>, LockError> {
2316        let parent = root.join(source_tree);
2317        let path = parent.join("pyproject.toml");
2318        match fs_err::tokio::read_to_string(&path).await {
2319            Ok(contents) => {
2320                let pyproject_toml = PyProjectToml::from_toml(&contents, path.user_display())
2321                    .map_err(|err| LockErrorKind::InvalidPyprojectToml {
2322                        path: path.clone(),
2323                        err,
2324                    })?;
2325                let version = pyproject_toml
2326                    .project
2327                    .as_ref()
2328                    .and_then(|project| project.version.clone());
2329                let metadata = database
2330                    .requires_dist(&parent, &pyproject_toml)
2331                    .await
2332                    .map_err(|err| LockErrorKind::Resolution {
2333                        id: package.id.clone(),
2334                        err,
2335                    })?;
2336                Ok(metadata.map(|metadata| SourceTreeRequiresDist { version, metadata }))
2337            }
2338            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
2339            Err(err) => Err(LockErrorKind::UnreadablePyprojectToml { path, err }.into()),
2340        }
2341    }
2342}
2343
2344/// The set of lockfile packages that should be audited, materialized from a
2345/// single traversal of the dependency graph.
2346///
2347/// Created via [`Lock::auditable`]. Exposes multiple views so that different
2348/// audit sources (e.g. per-version vulnerability databases and per-project
2349/// status markers) can share one walk rather than each re-traversing the
2350/// lockfile.
2351#[derive(Debug)]
2352pub struct Auditable<'lock> {
2353    /// Packages deduplicated by `(name, version)` and sorted by the same key.
2354    packages: Vec<(&'lock Package, &'lock Version)>,
2355}
2356
2357struct SourceTreeRequiresDist {
2358    version: Option<Version>,
2359    metadata: RequiresDist,
2360}
2361
2362impl<'lock> Auditable<'lock> {
2363    /// Return the number of distinct `(name, version)` pairs to audit.
2364    pub fn len(&self) -> usize {
2365        self.packages.len()
2366    }
2367
2368    /// Return `true` if there are no packages to audit.
2369    pub fn is_empty(&self) -> bool {
2370        self.packages.is_empty()
2371    }
2372
2373    /// Iterate over the distinct `(name, version)` pairs to audit, sorted by that key.
2374    pub fn packages(&self) -> impl Iterator<Item = (&'lock PackageName, &'lock Version)> + '_ {
2375        self.packages
2376            .iter()
2377            .map(|(package, version)| (package.name(), *version))
2378    }
2379
2380    /// Return the distinct registry-hosted projects among the auditable
2381    /// packages, deduplicated by `(name, index URL)`. Non-registry sources
2382    /// (Git, direct URL, path, editable) are excluded.
2383    pub fn projects(&self, root: &Path) -> Result<Vec<(&'lock PackageName, IndexUrl)>, LockError> {
2384        let mut seen: FxHashSet<(&PackageName, String)> = FxHashSet::default();
2385        let mut projects: Vec<(&PackageName, IndexUrl)> = Vec::with_capacity(self.packages.len());
2386        for (package, _version) in &self.packages {
2387            if let Some(index) = package.index(root)?
2388                && seen.insert((package.name(), index.url().to_string()))
2389            {
2390                projects.push((package.name(), index));
2391            }
2392        }
2393        Ok(projects)
2394    }
2395}
2396
2397#[derive(Debug, Copy, Clone)]
2398enum TagPolicy<'tags> {
2399    /// Exclusively consider wheels that match the specified platform tags.
2400    Required(&'tags Tags),
2401    /// Prefer wheels that match the specified platform tags, but fall back to incompatible wheels
2402    /// if necessary.
2403    Preferred(&'tags Tags),
2404}
2405
2406impl<'tags> TagPolicy<'tags> {
2407    /// Returns the platform tags to consider.
2408    fn tags(&self) -> &'tags Tags {
2409        match self {
2410            Self::Required(tags) | Self::Preferred(tags) => tags,
2411        }
2412    }
2413}
2414
2415/// The result of checking if a lockfile satisfies a set of requirements.
2416#[derive(Debug)]
2417pub enum SatisfiesResult<'lock> {
2418    /// The lockfile satisfies the requirements.
2419    Satisfied,
2420    /// The lockfile uses a different set of workspace members.
2421    MismatchedMembers(BTreeSet<PackageName>, &'lock BTreeSet<PackageName>),
2422    /// A workspace member switched from virtual to non-virtual or vice versa.
2423    MismatchedVirtual(PackageName, bool),
2424    /// A workspace member switched from editable to non-editable or vice versa.
2425    MismatchedEditable(PackageName, bool),
2426    /// A source tree switched from dynamic to non-dynamic or vice versa.
2427    MismatchedDynamic(&'lock PackageName, bool),
2428    /// The lockfile uses a different set of version for its workspace members.
2429    MismatchedVersion(&'lock PackageName, Version, Option<Version>),
2430    /// The lockfile uses a different set of requirements.
2431    MismatchedRequirements(BTreeSet<Requirement>, BTreeSet<Requirement>),
2432    /// The lockfile uses a different set of constraints.
2433    MismatchedConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
2434    /// The lockfile uses a different set of overrides.
2435    MismatchedOverrides(BTreeSet<Requirement>, BTreeSet<Requirement>),
2436    /// The lockfile uses a different set of excludes.
2437    MismatchedExcludes(BTreeSet<PackageName>, BTreeSet<PackageName>),
2438    /// The lockfile uses a different set of build constraints.
2439    MismatchedBuildConstraints(BTreeSet<Requirement>, BTreeSet<Requirement>),
2440    /// The lockfile uses a different set of dependency groups.
2441    MismatchedDependencyGroups(
2442        BTreeMap<GroupName, BTreeSet<Requirement>>,
2443        BTreeMap<GroupName, BTreeSet<Requirement>>,
2444    ),
2445    /// The lockfile uses different static metadata.
2446    MismatchedStaticMetadata(BTreeSet<StaticMetadata>, &'lock BTreeSet<StaticMetadata>),
2447    /// The lockfile is missing a workspace member.
2448    MissingRoot(PackageName),
2449    /// The lockfile referenced a remote index that was not provided
2450    MissingRemoteIndex(&'lock PackageName, &'lock Version, &'lock UrlString),
2451    /// The lockfile referenced a local index that was not provided
2452    MissingLocalIndex(&'lock PackageName, &'lock Version, &'lock Path),
2453    /// A package in the lockfile contains different `requires-dist` metadata than expected.
2454    MismatchedPackageRequirements(
2455        &'lock PackageName,
2456        Option<&'lock Version>,
2457        BTreeSet<Requirement>,
2458        BTreeSet<Requirement>,
2459    ),
2460    /// A package in the lockfile contains different `provides-extra` metadata than expected.
2461    MismatchedPackageProvidesExtra(
2462        &'lock PackageName,
2463        Option<&'lock Version>,
2464        BTreeSet<ExtraName>,
2465        BTreeSet<&'lock ExtraName>,
2466    ),
2467    /// A package in the lockfile contains different `dependency-groups` metadata than expected.
2468    MismatchedPackageDependencyGroups(
2469        &'lock PackageName,
2470        Option<&'lock Version>,
2471        BTreeMap<GroupName, BTreeSet<Requirement>>,
2472        BTreeMap<GroupName, BTreeSet<Requirement>>,
2473    ),
2474    /// The lockfile is missing a version.
2475    MissingVersion(&'lock PackageName),
2476}
2477
2478/// We discard the lockfile if these options match.
2479#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2480#[serde(rename_all = "kebab-case")]
2481struct ResolverOptions {
2482    /// The [`ResolutionMode`] used to generate this lock.
2483    #[serde(default)]
2484    resolution_mode: ResolutionMode,
2485    /// The [`PrereleaseMode`] used to generate this lock.
2486    #[serde(default)]
2487    prerelease_mode: PrereleaseMode,
2488    /// The [`ForkStrategy`] used to generate this lock.
2489    #[serde(default)]
2490    fork_strategy: ForkStrategy,
2491    /// The [`ExcludeNewer`] setting used to generate this lock.
2492    #[serde(flatten)]
2493    exclude_newer: ExcludeNewerWire,
2494}
2495
2496#[expect(clippy::struct_field_names)]
2497#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2498#[serde(rename_all = "kebab-case")]
2499struct ExcludeNewerWire {
2500    exclude_newer: Option<Timestamp>,
2501    exclude_newer_span: Option<ExcludeNewerSpan>,
2502    #[serde(default, skip_serializing_if = "ExcludeNewerPackage::is_empty")]
2503    exclude_newer_package: ExcludeNewerPackage,
2504}
2505
2506impl From<ExcludeNewerWire> for ExcludeNewer {
2507    fn from(wire: ExcludeNewerWire) -> Self {
2508        let global = match (wire.exclude_newer, wire.exclude_newer_span) {
2509            (Some(timestamp), None) => Some(ExcludeNewerValue::absolute(timestamp)),
2510            // We're phasing out writing a timestamp when spans are used. uv writes a dummy
2511            // timestamp for backwards compatibility that we can ignore on deserialization.
2512            (Some(_), Some(span)) => Some(ExcludeNewerValue::relative(span)),
2513            // A future version of uv will remove the timestamp entirely, so for forwards
2514            // compatibility we ignore a missing value.
2515            (None, Some(span)) => Some(ExcludeNewerValue::relative(span)),
2516            (None, None) => None,
2517        };
2518        Self {
2519            global,
2520            package: wire.exclude_newer_package,
2521        }
2522    }
2523}
2524
2525impl From<ExcludeNewer> for ExcludeNewerWire {
2526    fn from(exclude_newer: ExcludeNewer) -> Self {
2527        let (timestamp, span) = match exclude_newer.global {
2528            Some(ExcludeNewerValue::Absolute(timestamp)) => (Some(timestamp), None),
2529            Some(ExcludeNewerValue::Relative(span)) => (None, Some(span)),
2530            None => (None, None),
2531        };
2532        Self {
2533            exclude_newer: timestamp,
2534            exclude_newer_span: span,
2535            exclude_newer_package: exclude_newer.package,
2536        }
2537    }
2538}
2539
2540#[derive(Clone, Debug, Default, serde::Deserialize, PartialEq, Eq)]
2541#[serde(rename_all = "kebab-case")]
2542pub struct ResolverManifest {
2543    /// The workspace members included in the lockfile.
2544    #[serde(default)]
2545    members: BTreeSet<PackageName>,
2546    /// The requirements provided to the resolver, exclusive of the workspace members.
2547    ///
2548    /// These are requirements that are attached to the project, but not to any of its
2549    /// workspace members. For example, the requirements in a PEP 723 script would be included here.
2550    #[serde(default)]
2551    requirements: BTreeSet<Requirement>,
2552    /// The dependency groups provided to the resolver, exclusive of the workspace members.
2553    ///
2554    /// These are dependency groups that are attached to the project, but not to any of its
2555    /// workspace members. For example, the dependency groups in a `pyproject.toml` without a
2556    /// `[project]` table would be included here.
2557    #[serde(default)]
2558    dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
2559    /// The constraints provided to the resolver.
2560    #[serde(default)]
2561    constraints: BTreeSet<Requirement>,
2562    /// The overrides provided to the resolver.
2563    #[serde(default)]
2564    overrides: BTreeSet<Requirement>,
2565    /// The excludes provided to the resolver.
2566    #[serde(default)]
2567    excludes: BTreeSet<PackageName>,
2568    /// The build constraints provided to the resolver.
2569    #[serde(default)]
2570    build_constraints: BTreeSet<Requirement>,
2571    /// The static metadata provided to the resolver.
2572    #[serde(default)]
2573    dependency_metadata: BTreeSet<StaticMetadata>,
2574}
2575
2576impl ResolverManifest {
2577    /// Initialize a [`ResolverManifest`] with the given members, requirements, constraints, and
2578    /// overrides.
2579    pub fn new(
2580        members: impl IntoIterator<Item = PackageName>,
2581        requirements: impl IntoIterator<Item = Requirement>,
2582        constraints: impl IntoIterator<Item = Requirement>,
2583        overrides: impl IntoIterator<Item = Requirement>,
2584        excludes: impl IntoIterator<Item = PackageName>,
2585        build_constraints: impl IntoIterator<Item = Requirement>,
2586        dependency_groups: impl IntoIterator<Item = (GroupName, Vec<Requirement>)>,
2587        dependency_metadata: impl IntoIterator<Item = StaticMetadata>,
2588    ) -> Self {
2589        Self {
2590            members: members.into_iter().collect(),
2591            requirements: requirements.into_iter().collect(),
2592            constraints: constraints.into_iter().collect(),
2593            overrides: overrides.into_iter().collect(),
2594            excludes: excludes.into_iter().collect(),
2595            build_constraints: build_constraints.into_iter().collect(),
2596            dependency_groups: dependency_groups
2597                .into_iter()
2598                .map(|(group, requirements)| (group, requirements.into_iter().collect()))
2599                .collect(),
2600            dependency_metadata: dependency_metadata.into_iter().collect(),
2601        }
2602    }
2603
2604    /// Convert the manifest to a relative form using the given workspace.
2605    pub fn relative_to(self, root: &Path) -> Result<Self, io::Error> {
2606        Ok(Self {
2607            members: self.members,
2608            requirements: self
2609                .requirements
2610                .into_iter()
2611                .map(|requirement| requirement.relative_to(root))
2612                .collect::<Result<BTreeSet<_>, _>>()?,
2613            constraints: self
2614                .constraints
2615                .into_iter()
2616                .map(|requirement| requirement.relative_to(root))
2617                .collect::<Result<BTreeSet<_>, _>>()?,
2618            overrides: self
2619                .overrides
2620                .into_iter()
2621                .map(|requirement| requirement.relative_to(root))
2622                .collect::<Result<BTreeSet<_>, _>>()?,
2623            excludes: self.excludes,
2624            build_constraints: self
2625                .build_constraints
2626                .into_iter()
2627                .map(|requirement| requirement.relative_to(root))
2628                .collect::<Result<BTreeSet<_>, _>>()?,
2629            dependency_groups: self
2630                .dependency_groups
2631                .into_iter()
2632                .map(|(group, requirements)| {
2633                    Ok::<_, io::Error>((
2634                        group,
2635                        requirements
2636                            .into_iter()
2637                            .map(|requirement| requirement.relative_to(root))
2638                            .collect::<Result<BTreeSet<_>, _>>()?,
2639                    ))
2640                })
2641                .collect::<Result<BTreeMap<_, _>, _>>()?,
2642            dependency_metadata: self.dependency_metadata,
2643        })
2644    }
2645}
2646
2647#[derive(Clone, Debug, serde::Deserialize)]
2648#[serde(rename_all = "kebab-case")]
2649struct LockWire {
2650    version: u32,
2651    revision: Option<u32>,
2652    requires_python: RequiresPython,
2653    /// If this lockfile was built from a forking resolution with non-identical forks, store the
2654    /// forks in the lockfile so we can recreate them in subsequent resolutions.
2655    #[serde(rename = "resolution-markers", default)]
2656    fork_markers: Vec<SimplifiedMarkerTree>,
2657    #[serde(rename = "supported-markers", default)]
2658    supported_environments: Vec<SimplifiedMarkerTree>,
2659    #[serde(rename = "required-markers", default)]
2660    required_environments: Vec<SimplifiedMarkerTree>,
2661    #[serde(rename = "conflicts", default)]
2662    conflicts: Option<Conflicts>,
2663    /// We discard the lockfile if these options match.
2664    #[serde(default)]
2665    options: ResolverOptions,
2666    #[serde(default)]
2667    manifest: ResolverManifest,
2668    #[serde(rename = "package", alias = "distribution", default)]
2669    packages: Vec<PackageWire>,
2670}
2671
2672impl TryFrom<LockWire> for Lock {
2673    type Error = LockError;
2674
2675    fn try_from(wire: LockWire) -> Result<Self, LockError> {
2676        // Count the number of sources for each package name. When
2677        // there's only one source for a particular package name (the
2678        // overwhelmingly common case), we can omit some data (like source and
2679        // version) on dependency edges since it is strictly redundant.
2680        let mut unambiguous_package_ids: FxHashMap<PackageName, PackageId> = FxHashMap::default();
2681        let mut ambiguous = FxHashSet::default();
2682        for dist in &wire.packages {
2683            if ambiguous.contains(&dist.id.name) {
2684                continue;
2685            }
2686            if let Some(id) = unambiguous_package_ids.remove(&dist.id.name) {
2687                ambiguous.insert(id.name);
2688                continue;
2689            }
2690            unambiguous_package_ids.insert(dist.id.name.clone(), dist.id.clone());
2691        }
2692
2693        let packages = wire
2694            .packages
2695            .into_iter()
2696            .map(|dist| dist.unwire(&wire.requires_python, &unambiguous_package_ids))
2697            .collect::<Result<Vec<_>, _>>()?;
2698        let supported_environments = wire
2699            .supported_environments
2700            .into_iter()
2701            .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2702            .collect();
2703        let required_environments = wire
2704            .required_environments
2705            .into_iter()
2706            .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2707            .collect();
2708        let fork_markers = wire
2709            .fork_markers
2710            .into_iter()
2711            .map(|simplified_marker| simplified_marker.into_marker(&wire.requires_python))
2712            .map(UniversalMarker::from_combined)
2713            .collect();
2714        let mut options = wire.options;
2715        if options.exclude_newer.exclude_newer_span.is_some() {
2716            options.exclude_newer.exclude_newer = None;
2717        }
2718        let lock = Self::new(
2719            wire.version,
2720            wire.revision.unwrap_or(0),
2721            packages,
2722            wire.requires_python,
2723            options,
2724            wire.manifest,
2725            wire.conflicts.unwrap_or_else(Conflicts::empty),
2726            supported_environments,
2727            required_environments,
2728            fork_markers,
2729        )?;
2730
2731        Ok(lock)
2732    }
2733}
2734
2735/// Like [`Lock`], but limited to the version field. Used for error reporting: by limiting parsing
2736/// to the version field, we can verify compatibility for lockfiles that may otherwise be
2737/// unparsable.
2738#[derive(Clone, Debug, serde::Deserialize)]
2739#[serde(rename_all = "kebab-case")]
2740pub struct LockVersion {
2741    version: u32,
2742}
2743
2744impl LockVersion {
2745    /// Returns the lockfile version.
2746    pub fn version(&self) -> u32 {
2747        self.version
2748    }
2749}
2750
2751#[derive(Clone, Debug, PartialEq, Eq)]
2752pub struct Package {
2753    pub(crate) id: PackageId,
2754    sdist: Option<SourceDist>,
2755    wheels: Vec<Wheel>,
2756    /// If there are multiple versions or sources for the same package name, we add the markers of
2757    /// the fork(s) that contained this version or source, so we can set the correct preferences in
2758    /// the next resolution.
2759    ///
2760    /// Named `resolution-markers` in `uv.lock`.
2761    fork_markers: Vec<UniversalMarker>,
2762    /// The resolved dependencies of the package.
2763    dependencies: Vec<Dependency>,
2764    /// The resolved optional dependencies of the package.
2765    optional_dependencies: BTreeMap<ExtraName, Vec<Dependency>>,
2766    /// The resolved PEP 735 dependency groups of the package.
2767    dependency_groups: BTreeMap<GroupName, Vec<Dependency>>,
2768    /// The exact requirements from the package metadata.
2769    metadata: PackageMetadata,
2770}
2771
2772impl Package {
2773    fn from_annotated_dist(
2774        annotated_dist: &AnnotatedDist,
2775        fork_markers: Vec<UniversalMarker>,
2776        root: &Path,
2777    ) -> Result<Self, LockError> {
2778        let id = PackageId::from_annotated_dist(annotated_dist, root)?;
2779        let sdist = SourceDist::from_annotated_dist(&id, annotated_dist)?;
2780        let wheels = Wheel::from_annotated_dist(annotated_dist)?;
2781        let requires_dist = if id.source.is_immutable() {
2782            BTreeSet::default()
2783        } else {
2784            annotated_dist
2785                .metadata
2786                .as_ref()
2787                .expect("metadata is present")
2788                .requires_dist
2789                .iter()
2790                .cloned()
2791                .map(|requirement| requirement.relative_to(root))
2792                .collect::<Result<_, _>>()
2793                .map_err(LockErrorKind::RequirementRelativePath)?
2794        };
2795        let provides_extra = if id.source.is_immutable() {
2796            Box::default()
2797        } else {
2798            annotated_dist
2799                .metadata
2800                .as_ref()
2801                .expect("metadata is present")
2802                .provides_extra
2803                .clone()
2804        };
2805        let dependency_groups = if id.source.is_immutable() {
2806            BTreeMap::default()
2807        } else {
2808            annotated_dist
2809                .metadata
2810                .as_ref()
2811                .expect("metadata is present")
2812                .dependency_groups
2813                .iter()
2814                .map(|(group, requirements)| {
2815                    let requirements = requirements
2816                        .iter()
2817                        .cloned()
2818                        .map(|requirement| requirement.relative_to(root))
2819                        .collect::<Result<_, _>>()
2820                        .map_err(LockErrorKind::RequirementRelativePath)?;
2821                    Ok::<_, LockError>((group.clone(), requirements))
2822                })
2823                .collect::<Result<_, _>>()?
2824        };
2825        Ok(Self {
2826            id,
2827            sdist,
2828            wheels,
2829            fork_markers,
2830            dependencies: vec![],
2831            optional_dependencies: BTreeMap::default(),
2832            dependency_groups: BTreeMap::default(),
2833            metadata: PackageMetadata {
2834                requires_dist,
2835                provides_extra,
2836                dependency_groups,
2837            },
2838        })
2839    }
2840
2841    /// Add the [`AnnotatedDist`] as a dependency of the [`Package`].
2842    fn add_dependency(
2843        &mut self,
2844        requires_python: &RequiresPython,
2845        annotated_dist: &AnnotatedDist,
2846        marker: UniversalMarker,
2847        root: &Path,
2848    ) -> Result<(), LockError> {
2849        let new_dep =
2850            Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2851        for existing_dep in &mut self.dependencies {
2852            if existing_dep.package_id == new_dep.package_id
2853                // It's important that we do a comparison on
2854                // *simplified* markers here. In particular, when
2855                // we write markers out to the lock file, we use
2856                // "simplified" markers, or markers that are simplified
2857                // *given* that `requires-python` is satisfied. So if
2858                // we don't do equality based on what the simplified
2859                // marker is, we might wind up not merging dependencies
2860                // that ought to be merged and thus writing out extra
2861                // entries.
2862                //
2863                // For example, if `requires-python = '>=3.8'` and we
2864                // have `foo==1` and
2865                // `foo==1 ; python_version >= '3.8'` dependencies,
2866                // then they don't have equivalent complexified
2867                // markers, but their simplified markers are identical.
2868                //
2869                // NOTE: It does seem like perhaps this should
2870                // be implemented semantically/algebraically on
2871                // `MarkerTree` itself, but it wasn't totally clear
2872                // how to do that. I think `pep508` would need to
2873                // grow a concept of "requires python" and provide an
2874                // operation specifically for that.
2875                && existing_dep.simplified_marker == new_dep.simplified_marker
2876            {
2877                existing_dep.extra.extend(new_dep.extra);
2878                return Ok(());
2879            }
2880        }
2881
2882        self.dependencies.push(new_dep);
2883        Ok(())
2884    }
2885
2886    /// Add the [`AnnotatedDist`] as an optional dependency of the [`Package`].
2887    fn add_optional_dependency(
2888        &mut self,
2889        requires_python: &RequiresPython,
2890        extra: ExtraName,
2891        annotated_dist: &AnnotatedDist,
2892        marker: UniversalMarker,
2893        root: &Path,
2894    ) -> Result<(), LockError> {
2895        let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2896        let optional_deps = self.optional_dependencies.entry(extra).or_default();
2897        for existing_dep in &mut *optional_deps {
2898            if existing_dep.package_id == dep.package_id
2899                // See note in add_dependency for why we use
2900                // simplified markers here.
2901                && existing_dep.simplified_marker == dep.simplified_marker
2902            {
2903                existing_dep.extra.extend(dep.extra);
2904                return Ok(());
2905            }
2906        }
2907
2908        optional_deps.push(dep);
2909        Ok(())
2910    }
2911
2912    /// Add the [`AnnotatedDist`] to a dependency group of the [`Package`].
2913    fn add_group_dependency(
2914        &mut self,
2915        requires_python: &RequiresPython,
2916        group: GroupName,
2917        annotated_dist: &AnnotatedDist,
2918        marker: UniversalMarker,
2919        root: &Path,
2920    ) -> Result<(), LockError> {
2921        let dep = Dependency::from_annotated_dist(requires_python, annotated_dist, marker, root)?;
2922        let deps = self.dependency_groups.entry(group).or_default();
2923        for existing_dep in &mut *deps {
2924            if existing_dep.package_id == dep.package_id
2925                // See note in add_dependency for why we use
2926                // simplified markers here.
2927                && existing_dep.simplified_marker == dep.simplified_marker
2928            {
2929                existing_dep.extra.extend(dep.extra);
2930                return Ok(());
2931            }
2932        }
2933
2934        deps.push(dep);
2935        Ok(())
2936    }
2937
2938    /// Convert the [`Package`] to a [`Dist`] that can be used in installation, along with its hash.
2939    fn to_dist(
2940        &self,
2941        workspace_root: &Path,
2942        tag_policy: TagPolicy<'_>,
2943        build_options: &BuildOptions,
2944        markers: &MarkerEnvironment,
2945    ) -> Result<HashedDist, LockError> {
2946        let no_binary = build_options.no_binary_package(&self.id.name);
2947        let no_build = build_options.no_build_package(&self.id.name);
2948
2949        if !no_binary {
2950            if let Some(best_wheel_index) = self.find_best_wheel(tag_policy) {
2951                let hashes = {
2952                    let wheel = &self.wheels[best_wheel_index];
2953                    HashDigests::from(
2954                        wheel
2955                            .hash
2956                            .iter()
2957                            .chain(wheel.zstd.iter().flat_map(|z| z.hash.iter()))
2958                            .map(|h| h.0.clone())
2959                            .collect::<Vec<_>>(),
2960                    )
2961                };
2962
2963                let dist = match &self.id.source {
2964                    Source::Registry(source) => {
2965                        let wheels = self
2966                            .wheels
2967                            .iter()
2968                            .map(|wheel| wheel.to_registry_wheel(source, workspace_root))
2969                            .collect::<Result<_, LockError>>()?;
2970                        let reg_built_dist = RegistryBuiltDist {
2971                            wheels,
2972                            best_wheel_index,
2973                            sdist: None,
2974                        };
2975                        Dist::Built(BuiltDist::Registry(reg_built_dist))
2976                    }
2977                    Source::Path(path) => {
2978                        let filename: WheelFilename =
2979                            self.wheels[best_wheel_index].filename.clone();
2980                        let install_path = absolute_path(workspace_root, path)?;
2981                        let path_dist = PathBuiltDist {
2982                            filename,
2983                            url: verbatim_url(&install_path, &self.id)?,
2984                            install_path: absolute_path(workspace_root, path)?.into_boxed_path(),
2985                        };
2986                        let built_dist = BuiltDist::Path(path_dist);
2987                        Dist::Built(built_dist)
2988                    }
2989                    Source::Direct(url, direct) => {
2990                        let filename: WheelFilename =
2991                            self.wheels[best_wheel_index].filename.clone();
2992                        let url = DisplaySafeUrl::from(ParsedArchiveUrl {
2993                            url: url.to_url().map_err(LockErrorKind::InvalidUrl)?,
2994                            subdirectory: direct.subdirectory.clone(),
2995                            ext: DistExtension::Wheel,
2996                        });
2997                        let direct_dist = DirectUrlBuiltDist {
2998                            filename,
2999                            location: Box::new(url.clone()),
3000                            url: VerbatimUrl::from_url(url),
3001                        };
3002                        let built_dist = BuiltDist::DirectUrl(direct_dist);
3003                        Dist::Built(built_dist)
3004                    }
3005                    Source::Git(_, _) => {
3006                        return Err(LockErrorKind::InvalidWheelSource {
3007                            id: self.id.clone(),
3008                            source_type: "Git",
3009                        }
3010                        .into());
3011                    }
3012                    Source::Directory(_) => {
3013                        return Err(LockErrorKind::InvalidWheelSource {
3014                            id: self.id.clone(),
3015                            source_type: "directory",
3016                        }
3017                        .into());
3018                    }
3019                    Source::Editable(_) => {
3020                        return Err(LockErrorKind::InvalidWheelSource {
3021                            id: self.id.clone(),
3022                            source_type: "editable",
3023                        }
3024                        .into());
3025                    }
3026                    Source::Virtual(_) => {
3027                        return Err(LockErrorKind::InvalidWheelSource {
3028                            id: self.id.clone(),
3029                            source_type: "virtual",
3030                        }
3031                        .into());
3032                    }
3033                };
3034
3035                return Ok(HashedDist { dist, hashes });
3036            }
3037        }
3038
3039        if let Some(sdist) = self.to_source_dist(workspace_root)? {
3040            // Even with `--no-build`, allow virtual packages. (In the future, we may want to allow
3041            // any local source tree, or at least editable source trees, which we allow in
3042            // `uv pip`.)
3043            if !no_build || sdist.is_virtual() {
3044                let hashes = self
3045                    .sdist
3046                    .as_ref()
3047                    .and_then(|s| s.hash())
3048                    .map(|hash| HashDigests::from(vec![hash.0.clone()]))
3049                    .unwrap_or_else(|| HashDigests::from(vec![]));
3050                return Ok(HashedDist {
3051                    dist: Dist::Source(sdist),
3052                    hashes,
3053                });
3054            }
3055        }
3056
3057        match (no_binary, no_build) {
3058            (true, true) => Err(LockErrorKind::NoBinaryNoBuild {
3059                id: self.id.clone(),
3060            }
3061            .into()),
3062            (true, false) if self.id.source.is_wheel() => Err(LockErrorKind::NoBinaryWheelOnly {
3063                id: self.id.clone(),
3064            }
3065            .into()),
3066            (true, false) => Err(LockErrorKind::NoBinary {
3067                id: self.id.clone(),
3068            }
3069            .into()),
3070            (false, true) => Err(LockErrorKind::NoBuild {
3071                id: self.id.clone(),
3072            }
3073            .into()),
3074            (false, false) if self.id.source.is_wheel() => Err(LockError {
3075                kind: Box::new(LockErrorKind::IncompatibleWheelOnly {
3076                    id: self.id.clone(),
3077                }),
3078                hint: self.tag_hint(tag_policy, markers),
3079            }),
3080            (false, false) => Err(LockError {
3081                kind: Box::new(LockErrorKind::NeitherSourceDistNorWheel {
3082                    id: self.id.clone(),
3083                }),
3084                hint: self.tag_hint(tag_policy, markers),
3085            }),
3086        }
3087    }
3088
3089    /// Generate a [`WheelTagHint`] based on wheel-tag incompatibilities.
3090    fn tag_hint(
3091        &self,
3092        tag_policy: TagPolicy<'_>,
3093        markers: &MarkerEnvironment,
3094    ) -> Option<WheelTagHint> {
3095        let filenames = self
3096            .wheels
3097            .iter()
3098            .map(|wheel| &wheel.filename)
3099            .collect::<Vec<_>>();
3100        WheelTagHint::from_wheels(
3101            &self.id.name,
3102            self.id.version.as_ref(),
3103            &filenames,
3104            tag_policy.tags(),
3105            markers,
3106        )
3107    }
3108
3109    /// Convert the source of this [`Package`] to a [`SourceDist`] that can be used in installation.
3110    ///
3111    /// Returns `Ok(None)` if the source cannot be converted because `self.sdist` is `None`. This is required
3112    /// for registry sources.
3113    fn to_source_dist(
3114        &self,
3115        workspace_root: &Path,
3116    ) -> Result<Option<uv_distribution_types::SourceDist>, LockError> {
3117        let sdist = match &self.id.source {
3118            Source::Path(path) => {
3119                // A direct path source can also be a wheel, so validate the extension.
3120                let DistExtension::Source(ext) = DistExtension::from_path(path).map_err(|err| {
3121                    LockErrorKind::MissingExtension {
3122                        id: self.id.clone(),
3123                        err,
3124                    }
3125                })?
3126                else {
3127                    return Ok(None);
3128                };
3129                let install_path = absolute_path(workspace_root, path)?;
3130                let given = path.to_str().expect("lock file paths must be UTF-8");
3131                let path_dist = PathSourceDist {
3132                    name: self.id.name.clone(),
3133                    version: self.id.version.clone(),
3134                    url: verbatim_url(&install_path, &self.id)?.with_given(given),
3135                    install_path: install_path.into_boxed_path(),
3136                    ext,
3137                };
3138                uv_distribution_types::SourceDist::Path(path_dist)
3139            }
3140            Source::Directory(path) => {
3141                let install_path = absolute_path(workspace_root, path)?;
3142                let given = path.to_str().expect("lock file paths must be UTF-8");
3143                let dir_dist = DirectorySourceDist {
3144                    name: self.id.name.clone(),
3145                    url: verbatim_url(&install_path, &self.id)?.with_given(given),
3146                    install_path: install_path.into_boxed_path(),
3147                    editable: Some(false),
3148                    r#virtual: Some(false),
3149                };
3150                uv_distribution_types::SourceDist::Directory(dir_dist)
3151            }
3152            Source::Editable(path) => {
3153                let install_path = absolute_path(workspace_root, path)?;
3154                let given = path.to_str().expect("lock file paths must be UTF-8");
3155                let dir_dist = DirectorySourceDist {
3156                    name: self.id.name.clone(),
3157                    url: verbatim_url(&install_path, &self.id)?.with_given(given),
3158                    install_path: install_path.into_boxed_path(),
3159                    editable: Some(true),
3160                    r#virtual: Some(false),
3161                };
3162                uv_distribution_types::SourceDist::Directory(dir_dist)
3163            }
3164            Source::Virtual(path) => {
3165                let install_path = absolute_path(workspace_root, path)?;
3166                let given = path.to_str().expect("lock file paths must be UTF-8");
3167                let dir_dist = DirectorySourceDist {
3168                    name: self.id.name.clone(),
3169                    url: verbatim_url(&install_path, &self.id)?.with_given(given),
3170                    install_path: install_path.into_boxed_path(),
3171                    editable: Some(false),
3172                    r#virtual: Some(true),
3173                };
3174                uv_distribution_types::SourceDist::Directory(dir_dist)
3175            }
3176            Source::Git(url, git) => {
3177                // Remove the fragment and query from the URL; they're already present in the
3178                // `GitSource`.
3179                let mut url = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
3180                url.set_fragment(None);
3181                url.set_query(None);
3182
3183                // Reconstruct the `GitUrl` from the `GitSource`.
3184                let git_url = GitUrl::from_commit(
3185                    url,
3186                    GitReference::from(git.kind.clone()),
3187                    git.precise,
3188                    git.lfs,
3189                )?;
3190
3191                // Reconstruct the PEP 508-compatible URL from the `GitSource`.
3192                let url = DisplaySafeUrl::from(ParsedGitUrl {
3193                    url: git_url.clone(),
3194                    subdirectory: git.subdirectory.clone(),
3195                });
3196
3197                let git_dist = GitSourceDist {
3198                    name: self.id.name.clone(),
3199                    url: VerbatimUrl::from_url(url),
3200                    git: Box::new(git_url),
3201                    subdirectory: git.subdirectory.clone(),
3202                };
3203                uv_distribution_types::SourceDist::Git(git_dist)
3204            }
3205            Source::Direct(url, direct) => {
3206                // A direct URL source can also be a wheel, so validate the extension.
3207                let DistExtension::Source(ext) =
3208                    DistExtension::from_path(url.base_str()).map_err(|err| {
3209                        LockErrorKind::MissingExtension {
3210                            id: self.id.clone(),
3211                            err,
3212                        }
3213                    })?
3214                else {
3215                    return Ok(None);
3216                };
3217                let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
3218                let url = DisplaySafeUrl::from(ParsedArchiveUrl {
3219                    url: location.clone(),
3220                    subdirectory: direct.subdirectory.clone(),
3221                    ext: DistExtension::Source(ext),
3222                });
3223                let direct_dist = DirectUrlSourceDist {
3224                    name: self.id.name.clone(),
3225                    location: Box::new(location),
3226                    subdirectory: direct.subdirectory.clone(),
3227                    ext,
3228                    url: VerbatimUrl::from_url(url),
3229                };
3230                uv_distribution_types::SourceDist::DirectUrl(direct_dist)
3231            }
3232            Source::Registry(RegistrySource::Url(url)) => {
3233                let Some(ref sdist) = self.sdist else {
3234                    return Ok(None);
3235                };
3236
3237                let name = &self.id.name;
3238                let version = self
3239                    .id
3240                    .version
3241                    .as_ref()
3242                    .expect("version for registry source");
3243
3244                let file_url = sdist.url().ok_or_else(|| LockErrorKind::MissingUrl {
3245                    name: name.clone(),
3246                    version: version.clone(),
3247                })?;
3248                let filename = sdist
3249                    .filename()
3250                    .ok_or_else(|| LockErrorKind::MissingFilename {
3251                        id: self.id.clone(),
3252                    })?;
3253                let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
3254                    LockErrorKind::MissingExtension {
3255                        id: self.id.clone(),
3256                        err,
3257                    }
3258                })?;
3259                let file = Box::new(uv_distribution_types::File {
3260                    dist_info_metadata: false,
3261                    filename: SmallString::from(filename),
3262                    hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
3263                        HashDigests::from(hash.0.clone())
3264                    }),
3265                    requires_python: None,
3266                    size: sdist.size(),
3267                    upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
3268                    url: FileLocation::AbsoluteUrl(file_url.clone()),
3269                    yanked: None,
3270                    zstd: None,
3271                });
3272
3273                let index = IndexUrl::from(VerbatimUrl::from_url(
3274                    url.to_url().map_err(LockErrorKind::InvalidUrl)?,
3275                ));
3276
3277                let reg_dist = RegistrySourceDist {
3278                    name: name.clone(),
3279                    version: version.clone(),
3280                    file,
3281                    ext,
3282                    index,
3283                    wheels: vec![],
3284                };
3285                uv_distribution_types::SourceDist::Registry(reg_dist)
3286            }
3287            Source::Registry(RegistrySource::Path(path)) => {
3288                let Some(ref sdist) = self.sdist else {
3289                    return Ok(None);
3290                };
3291
3292                let name = &self.id.name;
3293                let version = self
3294                    .id
3295                    .version
3296                    .as_ref()
3297                    .expect("version for registry source");
3298
3299                let file_url = match sdist {
3300                    SourceDist::Url { url: file_url, .. } => {
3301                        FileLocation::AbsoluteUrl(file_url.clone())
3302                    }
3303                    SourceDist::Path {
3304                        path: file_path, ..
3305                    } => {
3306                        let file_path = workspace_root.join(path).join(file_path);
3307                        let file_url =
3308                            DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
3309                                LockErrorKind::PathToUrl {
3310                                    path: file_path.into_boxed_path(),
3311                                }
3312                            })?;
3313                        FileLocation::AbsoluteUrl(UrlString::from(file_url))
3314                    }
3315                    SourceDist::Metadata { .. } => {
3316                        return Err(LockErrorKind::MissingPath {
3317                            name: name.clone(),
3318                            version: version.clone(),
3319                        }
3320                        .into());
3321                    }
3322                };
3323                let filename = sdist
3324                    .filename()
3325                    .ok_or_else(|| LockErrorKind::MissingFilename {
3326                        id: self.id.clone(),
3327                    })?;
3328                let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
3329                    LockErrorKind::MissingExtension {
3330                        id: self.id.clone(),
3331                        err,
3332                    }
3333                })?;
3334                let file = Box::new(uv_distribution_types::File {
3335                    dist_info_metadata: false,
3336                    filename: SmallString::from(filename),
3337                    hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
3338                        HashDigests::from(hash.0.clone())
3339                    }),
3340                    requires_python: None,
3341                    size: sdist.size(),
3342                    upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
3343                    url: file_url,
3344                    yanked: None,
3345                    zstd: None,
3346                });
3347
3348                let index = IndexUrl::from(
3349                    VerbatimUrl::from_absolute_path(workspace_root.join(path))
3350                        .map_err(LockErrorKind::RegistryVerbatimUrl)?,
3351                );
3352
3353                let reg_dist = RegistrySourceDist {
3354                    name: name.clone(),
3355                    version: version.clone(),
3356                    file,
3357                    ext,
3358                    index,
3359                    wheels: vec![],
3360                };
3361                uv_distribution_types::SourceDist::Registry(reg_dist)
3362            }
3363        };
3364
3365        Ok(Some(sdist))
3366    }
3367
3368    fn to_toml(
3369        &self,
3370        requires_python: &RequiresPython,
3371        dist_count_by_name: &FxHashMap<PackageName, u64>,
3372    ) -> Result<Table, toml_edit::ser::Error> {
3373        let mut table = Table::new();
3374
3375        self.id.to_toml(None, &mut table);
3376
3377        if !self.fork_markers.is_empty() {
3378            let fork_markers = each_element_on_its_line_array(
3379                simplified_universal_markers(&self.fork_markers, requires_python).into_iter(),
3380            );
3381            if !fork_markers.is_empty() {
3382                table.insert("resolution-markers", value(fork_markers));
3383            }
3384        }
3385
3386        if !self.dependencies.is_empty() {
3387            let deps = each_element_on_its_line_array(self.dependencies.iter().map(|dep| {
3388                dep.to_toml(requires_python, dist_count_by_name)
3389                    .into_inline_table()
3390            }));
3391            table.insert("dependencies", value(deps));
3392        }
3393
3394        if !self.optional_dependencies.is_empty() {
3395            let mut optional_deps = Table::new();
3396            for (extra, deps) in &self.optional_dependencies {
3397                let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3398                    dep.to_toml(requires_python, dist_count_by_name)
3399                        .into_inline_table()
3400                }));
3401                if !deps.is_empty() {
3402                    optional_deps.insert(extra.as_ref(), value(deps));
3403                }
3404            }
3405            if !optional_deps.is_empty() {
3406                table.insert("optional-dependencies", Item::Table(optional_deps));
3407            }
3408        }
3409
3410        if !self.dependency_groups.is_empty() {
3411            let mut dependency_groups = Table::new();
3412            for (extra, deps) in &self.dependency_groups {
3413                let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3414                    dep.to_toml(requires_python, dist_count_by_name)
3415                        .into_inline_table()
3416                }));
3417                if !deps.is_empty() {
3418                    dependency_groups.insert(extra.as_ref(), value(deps));
3419                }
3420            }
3421            if !dependency_groups.is_empty() {
3422                table.insert("dev-dependencies", Item::Table(dependency_groups));
3423            }
3424        }
3425
3426        if let Some(ref sdist) = self.sdist {
3427            table.insert("sdist", value(sdist.to_toml()?));
3428        }
3429
3430        if !self.wheels.is_empty() {
3431            let wheels = each_element_on_its_line_array(
3432                self.wheels
3433                    .iter()
3434                    .map(Wheel::to_toml)
3435                    .collect::<Result<Vec<_>, _>>()?
3436                    .into_iter(),
3437            );
3438            table.insert("wheels", value(wheels));
3439        }
3440
3441        // Write the package metadata, if non-empty.
3442        {
3443            let mut metadata_table = Table::new();
3444
3445            if !self.metadata.requires_dist.is_empty() {
3446                let requires_dist = self
3447                    .metadata
3448                    .requires_dist
3449                    .iter()
3450                    .map(|requirement| {
3451                        serde::Serialize::serialize(
3452                            &requirement,
3453                            toml_edit::ser::ValueSerializer::new(),
3454                        )
3455                    })
3456                    .collect::<Result<Vec<_>, _>>()?;
3457                let requires_dist = match requires_dist.as_slice() {
3458                    [] => Array::new(),
3459                    [requirement] => Array::from_iter([requirement]),
3460                    requires_dist => each_element_on_its_line_array(requires_dist.iter()),
3461                };
3462                metadata_table.insert("requires-dist", value(requires_dist));
3463            }
3464
3465            if !self.metadata.dependency_groups.is_empty() {
3466                let mut dependency_groups = Table::new();
3467                for (extra, deps) in &self.metadata.dependency_groups {
3468                    let deps = deps
3469                        .iter()
3470                        .map(|requirement| {
3471                            serde::Serialize::serialize(
3472                                &requirement,
3473                                toml_edit::ser::ValueSerializer::new(),
3474                            )
3475                        })
3476                        .collect::<Result<Vec<_>, _>>()?;
3477                    let deps = match deps.as_slice() {
3478                        [] => Array::new(),
3479                        [requirement] => Array::from_iter([requirement]),
3480                        deps => each_element_on_its_line_array(deps.iter()),
3481                    };
3482                    dependency_groups.insert(extra.as_ref(), value(deps));
3483                }
3484                if !dependency_groups.is_empty() {
3485                    metadata_table.insert("requires-dev", Item::Table(dependency_groups));
3486                }
3487            }
3488
3489            if !self.metadata.provides_extra.is_empty() {
3490                let provides_extras = self
3491                    .metadata
3492                    .provides_extra
3493                    .iter()
3494                    .map(|extra| {
3495                        serde::Serialize::serialize(&extra, toml_edit::ser::ValueSerializer::new())
3496                    })
3497                    .collect::<Result<Vec<_>, _>>()?;
3498                // This is just a list of names, so linebreaking it is excessive.
3499                let provides_extras = Array::from_iter(provides_extras);
3500                metadata_table.insert("provides-extras", value(provides_extras));
3501            }
3502
3503            if !metadata_table.is_empty() {
3504                table.insert("metadata", Item::Table(metadata_table));
3505            }
3506        }
3507
3508        Ok(table)
3509    }
3510
3511    fn find_best_wheel(&self, tag_policy: TagPolicy<'_>) -> Option<usize> {
3512        type WheelPriority<'lock> = (TagPriority, Option<&'lock BuildTag>);
3513
3514        let mut best: Option<(WheelPriority, usize)> = None;
3515        for (i, wheel) in self.wheels.iter().enumerate() {
3516            let TagCompatibility::Compatible(tag_priority) =
3517                wheel.filename.compatibility(tag_policy.tags())
3518            else {
3519                continue;
3520            };
3521            let build_tag = wheel.filename.build_tag();
3522            let wheel_priority = (tag_priority, build_tag);
3523            match best {
3524                None => {
3525                    best = Some((wheel_priority, i));
3526                }
3527                Some((best_priority, _)) => {
3528                    if wheel_priority > best_priority {
3529                        best = Some((wheel_priority, i));
3530                    }
3531                }
3532            }
3533        }
3534
3535        let best = best.map(|(_, i)| i);
3536        match tag_policy {
3537            TagPolicy::Required(_) => best,
3538            TagPolicy::Preferred(_) => best.or_else(|| self.wheels.first().map(|_| 0)),
3539        }
3540    }
3541
3542    /// Returns the [`PackageName`] of the package.
3543    pub fn name(&self) -> &PackageName {
3544        &self.id.name
3545    }
3546
3547    /// Returns the [`Version`] of the package.
3548    pub fn version(&self) -> Option<&Version> {
3549        self.id.version.as_ref()
3550    }
3551
3552    /// Returns the Git SHA of the package, if it is a Git source.
3553    pub fn git_sha(&self) -> Option<&GitOid> {
3554        match &self.id.source {
3555            Source::Git(_, git) => Some(&git.precise),
3556            _ => None,
3557        }
3558    }
3559
3560    /// Return the fork markers for this package, if any.
3561    pub fn fork_markers(&self) -> &[UniversalMarker] {
3562        self.fork_markers.as_slice()
3563    }
3564
3565    /// Returns the [`IndexUrl`] for the package, if it is a registry source.
3566    pub fn index(&self, root: &Path) -> Result<Option<IndexUrl>, LockError> {
3567        match &self.id.source {
3568            Source::Registry(RegistrySource::Url(url)) => {
3569                let index = IndexUrl::from(VerbatimUrl::from_url(
3570                    url.to_url().map_err(LockErrorKind::InvalidUrl)?,
3571                ));
3572                Ok(Some(index))
3573            }
3574            Source::Registry(RegistrySource::Path(path)) => {
3575                let index = IndexUrl::from(
3576                    VerbatimUrl::from_absolute_path(root.join(path))
3577                        .map_err(LockErrorKind::RegistryVerbatimUrl)?,
3578                );
3579                Ok(Some(index))
3580            }
3581            _ => Ok(None),
3582        }
3583    }
3584
3585    /// Returns all the hashes associated with this [`Package`].
3586    fn hashes(&self) -> HashDigests {
3587        let mut hashes = Vec::with_capacity(
3588            usize::from(self.sdist.as_ref().and_then(|sdist| sdist.hash()).is_some())
3589                + self
3590                    .wheels
3591                    .iter()
3592                    .map(|wheel| usize::from(wheel.hash.is_some()))
3593                    .sum::<usize>(),
3594        );
3595        if let Some(ref sdist) = self.sdist {
3596            if let Some(hash) = sdist.hash() {
3597                hashes.push(hash.0.clone());
3598            }
3599        }
3600        for wheel in &self.wheels {
3601            hashes.extend(wheel.hash.as_ref().map(|h| h.0.clone()));
3602            if let Some(zstd) = wheel.zstd.as_ref() {
3603                hashes.extend(zstd.hash.as_ref().map(|h| h.0.clone()));
3604            }
3605        }
3606        HashDigests::from(hashes)
3607    }
3608
3609    /// Returns the [`ResolvedRepositoryReference`] for the package, if it is a Git source.
3610    pub fn as_git_ref(&self) -> Result<Option<ResolvedRepositoryReference>, LockError> {
3611        match &self.id.source {
3612            Source::Git(url, git) => Ok(Some(ResolvedRepositoryReference {
3613                reference: RepositoryReference {
3614                    url: RepositoryUrl::new(&url.to_url().map_err(LockErrorKind::InvalidUrl)?),
3615                    reference: GitReference::from(git.kind.clone()),
3616                },
3617                sha: git.precise,
3618            })),
3619            _ => Ok(None),
3620        }
3621    }
3622
3623    /// Returns `true` if the package is a dynamic source tree.
3624    fn is_dynamic(&self) -> bool {
3625        self.id.version.is_none()
3626    }
3627
3628    /// Returns the extras the package provides, if any.
3629    pub fn provides_extras(&self) -> &[ExtraName] {
3630        &self.metadata.provides_extra
3631    }
3632
3633    /// Returns the dependency groups the package provides, if any.
3634    pub fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
3635        &self.metadata.dependency_groups
3636    }
3637
3638    /// Returns the dependencies of the package.
3639    pub fn dependencies(&self) -> &[Dependency] {
3640        &self.dependencies
3641    }
3642
3643    /// Returns the optional dependencies of the package.
3644    pub fn optional_dependencies(&self) -> &BTreeMap<ExtraName, Vec<Dependency>> {
3645        &self.optional_dependencies
3646    }
3647
3648    /// Returns the resolved PEP 735 dependency groups of the package.
3649    pub fn resolved_dependency_groups(&self) -> &BTreeMap<GroupName, Vec<Dependency>> {
3650        &self.dependency_groups
3651    }
3652
3653    /// Returns an [`InstallTarget`] view for filtering decisions.
3654    pub fn as_install_target(&self) -> InstallTarget<'_> {
3655        InstallTarget {
3656            name: self.name(),
3657            is_local: self.id.source.is_local(),
3658        }
3659    }
3660}
3661
3662/// Attempts to construct a `VerbatimUrl` from the given normalized `Path`.
3663fn verbatim_url(path: &Path, id: &PackageId) -> Result<VerbatimUrl, LockError> {
3664    let url =
3665        VerbatimUrl::from_normalized_path(path).map_err(|err| LockErrorKind::VerbatimUrl {
3666            id: id.clone(),
3667            err,
3668        })?;
3669    Ok(url)
3670}
3671
3672/// Attempts to construct an absolute path from the given `Path`.
3673fn absolute_path(workspace_root: &Path, path: &Path) -> Result<PathBuf, LockError> {
3674    let path = uv_fs::normalize_absolute_path(&workspace_root.join(path))
3675        .map_err(LockErrorKind::AbsolutePath)?;
3676    Ok(path)
3677}
3678
3679#[derive(Clone, Debug, serde::Deserialize)]
3680#[serde(rename_all = "kebab-case")]
3681struct PackageWire {
3682    #[serde(flatten)]
3683    id: PackageId,
3684    #[serde(default)]
3685    metadata: PackageMetadata,
3686    #[serde(default)]
3687    sdist: Option<SourceDist>,
3688    #[serde(default)]
3689    wheels: Vec<Wheel>,
3690    #[serde(default, rename = "resolution-markers")]
3691    fork_markers: Vec<SimplifiedMarkerTree>,
3692    #[serde(default)]
3693    dependencies: Vec<DependencyWire>,
3694    #[serde(default)]
3695    optional_dependencies: BTreeMap<ExtraName, Vec<DependencyWire>>,
3696    #[serde(default, rename = "dev-dependencies", alias = "dependency-groups")]
3697    dependency_groups: BTreeMap<GroupName, Vec<DependencyWire>>,
3698}
3699
3700#[derive(Clone, Default, Debug, Eq, PartialEq, serde::Deserialize)]
3701#[serde(rename_all = "kebab-case")]
3702struct PackageMetadata {
3703    #[serde(default)]
3704    requires_dist: BTreeSet<Requirement>,
3705    #[serde(default, rename = "provides-extras")]
3706    provides_extra: Box<[ExtraName]>,
3707    #[serde(default, rename = "requires-dev", alias = "dependency-groups")]
3708    dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
3709}
3710
3711impl PackageWire {
3712    fn unwire(
3713        self,
3714        requires_python: &RequiresPython,
3715        unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3716    ) -> Result<Package, LockError> {
3717        // Consistency check
3718        if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) {
3719            if let Some(version) = &self.id.version {
3720                for wheel in &self.wheels {
3721                    if *version != wheel.filename.version
3722                        && *version != wheel.filename.version.clone().without_local()
3723                    {
3724                        return Err(LockError::from(LockErrorKind::InconsistentVersions {
3725                            name: self.id.name,
3726                            version: version.clone(),
3727                            wheel: wheel.clone(),
3728                        }));
3729                    }
3730                }
3731                // We can't check the source dist version since it does not need to contain the version
3732                // in the filename.
3733            }
3734        }
3735
3736        let unwire_deps = |deps: Vec<DependencyWire>| -> Result<Vec<Dependency>, LockError> {
3737            deps.into_iter()
3738                .map(|dep| dep.unwire(requires_python, unambiguous_package_ids))
3739                .collect()
3740        };
3741
3742        Ok(Package {
3743            id: self.id,
3744            metadata: self.metadata,
3745            sdist: self.sdist,
3746            wheels: self.wheels,
3747            fork_markers: self
3748                .fork_markers
3749                .into_iter()
3750                .map(|simplified_marker| simplified_marker.into_marker(requires_python))
3751                .map(UniversalMarker::from_combined)
3752                .collect(),
3753            dependencies: unwire_deps(self.dependencies)?,
3754            optional_dependencies: self
3755                .optional_dependencies
3756                .into_iter()
3757                .map(|(extra, deps)| Ok((extra, unwire_deps(deps)?)))
3758                .collect::<Result<_, LockError>>()?,
3759            dependency_groups: self
3760                .dependency_groups
3761                .into_iter()
3762                .map(|(group, deps)| Ok((group, unwire_deps(deps)?)))
3763                .collect::<Result<_, LockError>>()?,
3764        })
3765    }
3766}
3767
3768/// Inside the lockfile, we match a dependency entry to a package entry through a key made up
3769/// of the name, the version and the source url.
3770#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3771#[serde(rename_all = "kebab-case")]
3772pub(crate) struct PackageId {
3773    pub(crate) name: PackageName,
3774    pub(crate) version: Option<Version>,
3775    source: Source,
3776}
3777
3778impl PackageId {
3779    fn from_annotated_dist(annotated_dist: &AnnotatedDist, root: &Path) -> Result<Self, LockError> {
3780        // Identify the source of the package.
3781        let source = Source::from_resolved_dist(&annotated_dist.dist, root)?;
3782        // Omit versions for dynamic source trees.
3783        let version = if source.is_source_tree()
3784            && annotated_dist
3785                .metadata
3786                .as_ref()
3787                .is_some_and(|metadata| metadata.dynamic)
3788        {
3789            None
3790        } else {
3791            Some(annotated_dist.version.clone())
3792        };
3793        let name = annotated_dist.name.clone();
3794        Ok(Self {
3795            name,
3796            version,
3797            source,
3798        })
3799    }
3800
3801    /// Writes this package ID inline into the table given.
3802    ///
3803    /// When a map is given, and if the package name in this ID is unambiguous
3804    /// (i.e., it has a count of 1 in the map), then the `version` and `source`
3805    /// fields are omitted. In all other cases, including when a map is not
3806    /// given, the `version` and `source` fields are written.
3807    fn to_toml(&self, dist_count_by_name: Option<&FxHashMap<PackageName, u64>>, table: &mut Table) {
3808        let count = dist_count_by_name.and_then(|map| map.get(&self.name).copied());
3809        table.insert("name", value(self.name.to_string()));
3810        if count.map(|count| count > 1).unwrap_or(true) {
3811            if let Some(version) = &self.version {
3812                table.insert("version", value(version.to_string()));
3813            }
3814            self.source.to_toml(table);
3815        }
3816    }
3817}
3818
3819impl Display for PackageId {
3820    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3821        if let Some(version) = &self.version {
3822            write!(f, "{}=={} @ {}", self.name, version, self.source)
3823        } else {
3824            write!(f, "{} @ {}", self.name, self.source)
3825        }
3826    }
3827}
3828
3829#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3830#[serde(rename_all = "kebab-case")]
3831struct PackageIdForDependency {
3832    name: PackageName,
3833    version: Option<Version>,
3834    source: Option<Source>,
3835}
3836
3837impl PackageIdForDependency {
3838    fn unwire(
3839        self,
3840        unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3841    ) -> Result<PackageId, LockError> {
3842        let unambiguous_package_id = unambiguous_package_ids.get(&self.name);
3843        let source = self.source.map(Ok::<_, LockError>).unwrap_or_else(|| {
3844            let Some(package_id) = unambiguous_package_id else {
3845                return Err(LockErrorKind::MissingDependencySource {
3846                    name: self.name.clone(),
3847                }
3848                .into());
3849            };
3850            Ok(package_id.source.clone())
3851        })?;
3852        let version = if let Some(version) = self.version {
3853            Some(version)
3854        } else {
3855            if let Some(package_id) = unambiguous_package_id {
3856                package_id.version.clone()
3857            } else {
3858                // If the package is a source tree, assume that the missing `self.version` field is
3859                // indicative of a dynamic version.
3860                if source.is_source_tree() {
3861                    None
3862                } else {
3863                    return Err(LockErrorKind::MissingDependencyVersion {
3864                        name: self.name.clone(),
3865                    }
3866                    .into());
3867                }
3868            }
3869        };
3870        Ok(PackageId {
3871            name: self.name,
3872            version,
3873            source,
3874        })
3875    }
3876}
3877
3878impl From<PackageId> for PackageIdForDependency {
3879    fn from(id: PackageId) -> Self {
3880        Self {
3881            name: id.name,
3882            version: id.version,
3883            source: Some(id.source),
3884        }
3885    }
3886}
3887
3888/// A unique identifier to differentiate between different sources for the same version of a
3889/// package.
3890///
3891/// NOTE: Care should be taken when adding variants to this enum. Namely, new
3892/// variants should be added without changing the relative ordering of other
3893/// variants. Otherwise, this could cause the lockfile to have a different
3894/// canonical ordering of sources.
3895#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3896#[serde(try_from = "SourceWire")]
3897enum Source {
3898    /// A registry or `--find-links` index.
3899    Registry(RegistrySource),
3900    /// A Git repository.
3901    Git(UrlString, GitSource),
3902    /// A direct HTTP(S) URL.
3903    Direct(UrlString, DirectSource),
3904    /// A path to a local source or built archive.
3905    Path(Box<Path>),
3906    /// A path to a local directory.
3907    Directory(Box<Path>),
3908    /// A path to a local directory that should be installed as editable.
3909    Editable(Box<Path>),
3910    /// A path to a local directory that should not be built or installed.
3911    Virtual(Box<Path>),
3912}
3913
3914impl Source {
3915    fn from_resolved_dist(resolved_dist: &ResolvedDist, root: &Path) -> Result<Self, LockError> {
3916        match *resolved_dist {
3917            // We pass empty installed packages for locking.
3918            ResolvedDist::Installed { .. } => unreachable!(),
3919            ResolvedDist::Installable { ref dist, .. } => Self::from_dist(dist, root),
3920        }
3921    }
3922
3923    fn from_dist(dist: &Dist, root: &Path) -> Result<Self, LockError> {
3924        match *dist {
3925            Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, root),
3926            Dist::Source(ref source_dist) => Self::from_source_dist(source_dist, root),
3927        }
3928    }
3929
3930    fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result<Self, LockError> {
3931        match *built_dist {
3932            BuiltDist::Registry(ref reg_dist) => Self::from_registry_built_dist(reg_dist, root),
3933            BuiltDist::DirectUrl(ref direct_dist) => Ok(Self::from_direct_built_dist(direct_dist)),
3934            BuiltDist::Path(ref path_dist) => Self::from_path_built_dist(path_dist, root),
3935        }
3936    }
3937
3938    fn from_source_dist(
3939        source_dist: &uv_distribution_types::SourceDist,
3940        root: &Path,
3941    ) -> Result<Self, LockError> {
3942        match *source_dist {
3943            uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
3944                Self::from_registry_source_dist(reg_dist, root)
3945            }
3946            uv_distribution_types::SourceDist::DirectUrl(ref direct_dist) => {
3947                Ok(Self::from_direct_source_dist(direct_dist))
3948            }
3949            uv_distribution_types::SourceDist::Git(ref git_dist) => {
3950                Ok(Self::from_git_dist(git_dist))
3951            }
3952            uv_distribution_types::SourceDist::Path(ref path_dist) => {
3953                Self::from_path_source_dist(path_dist, root)
3954            }
3955            uv_distribution_types::SourceDist::Directory(ref directory) => {
3956                Self::from_directory_source_dist(directory, root)
3957            }
3958        }
3959    }
3960
3961    fn from_registry_built_dist(
3962        reg_dist: &RegistryBuiltDist,
3963        root: &Path,
3964    ) -> Result<Self, LockError> {
3965        Self::from_index_url(&reg_dist.best_wheel().index, root)
3966    }
3967
3968    fn from_registry_source_dist(
3969        reg_dist: &RegistrySourceDist,
3970        root: &Path,
3971    ) -> Result<Self, LockError> {
3972        Self::from_index_url(&reg_dist.index, root)
3973    }
3974
3975    fn from_direct_built_dist(direct_dist: &DirectUrlBuiltDist) -> Self {
3976        Self::Direct(
3977            normalize_url(direct_dist.url.to_url()),
3978            DirectSource { subdirectory: None },
3979        )
3980    }
3981
3982    fn from_direct_source_dist(direct_dist: &DirectUrlSourceDist) -> Self {
3983        Self::Direct(
3984            normalize_url(direct_dist.url.to_url()),
3985            DirectSource {
3986                subdirectory: direct_dist.subdirectory.clone(),
3987            },
3988        )
3989    }
3990
3991    fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result<Self, LockError> {
3992        let path = try_relative_to_if(
3993            &path_dist.install_path,
3994            root,
3995            !path_dist.url.was_given_absolute(),
3996        )
3997        .map_err(LockErrorKind::DistributionRelativePath)?;
3998        Ok(Self::Path(path.into_boxed_path()))
3999    }
4000
4001    fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Self, LockError> {
4002        let path = try_relative_to_if(
4003            &path_dist.install_path,
4004            root,
4005            !path_dist.url.was_given_absolute(),
4006        )
4007        .map_err(LockErrorKind::DistributionRelativePath)?;
4008        Ok(Self::Path(path.into_boxed_path()))
4009    }
4010
4011    fn from_directory_source_dist(
4012        directory_dist: &DirectorySourceDist,
4013        root: &Path,
4014    ) -> Result<Self, LockError> {
4015        let path = try_relative_to_if(
4016            &directory_dist.install_path,
4017            root,
4018            !directory_dist.url.was_given_absolute(),
4019        )
4020        .map_err(LockErrorKind::DistributionRelativePath)?;
4021        if directory_dist.editable.unwrap_or(false) {
4022            Ok(Self::Editable(path.into_boxed_path()))
4023        } else if directory_dist.r#virtual.unwrap_or(false) {
4024            Ok(Self::Virtual(path.into_boxed_path()))
4025        } else {
4026            Ok(Self::Directory(path.into_boxed_path()))
4027        }
4028    }
4029
4030    fn from_index_url(index_url: &IndexUrl, root: &Path) -> Result<Self, LockError> {
4031        match index_url {
4032            IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4033                // Remove any sensitive credentials from the index URL.
4034                let redacted = index_url.without_credentials();
4035                let source = RegistrySource::Url(UrlString::from(redacted.as_ref()));
4036                Ok(Self::Registry(source))
4037            }
4038            IndexUrl::Path(url) => {
4039                let path = url
4040                    .to_file_path()
4041                    .map_err(|()| LockErrorKind::UrlToPath { url: url.to_url() })?;
4042                let path = try_relative_to_if(&path, root, !url.was_given_absolute())
4043                    .map_err(LockErrorKind::IndexRelativePath)?;
4044                let source = RegistrySource::Path(path.into_boxed_path());
4045                Ok(Self::Registry(source))
4046            }
4047        }
4048    }
4049
4050    fn from_git_dist(git_dist: &GitSourceDist) -> Self {
4051        Self::Git(
4052            UrlString::from(locked_git_url(git_dist)),
4053            GitSource {
4054                kind: GitSourceKind::from(git_dist.git.reference().clone()),
4055                precise: git_dist.git.precise().unwrap_or_else(|| {
4056                    panic!("Git distribution is missing a precise hash: {git_dist}")
4057                }),
4058                subdirectory: git_dist.subdirectory.clone(),
4059                lfs: git_dist.git.lfs(),
4060            },
4061        )
4062    }
4063
4064    /// Returns `true` if the source should be considered immutable.
4065    ///
4066    /// We assume that registry sources are immutable. In other words, we expect that once a
4067    /// package-version is published to a registry, its metadata will not change.
4068    ///
4069    /// We also assume that Git sources are immutable, since a Git source encodes a specific commit.
4070    fn is_immutable(&self) -> bool {
4071        matches!(self, Self::Registry(..) | Self::Git(_, _))
4072    }
4073
4074    /// Returns `true` if the source is that of a wheel.
4075    fn is_wheel(&self) -> bool {
4076        match self {
4077            Self::Path(path) => {
4078                matches!(
4079                    DistExtension::from_path(path).ok(),
4080                    Some(DistExtension::Wheel)
4081                )
4082            }
4083            Self::Direct(url, _) => {
4084                matches!(
4085                    DistExtension::from_path(url.as_ref()).ok(),
4086                    Some(DistExtension::Wheel)
4087                )
4088            }
4089            Self::Directory(..) => false,
4090            Self::Editable(..) => false,
4091            Self::Virtual(..) => false,
4092            Self::Git(..) => false,
4093            Self::Registry(..) => false,
4094        }
4095    }
4096
4097    /// Returns `true` if the source is that of a source tree.
4098    fn is_source_tree(&self) -> bool {
4099        match self {
4100            Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => true,
4101            Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => false,
4102        }
4103    }
4104
4105    /// Returns the path to the source tree, if the source is a source tree.
4106    fn as_source_tree(&self) -> Option<&Path> {
4107        match self {
4108            Self::Directory(path) | Self::Editable(path) | Self::Virtual(path) => Some(path),
4109            Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => None,
4110        }
4111    }
4112
4113    fn to_toml(&self, table: &mut Table) {
4114        let mut source_table = InlineTable::new();
4115        match self {
4116            Self::Registry(source) => match source {
4117                RegistrySource::Url(url) => {
4118                    source_table.insert("registry", Value::from(url.as_ref()));
4119                }
4120                RegistrySource::Path(path) => {
4121                    source_table.insert(
4122                        "registry",
4123                        Value::from(PortablePath::from(path).to_string()),
4124                    );
4125                }
4126            },
4127            Self::Git(url, _) => {
4128                source_table.insert("git", Value::from(url.as_ref()));
4129            }
4130            Self::Direct(url, DirectSource { subdirectory }) => {
4131                source_table.insert("url", Value::from(url.as_ref()));
4132                if let Some(ref subdirectory) = *subdirectory {
4133                    source_table.insert(
4134                        "subdirectory",
4135                        Value::from(PortablePath::from(subdirectory).to_string()),
4136                    );
4137                }
4138            }
4139            Self::Path(path) => {
4140                source_table.insert("path", Value::from(PortablePath::from(path).to_string()));
4141            }
4142            Self::Directory(path) => {
4143                source_table.insert(
4144                    "directory",
4145                    Value::from(PortablePath::from(path).to_string()),
4146                );
4147            }
4148            Self::Editable(path) => {
4149                source_table.insert(
4150                    "editable",
4151                    Value::from(PortablePath::from(path).to_string()),
4152                );
4153            }
4154            Self::Virtual(path) => {
4155                source_table.insert("virtual", Value::from(PortablePath::from(path).to_string()));
4156            }
4157        }
4158        table.insert("source", value(source_table));
4159    }
4160
4161    /// Check if a package is local by examining its source.
4162    pub(crate) fn is_local(&self) -> bool {
4163        matches!(
4164            self,
4165            Self::Path(_) | Self::Directory(_) | Self::Editable(_) | Self::Virtual(_)
4166        )
4167    }
4168}
4169
4170impl Display for Source {
4171    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
4172        match self {
4173            Self::Registry(RegistrySource::Url(url)) | Self::Git(url, _) | Self::Direct(url, _) => {
4174                write!(f, "{}+{}", self.name(), url)
4175            }
4176            Self::Registry(RegistrySource::Path(path))
4177            | Self::Path(path)
4178            | Self::Directory(path)
4179            | Self::Editable(path)
4180            | Self::Virtual(path) => {
4181                write!(f, "{}+{}", self.name(), PortablePath::from(path))
4182            }
4183        }
4184    }
4185}
4186
4187impl Source {
4188    fn name(&self) -> &str {
4189        match self {
4190            Self::Registry(..) => "registry",
4191            Self::Git(..) => "git",
4192            Self::Direct(..) => "direct",
4193            Self::Path(..) => "path",
4194            Self::Directory(..) => "directory",
4195            Self::Editable(..) => "editable",
4196            Self::Virtual(..) => "virtual",
4197        }
4198    }
4199
4200    /// Returns `Some(true)` to indicate that the source kind _must_ include a
4201    /// hash.
4202    ///
4203    /// Returns `Some(false)` to indicate that the source kind _must not_
4204    /// include a hash.
4205    ///
4206    /// Returns `None` to indicate that the source kind _may_ include a hash.
4207    fn requires_hash(&self) -> Option<bool> {
4208        match self {
4209            Self::Registry(..) => None,
4210            Self::Direct(..) | Self::Path(..) => Some(true),
4211            Self::Git(..) | Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => {
4212                Some(false)
4213            }
4214        }
4215    }
4216}
4217
4218#[derive(Clone, Debug, serde::Deserialize)]
4219#[serde(untagged, rename_all = "kebab-case")]
4220enum SourceWire {
4221    Registry {
4222        registry: RegistrySourceWire,
4223    },
4224    Git {
4225        git: String,
4226    },
4227    Direct {
4228        url: UrlString,
4229        subdirectory: Option<PortablePathBuf>,
4230    },
4231    Path {
4232        path: PortablePathBuf,
4233    },
4234    Directory {
4235        directory: PortablePathBuf,
4236    },
4237    Editable {
4238        editable: PortablePathBuf,
4239    },
4240    Virtual {
4241        r#virtual: PortablePathBuf,
4242    },
4243}
4244
4245impl TryFrom<SourceWire> for Source {
4246    type Error = LockError;
4247
4248    fn try_from(wire: SourceWire) -> Result<Self, LockError> {
4249        use self::SourceWire::{Direct, Directory, Editable, Git, Path, Registry, Virtual};
4250
4251        match wire {
4252            Registry { registry } => Ok(Self::Registry(registry.into())),
4253            Git { git } => {
4254                let url = DisplaySafeUrl::parse(&git)
4255                    .map_err(|err| SourceParseError::InvalidUrl {
4256                        given: git.clone(),
4257                        err,
4258                    })
4259                    .map_err(LockErrorKind::InvalidGitSourceUrl)?;
4260
4261                let git_source = GitSource::from_url(&url)
4262                    .map_err(|err| match err {
4263                        GitSourceError::InvalidSha => SourceParseError::InvalidSha { given: git },
4264                        GitSourceError::MissingSha => SourceParseError::MissingSha { given: git },
4265                    })
4266                    .map_err(LockErrorKind::InvalidGitSourceUrl)?;
4267
4268                Ok(Self::Git(UrlString::from(url), git_source))
4269            }
4270            Direct { url, subdirectory } => Ok(Self::Direct(
4271                url,
4272                DirectSource {
4273                    subdirectory: subdirectory.map(Box::<std::path::Path>::from),
4274                },
4275            )),
4276            Path { path } => Ok(Self::Path(path.into())),
4277            Directory { directory } => Ok(Self::Directory(directory.into())),
4278            Editable { editable } => Ok(Self::Editable(editable.into())),
4279            Virtual { r#virtual } => Ok(Self::Virtual(r#virtual.into())),
4280        }
4281    }
4282}
4283
4284/// The source for a registry, which could be a URL or a relative path.
4285#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
4286enum RegistrySource {
4287    /// Ex) `https://pypi.org/simple`
4288    Url(UrlString),
4289    /// Ex) `../path/to/local/index`
4290    Path(Box<Path>),
4291}
4292
4293impl Display for RegistrySource {
4294    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
4295        match self {
4296            Self::Url(url) => write!(f, "{url}"),
4297            Self::Path(path) => write!(f, "{}", path.display()),
4298        }
4299    }
4300}
4301
4302#[derive(Clone, Debug)]
4303enum RegistrySourceWire {
4304    /// Ex) `https://pypi.org/simple`
4305    Url(UrlString),
4306    /// Ex) `../path/to/local/index`
4307    Path(PortablePathBuf),
4308}
4309
4310impl<'de> serde::de::Deserialize<'de> for RegistrySourceWire {
4311    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4312    where
4313        D: serde::de::Deserializer<'de>,
4314    {
4315        struct Visitor;
4316
4317        impl serde::de::Visitor<'_> for Visitor {
4318            type Value = RegistrySourceWire;
4319
4320            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
4321                formatter.write_str("a valid URL or a file path")
4322            }
4323
4324            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
4325            where
4326                E: serde::de::Error,
4327            {
4328                if split_scheme(value).is_some_and(|(scheme, _)| Scheme::parse(scheme).is_some()) {
4329                    Ok(
4330                        serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
4331                            value,
4332                        ))
4333                        .map(RegistrySourceWire::Url)?,
4334                    )
4335                } else {
4336                    Ok(
4337                        serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
4338                            value,
4339                        ))
4340                        .map(RegistrySourceWire::Path)?,
4341                    )
4342                }
4343            }
4344        }
4345
4346        deserializer.deserialize_str(Visitor)
4347    }
4348}
4349
4350impl From<RegistrySourceWire> for RegistrySource {
4351    fn from(wire: RegistrySourceWire) -> Self {
4352        match wire {
4353            RegistrySourceWire::Url(url) => Self::Url(url),
4354            RegistrySourceWire::Path(path) => Self::Path(path.into()),
4355        }
4356    }
4357}
4358
4359#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4360#[serde(rename_all = "kebab-case")]
4361struct DirectSource {
4362    subdirectory: Option<Box<Path>>,
4363}
4364
4365/// NOTE: Care should be taken when adding variants to this enum. Namely, new
4366/// variants should be added without changing the relative ordering of other
4367/// variants. Otherwise, this could cause the lockfile to have a different
4368/// canonical ordering of package entries.
4369#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
4370struct GitSource {
4371    precise: GitOid,
4372    subdirectory: Option<Box<Path>>,
4373    kind: GitSourceKind,
4374    lfs: GitLfs,
4375}
4376
4377/// An error that occurs when a source string could not be parsed.
4378#[derive(Clone, Debug, Eq, PartialEq)]
4379enum GitSourceError {
4380    InvalidSha,
4381    MissingSha,
4382}
4383
4384impl GitSource {
4385    /// Extracts a Git source reference from the query pairs and the hash
4386    /// fragment in the given URL.
4387    fn from_url(url: &Url) -> Result<Self, GitSourceError> {
4388        let mut kind = GitSourceKind::DefaultBranch;
4389        let mut subdirectory = None;
4390        let mut lfs = GitLfs::Disabled;
4391        for (key, val) in url.query_pairs() {
4392            match &*key {
4393                "tag" => kind = GitSourceKind::Tag(val.into_owned()),
4394                "branch" => kind = GitSourceKind::Branch(val.into_owned()),
4395                "rev" => kind = GitSourceKind::Rev(val.into_owned()),
4396                "subdirectory" => subdirectory = Some(PortablePathBuf::from(val.as_ref()).into()),
4397                "lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
4398                _ => {}
4399            }
4400        }
4401
4402        let precise = GitOid::from_str(url.fragment().ok_or(GitSourceError::MissingSha)?)
4403            .map_err(|_| GitSourceError::InvalidSha)?;
4404
4405        Ok(Self {
4406            precise,
4407            subdirectory,
4408            kind,
4409            lfs,
4410        })
4411    }
4412}
4413
4414#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4415#[serde(rename_all = "kebab-case")]
4416enum GitSourceKind {
4417    Tag(String),
4418    Branch(String),
4419    Rev(String),
4420    DefaultBranch,
4421}
4422
4423/// Inspired by: <https://discuss.python.org/t/lock-files-again-but-this-time-w-sdists/46593>
4424#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4425#[serde(rename_all = "kebab-case")]
4426struct SourceDistMetadata {
4427    /// A hash of the source distribution.
4428    hash: Option<Hash>,
4429    /// The size of the source distribution in bytes.
4430    ///
4431    /// This is only present for source distributions that come from registries.
4432    size: Option<u64>,
4433    /// The upload time of the source distribution.
4434    #[serde(alias = "upload_time")]
4435    upload_time: Option<Timestamp>,
4436}
4437
4438/// A URL or file path where the source dist that was
4439/// locked against was found. The location does not need to exist in the
4440/// future, so this should be treated as only a hint to where to look
4441/// and/or recording where the source dist file originally came from.
4442#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4443#[serde(from = "SourceDistWire")]
4444enum SourceDist {
4445    Url {
4446        url: UrlString,
4447        #[serde(flatten)]
4448        metadata: SourceDistMetadata,
4449    },
4450    Path {
4451        path: Box<Path>,
4452        #[serde(flatten)]
4453        metadata: SourceDistMetadata,
4454    },
4455    Metadata {
4456        #[serde(flatten)]
4457        metadata: SourceDistMetadata,
4458    },
4459}
4460
4461impl SourceDist {
4462    fn filename(&self) -> Option<Cow<'_, str>> {
4463        match self {
4464            Self::Metadata { .. } => None,
4465            Self::Url { url, .. } => url.filename().ok(),
4466            Self::Path { path, .. } => path.file_name().map(|filename| filename.to_string_lossy()),
4467        }
4468    }
4469
4470    fn url(&self) -> Option<&UrlString> {
4471        match self {
4472            Self::Metadata { .. } => None,
4473            Self::Url { url, .. } => Some(url),
4474            Self::Path { .. } => None,
4475        }
4476    }
4477
4478    pub(crate) fn hash(&self) -> Option<&Hash> {
4479        match self {
4480            Self::Metadata { metadata } => metadata.hash.as_ref(),
4481            Self::Url { metadata, .. } => metadata.hash.as_ref(),
4482            Self::Path { metadata, .. } => metadata.hash.as_ref(),
4483        }
4484    }
4485
4486    pub(crate) fn size(&self) -> Option<u64> {
4487        match self {
4488            Self::Metadata { metadata } => metadata.size,
4489            Self::Url { metadata, .. } => metadata.size,
4490            Self::Path { metadata, .. } => metadata.size,
4491        }
4492    }
4493
4494    pub(crate) fn upload_time(&self) -> Option<Timestamp> {
4495        match self {
4496            Self::Metadata { metadata } => metadata.upload_time,
4497            Self::Url { metadata, .. } => metadata.upload_time,
4498            Self::Path { metadata, .. } => metadata.upload_time,
4499        }
4500    }
4501}
4502
4503impl SourceDist {
4504    fn from_annotated_dist(
4505        id: &PackageId,
4506        annotated_dist: &AnnotatedDist,
4507    ) -> Result<Option<Self>, LockError> {
4508        match annotated_dist.dist {
4509            // We pass empty installed packages for locking.
4510            ResolvedDist::Installed { .. } => unreachable!(),
4511            ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4512                id,
4513                dist,
4514                annotated_dist.hashes.as_slice(),
4515                annotated_dist.index(),
4516            ),
4517        }
4518    }
4519
4520    fn from_dist(
4521        id: &PackageId,
4522        dist: &Dist,
4523        hashes: &[HashDigest],
4524        index: Option<&IndexUrl>,
4525    ) -> Result<Option<Self>, LockError> {
4526        match *dist {
4527            Dist::Built(BuiltDist::Registry(ref built_dist)) => {
4528                let Some(sdist) = built_dist.sdist.as_ref() else {
4529                    return Ok(None);
4530                };
4531                Self::from_registry_dist(sdist, index)
4532            }
4533            Dist::Built(_) => Ok(None),
4534            Dist::Source(ref source_dist) => Self::from_source_dist(id, source_dist, hashes, index),
4535        }
4536    }
4537
4538    fn from_source_dist(
4539        id: &PackageId,
4540        source_dist: &uv_distribution_types::SourceDist,
4541        hashes: &[HashDigest],
4542        index: Option<&IndexUrl>,
4543    ) -> Result<Option<Self>, LockError> {
4544        match *source_dist {
4545            uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
4546                Self::from_registry_dist(reg_dist, index)
4547            }
4548            uv_distribution_types::SourceDist::DirectUrl(_) => {
4549                Self::from_direct_dist(id, hashes).map(Some)
4550            }
4551            uv_distribution_types::SourceDist::Path(_) => {
4552                Self::from_path_dist(id, hashes).map(Some)
4553            }
4554            // An actual sdist entry in the lockfile is only required when
4555            // it's from a registry or a direct URL. Otherwise, it's strictly
4556            // redundant with the information in all other kinds of `source`.
4557            uv_distribution_types::SourceDist::Git(_)
4558            | uv_distribution_types::SourceDist::Directory(_) => Ok(None),
4559        }
4560    }
4561
4562    fn from_registry_dist(
4563        reg_dist: &RegistrySourceDist,
4564        index: Option<&IndexUrl>,
4565    ) -> Result<Option<Self>, LockError> {
4566        // Reject distributions from registries that don't match the index URL, as can occur with
4567        // `--find-links`.
4568        if index.is_none_or(|index| *index != reg_dist.index) {
4569            return Ok(None);
4570        }
4571
4572        match &reg_dist.index {
4573            IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4574                let url = normalize_file_location(&reg_dist.file.url)
4575                    .map_err(LockErrorKind::InvalidUrl)
4576                    .map_err(LockError::from)?;
4577                let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4578                let size = reg_dist.file.size;
4579                let upload_time = reg_dist
4580                    .file
4581                    .upload_time_utc_ms
4582                    .map(Timestamp::from_millisecond)
4583                    .transpose()
4584                    .map_err(LockErrorKind::InvalidTimestamp)?;
4585                Ok(Some(Self::Url {
4586                    url,
4587                    metadata: SourceDistMetadata {
4588                        hash,
4589                        size,
4590                        upload_time,
4591                    },
4592                }))
4593            }
4594            IndexUrl::Path(path) => {
4595                let index_path = path
4596                    .to_file_path()
4597                    .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4598                let url = reg_dist
4599                    .file
4600                    .url
4601                    .to_url()
4602                    .map_err(LockErrorKind::InvalidUrl)?;
4603
4604                if url.scheme() == "file" {
4605                    let reg_dist_path = url
4606                        .to_file_path()
4607                        .map_err(|()| LockErrorKind::UrlToPath { url })?;
4608                    let path =
4609                        try_relative_to_if(&reg_dist_path, index_path, !path.was_given_absolute())
4610                            .map_err(LockErrorKind::DistributionRelativePath)?
4611                            .into_boxed_path();
4612                    let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4613                    let size = reg_dist.file.size;
4614                    let upload_time = reg_dist
4615                        .file
4616                        .upload_time_utc_ms
4617                        .map(Timestamp::from_millisecond)
4618                        .transpose()
4619                        .map_err(LockErrorKind::InvalidTimestamp)?;
4620                    Ok(Some(Self::Path {
4621                        path,
4622                        metadata: SourceDistMetadata {
4623                            hash,
4624                            size,
4625                            upload_time,
4626                        },
4627                    }))
4628                } else {
4629                    let url = normalize_file_location(&reg_dist.file.url)
4630                        .map_err(LockErrorKind::InvalidUrl)
4631                        .map_err(LockError::from)?;
4632                    let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4633                    let size = reg_dist.file.size;
4634                    let upload_time = reg_dist
4635                        .file
4636                        .upload_time_utc_ms
4637                        .map(Timestamp::from_millisecond)
4638                        .transpose()
4639                        .map_err(LockErrorKind::InvalidTimestamp)?;
4640                    Ok(Some(Self::Url {
4641                        url,
4642                        metadata: SourceDistMetadata {
4643                            hash,
4644                            size,
4645                            upload_time,
4646                        },
4647                    }))
4648                }
4649            }
4650        }
4651    }
4652
4653    fn from_direct_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4654        let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4655            let kind = LockErrorKind::Hash {
4656                id: id.clone(),
4657                artifact_type: "direct URL source distribution",
4658                expected: true,
4659            };
4660            return Err(kind.into());
4661        };
4662        Ok(Self::Metadata {
4663            metadata: SourceDistMetadata {
4664                hash: Some(hash),
4665                size: None,
4666                upload_time: None,
4667            },
4668        })
4669    }
4670
4671    fn from_path_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4672        let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4673            let kind = LockErrorKind::Hash {
4674                id: id.clone(),
4675                artifact_type: "path source distribution",
4676                expected: true,
4677            };
4678            return Err(kind.into());
4679        };
4680        Ok(Self::Metadata {
4681            metadata: SourceDistMetadata {
4682                hash: Some(hash),
4683                size: None,
4684                upload_time: None,
4685            },
4686        })
4687    }
4688}
4689
4690#[derive(Clone, Debug, serde::Deserialize)]
4691#[serde(untagged, rename_all = "kebab-case")]
4692enum SourceDistWire {
4693    Url {
4694        url: UrlString,
4695        #[serde(flatten)]
4696        metadata: SourceDistMetadata,
4697    },
4698    Path {
4699        path: PortablePathBuf,
4700        #[serde(flatten)]
4701        metadata: SourceDistMetadata,
4702    },
4703    Metadata {
4704        #[serde(flatten)]
4705        metadata: SourceDistMetadata,
4706    },
4707}
4708
4709impl SourceDist {
4710    /// Returns the TOML representation of this source distribution.
4711    fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
4712        let mut table = InlineTable::new();
4713        match self {
4714            Self::Metadata { .. } => {}
4715            Self::Url { url, .. } => {
4716                table.insert("url", Value::from(url.as_ref()));
4717            }
4718            Self::Path { path, .. } => {
4719                table.insert("path", Value::from(PortablePath::from(path).to_string()));
4720            }
4721        }
4722        if let Some(hash) = self.hash() {
4723            table.insert("hash", Value::from(hash.to_string()));
4724        }
4725        if let Some(size) = self.size() {
4726            table.insert(
4727                "size",
4728                toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4729            );
4730        }
4731        if let Some(upload_time) = self.upload_time() {
4732            table.insert("upload-time", Value::from(upload_time.to_string()));
4733        }
4734        Ok(table)
4735    }
4736}
4737
4738impl From<SourceDistWire> for SourceDist {
4739    fn from(wire: SourceDistWire) -> Self {
4740        match wire {
4741            SourceDistWire::Url { url, metadata } => Self::Url { url, metadata },
4742            SourceDistWire::Path { path, metadata } => Self::Path {
4743                path: path.into(),
4744                metadata,
4745            },
4746            SourceDistWire::Metadata { metadata } => Self::Metadata { metadata },
4747        }
4748    }
4749}
4750
4751impl From<GitReference> for GitSourceKind {
4752    fn from(value: GitReference) -> Self {
4753        match value {
4754            GitReference::Branch(branch) => Self::Branch(branch),
4755            GitReference::Tag(tag) => Self::Tag(tag),
4756            GitReference::BranchOrTag(rev) => Self::Rev(rev),
4757            GitReference::BranchOrTagOrCommit(rev) => Self::Rev(rev),
4758            GitReference::NamedRef(rev) => Self::Rev(rev),
4759            GitReference::DefaultBranch => Self::DefaultBranch,
4760        }
4761    }
4762}
4763
4764impl From<GitSourceKind> for GitReference {
4765    fn from(value: GitSourceKind) -> Self {
4766        match value {
4767            GitSourceKind::Branch(branch) => Self::Branch(branch),
4768            GitSourceKind::Tag(tag) => Self::Tag(tag),
4769            GitSourceKind::Rev(rev) => Self::from_rev(rev),
4770            GitSourceKind::DefaultBranch => Self::DefaultBranch,
4771        }
4772    }
4773}
4774
4775/// Construct the lockfile-compatible [`DisplaySafeUrl`] for a [`GitSourceDist`].
4776fn locked_git_url(git_dist: &GitSourceDist) -> DisplaySafeUrl {
4777    let mut url = git_dist.git.url().clone();
4778
4779    // Remove the credentials.
4780    url.remove_credentials();
4781
4782    // Clear out any existing state.
4783    url.set_fragment(None);
4784    url.set_query(None);
4785
4786    // Put the subdirectory in the query.
4787    if let Some(subdirectory) = git_dist
4788        .subdirectory
4789        .as_deref()
4790        .map(PortablePath::from)
4791        .as_ref()
4792        .map(PortablePath::to_string)
4793    {
4794        url.query_pairs_mut()
4795            .append_pair("subdirectory", &subdirectory);
4796    }
4797
4798    // Put lfs=true in the package source git url only when explicitly enabled.
4799    if git_dist.git.lfs().enabled() {
4800        url.query_pairs_mut().append_pair("lfs", "true");
4801    }
4802
4803    // Put the requested reference in the query.
4804    match git_dist.git.reference() {
4805        GitReference::Branch(branch) => {
4806            url.query_pairs_mut().append_pair("branch", branch.as_str());
4807        }
4808        GitReference::Tag(tag) => {
4809            url.query_pairs_mut().append_pair("tag", tag.as_str());
4810        }
4811        GitReference::BranchOrTag(rev)
4812        | GitReference::BranchOrTagOrCommit(rev)
4813        | GitReference::NamedRef(rev) => {
4814            url.query_pairs_mut().append_pair("rev", rev.as_str());
4815        }
4816        GitReference::DefaultBranch => {}
4817    }
4818
4819    // Put the precise commit in the fragment.
4820    url.set_fragment(
4821        git_dist
4822            .git
4823            .precise()
4824            .as_ref()
4825            .map(GitOid::to_string)
4826            .as_deref(),
4827    );
4828
4829    url
4830}
4831
4832#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4833struct ZstdWheel {
4834    hash: Option<Hash>,
4835    size: Option<u64>,
4836}
4837
4838/// Inspired by: <https://discuss.python.org/t/lock-files-again-but-this-time-w-sdists/46593>
4839#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4840#[serde(try_from = "WheelWire")]
4841struct Wheel {
4842    /// A URL or file path (via `file://`) where the wheel that was locked
4843    /// against was found. The location does not need to exist in the future,
4844    /// so this should be treated as only a hint to where to look and/or
4845    /// recording where the wheel file originally came from.
4846    url: WheelWireSource,
4847    /// A hash of the built distribution.
4848    ///
4849    /// This is only present for wheels that come from registries and direct
4850    /// URLs. Wheels from git or path dependencies do not have hashes
4851    /// associated with them.
4852    hash: Option<Hash>,
4853    /// The size of the built distribution in bytes.
4854    ///
4855    /// This is only present for wheels that come from registries.
4856    size: Option<u64>,
4857    /// The upload time of the built distribution.
4858    ///
4859    /// This is only present for wheels that come from registries.
4860    upload_time: Option<Timestamp>,
4861    /// The filename of the wheel.
4862    ///
4863    /// This isn't part of the wire format since it's redundant with the
4864    /// URL. But we do use it for various things, and thus compute it at
4865    /// deserialization time. Not being able to extract a wheel filename from a
4866    /// wheel URL is thus a deserialization error.
4867    filename: WheelFilename,
4868    /// The zstandard-compressed wheel metadata, if any.
4869    zstd: Option<ZstdWheel>,
4870}
4871
4872impl Wheel {
4873    fn from_annotated_dist(annotated_dist: &AnnotatedDist) -> Result<Vec<Self>, LockError> {
4874        match annotated_dist.dist {
4875            // We pass empty installed packages for locking.
4876            ResolvedDist::Installed { .. } => unreachable!(),
4877            ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4878                dist,
4879                annotated_dist.hashes.as_slice(),
4880                annotated_dist.index(),
4881            ),
4882        }
4883    }
4884
4885    fn from_dist(
4886        dist: &Dist,
4887        hashes: &[HashDigest],
4888        index: Option<&IndexUrl>,
4889    ) -> Result<Vec<Self>, LockError> {
4890        match *dist {
4891            Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, hashes, index),
4892            Dist::Source(uv_distribution_types::SourceDist::Registry(ref source_dist)) => {
4893                source_dist
4894                    .wheels
4895                    .iter()
4896                    .filter(|wheel| {
4897                        // Reject distributions from registries that don't match the index URL, as can occur with
4898                        // `--find-links`.
4899                        index.is_some_and(|index| *index == wheel.index)
4900                    })
4901                    .map(Self::from_registry_wheel)
4902                    .collect()
4903            }
4904            Dist::Source(_) => Ok(vec![]),
4905        }
4906    }
4907
4908    fn from_built_dist(
4909        built_dist: &BuiltDist,
4910        hashes: &[HashDigest],
4911        index: Option<&IndexUrl>,
4912    ) -> Result<Vec<Self>, LockError> {
4913        match *built_dist {
4914            BuiltDist::Registry(ref reg_dist) => Self::from_registry_dist(reg_dist, index),
4915            BuiltDist::DirectUrl(ref direct_dist) => {
4916                Ok(vec![Self::from_direct_dist(direct_dist, hashes)])
4917            }
4918            BuiltDist::Path(ref path_dist) => Ok(vec![Self::from_path_dist(path_dist, hashes)]),
4919        }
4920    }
4921
4922    fn from_registry_dist(
4923        reg_dist: &RegistryBuiltDist,
4924        index: Option<&IndexUrl>,
4925    ) -> Result<Vec<Self>, LockError> {
4926        reg_dist
4927            .wheels
4928            .iter()
4929            .filter(|wheel| {
4930                // Reject distributions from registries that don't match the index URL, as can occur with
4931                // `--find-links`.
4932                index.is_some_and(|index| *index == wheel.index)
4933            })
4934            .map(Self::from_registry_wheel)
4935            .collect()
4936    }
4937
4938    fn from_registry_wheel(wheel: &RegistryBuiltWheel) -> Result<Self, LockError> {
4939        let url = match &wheel.index {
4940            IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4941                let url = normalize_file_location(&wheel.file.url)
4942                    .map_err(LockErrorKind::InvalidUrl)
4943                    .map_err(LockError::from)?;
4944                WheelWireSource::Url { url }
4945            }
4946            IndexUrl::Path(path) => {
4947                let index_path = path
4948                    .to_file_path()
4949                    .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4950                let wheel_url = wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?;
4951
4952                if wheel_url.scheme() == "file" {
4953                    let wheel_path = wheel_url
4954                        .to_file_path()
4955                        .map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?;
4956                    let path =
4957                        try_relative_to_if(&wheel_path, index_path, !path.was_given_absolute())
4958                            .map_err(LockErrorKind::DistributionRelativePath)?
4959                            .into_boxed_path();
4960                    WheelWireSource::Path { path }
4961                } else {
4962                    let url = normalize_file_location(&wheel.file.url)
4963                        .map_err(LockErrorKind::InvalidUrl)
4964                        .map_err(LockError::from)?;
4965                    WheelWireSource::Url { url }
4966                }
4967            }
4968        };
4969        let filename = wheel.filename.clone();
4970        let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from);
4971        let size = wheel.file.size;
4972        let upload_time = wheel
4973            .file
4974            .upload_time_utc_ms
4975            .map(Timestamp::from_millisecond)
4976            .transpose()
4977            .map_err(LockErrorKind::InvalidTimestamp)?;
4978        let zstd = wheel.file.zstd.as_ref().map(|zstd| ZstdWheel {
4979            hash: zstd.hashes.iter().max().cloned().map(Hash::from),
4980            size: zstd.size,
4981        });
4982        Ok(Self {
4983            url,
4984            hash,
4985            size,
4986            upload_time,
4987            filename,
4988            zstd,
4989        })
4990    }
4991
4992    fn from_direct_dist(direct_dist: &DirectUrlBuiltDist, hashes: &[HashDigest]) -> Self {
4993        Self {
4994            url: WheelWireSource::Url {
4995                url: normalize_url(direct_dist.url.to_url()),
4996            },
4997            hash: hashes.iter().max().cloned().map(Hash::from),
4998            size: None,
4999            upload_time: None,
5000            filename: direct_dist.filename.clone(),
5001            zstd: None,
5002        }
5003    }
5004
5005    fn from_path_dist(path_dist: &PathBuiltDist, hashes: &[HashDigest]) -> Self {
5006        Self {
5007            url: WheelWireSource::Filename {
5008                filename: path_dist.filename.clone(),
5009            },
5010            hash: hashes.iter().max().cloned().map(Hash::from),
5011            size: None,
5012            upload_time: None,
5013            filename: path_dist.filename.clone(),
5014            zstd: None,
5015        }
5016    }
5017
5018    pub(crate) fn to_registry_wheel(
5019        &self,
5020        source: &RegistrySource,
5021        root: &Path,
5022    ) -> Result<RegistryBuiltWheel, LockError> {
5023        let filename: WheelFilename = self.filename.clone();
5024
5025        match source {
5026            RegistrySource::Url(url) => {
5027                let file_location = match &self.url {
5028                    WheelWireSource::Url { url: file_url } => {
5029                        FileLocation::AbsoluteUrl(file_url.clone())
5030                    }
5031                    WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => {
5032                        return Err(LockErrorKind::MissingUrl {
5033                            name: filename.name,
5034                            version: filename.version,
5035                        }
5036                        .into());
5037                    }
5038                };
5039                let file = Box::new(uv_distribution_types::File {
5040                    dist_info_metadata: false,
5041                    filename: SmallString::from(filename.to_string()),
5042                    hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
5043                    requires_python: None,
5044                    size: self.size,
5045                    upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
5046                    url: file_location,
5047                    yanked: None,
5048                    zstd: self
5049                        .zstd
5050                        .as_ref()
5051                        .map(|zstd| uv_distribution_types::Zstd {
5052                            hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
5053                            size: zstd.size,
5054                        })
5055                        .map(Box::new),
5056                });
5057                let index = IndexUrl::from(VerbatimUrl::from_url(
5058                    url.to_url().map_err(LockErrorKind::InvalidUrl)?,
5059                ));
5060                Ok(RegistryBuiltWheel {
5061                    filename,
5062                    file,
5063                    index,
5064                })
5065            }
5066            RegistrySource::Path(index_path) => {
5067                let file_location = match &self.url {
5068                    WheelWireSource::Url { url: file_url } => {
5069                        FileLocation::AbsoluteUrl(file_url.clone())
5070                    }
5071                    WheelWireSource::Path { path: file_path } => {
5072                        let file_path = root.join(index_path).join(file_path);
5073                        let file_url =
5074                            DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
5075                                LockErrorKind::PathToUrl {
5076                                    path: file_path.into_boxed_path(),
5077                                }
5078                            })?;
5079                        FileLocation::AbsoluteUrl(UrlString::from(file_url))
5080                    }
5081                    WheelWireSource::Filename { .. } => {
5082                        return Err(LockErrorKind::MissingPath {
5083                            name: filename.name,
5084                            version: filename.version,
5085                        }
5086                        .into());
5087                    }
5088                };
5089                let file = Box::new(uv_distribution_types::File {
5090                    dist_info_metadata: false,
5091                    filename: SmallString::from(filename.to_string()),
5092                    hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
5093                    requires_python: None,
5094                    size: self.size,
5095                    upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
5096                    url: file_location,
5097                    yanked: None,
5098                    zstd: self
5099                        .zstd
5100                        .as_ref()
5101                        .map(|zstd| uv_distribution_types::Zstd {
5102                            hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
5103                            size: zstd.size,
5104                        })
5105                        .map(Box::new),
5106                });
5107                let index = IndexUrl::from(
5108                    VerbatimUrl::from_absolute_path(root.join(index_path))
5109                        .map_err(LockErrorKind::RegistryVerbatimUrl)?,
5110                );
5111                Ok(RegistryBuiltWheel {
5112                    filename,
5113                    file,
5114                    index,
5115                })
5116            }
5117        }
5118    }
5119}
5120
5121#[derive(Clone, Debug, serde::Deserialize)]
5122#[serde(rename_all = "kebab-case")]
5123struct WheelWire {
5124    #[serde(flatten)]
5125    url: WheelWireSource,
5126    /// A hash of the built distribution.
5127    ///
5128    /// This is only present for wheels that come from registries and direct
5129    /// URLs. Wheels from git or path dependencies do not have hashes
5130    /// associated with them.
5131    hash: Option<Hash>,
5132    /// The size of the built distribution in bytes.
5133    ///
5134    /// This is only present for wheels that come from registries.
5135    size: Option<u64>,
5136    /// The upload time of the built distribution.
5137    ///
5138    /// This is only present for wheels that come from registries.
5139    #[serde(alias = "upload_time")]
5140    upload_time: Option<Timestamp>,
5141    /// The zstandard-compressed wheel metadata, if any.
5142    #[serde(alias = "zstd")]
5143    zstd: Option<ZstdWheel>,
5144}
5145
5146#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
5147#[serde(untagged, rename_all = "kebab-case")]
5148enum WheelWireSource {
5149    /// Used for all wheels that come from remote sources.
5150    Url {
5151        /// A URL where the wheel that was locked against was found. The location
5152        /// does not need to exist in the future, so this should be treated as
5153        /// only a hint to where to look and/or recording where the wheel file
5154        /// originally came from.
5155        url: UrlString,
5156    },
5157    /// Used for wheels that come from local registries (like `--find-links`).
5158    Path {
5159        /// The path to the wheel, relative to the index.
5160        path: Box<Path>,
5161    },
5162    /// Used for path wheels.
5163    ///
5164    /// We only store the filename for path wheel, since we can't store a relative path in the url
5165    Filename {
5166        /// We duplicate the filename since a lot of code relies on having the filename on the
5167        /// wheel entry.
5168        filename: WheelFilename,
5169    },
5170}
5171
5172impl Wheel {
5173    /// Returns the TOML representation of this wheel.
5174    fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
5175        let mut table = InlineTable::new();
5176        match &self.url {
5177            WheelWireSource::Url { url } => {
5178                table.insert("url", Value::from(url.as_ref()));
5179            }
5180            WheelWireSource::Path { path } => {
5181                table.insert("path", Value::from(PortablePath::from(path).to_string()));
5182            }
5183            WheelWireSource::Filename { filename } => {
5184                table.insert("filename", Value::from(filename.to_string()));
5185            }
5186        }
5187        if let Some(ref hash) = self.hash {
5188            table.insert("hash", Value::from(hash.to_string()));
5189        }
5190        if let Some(size) = self.size {
5191            table.insert(
5192                "size",
5193                toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
5194            );
5195        }
5196        if let Some(upload_time) = self.upload_time {
5197            table.insert("upload-time", Value::from(upload_time.to_string()));
5198        }
5199        if let Some(zstd) = &self.zstd {
5200            let mut inner = InlineTable::new();
5201            if let Some(ref hash) = zstd.hash {
5202                inner.insert("hash", Value::from(hash.to_string()));
5203            }
5204            if let Some(size) = zstd.size {
5205                inner.insert(
5206                    "size",
5207                    toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
5208                );
5209            }
5210            table.insert("zstd", Value::from(inner));
5211        }
5212        Ok(table)
5213    }
5214}
5215
5216impl TryFrom<WheelWire> for Wheel {
5217    type Error = String;
5218
5219    fn try_from(wire: WheelWire) -> Result<Self, String> {
5220        let filename = match &wire.url {
5221            WheelWireSource::Url { url } => {
5222                let filename = url.filename().map_err(|err| err.to_string())?;
5223                filename.parse::<WheelFilename>().map_err(|err| {
5224                    format!("failed to parse `{filename}` as wheel filename: {err}")
5225                })?
5226            }
5227            WheelWireSource::Path { path } => {
5228                let filename = path
5229                    .file_name()
5230                    .and_then(|file_name| file_name.to_str())
5231                    .ok_or_else(|| {
5232                        format!("path `{}` has no filename component", path.display())
5233                    })?;
5234                filename.parse::<WheelFilename>().map_err(|err| {
5235                    format!("failed to parse `{filename}` as wheel filename: {err}")
5236                })?
5237            }
5238            WheelWireSource::Filename { filename } => filename.clone(),
5239        };
5240
5241        Ok(Self {
5242            url: wire.url,
5243            hash: wire.hash,
5244            size: wire.size,
5245            upload_time: wire.upload_time,
5246            zstd: wire.zstd,
5247            filename,
5248        })
5249    }
5250}
5251
5252/// A single dependency of a package in a lockfile.
5253#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
5254pub struct Dependency {
5255    package_id: PackageId,
5256    extra: BTreeSet<ExtraName>,
5257    /// A marker simplified from the PEP 508 marker in `complexified_marker`
5258    /// by assuming `requires-python` is satisfied. So if
5259    /// `requires-python = '>=3.8'`, then
5260    /// `python_version >= '3.8' and python_version < '3.12'`
5261    /// gets simplified to `python_version < '3.12'`.
5262    ///
5263    /// Generally speaking, this marker should not be exposed to
5264    /// anything outside this module unless it's for a specialized use
5265    /// case. But specifically, it should never be used to evaluate
5266    /// against a marker environment or for disjointness checks or any
5267    /// other kind of marker algebra.
5268    ///
5269    /// It exists because there are some cases where we do actually
5270    /// want to compare markers in their "simplified" form. For
5271    /// example, when collapsing the extras on duplicate dependencies.
5272    /// Even if a dependency has different complexified markers,
5273    /// they might have identical markers once simplified. And since
5274    /// `requires-python` applies to the entire lock file, it's
5275    /// acceptable to do comparisons on the simplified form.
5276    simplified_marker: SimplifiedMarkerTree,
5277    /// The "complexified" marker is a universal marker whose PEP 508
5278    /// marker can stand on its own independent of `requires-python`.
5279    /// It can be safely used for any kind of marker algebra.
5280    complexified_marker: UniversalMarker,
5281}
5282
5283impl Dependency {
5284    fn new(
5285        requires_python: &RequiresPython,
5286        package_id: PackageId,
5287        extra: BTreeSet<ExtraName>,
5288        complexified_marker: UniversalMarker,
5289    ) -> Self {
5290        let simplified_marker =
5291            SimplifiedMarkerTree::new(requires_python, complexified_marker.combined());
5292        let complexified_marker = simplified_marker.into_marker(requires_python);
5293        Self {
5294            package_id,
5295            extra,
5296            simplified_marker,
5297            complexified_marker: UniversalMarker::from_combined(complexified_marker),
5298        }
5299    }
5300
5301    fn from_annotated_dist(
5302        requires_python: &RequiresPython,
5303        annotated_dist: &AnnotatedDist,
5304        complexified_marker: UniversalMarker,
5305        root: &Path,
5306    ) -> Result<Self, LockError> {
5307        let package_id = PackageId::from_annotated_dist(annotated_dist, root)?;
5308        let extra = annotated_dist.extra.iter().cloned().collect();
5309        Ok(Self::new(
5310            requires_python,
5311            package_id,
5312            extra,
5313            complexified_marker,
5314        ))
5315    }
5316
5317    /// Returns the TOML representation of this dependency.
5318    fn to_toml(
5319        &self,
5320        _requires_python: &RequiresPython,
5321        dist_count_by_name: &FxHashMap<PackageName, u64>,
5322    ) -> Table {
5323        let mut table = Table::new();
5324        self.package_id
5325            .to_toml(Some(dist_count_by_name), &mut table);
5326        if !self.extra.is_empty() {
5327            let extra_array = self
5328                .extra
5329                .iter()
5330                .map(ToString::to_string)
5331                .collect::<Array>();
5332            table.insert("extra", value(extra_array));
5333        }
5334        if let Some(marker) = self.simplified_marker.try_to_string() {
5335            table.insert("marker", value(marker));
5336        }
5337
5338        table
5339    }
5340
5341    /// Returns the package name of this dependency.
5342    pub fn package_name(&self) -> &PackageName {
5343        &self.package_id.name
5344    }
5345
5346    /// Returns the extras specified on this dependency.
5347    pub fn extra(&self) -> &BTreeSet<ExtraName> {
5348        &self.extra
5349    }
5350}
5351
5352impl Display for Dependency {
5353    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5354        match (self.extra.is_empty(), self.package_id.version.as_ref()) {
5355            (true, Some(version)) => write!(f, "{}=={}", self.package_id.name, version),
5356            (true, None) => write!(f, "{}", self.package_id.name),
5357            (false, Some(version)) => write!(
5358                f,
5359                "{}[{}]=={}",
5360                self.package_id.name,
5361                self.extra.iter().join(","),
5362                version
5363            ),
5364            (false, None) => write!(
5365                f,
5366                "{}[{}]",
5367                self.package_id.name,
5368                self.extra.iter().join(",")
5369            ),
5370        }
5371    }
5372}
5373
5374/// A single dependency of a package in a lockfile.
5375#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)]
5376#[serde(rename_all = "kebab-case")]
5377struct DependencyWire {
5378    #[serde(flatten)]
5379    package_id: PackageIdForDependency,
5380    #[serde(default)]
5381    extra: BTreeSet<ExtraName>,
5382    #[serde(default)]
5383    marker: SimplifiedMarkerTree,
5384}
5385
5386impl DependencyWire {
5387    fn unwire(
5388        self,
5389        requires_python: &RequiresPython,
5390        unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
5391    ) -> Result<Dependency, LockError> {
5392        let complexified_marker = self.marker.into_marker(requires_python);
5393        Ok(Dependency {
5394            package_id: self.package_id.unwire(unambiguous_package_ids)?,
5395            extra: self.extra,
5396            simplified_marker: self.marker,
5397            complexified_marker: UniversalMarker::from_combined(complexified_marker),
5398        })
5399    }
5400}
5401
5402/// A single hash for a distribution artifact in a lockfile.
5403///
5404/// A hash is encoded as a single TOML string in the format
5405/// `{algorithm}:{digest}`.
5406#[derive(Clone, Debug, PartialEq, Eq)]
5407struct Hash(HashDigest);
5408
5409impl From<HashDigest> for Hash {
5410    fn from(hd: HashDigest) -> Self {
5411        Self(hd)
5412    }
5413}
5414
5415impl FromStr for Hash {
5416    type Err = HashParseError;
5417
5418    fn from_str(s: &str) -> Result<Self, HashParseError> {
5419        let (algorithm, digest) = s.split_once(':').ok_or(HashParseError(
5420            "expected '{algorithm}:{digest}', but found no ':' in hash digest",
5421        ))?;
5422        let algorithm = algorithm
5423            .parse()
5424            .map_err(|_| HashParseError("unrecognized hash algorithm"))?;
5425        Ok(Self(HashDigest {
5426            algorithm,
5427            digest: digest.into(),
5428        }))
5429    }
5430}
5431
5432impl Display for Hash {
5433    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5434        write!(f, "{}:{}", self.0.algorithm, self.0.digest)
5435    }
5436}
5437
5438impl<'de> serde::Deserialize<'de> for Hash {
5439    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5440    where
5441        D: serde::de::Deserializer<'de>,
5442    {
5443        struct Visitor;
5444
5445        impl serde::de::Visitor<'_> for Visitor {
5446            type Value = Hash;
5447
5448            fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
5449                f.write_str("a string")
5450            }
5451
5452            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
5453                Hash::from_str(v).map_err(serde::de::Error::custom)
5454            }
5455        }
5456
5457        deserializer.deserialize_str(Visitor)
5458    }
5459}
5460
5461impl From<Hash> for Hashes {
5462    fn from(value: Hash) -> Self {
5463        match value.0.algorithm {
5464            HashAlgorithm::Md5 => Self {
5465                md5: Some(value.0.digest),
5466                sha256: None,
5467                sha384: None,
5468                sha512: None,
5469                blake2b: None,
5470            },
5471            HashAlgorithm::Sha256 => Self {
5472                md5: None,
5473                sha256: Some(value.0.digest),
5474                sha384: None,
5475                sha512: None,
5476                blake2b: None,
5477            },
5478            HashAlgorithm::Sha384 => Self {
5479                md5: None,
5480                sha256: None,
5481                sha384: Some(value.0.digest),
5482                sha512: None,
5483                blake2b: None,
5484            },
5485            HashAlgorithm::Sha512 => Self {
5486                md5: None,
5487                sha256: None,
5488                sha384: None,
5489                sha512: Some(value.0.digest),
5490                blake2b: None,
5491            },
5492            HashAlgorithm::Blake2b => Self {
5493                md5: None,
5494                sha256: None,
5495                sha384: None,
5496                sha512: None,
5497                blake2b: Some(value.0.digest),
5498            },
5499        }
5500    }
5501}
5502
5503/// Convert a [`FileLocation`] into a normalized [`UrlString`].
5504fn normalize_file_location(location: &FileLocation) -> Result<UrlString, ToUrlError> {
5505    match location {
5506        FileLocation::AbsoluteUrl(absolute) => Ok(absolute.without_fragment().into_owned()),
5507        FileLocation::RelativeUrl(_, _) => Ok(normalize_url(location.to_url()?)),
5508    }
5509}
5510
5511/// Convert a [`DisplaySafeUrl`] into a normalized [`UrlString`] by removing the fragment.
5512fn normalize_url(mut url: DisplaySafeUrl) -> UrlString {
5513    url.set_fragment(None);
5514    UrlString::from(url)
5515}
5516
5517/// Normalize a [`Requirement`], which could come from a lockfile, a `pyproject.toml`, etc.
5518///
5519/// Performs the following steps:
5520///
5521/// 1. Removes any sensitive credentials.
5522/// 2. Ensures that the lock and install paths are appropriately framed with respect to the
5523///    current [`Workspace`].
5524/// 3. Removes the `origin` field, which is only used in `requirements.txt`.
5525/// 4. Simplifies the markers using the provided [`RequiresPython`] instance.
5526fn normalize_requirement(
5527    mut requirement: Requirement,
5528    root: &Path,
5529    requires_python: &RequiresPython,
5530) -> Result<Requirement, LockError> {
5531    // Sort the extras and groups for consistency.
5532    requirement.extras.sort();
5533    requirement.groups.sort();
5534
5535    // Normalize the requirement source.
5536    match requirement.source {
5537        RequirementSource::Git {
5538            git,
5539            subdirectory,
5540            url: _,
5541        } => {
5542            // Reconstruct the Git URL.
5543            let git = {
5544                let mut repository = git.url().clone();
5545
5546                // Remove the credentials.
5547                repository.remove_credentials();
5548
5549                // Remove the fragment and query from the URL; they're already present in the source.
5550                repository.set_fragment(None);
5551                repository.set_query(None);
5552
5553                GitUrl::from_fields(
5554                    repository,
5555                    git.reference().clone(),
5556                    git.precise(),
5557                    git.lfs(),
5558                )?
5559            };
5560
5561            // Reconstruct the PEP 508 URL from the underlying data.
5562            let url = DisplaySafeUrl::from(ParsedGitUrl {
5563                url: git.clone(),
5564                subdirectory: subdirectory.clone(),
5565            });
5566
5567            Ok(Requirement {
5568                name: requirement.name,
5569                extras: requirement.extras,
5570                groups: requirement.groups,
5571                marker: requires_python.simplify_markers(requirement.marker),
5572                source: RequirementSource::Git {
5573                    git,
5574                    subdirectory,
5575                    url: VerbatimUrl::from_url(url),
5576                },
5577                origin: None,
5578            })
5579        }
5580        RequirementSource::Path {
5581            install_path,
5582            ext,
5583            url: _,
5584        } => {
5585            let path = root.join(&install_path);
5586            let install_path = normalize_path(path).into_owned().into_boxed_path();
5587            let url = VerbatimUrl::from_normalized_path(&install_path)
5588                .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5589
5590            Ok(Requirement {
5591                name: requirement.name,
5592                extras: requirement.extras,
5593                groups: requirement.groups,
5594                marker: requires_python.simplify_markers(requirement.marker),
5595                source: RequirementSource::Path {
5596                    install_path,
5597                    ext,
5598                    url,
5599                },
5600                origin: None,
5601            })
5602        }
5603        RequirementSource::Directory {
5604            install_path,
5605            editable,
5606            r#virtual,
5607            url: _,
5608        } => {
5609            let path = root.join(&install_path);
5610            let install_path = normalize_path(path).into_owned().into_boxed_path();
5611            let url = VerbatimUrl::from_normalized_path(&install_path)
5612                .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5613
5614            Ok(Requirement {
5615                name: requirement.name,
5616                extras: requirement.extras,
5617                groups: requirement.groups,
5618                marker: requires_python.simplify_markers(requirement.marker),
5619                source: RequirementSource::Directory {
5620                    install_path,
5621                    editable: Some(editable.unwrap_or(false)),
5622                    r#virtual: Some(r#virtual.unwrap_or(false)),
5623                    url,
5624                },
5625                origin: None,
5626            })
5627        }
5628        RequirementSource::Registry {
5629            specifier,
5630            index,
5631            conflict,
5632        } => {
5633            // Round-trip the index to remove anything apart from the URL.
5634            let index = index
5635                .map(|index| index.url.into_url())
5636                .map(|mut index| {
5637                    index.remove_credentials();
5638                    index
5639                })
5640                .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index))));
5641            Ok(Requirement {
5642                name: requirement.name,
5643                extras: requirement.extras,
5644                groups: requirement.groups,
5645                marker: requires_python.simplify_markers(requirement.marker),
5646                source: RequirementSource::Registry {
5647                    specifier,
5648                    index,
5649                    conflict,
5650                },
5651                origin: None,
5652            })
5653        }
5654        RequirementSource::Url {
5655            mut location,
5656            subdirectory,
5657            ext,
5658            url: _,
5659        } => {
5660            // Remove the credentials.
5661            location.remove_credentials();
5662
5663            // Remove the fragment from the URL; it's already present in the source.
5664            location.set_fragment(None);
5665
5666            // Reconstruct the PEP 508 URL from the underlying data.
5667            let url = DisplaySafeUrl::from(ParsedArchiveUrl {
5668                url: location.clone(),
5669                subdirectory: subdirectory.clone(),
5670                ext,
5671            });
5672
5673            Ok(Requirement {
5674                name: requirement.name,
5675                extras: requirement.extras,
5676                groups: requirement.groups,
5677                marker: requires_python.simplify_markers(requirement.marker),
5678                source: RequirementSource::Url {
5679                    location,
5680                    subdirectory,
5681                    ext,
5682                    url: VerbatimUrl::from_url(url),
5683                },
5684                origin: None,
5685            })
5686        }
5687    }
5688}
5689
5690#[derive(Debug)]
5691pub struct LockError {
5692    kind: Box<LockErrorKind>,
5693    hint: Option<WheelTagHint>,
5694}
5695
5696impl std::error::Error for LockError {
5697    fn source(&self) -> Option<&(dyn Error + 'static)> {
5698        self.kind.source()
5699    }
5700}
5701
5702impl std::fmt::Display for LockError {
5703    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5704        write!(f, "{}", self.kind)?;
5705        if let Some(hint) = &self.hint {
5706            write!(f, "\n\n{hint}")?;
5707        }
5708        Ok(())
5709    }
5710}
5711
5712impl LockError {
5713    /// Returns true if the [`LockError`] is a resolver error.
5714    pub fn is_resolution(&self) -> bool {
5715        matches!(&*self.kind, LockErrorKind::Resolution { .. })
5716    }
5717
5718    /// Returns true if the [`LockError`] is caused by disabled builds.
5719    pub fn is_no_build(&self) -> bool {
5720        matches!(
5721            &*self.kind,
5722            LockErrorKind::NoBuild { .. } | LockErrorKind::NoBinaryNoBuild { .. }
5723        )
5724    }
5725}
5726
5727impl<E> From<E> for LockError
5728where
5729    LockErrorKind: From<E>,
5730{
5731    fn from(err: E) -> Self {
5732        Self {
5733            kind: Box::new(LockErrorKind::from(err)),
5734            hint: None,
5735        }
5736    }
5737}
5738
5739#[derive(Debug, Clone, PartialEq, Eq)]
5740#[expect(clippy::enum_variant_names)]
5741enum WheelTagHint {
5742    /// None of the available wheels for a package have a compatible Python language tag (e.g.,
5743    /// `cp310` in `cp310-abi3-manylinux_2_17_x86_64.whl`).
5744    LanguageTags {
5745        package: PackageName,
5746        version: Option<Version>,
5747        tags: BTreeSet<LanguageTag>,
5748        best: Option<LanguageTag>,
5749    },
5750    /// None of the available wheels for a package have a compatible ABI tag (e.g., `abi3` in
5751    /// `cp310-abi3-manylinux_2_17_x86_64.whl`).
5752    AbiTags {
5753        package: PackageName,
5754        version: Option<Version>,
5755        tags: BTreeSet<AbiTag>,
5756        best: Option<AbiTag>,
5757    },
5758    /// None of the available wheels for a package have a compatible platform tag (e.g.,
5759    /// `manylinux_2_17_x86_64` in `cp310-abi3-manylinux_2_17_x86_64.whl`).
5760    PlatformTags {
5761        package: PackageName,
5762        version: Option<Version>,
5763        tags: BTreeSet<PlatformTag>,
5764        best: Option<PlatformTag>,
5765        markers: MarkerEnvironment,
5766    },
5767}
5768
5769impl WheelTagHint {
5770    /// Generate a [`WheelTagHint`] from the given (incompatible) wheels.
5771    fn from_wheels(
5772        name: &PackageName,
5773        version: Option<&Version>,
5774        filenames: &[&WheelFilename],
5775        tags: &Tags,
5776        markers: &MarkerEnvironment,
5777    ) -> Option<Self> {
5778        let incompatibility = filenames
5779            .iter()
5780            .map(|filename| {
5781                tags.compatibility(
5782                    filename.python_tags(),
5783                    filename.abi_tags(),
5784                    filename.platform_tags(),
5785                )
5786            })
5787            .max()?;
5788        match incompatibility {
5789            TagCompatibility::Incompatible(IncompatibleTag::Python) => {
5790                let best = tags.python_tag();
5791                let tags = Self::python_tags(filenames.iter().copied()).collect::<BTreeSet<_>>();
5792                if tags.is_empty() {
5793                    None
5794                } else {
5795                    Some(Self::LanguageTags {
5796                        package: name.clone(),
5797                        version: version.cloned(),
5798                        tags,
5799                        best,
5800                    })
5801                }
5802            }
5803            TagCompatibility::Incompatible(IncompatibleTag::Abi) => {
5804                let best = tags.abi_tag();
5805                let tags = Self::abi_tags(filenames.iter().copied())
5806                    // Ignore `none`, which is universally compatible.
5807                    //
5808                    // As an example, `none` can appear here if we're solving for Python 3.13, and
5809                    // the distribution includes a wheel for `cp312-none-macosx_11_0_arm64`.
5810                    //
5811                    // In that case, the wheel isn't compatible, but when solving for Python 3.13,
5812                    // the `cp312` Python tag _can_ be compatible (e.g., for `cp312-abi3-macosx_11_0_arm64.whl`),
5813                    // so this is considered an ABI incompatibility rather than Python incompatibility.
5814                    .filter(|tag| *tag != AbiTag::None)
5815                    .collect::<BTreeSet<_>>();
5816                if tags.is_empty() {
5817                    None
5818                } else {
5819                    Some(Self::AbiTags {
5820                        package: name.clone(),
5821                        version: version.cloned(),
5822                        tags,
5823                        best,
5824                    })
5825                }
5826            }
5827            TagCompatibility::Incompatible(IncompatibleTag::Platform) => {
5828                let best = tags.platform_tag().cloned();
5829                let incompatible_tags = Self::platform_tags(filenames.iter().copied(), tags)
5830                    .cloned()
5831                    .collect::<BTreeSet<_>>();
5832                if incompatible_tags.is_empty() {
5833                    None
5834                } else {
5835                    Some(Self::PlatformTags {
5836                        package: name.clone(),
5837                        version: version.cloned(),
5838                        tags: incompatible_tags,
5839                        best,
5840                        markers: markers.clone(),
5841                    })
5842                }
5843            }
5844            _ => None,
5845        }
5846    }
5847
5848    /// Returns an iterator over the compatible Python tags of the available wheels.
5849    fn python_tags<'a>(
5850        filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5851    ) -> impl Iterator<Item = LanguageTag> + 'a {
5852        filenames.flat_map(WheelFilename::python_tags).copied()
5853    }
5854
5855    /// Returns an iterator over the compatible Python tags of the available wheels.
5856    fn abi_tags<'a>(
5857        filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5858    ) -> impl Iterator<Item = AbiTag> + 'a {
5859        filenames.flat_map(WheelFilename::abi_tags).copied()
5860    }
5861
5862    /// Returns the set of platform tags for the distribution that are ABI-compatible with the given
5863    /// tags.
5864    fn platform_tags<'a>(
5865        filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5866        tags: &'a Tags,
5867    ) -> impl Iterator<Item = &'a PlatformTag> + 'a {
5868        filenames.flat_map(move |filename| {
5869            if filename.python_tags().iter().any(|wheel_py| {
5870                filename
5871                    .abi_tags()
5872                    .iter()
5873                    .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
5874            }) {
5875                filename.platform_tags().iter()
5876            } else {
5877                [].iter()
5878            }
5879        })
5880    }
5881
5882    fn suggest_environment_marker(markers: &MarkerEnvironment) -> String {
5883        let sys_platform = markers.sys_platform();
5884        let platform_machine = markers.platform_machine();
5885
5886        // Generate the marker string based on actual environment values
5887        if platform_machine.is_empty() {
5888            format!("sys_platform == '{sys_platform}'")
5889        } else {
5890            format!("sys_platform == '{sys_platform}' and platform_machine == '{platform_machine}'")
5891        }
5892    }
5893}
5894
5895impl std::fmt::Display for WheelTagHint {
5896    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5897        match self {
5898            Self::LanguageTags {
5899                package,
5900                version,
5901                tags,
5902                best,
5903            } => {
5904                if let Some(best) = best {
5905                    let s = if tags.len() == 1 { "" } else { "s" };
5906                    let best = if let Some(pretty) = best.pretty() {
5907                        format!("{} (`{}`)", pretty.cyan(), best.cyan())
5908                    } else {
5909                        format!("{}", best.cyan())
5910                    };
5911                    if let Some(version) = version {
5912                        write!(
5913                            f,
5914                            "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python implementation tag{s}: {}",
5915                            "hint".bold().cyan(),
5916                            ":".bold(),
5917                            best,
5918                            package.cyan(),
5919                            format!("v{version}").cyan(),
5920                            tags.iter()
5921                                .map(|tag| format!("`{}`", tag.cyan()))
5922                                .join(", "),
5923                        )
5924                    } else {
5925                        write!(
5926                            f,
5927                            "{}{} You're using {}, but `{}` only has wheels with the following Python implementation tag{s}: {}",
5928                            "hint".bold().cyan(),
5929                            ":".bold(),
5930                            best,
5931                            package.cyan(),
5932                            tags.iter()
5933                                .map(|tag| format!("`{}`", tag.cyan()))
5934                                .join(", "),
5935                        )
5936                    }
5937                } else {
5938                    let s = if tags.len() == 1 { "" } else { "s" };
5939                    if let Some(version) = version {
5940                        write!(
5941                            f,
5942                            "{}{} Wheels are available for `{}` ({}) with the following Python implementation tag{s}: {}",
5943                            "hint".bold().cyan(),
5944                            ":".bold(),
5945                            package.cyan(),
5946                            format!("v{version}").cyan(),
5947                            tags.iter()
5948                                .map(|tag| format!("`{}`", tag.cyan()))
5949                                .join(", "),
5950                        )
5951                    } else {
5952                        write!(
5953                            f,
5954                            "{}{} Wheels are available for `{}` with the following Python implementation tag{s}: {}",
5955                            "hint".bold().cyan(),
5956                            ":".bold(),
5957                            package.cyan(),
5958                            tags.iter()
5959                                .map(|tag| format!("`{}`", tag.cyan()))
5960                                .join(", "),
5961                        )
5962                    }
5963                }
5964            }
5965            Self::AbiTags {
5966                package,
5967                version,
5968                tags,
5969                best,
5970            } => {
5971                if let Some(best) = best {
5972                    let s = if tags.len() == 1 { "" } else { "s" };
5973                    let best = if let Some(pretty) = best.pretty() {
5974                        format!("{} (`{}`)", pretty.cyan(), best.cyan())
5975                    } else {
5976                        format!("{}", best.cyan())
5977                    };
5978                    if let Some(version) = version {
5979                        write!(
5980                            f,
5981                            "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python ABI tag{s}: {}",
5982                            "hint".bold().cyan(),
5983                            ":".bold(),
5984                            best,
5985                            package.cyan(),
5986                            format!("v{version}").cyan(),
5987                            tags.iter()
5988                                .map(|tag| format!("`{}`", tag.cyan()))
5989                                .join(", "),
5990                        )
5991                    } else {
5992                        write!(
5993                            f,
5994                            "{}{} You're using {}, but `{}` only has wheels with the following Python ABI tag{s}: {}",
5995                            "hint".bold().cyan(),
5996                            ":".bold(),
5997                            best,
5998                            package.cyan(),
5999                            tags.iter()
6000                                .map(|tag| format!("`{}`", tag.cyan()))
6001                                .join(", "),
6002                        )
6003                    }
6004                } else {
6005                    let s = if tags.len() == 1 { "" } else { "s" };
6006                    if let Some(version) = version {
6007                        write!(
6008                            f,
6009                            "{}{} Wheels are available for `{}` ({}) with the following Python ABI tag{s}: {}",
6010                            "hint".bold().cyan(),
6011                            ":".bold(),
6012                            package.cyan(),
6013                            format!("v{version}").cyan(),
6014                            tags.iter()
6015                                .map(|tag| format!("`{}`", tag.cyan()))
6016                                .join(", "),
6017                        )
6018                    } else {
6019                        write!(
6020                            f,
6021                            "{}{} Wheels are available for `{}` with the following Python ABI tag{s}: {}",
6022                            "hint".bold().cyan(),
6023                            ":".bold(),
6024                            package.cyan(),
6025                            tags.iter()
6026                                .map(|tag| format!("`{}`", tag.cyan()))
6027                                .join(", "),
6028                        )
6029                    }
6030                }
6031            }
6032            Self::PlatformTags {
6033                package,
6034                version,
6035                tags,
6036                best,
6037                markers,
6038            } => {
6039                let s = if tags.len() == 1 { "" } else { "s" };
6040                if let Some(best) = best {
6041                    let example_marker = Self::suggest_environment_marker(markers);
6042                    let best = if let Some(pretty) = best.pretty() {
6043                        format!("{} (`{}`)", pretty.cyan(), best.cyan())
6044                    } else {
6045                        format!("`{}`", best.cyan())
6046                    };
6047                    let package_ref = if let Some(version) = version {
6048                        format!("`{}` ({})", package.cyan(), format!("v{version}").cyan())
6049                    } else {
6050                        format!("`{}`", package.cyan())
6051                    };
6052                    write!(
6053                        f,
6054                        "{}{} 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",
6055                        "hint".bold().cyan(),
6056                        ":".bold(),
6057                        best,
6058                        package_ref,
6059                        tags.iter()
6060                            .map(|tag| format!("`{}`", tag.cyan()))
6061                            .join(", "),
6062                        format!("\"{example_marker}\"").cyan(),
6063                        "tool.uv.required-environments".green()
6064                    )
6065                } else {
6066                    if let Some(version) = version {
6067                        write!(
6068                            f,
6069                            "{}{} Wheels are available for `{}` ({}) on the following platform{s}: {}",
6070                            "hint".bold().cyan(),
6071                            ":".bold(),
6072                            package.cyan(),
6073                            format!("v{version}").cyan(),
6074                            tags.iter()
6075                                .map(|tag| format!("`{}`", tag.cyan()))
6076                                .join(", "),
6077                        )
6078                    } else {
6079                        write!(
6080                            f,
6081                            "{}{} Wheels are available for `{}` on the following platform{s}: {}",
6082                            "hint".bold().cyan(),
6083                            ":".bold(),
6084                            package.cyan(),
6085                            tags.iter()
6086                                .map(|tag| format!("`{}`", tag.cyan()))
6087                                .join(", "),
6088                        )
6089                    }
6090                }
6091            }
6092        }
6093    }
6094}
6095
6096/// An error that occurs when generating a `Lock` data structure.
6097///
6098/// These errors are sometimes the result of possible programming bugs.
6099/// For example, if there are two or more duplicative distributions given
6100/// to `Lock::new`, then an error is returned. It's likely that the fault
6101/// is with the caller somewhere in such cases.
6102#[derive(Debug, thiserror::Error)]
6103enum LockErrorKind {
6104    /// An error that occurs when multiple packages with the same
6105    /// ID were found.
6106    #[error("Found duplicate package `{id}`", id = id.cyan())]
6107    DuplicatePackage {
6108        /// The ID of the conflicting package.
6109        id: PackageId,
6110    },
6111    /// An error that occurs when there are multiple dependencies for the
6112    /// same package that have identical identifiers.
6113    #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = id.cyan(), dependency = dependency.cyan())]
6114    DuplicateDependency {
6115        /// The ID of the package for which a duplicate dependency was
6116        /// found.
6117        id: PackageId,
6118        /// The ID of the conflicting dependency.
6119        dependency: Dependency,
6120    },
6121    /// An error that occurs when there are multiple dependencies for the
6122    /// same package that have identical identifiers, as part of the
6123    /// that package's optional dependencies.
6124    #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}[{extra}]").cyan(), dependency = dependency.cyan())]
6125    DuplicateOptionalDependency {
6126        /// The ID of the package for which a duplicate dependency was
6127        /// found.
6128        id: PackageId,
6129        /// The name of the extra.
6130        extra: ExtraName,
6131        /// The ID of the conflicting dependency.
6132        dependency: Dependency,
6133    },
6134    /// An error that occurs when there are multiple dependencies for the
6135    /// same package that have identical identifiers, as part of the
6136    /// that package's development dependencies.
6137    #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}:{group}").cyan(), dependency = dependency.cyan())]
6138    DuplicateDevDependency {
6139        /// The ID of the package for which a duplicate dependency was
6140        /// found.
6141        id: PackageId,
6142        /// The name of the dev dependency group.
6143        group: GroupName,
6144        /// The ID of the conflicting dependency.
6145        dependency: Dependency,
6146    },
6147    /// An error that occurs when the URL to a file for a wheel or
6148    /// source dist could not be converted to a structured `url::Url`.
6149    #[error(transparent)]
6150    InvalidUrl(
6151        /// The underlying error that occurred. This includes the
6152        /// errant URL in its error message.
6153        #[from]
6154        ToUrlError,
6155    ),
6156    /// An error that occurs when the extension can't be determined
6157    /// for a given wheel or source distribution.
6158    #[error("Failed to parse file extension for `{id}`; expected one of: {err}", id = id.cyan())]
6159    MissingExtension {
6160        /// The filename that was expected to have an extension.
6161        id: PackageId,
6162        /// The list of valid extensions that were expected.
6163        err: ExtensionError,
6164    },
6165    /// Failed to parse a Git source URL.
6166    #[error("Failed to parse Git URL")]
6167    InvalidGitSourceUrl(
6168        /// The underlying error that occurred. This includes the
6169        /// errant URL in the message.
6170        #[source]
6171        SourceParseError,
6172    ),
6173    #[error("Failed to parse timestamp")]
6174    InvalidTimestamp(
6175        /// The underlying error that occurred. This includes the
6176        /// errant timestamp in the message.
6177        #[source]
6178        jiff::Error,
6179    ),
6180    /// An error that occurs when there's an unrecognized dependency.
6181    ///
6182    /// That is, a dependency for a package that isn't in the lockfile.
6183    #[error("For package `{id}`, found dependency `{dependency}` with no locked package", id = id.cyan(), dependency = dependency.cyan())]
6184    UnrecognizedDependency {
6185        /// The ID of the package that has an unrecognized dependency.
6186        id: PackageId,
6187        /// The ID of the dependency that doesn't have a corresponding package
6188        /// entry.
6189        dependency: Dependency,
6190    },
6191    /// An error that occurs when a hash is expected (or not) for a particular
6192    /// artifact, but one was not found (or was).
6193    #[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" })]
6194    Hash {
6195        /// The ID of the package that has a missing hash.
6196        id: PackageId,
6197        /// The specific type of artifact, e.g., "source package"
6198        /// or "wheel".
6199        artifact_type: &'static str,
6200        /// When true, a hash is expected to be present.
6201        expected: bool,
6202    },
6203    /// An error that occurs when a package is included with an extra name,
6204    /// but no corresponding base package (i.e., without the extra) exists.
6205    #[error("Found package `{id}` with extra `{extra}` but no base package", id = id.cyan(), extra = extra.cyan())]
6206    MissingExtraBase {
6207        /// The ID of the package that has a missing base.
6208        id: PackageId,
6209        /// The extra name that was found.
6210        extra: ExtraName,
6211    },
6212    /// An error that occurs when a package is included with a development
6213    /// dependency group, but no corresponding base package (i.e., without
6214    /// the group) exists.
6215    #[error("Found package `{id}` with development dependency group `{group}` but no base package", id = id.cyan())]
6216    MissingDevBase {
6217        /// The ID of the package that has a missing base.
6218        id: PackageId,
6219        /// The development dependency group that was found.
6220        group: GroupName,
6221    },
6222    /// An error that occurs from an invalid lockfile where a wheel comes from a non-wheel source
6223    /// such as a directory.
6224    #[error("Wheels cannot come from {source_type} sources")]
6225    InvalidWheelSource {
6226        /// The ID of the distribution that has a missing base.
6227        id: PackageId,
6228        /// The kind of the invalid source.
6229        source_type: &'static str,
6230    },
6231    /// An error that occurs when a distribution indicates that it is sourced from a remote
6232    /// registry, but is missing a URL.
6233    #[error("Found registry distribution `{name}` ({version}) without a valid URL", name = name.cyan(), version = format!("v{version}").cyan())]
6234    MissingUrl {
6235        /// The name of the distribution that is missing a URL.
6236        name: PackageName,
6237        /// The version of the distribution that is missing a URL.
6238        version: Version,
6239    },
6240    /// An error that occurs when a distribution indicates that it is sourced from a local registry,
6241    /// but is missing a path.
6242    #[error("Found registry distribution `{name}` ({version}) without a valid path", name = name.cyan(), version = format!("v{version}").cyan())]
6243    MissingPath {
6244        /// The name of the distribution that is missing a path.
6245        name: PackageName,
6246        /// The version of the distribution that is missing a path.
6247        version: Version,
6248    },
6249    /// An error that occurs when a distribution indicates that it is sourced from a registry, but
6250    /// is missing a filename.
6251    #[error("Found registry distribution `{id}` without a valid filename", id = id.cyan())]
6252    MissingFilename {
6253        /// The ID of the distribution that is missing a filename.
6254        id: PackageId,
6255    },
6256    /// An error that occurs when a distribution is included with neither wheels nor a source
6257    /// distribution.
6258    #[error("Distribution `{id}` can't be installed because it doesn't have a source distribution or wheel for the current platform", id = id.cyan())]
6259    NeitherSourceDistNorWheel {
6260        /// The ID of the distribution.
6261        id: PackageId,
6262    },
6263    /// An error that occurs when a distribution is marked as both `--no-binary` and `--no-build`.
6264    #[error("Distribution `{id}` can't be installed because it is marked as both `--no-binary` and `--no-build`", id = id.cyan())]
6265    NoBinaryNoBuild {
6266        /// The ID of the distribution.
6267        id: PackageId,
6268    },
6269    /// An error that occurs when a distribution is marked as `--no-binary`, but no source
6270    /// distribution is available.
6271    #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but has no source distribution", id = id.cyan())]
6272    NoBinary {
6273        /// The ID of the distribution.
6274        id: PackageId,
6275    },
6276    /// An error that occurs when a distribution is marked as `--no-build`, but no binary
6277    /// distribution is available.
6278    #[error("Distribution `{id}` can't be installed because it is marked as `--no-build` but has no binary distribution", id = id.cyan())]
6279    NoBuild {
6280        /// The ID of the distribution.
6281        id: PackageId,
6282    },
6283    /// An error that occurs when a wheel-only distribution is incompatible with the current
6284    /// platform.
6285    #[error("Distribution `{id}` can't be installed because the binary distribution is incompatible with the current platform", id = id.cyan())]
6286    IncompatibleWheelOnly {
6287        /// The ID of the distribution.
6288        id: PackageId,
6289    },
6290    /// An error that occurs when a wheel-only source is marked as `--no-binary`.
6291    #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but is itself a binary distribution", id = id.cyan())]
6292    NoBinaryWheelOnly {
6293        /// The ID of the distribution.
6294        id: PackageId,
6295    },
6296    /// An error that occurs when converting between URLs and paths.
6297    #[error("Found dependency `{id}` with no locked distribution", id = id.cyan())]
6298    VerbatimUrl {
6299        /// The ID of the distribution that has a missing base.
6300        id: PackageId,
6301        /// The inner error we forward.
6302        #[source]
6303        err: VerbatimUrlError,
6304    },
6305    /// An error that occurs when parsing an existing requirement.
6306    #[error("Could not compute relative path between workspace and distribution")]
6307    DistributionRelativePath(
6308        /// The inner error we forward.
6309        #[source]
6310        io::Error,
6311    ),
6312    /// An error that occurs when converting an index URL to a relative path
6313    #[error("Could not compute relative path between workspace and index")]
6314    IndexRelativePath(
6315        /// The inner error we forward.
6316        #[source]
6317        io::Error,
6318    ),
6319    /// An error that occurs when converting a lockfile path from relative to absolute.
6320    #[error("Could not compute absolute path from workspace root and lockfile path")]
6321    AbsolutePath(
6322        /// The inner error we forward.
6323        #[source]
6324        io::Error,
6325    ),
6326    /// An error that occurs when an ambiguous `package.dependency` is
6327    /// missing a `version` field.
6328    #[error("Dependency `{name}` has missing `version` field but has more than one matching package", name = name.cyan())]
6329    MissingDependencyVersion {
6330        /// The name of the dependency that is missing a `version` field.
6331        name: PackageName,
6332    },
6333    /// An error that occurs when an ambiguous `package.dependency` is
6334    /// missing a `source` field.
6335    #[error("Dependency `{name}` has missing `source` field but has more than one matching package", name = name.cyan())]
6336    MissingDependencySource {
6337        /// The name of the dependency that is missing a `source` field.
6338        name: PackageName,
6339    },
6340    /// An error that occurs when parsing an existing requirement.
6341    #[error("Could not compute relative path between workspace and requirement")]
6342    RequirementRelativePath(
6343        /// The inner error we forward.
6344        #[source]
6345        io::Error,
6346    ),
6347    /// An error that occurs when parsing an existing requirement.
6348    #[error("Could not convert between URL and path")]
6349    RequirementVerbatimUrl(
6350        /// The inner error we forward.
6351        #[source]
6352        VerbatimUrlError,
6353    ),
6354    /// An error that occurs when parsing a registry's index URL.
6355    #[error("Could not convert between URL and path")]
6356    RegistryVerbatimUrl(
6357        /// The inner error we forward.
6358        #[source]
6359        VerbatimUrlError,
6360    ),
6361    /// An error that occurs when converting a path to a URL.
6362    #[error("Failed to convert path to URL: {path}", path = path.display().cyan())]
6363    PathToUrl { path: Box<Path> },
6364    /// An error that occurs when converting a URL to a path
6365    #[error("Failed to convert URL to path: {url}", url = url.cyan())]
6366    UrlToPath { url: DisplaySafeUrl },
6367    /// An error that occurs when multiple packages with the same
6368    /// name were found when identifying the root packages.
6369    #[error("Found multiple packages matching `{name}`", name = name.cyan())]
6370    MultipleRootPackages {
6371        /// The ID of the package.
6372        name: PackageName,
6373    },
6374    /// An error that occurs when a root package can't be found.
6375    #[error("Could not find root package `{name}`", name = name.cyan())]
6376    MissingRootPackage {
6377        /// The ID of the package.
6378        name: PackageName,
6379    },
6380    /// An error that occurs when resolving metadata for a package.
6381    #[error("Failed to generate package metadata for `{id}`", id = id.cyan())]
6382    Resolution {
6383        /// The ID of the distribution that failed to resolve.
6384        id: PackageId,
6385        /// The inner error we forward.
6386        #[source]
6387        err: uv_distribution::Error,
6388    },
6389    /// A package has inconsistent versions in a single entry
6390    // Using name instead of id since the version in the id is part of the conflict.
6391    #[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())]
6392    InconsistentVersions {
6393        /// The name of the package with the inconsistent entry.
6394        name: PackageName,
6395        /// The version of the package with the inconsistent entry.
6396        version: Version,
6397        /// The wheel with the inconsistent version.
6398        wheel: Wheel,
6399    },
6400    #[error(
6401        "Found conflicting extras `{package1}[{extra1}]` \
6402         and `{package2}[{extra2}]` enabled simultaneously"
6403    )]
6404    ConflictingExtra {
6405        package1: PackageName,
6406        extra1: ExtraName,
6407        package2: PackageName,
6408        extra2: ExtraName,
6409    },
6410    #[error(transparent)]
6411    GitUrlParse(#[from] GitUrlParseError),
6412    #[error("Failed to read `{path}`")]
6413    UnreadablePyprojectToml {
6414        path: PathBuf,
6415        #[source]
6416        err: std::io::Error,
6417    },
6418    #[error("Failed to parse `{path}`")]
6419    InvalidPyprojectToml {
6420        path: PathBuf,
6421        #[source]
6422        err: uv_pypi_types::MetadataError,
6423    },
6424    /// An error that occurs when a workspace member has a non-local source.
6425    #[error("Workspace member `{id}` has non-local source", id = id.cyan())]
6426    NonLocalWorkspaceMember {
6427        /// The ID of the workspace member with an invalid source.
6428        id: PackageId,
6429    },
6430}
6431
6432/// An error that occurs when a source string could not be parsed.
6433#[derive(Debug, thiserror::Error)]
6434enum SourceParseError {
6435    /// An error that occurs when the URL in the source is invalid.
6436    #[error("Invalid URL in source `{given}`")]
6437    InvalidUrl {
6438        /// The source string given.
6439        given: String,
6440        /// The URL parse error.
6441        #[source]
6442        err: DisplaySafeUrlError,
6443    },
6444    /// An error that occurs when a Git URL is missing a precise commit SHA.
6445    #[error("Missing SHA in source `{given}`")]
6446    MissingSha {
6447        /// The source string given.
6448        given: String,
6449    },
6450    /// An error that occurs when a Git URL has an invalid SHA.
6451    #[error("Invalid SHA in source `{given}`")]
6452    InvalidSha {
6453        /// The source string given.
6454        given: String,
6455    },
6456}
6457
6458/// An error that occurs when a hash digest could not be parsed.
6459#[derive(Clone, Debug, Eq, PartialEq)]
6460struct HashParseError(&'static str);
6461
6462impl std::error::Error for HashParseError {}
6463
6464impl Display for HashParseError {
6465    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
6466        Display::fmt(self.0, f)
6467    }
6468}
6469
6470/// Format an array so that each element is on its own line and has a trailing comma.
6471///
6472/// Example:
6473///
6474/// ```toml
6475/// dependencies = [
6476///     { name = "idna" },
6477///     { name = "sniffio" },
6478/// ]
6479/// ```
6480fn each_element_on_its_line_array(elements: impl Iterator<Item = impl Into<Value>>) -> Array {
6481    let mut array = elements
6482        .map(|item| {
6483            let mut value = item.into();
6484            // Each dependency is on its own line and indented.
6485            value.decor_mut().set_prefix("\n    ");
6486            value
6487        })
6488        .collect::<Array>();
6489    // With a trailing comma, inserting another entry doesn't change the preceding line,
6490    // reducing the diff noise.
6491    array.set_trailing_comma(true);
6492    // The line break between the last element's comma and the closing square bracket.
6493    array.set_trailing("\n");
6494    array
6495}
6496
6497/// Returns the simplified string-ified version of each marker given.
6498///
6499/// Note that the marker strings returned will include conflict markers if they
6500/// are present.
6501fn simplified_universal_markers(
6502    markers: &[UniversalMarker],
6503    requires_python: &RequiresPython,
6504) -> Vec<String> {
6505    canonical_marker_trees(markers, requires_python)
6506        .into_iter()
6507        .filter_map(MarkerTree::try_to_string)
6508        .collect()
6509}
6510
6511/// Canonicalize universal markers to match the form persisted in `uv.lock`.
6512///
6513/// When the PEP 508 portions of the markers are disjoint, the lockfile stores
6514/// only those simplified PEP 508 markers. Otherwise, it stores the simplified
6515/// combined markers (including conflict markers). Markers that serialize to
6516/// `true` are omitted.
6517fn canonicalize_universal_markers(
6518    markers: &[UniversalMarker],
6519    requires_python: &RequiresPython,
6520) -> Vec<UniversalMarker> {
6521    canonical_marker_trees(markers, requires_python)
6522        .into_iter()
6523        .map(|marker| {
6524            let simplified = SimplifiedMarkerTree::new(requires_python, marker);
6525            UniversalMarker::from_combined(simplified.into_marker(requires_python))
6526        })
6527        .collect()
6528}
6529
6530/// Return the simplified marker trees that would be persisted in `uv.lock`.
6531fn canonical_marker_trees(
6532    markers: &[UniversalMarker],
6533    requires_python: &RequiresPython,
6534) -> Vec<MarkerTree> {
6535    let mut pep508_only = vec![];
6536    let mut seen = FxHashSet::default();
6537    for marker in markers {
6538        let simplified =
6539            SimplifiedMarkerTree::new(requires_python, marker.pep508()).as_simplified_marker_tree();
6540        if seen.insert(simplified) {
6541            pep508_only.push(simplified);
6542        }
6543    }
6544    let any_overlap = pep508_only
6545        .iter()
6546        .tuple_combinations()
6547        .any(|(&marker1, &marker2)| !marker1.is_disjoint(marker2));
6548    let markers = if !any_overlap {
6549        pep508_only
6550    } else {
6551        markers
6552            .iter()
6553            .map(|marker| {
6554                SimplifiedMarkerTree::new(requires_python, marker.combined())
6555                    .as_simplified_marker_tree()
6556            })
6557            .collect()
6558    };
6559    markers
6560        .into_iter()
6561        .filter(|marker| !marker.is_true())
6562        .collect()
6563}
6564
6565/// Filter out wheels that can't be selected for installation due to environment markers.
6566///
6567/// For example, a package included under `sys_platform == 'win32'` does not need Linux
6568/// wheels.
6569///
6570/// Returns `true` if the wheel is definitely unreachable, and `false` if it may be reachable,
6571/// including if the wheel tag isn't recognized.
6572fn is_wheel_unreachable_for_marker(
6573    filename: &WheelFilename,
6574    requires_python: &RequiresPython,
6575    marker: &UniversalMarker,
6576    tags: Option<&Tags>,
6577) -> bool {
6578    if let Some(tags) = tags
6579        && !filename.compatibility(tags).is_compatible()
6580    {
6581        return true;
6582    }
6583    // Remove wheels that don't match `requires-python` and can't be selected for installation.
6584    if !requires_python.matches_wheel_tag(filename) {
6585        return true;
6586    }
6587
6588    // Filter by platform tags.
6589
6590    // Naively, we'd check whether `platform_system == 'Linux'` is disjoint, or
6591    // `os_name == 'posix'` is disjoint, or `sys_platform == 'linux'` is disjoint (each on its
6592    // own sufficient to exclude linux wheels), but due to
6593    // `(A ∩ (B ∩ C) = ∅) => ((A ∩ B = ∅) or (A ∩ C = ∅))`
6594    // a single disjointness check with the intersection is sufficient, so we have one
6595    // constant per platform.
6596    let platform_tags = filename.platform_tags();
6597
6598    if platform_tags.iter().all(PlatformTag::is_any) {
6599        return false;
6600    }
6601
6602    if platform_tags.iter().all(PlatformTag::is_linux) {
6603        if platform_tags.iter().all(PlatformTag::is_arm) {
6604            if marker.is_disjoint(*LINUX_ARM_MARKERS) {
6605                return true;
6606            }
6607        } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6608            if marker.is_disjoint(*LINUX_X86_64_MARKERS) {
6609                return true;
6610            }
6611        } else if platform_tags.iter().all(PlatformTag::is_x86) {
6612            if marker.is_disjoint(*LINUX_X86_MARKERS) {
6613                return true;
6614            }
6615        } else if platform_tags.iter().all(PlatformTag::is_ppc64le) {
6616            if marker.is_disjoint(*LINUX_PPC64LE_MARKERS) {
6617                return true;
6618            }
6619        } else if platform_tags.iter().all(PlatformTag::is_ppc64) {
6620            if marker.is_disjoint(*LINUX_PPC64_MARKERS) {
6621                return true;
6622            }
6623        } else if platform_tags.iter().all(PlatformTag::is_s390x) {
6624            if marker.is_disjoint(*LINUX_S390X_MARKERS) {
6625                return true;
6626            }
6627        } else if platform_tags.iter().all(PlatformTag::is_riscv64) {
6628            if marker.is_disjoint(*LINUX_RISCV64_MARKERS) {
6629                return true;
6630            }
6631        } else if platform_tags.iter().all(PlatformTag::is_loongarch64) {
6632            if marker.is_disjoint(*LINUX_LOONGARCH64_MARKERS) {
6633                return true;
6634            }
6635        } else if platform_tags.iter().all(PlatformTag::is_armv7l) {
6636            if marker.is_disjoint(*LINUX_ARMV7L_MARKERS) {
6637                return true;
6638            }
6639        } else if platform_tags.iter().all(PlatformTag::is_armv6l) {
6640            if marker.is_disjoint(*LINUX_ARMV6L_MARKERS) {
6641                return true;
6642            }
6643        } else if marker.is_disjoint(*LINUX_MARKERS) {
6644            return true;
6645        }
6646    }
6647
6648    if platform_tags.iter().all(PlatformTag::is_windows) {
6649        if platform_tags.iter().all(PlatformTag::is_arm) {
6650            if marker.is_disjoint(*WINDOWS_ARM_MARKERS) {
6651                return true;
6652            }
6653        } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6654            if marker.is_disjoint(*WINDOWS_X86_64_MARKERS) {
6655                return true;
6656            }
6657        } else if platform_tags.iter().all(PlatformTag::is_x86) {
6658            if marker.is_disjoint(*WINDOWS_X86_MARKERS) {
6659                return true;
6660            }
6661        } else if marker.is_disjoint(*WINDOWS_MARKERS) {
6662            return true;
6663        }
6664    }
6665
6666    if platform_tags.iter().all(PlatformTag::is_macos) {
6667        if platform_tags.iter().all(PlatformTag::is_arm) {
6668            if marker.is_disjoint(*MAC_ARM_MARKERS) {
6669                return true;
6670            }
6671        } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6672            if marker.is_disjoint(*MAC_X86_64_MARKERS) {
6673                return true;
6674            }
6675        } else if platform_tags.iter().all(PlatformTag::is_x86) {
6676            if marker.is_disjoint(*MAC_X86_MARKERS) {
6677                return true;
6678            }
6679        } else if marker.is_disjoint(*MAC_MARKERS) {
6680            return true;
6681        }
6682    }
6683
6684    if platform_tags.iter().all(PlatformTag::is_android) {
6685        if platform_tags.iter().all(PlatformTag::is_arm) {
6686            if marker.is_disjoint(*ANDROID_ARM_MARKERS) {
6687                return true;
6688            }
6689        } else if platform_tags.iter().all(PlatformTag::is_x86_64) {
6690            if marker.is_disjoint(*ANDROID_X86_64_MARKERS) {
6691                return true;
6692            }
6693        } else if platform_tags.iter().all(PlatformTag::is_x86) {
6694            if marker.is_disjoint(*ANDROID_X86_MARKERS) {
6695                return true;
6696            }
6697        } else if marker.is_disjoint(*ANDROID_MARKERS) {
6698            return true;
6699        }
6700    }
6701
6702    if platform_tags.iter().all(PlatformTag::is_arm) {
6703        if marker.is_disjoint(*ARM_MARKERS) {
6704            return true;
6705        }
6706    }
6707
6708    if platform_tags.iter().all(PlatformTag::is_x86_64) {
6709        if marker.is_disjoint(*X86_64_MARKERS) {
6710            return true;
6711        }
6712    }
6713
6714    if platform_tags.iter().all(PlatformTag::is_x86) {
6715        if marker.is_disjoint(*X86_MARKERS) {
6716            return true;
6717        }
6718    }
6719
6720    if platform_tags.iter().all(PlatformTag::is_ppc64le) {
6721        if marker.is_disjoint(*PPC64LE_MARKERS) {
6722            return true;
6723        }
6724    }
6725
6726    if platform_tags.iter().all(PlatformTag::is_ppc64) {
6727        if marker.is_disjoint(*PPC64_MARKERS) {
6728            return true;
6729        }
6730    }
6731
6732    if platform_tags.iter().all(PlatformTag::is_s390x) {
6733        if marker.is_disjoint(*S390X_MARKERS) {
6734            return true;
6735        }
6736    }
6737
6738    if platform_tags.iter().all(PlatformTag::is_riscv64) {
6739        if marker.is_disjoint(*RISCV64_MARKERS) {
6740            return true;
6741        }
6742    }
6743
6744    if platform_tags.iter().all(PlatformTag::is_loongarch64) {
6745        if marker.is_disjoint(*LOONGARCH64_MARKERS) {
6746            return true;
6747        }
6748    }
6749
6750    if platform_tags.iter().all(PlatformTag::is_armv7l) {
6751        if marker.is_disjoint(*ARMV7L_MARKERS) {
6752            return true;
6753        }
6754    }
6755
6756    if platform_tags.iter().all(PlatformTag::is_armv6l) {
6757        if marker.is_disjoint(*ARMV6L_MARKERS) {
6758            return true;
6759        }
6760    }
6761
6762    false
6763}
6764
6765pub(crate) fn is_wheel_unreachable(
6766    filename: &WheelFilename,
6767    graph: &ResolverOutput,
6768    requires_python: &RequiresPython,
6769    node_index: NodeIndex,
6770    tags: Option<&Tags>,
6771) -> bool {
6772    is_wheel_unreachable_for_marker(
6773        filename,
6774        requires_python,
6775        graph.graph[node_index].marker(),
6776        tags,
6777    )
6778}
6779
6780#[cfg(test)]
6781mod tests {
6782    use uv_warnings::anstream;
6783
6784    use super::*;
6785
6786    /// Assert a given display snapshot, stripping ANSI color codes.
6787    macro_rules! assert_stripped_snapshot {
6788        ($expr:expr, @$snapshot:literal) => {{
6789            let expr = format!("{}", $expr);
6790            let expr = format!("{}", anstream::adapter::strip_str(&expr));
6791            insta::assert_snapshot!(expr, @$snapshot);
6792        }};
6793    }
6794
6795    #[test]
6796    fn missing_dependency_source_unambiguous() {
6797        let data = r#"
6798version = 1
6799requires-python = ">=3.12"
6800
6801[[package]]
6802name = "a"
6803version = "0.1.0"
6804source = { registry = "https://pypi.org/simple" }
6805sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6806
6807[[package]]
6808name = "b"
6809version = "0.1.0"
6810source = { registry = "https://pypi.org/simple" }
6811sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6812
6813[[package.dependencies]]
6814name = "a"
6815version = "0.1.0"
6816"#;
6817        let result: Result<Lock, _> = toml::from_str(data);
6818        insta::assert_debug_snapshot!(result);
6819    }
6820
6821    #[test]
6822    fn missing_dependency_version_unambiguous() {
6823        let data = r#"
6824version = 1
6825requires-python = ">=3.12"
6826
6827[[package]]
6828name = "a"
6829version = "0.1.0"
6830source = { registry = "https://pypi.org/simple" }
6831sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6832
6833[[package]]
6834name = "b"
6835version = "0.1.0"
6836source = { registry = "https://pypi.org/simple" }
6837sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6838
6839[[package.dependencies]]
6840name = "a"
6841source = { registry = "https://pypi.org/simple" }
6842"#;
6843        let result: Result<Lock, _> = toml::from_str(data);
6844        insta::assert_debug_snapshot!(result);
6845    }
6846
6847    #[test]
6848    fn missing_dependency_source_version_unambiguous() {
6849        let data = r#"
6850version = 1
6851requires-python = ">=3.12"
6852
6853[[package]]
6854name = "a"
6855version = "0.1.0"
6856source = { registry = "https://pypi.org/simple" }
6857sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6858
6859[[package]]
6860name = "b"
6861version = "0.1.0"
6862source = { registry = "https://pypi.org/simple" }
6863sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6864
6865[[package.dependencies]]
6866name = "a"
6867"#;
6868        let result: Result<Lock, _> = toml::from_str(data);
6869        insta::assert_debug_snapshot!(result);
6870    }
6871
6872    #[test]
6873    fn missing_dependency_source_ambiguous() {
6874        let data = r#"
6875version = 1
6876requires-python = ">=3.12"
6877
6878[[package]]
6879name = "a"
6880version = "0.1.0"
6881source = { registry = "https://pypi.org/simple" }
6882sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6883
6884[[package]]
6885name = "a"
6886version = "0.1.1"
6887source = { registry = "https://pypi.org/simple" }
6888sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6889
6890[[package]]
6891name = "b"
6892version = "0.1.0"
6893source = { registry = "https://pypi.org/simple" }
6894sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6895
6896[[package.dependencies]]
6897name = "a"
6898version = "0.1.0"
6899"#;
6900        let result = toml::from_str::<Lock>(data).unwrap_err();
6901        assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6902    }
6903
6904    #[test]
6905    fn missing_dependency_version_ambiguous() {
6906        let data = r#"
6907version = 1
6908requires-python = ">=3.12"
6909
6910[[package]]
6911name = "a"
6912version = "0.1.0"
6913source = { registry = "https://pypi.org/simple" }
6914sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6915
6916[[package]]
6917name = "a"
6918version = "0.1.1"
6919source = { registry = "https://pypi.org/simple" }
6920sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6921
6922[[package]]
6923name = "b"
6924version = "0.1.0"
6925source = { registry = "https://pypi.org/simple" }
6926sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6927
6928[[package.dependencies]]
6929name = "a"
6930source = { registry = "https://pypi.org/simple" }
6931"#;
6932        let result = toml::from_str::<Lock>(data).unwrap_err();
6933        assert_stripped_snapshot!(result, @"Dependency `a` has missing `version` field but has more than one matching package");
6934    }
6935
6936    #[test]
6937    fn missing_dependency_source_version_ambiguous() {
6938        let data = r#"
6939version = 1
6940requires-python = ">=3.12"
6941
6942[[package]]
6943name = "a"
6944version = "0.1.0"
6945source = { registry = "https://pypi.org/simple" }
6946sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6947
6948[[package]]
6949name = "a"
6950version = "0.1.1"
6951source = { registry = "https://pypi.org/simple" }
6952sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6953
6954[[package]]
6955name = "b"
6956version = "0.1.0"
6957source = { registry = "https://pypi.org/simple" }
6958sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6959
6960[[package.dependencies]]
6961name = "a"
6962"#;
6963        let result = toml::from_str::<Lock>(data).unwrap_err();
6964        assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6965    }
6966
6967    #[test]
6968    fn missing_dependency_version_dynamic() {
6969        let data = r#"
6970version = 1
6971requires-python = ">=3.12"
6972
6973[[package]]
6974name = "a"
6975source = { editable = "path/to/a" }
6976
6977[[package]]
6978name = "a"
6979version = "0.1.1"
6980source = { registry = "https://pypi.org/simple" }
6981sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6982
6983[[package]]
6984name = "b"
6985version = "0.1.0"
6986source = { registry = "https://pypi.org/simple" }
6987sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6988
6989[[package.dependencies]]
6990name = "a"
6991source = { editable = "path/to/a" }
6992"#;
6993        let result = toml::from_str::<Lock>(data);
6994        insta::assert_debug_snapshot!(result);
6995    }
6996
6997    #[test]
6998    fn hash_optional_missing() {
6999        let data = r#"
7000version = 1
7001requires-python = ">=3.12"
7002
7003[[package]]
7004name = "anyio"
7005version = "4.3.0"
7006source = { registry = "https://pypi.org/simple" }
7007wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl" }]
7008"#;
7009        let result: Result<Lock, _> = toml::from_str(data);
7010        insta::assert_debug_snapshot!(result);
7011    }
7012
7013    #[test]
7014    fn hash_optional_present() {
7015        let data = r#"
7016version = 1
7017requires-python = ">=3.12"
7018
7019[[package]]
7020name = "anyio"
7021version = "4.3.0"
7022source = { registry = "https://pypi.org/simple" }
7023wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
7024"#;
7025        let result: Result<Lock, _> = toml::from_str(data);
7026        insta::assert_debug_snapshot!(result);
7027    }
7028
7029    #[test]
7030    fn hash_required_present() {
7031        let data = r#"
7032version = 1
7033requires-python = ">=3.12"
7034
7035[[package]]
7036name = "anyio"
7037version = "4.3.0"
7038source = { path = "file:///foo/bar" }
7039wheels = [{ url = "file:///foo/bar/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
7040"#;
7041        let result: Result<Lock, _> = toml::from_str(data);
7042        insta::assert_debug_snapshot!(result);
7043    }
7044
7045    #[test]
7046    fn source_direct_no_subdir() {
7047        let data = r#"
7048version = 1
7049requires-python = ">=3.12"
7050
7051[[package]]
7052name = "anyio"
7053version = "4.3.0"
7054source = { url = "https://burntsushi.net" }
7055"#;
7056        let result: Result<Lock, _> = toml::from_str(data);
7057        insta::assert_debug_snapshot!(result);
7058    }
7059
7060    #[test]
7061    fn source_direct_has_subdir() {
7062        let data = r#"
7063version = 1
7064requires-python = ">=3.12"
7065
7066[[package]]
7067name = "anyio"
7068version = "4.3.0"
7069source = { url = "https://burntsushi.net", subdirectory = "wat/foo/bar" }
7070"#;
7071        let result: Result<Lock, _> = toml::from_str(data);
7072        insta::assert_debug_snapshot!(result);
7073    }
7074
7075    #[test]
7076    fn source_directory() {
7077        let data = r#"
7078version = 1
7079requires-python = ">=3.12"
7080
7081[[package]]
7082name = "anyio"
7083version = "4.3.0"
7084source = { directory = "path/to/dir" }
7085"#;
7086        let result: Result<Lock, _> = toml::from_str(data);
7087        insta::assert_debug_snapshot!(result);
7088    }
7089
7090    #[test]
7091    fn source_editable() {
7092        let data = r#"
7093version = 1
7094requires-python = ">=3.12"
7095
7096[[package]]
7097name = "anyio"
7098version = "4.3.0"
7099source = { editable = "path/to/dir" }
7100"#;
7101        let result: Result<Lock, _> = toml::from_str(data);
7102        insta::assert_debug_snapshot!(result);
7103    }
7104
7105    /// Windows drive letter paths like `C:/...` should be deserialized as local path registry
7106    /// sources, not as URLs. The `C:` prefix must not be misinterpreted as a URL scheme.
7107    #[test]
7108    fn registry_source_windows_drive_letter() {
7109        let data = r#"
7110version = 1
7111requires-python = ">=3.12"
7112
7113[[package]]
7114name = "tqdm"
7115version = "1000.0.0"
7116source = { registry = "C:/Users/user/links" }
7117wheels = [
7118    { path = "C:/Users/user/links/tqdm-1000.0.0-py3-none-any.whl" },
7119]
7120"#;
7121        let lock: Lock = toml::from_str(data).unwrap();
7122        assert_eq!(
7123            lock.packages[0].id.source,
7124            Source::Registry(RegistrySource::Path(
7125                Path::new("C:/Users/user/links").into()
7126            ))
7127        );
7128    }
7129}