Skip to main content

uv_resolver/lock/
mod.rs

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