Skip to main content

uv_resolver/lock/
mod.rs

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