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