Skip to main content

uv_resolver/lock/
mod.rs

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