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