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