Skip to main content

uv_resolver/lock/
mod.rs

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