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 = self.wheels[best_wheel_index]
2605                    .hash
2606                    .as_ref()
2607                    .map(|hash| HashDigests::from(vec![hash.0.clone()]))
2608                    .unwrap_or_else(|| HashDigests::from(vec![]));
2609
2610                let dist = match &self.id.source {
2611                    Source::Registry(source) => {
2612                        let wheels = self
2613                            .wheels
2614                            .iter()
2615                            .map(|wheel| wheel.to_registry_wheel(source, workspace_root))
2616                            .collect::<Result<_, LockError>>()?;
2617                        let reg_built_dist = RegistryBuiltDist {
2618                            wheels,
2619                            best_wheel_index,
2620                            sdist: None,
2621                        };
2622                        Dist::Built(BuiltDist::Registry(reg_built_dist))
2623                    }
2624                    Source::Path(path) => {
2625                        let filename: WheelFilename =
2626                            self.wheels[best_wheel_index].filename.clone();
2627                        let install_path = absolute_path(workspace_root, path)?;
2628                        let path_dist = PathBuiltDist {
2629                            filename,
2630                            url: verbatim_url(&install_path, &self.id)?,
2631                            install_path: absolute_path(workspace_root, path)?.into_boxed_path(),
2632                        };
2633                        let built_dist = BuiltDist::Path(path_dist);
2634                        Dist::Built(built_dist)
2635                    }
2636                    Source::Direct(url, direct) => {
2637                        let filename: WheelFilename =
2638                            self.wheels[best_wheel_index].filename.clone();
2639                        let url = DisplaySafeUrl::from(ParsedArchiveUrl {
2640                            url: url.to_url().map_err(LockErrorKind::InvalidUrl)?,
2641                            subdirectory: direct.subdirectory.clone(),
2642                            ext: DistExtension::Wheel,
2643                        });
2644                        let direct_dist = DirectUrlBuiltDist {
2645                            filename,
2646                            location: Box::new(url.clone()),
2647                            url: VerbatimUrl::from_url(url),
2648                        };
2649                        let built_dist = BuiltDist::DirectUrl(direct_dist);
2650                        Dist::Built(built_dist)
2651                    }
2652                    Source::Git(_, _) => {
2653                        return Err(LockErrorKind::InvalidWheelSource {
2654                            id: self.id.clone(),
2655                            source_type: "Git",
2656                        }
2657                        .into());
2658                    }
2659                    Source::Directory(_) => {
2660                        return Err(LockErrorKind::InvalidWheelSource {
2661                            id: self.id.clone(),
2662                            source_type: "directory",
2663                        }
2664                        .into());
2665                    }
2666                    Source::Editable(_) => {
2667                        return Err(LockErrorKind::InvalidWheelSource {
2668                            id: self.id.clone(),
2669                            source_type: "editable",
2670                        }
2671                        .into());
2672                    }
2673                    Source::Virtual(_) => {
2674                        return Err(LockErrorKind::InvalidWheelSource {
2675                            id: self.id.clone(),
2676                            source_type: "virtual",
2677                        }
2678                        .into());
2679                    }
2680                };
2681
2682                return Ok(HashedDist { dist, hashes });
2683            }
2684        }
2685
2686        if let Some(sdist) = self.to_source_dist(workspace_root)? {
2687            // Even with `--no-build`, allow virtual packages. (In the future, we may want to allow
2688            // any local source tree, or at least editable source trees, which we allow in
2689            // `uv pip`.)
2690            if !no_build || sdist.is_virtual() {
2691                let hashes = self
2692                    .sdist
2693                    .as_ref()
2694                    .and_then(|s| s.hash())
2695                    .map(|hash| HashDigests::from(vec![hash.0.clone()]))
2696                    .unwrap_or_else(|| HashDigests::from(vec![]));
2697                return Ok(HashedDist {
2698                    dist: Dist::Source(sdist),
2699                    hashes,
2700                });
2701            }
2702        }
2703
2704        match (no_binary, no_build) {
2705            (true, true) => Err(LockErrorKind::NoBinaryNoBuild {
2706                id: self.id.clone(),
2707            }
2708            .into()),
2709            (true, false) if self.id.source.is_wheel() => Err(LockErrorKind::NoBinaryWheelOnly {
2710                id: self.id.clone(),
2711            }
2712            .into()),
2713            (true, false) => Err(LockErrorKind::NoBinary {
2714                id: self.id.clone(),
2715            }
2716            .into()),
2717            (false, true) => Err(LockErrorKind::NoBuild {
2718                id: self.id.clone(),
2719            }
2720            .into()),
2721            (false, false) if self.id.source.is_wheel() => Err(LockError {
2722                kind: Box::new(LockErrorKind::IncompatibleWheelOnly {
2723                    id: self.id.clone(),
2724                }),
2725                hint: self.tag_hint(tag_policy, markers),
2726            }),
2727            (false, false) => Err(LockError {
2728                kind: Box::new(LockErrorKind::NeitherSourceDistNorWheel {
2729                    id: self.id.clone(),
2730                }),
2731                hint: self.tag_hint(tag_policy, markers),
2732            }),
2733        }
2734    }
2735
2736    /// Generate a [`WheelTagHint`] based on wheel-tag incompatibilities.
2737    fn tag_hint(
2738        &self,
2739        tag_policy: TagPolicy<'_>,
2740        markers: &MarkerEnvironment,
2741    ) -> Option<WheelTagHint> {
2742        let filenames = self
2743            .wheels
2744            .iter()
2745            .map(|wheel| &wheel.filename)
2746            .collect::<Vec<_>>();
2747        WheelTagHint::from_wheels(
2748            &self.id.name,
2749            self.id.version.as_ref(),
2750            &filenames,
2751            tag_policy.tags(),
2752            markers,
2753        )
2754    }
2755
2756    /// Convert the source of this [`Package`] to a [`SourceDist`] that can be used in installation.
2757    ///
2758    /// Returns `Ok(None)` if the source cannot be converted because `self.sdist` is `None`. This is required
2759    /// for registry sources.
2760    fn to_source_dist(
2761        &self,
2762        workspace_root: &Path,
2763    ) -> Result<Option<uv_distribution_types::SourceDist>, LockError> {
2764        let sdist = match &self.id.source {
2765            Source::Path(path) => {
2766                // A direct path source can also be a wheel, so validate the extension.
2767                let DistExtension::Source(ext) = DistExtension::from_path(path).map_err(|err| {
2768                    LockErrorKind::MissingExtension {
2769                        id: self.id.clone(),
2770                        err,
2771                    }
2772                })?
2773                else {
2774                    return Ok(None);
2775                };
2776                let install_path = absolute_path(workspace_root, path)?;
2777                let path_dist = PathSourceDist {
2778                    name: self.id.name.clone(),
2779                    version: self.id.version.clone(),
2780                    url: verbatim_url(&install_path, &self.id)?,
2781                    install_path: install_path.into_boxed_path(),
2782                    ext,
2783                };
2784                uv_distribution_types::SourceDist::Path(path_dist)
2785            }
2786            Source::Directory(path) => {
2787                let install_path = absolute_path(workspace_root, path)?;
2788                let dir_dist = DirectorySourceDist {
2789                    name: self.id.name.clone(),
2790                    url: verbatim_url(&install_path, &self.id)?,
2791                    install_path: install_path.into_boxed_path(),
2792                    editable: Some(false),
2793                    r#virtual: Some(false),
2794                };
2795                uv_distribution_types::SourceDist::Directory(dir_dist)
2796            }
2797            Source::Editable(path) => {
2798                let install_path = absolute_path(workspace_root, path)?;
2799                let dir_dist = DirectorySourceDist {
2800                    name: self.id.name.clone(),
2801                    url: verbatim_url(&install_path, &self.id)?,
2802                    install_path: install_path.into_boxed_path(),
2803                    editable: Some(true),
2804                    r#virtual: Some(false),
2805                };
2806                uv_distribution_types::SourceDist::Directory(dir_dist)
2807            }
2808            Source::Virtual(path) => {
2809                let install_path = absolute_path(workspace_root, path)?;
2810                let dir_dist = DirectorySourceDist {
2811                    name: self.id.name.clone(),
2812                    url: verbatim_url(&install_path, &self.id)?,
2813                    install_path: install_path.into_boxed_path(),
2814                    editable: Some(false),
2815                    r#virtual: Some(true),
2816                };
2817                uv_distribution_types::SourceDist::Directory(dir_dist)
2818            }
2819            Source::Git(url, git) => {
2820                // Remove the fragment and query from the URL; they're already present in the
2821                // `GitSource`.
2822                let mut url = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
2823                url.set_fragment(None);
2824                url.set_query(None);
2825
2826                // Reconstruct the `GitUrl` from the `GitSource`.
2827                let git_url = GitUrl::from_commit(
2828                    url,
2829                    GitReference::from(git.kind.clone()),
2830                    git.precise,
2831                    git.lfs,
2832                )?;
2833
2834                // Reconstruct the PEP 508-compatible URL from the `GitSource`.
2835                let url = DisplaySafeUrl::from(ParsedGitUrl {
2836                    url: git_url.clone(),
2837                    subdirectory: git.subdirectory.clone(),
2838                });
2839
2840                let git_dist = GitSourceDist {
2841                    name: self.id.name.clone(),
2842                    url: VerbatimUrl::from_url(url),
2843                    git: Box::new(git_url),
2844                    subdirectory: git.subdirectory.clone(),
2845                };
2846                uv_distribution_types::SourceDist::Git(git_dist)
2847            }
2848            Source::Direct(url, direct) => {
2849                // A direct URL source can also be a wheel, so validate the extension.
2850                let DistExtension::Source(ext) =
2851                    DistExtension::from_path(url.base_str()).map_err(|err| {
2852                        LockErrorKind::MissingExtension {
2853                            id: self.id.clone(),
2854                            err,
2855                        }
2856                    })?
2857                else {
2858                    return Ok(None);
2859                };
2860                let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?;
2861                let url = DisplaySafeUrl::from(ParsedArchiveUrl {
2862                    url: location.clone(),
2863                    subdirectory: direct.subdirectory.clone(),
2864                    ext: DistExtension::Source(ext),
2865                });
2866                let direct_dist = DirectUrlSourceDist {
2867                    name: self.id.name.clone(),
2868                    location: Box::new(location),
2869                    subdirectory: direct.subdirectory.clone(),
2870                    ext,
2871                    url: VerbatimUrl::from_url(url),
2872                };
2873                uv_distribution_types::SourceDist::DirectUrl(direct_dist)
2874            }
2875            Source::Registry(RegistrySource::Url(url)) => {
2876                let Some(ref sdist) = self.sdist else {
2877                    return Ok(None);
2878                };
2879
2880                let name = &self.id.name;
2881                let version = self
2882                    .id
2883                    .version
2884                    .as_ref()
2885                    .expect("version for registry source");
2886
2887                let file_url = sdist.url().ok_or_else(|| LockErrorKind::MissingUrl {
2888                    name: name.clone(),
2889                    version: version.clone(),
2890                })?;
2891                let filename = sdist
2892                    .filename()
2893                    .ok_or_else(|| LockErrorKind::MissingFilename {
2894                        id: self.id.clone(),
2895                    })?;
2896                let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
2897                    LockErrorKind::MissingExtension {
2898                        id: self.id.clone(),
2899                        err,
2900                    }
2901                })?;
2902                let file = Box::new(uv_distribution_types::File {
2903                    dist_info_metadata: false,
2904                    filename: SmallString::from(filename),
2905                    hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
2906                        HashDigests::from(hash.0.clone())
2907                    }),
2908                    requires_python: None,
2909                    size: sdist.size(),
2910                    upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
2911                    url: FileLocation::AbsoluteUrl(file_url.clone()),
2912                    yanked: None,
2913                    zstd: None,
2914                });
2915
2916                let index = IndexUrl::from(VerbatimUrl::from_url(
2917                    url.to_url().map_err(LockErrorKind::InvalidUrl)?,
2918                ));
2919
2920                let reg_dist = RegistrySourceDist {
2921                    name: name.clone(),
2922                    version: version.clone(),
2923                    file,
2924                    ext,
2925                    index,
2926                    wheels: vec![],
2927                };
2928                uv_distribution_types::SourceDist::Registry(reg_dist)
2929            }
2930            Source::Registry(RegistrySource::Path(path)) => {
2931                let Some(ref sdist) = self.sdist else {
2932                    return Ok(None);
2933                };
2934
2935                let name = &self.id.name;
2936                let version = self
2937                    .id
2938                    .version
2939                    .as_ref()
2940                    .expect("version for registry source");
2941
2942                let file_url = match sdist {
2943                    SourceDist::Url { url: file_url, .. } => {
2944                        FileLocation::AbsoluteUrl(file_url.clone())
2945                    }
2946                    SourceDist::Path {
2947                        path: file_path, ..
2948                    } => {
2949                        let file_path = workspace_root.join(path).join(file_path);
2950                        let file_url =
2951                            DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
2952                                LockErrorKind::PathToUrl {
2953                                    path: file_path.into_boxed_path(),
2954                                }
2955                            })?;
2956                        FileLocation::AbsoluteUrl(UrlString::from(file_url))
2957                    }
2958                    SourceDist::Metadata { .. } => {
2959                        return Err(LockErrorKind::MissingPath {
2960                            name: name.clone(),
2961                            version: version.clone(),
2962                        }
2963                        .into());
2964                    }
2965                };
2966                let filename = sdist
2967                    .filename()
2968                    .ok_or_else(|| LockErrorKind::MissingFilename {
2969                        id: self.id.clone(),
2970                    })?;
2971                let ext = SourceDistExtension::from_path(filename.as_ref()).map_err(|err| {
2972                    LockErrorKind::MissingExtension {
2973                        id: self.id.clone(),
2974                        err,
2975                    }
2976                })?;
2977                let file = Box::new(uv_distribution_types::File {
2978                    dist_info_metadata: false,
2979                    filename: SmallString::from(filename),
2980                    hashes: sdist.hash().map_or(HashDigests::empty(), |hash| {
2981                        HashDigests::from(hash.0.clone())
2982                    }),
2983                    requires_python: None,
2984                    size: sdist.size(),
2985                    upload_time_utc_ms: sdist.upload_time().map(Timestamp::as_millisecond),
2986                    url: file_url,
2987                    yanked: None,
2988                    zstd: None,
2989                });
2990
2991                let index = IndexUrl::from(
2992                    VerbatimUrl::from_absolute_path(workspace_root.join(path))
2993                        .map_err(LockErrorKind::RegistryVerbatimUrl)?,
2994                );
2995
2996                let reg_dist = RegistrySourceDist {
2997                    name: name.clone(),
2998                    version: version.clone(),
2999                    file,
3000                    ext,
3001                    index,
3002                    wheels: vec![],
3003                };
3004                uv_distribution_types::SourceDist::Registry(reg_dist)
3005            }
3006        };
3007
3008        Ok(Some(sdist))
3009    }
3010
3011    fn to_toml(
3012        &self,
3013        requires_python: &RequiresPython,
3014        dist_count_by_name: &FxHashMap<PackageName, u64>,
3015    ) -> Result<Table, toml_edit::ser::Error> {
3016        let mut table = Table::new();
3017
3018        self.id.to_toml(None, &mut table);
3019
3020        if !self.fork_markers.is_empty() {
3021            let fork_markers = each_element_on_its_line_array(
3022                simplified_universal_markers(&self.fork_markers, requires_python).into_iter(),
3023            );
3024            if !fork_markers.is_empty() {
3025                table.insert("resolution-markers", value(fork_markers));
3026            }
3027        }
3028
3029        if !self.dependencies.is_empty() {
3030            let deps = each_element_on_its_line_array(self.dependencies.iter().map(|dep| {
3031                dep.to_toml(requires_python, dist_count_by_name)
3032                    .into_inline_table()
3033            }));
3034            table.insert("dependencies", value(deps));
3035        }
3036
3037        if !self.optional_dependencies.is_empty() {
3038            let mut optional_deps = Table::new();
3039            for (extra, deps) in &self.optional_dependencies {
3040                let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3041                    dep.to_toml(requires_python, dist_count_by_name)
3042                        .into_inline_table()
3043                }));
3044                if !deps.is_empty() {
3045                    optional_deps.insert(extra.as_ref(), value(deps));
3046                }
3047            }
3048            if !optional_deps.is_empty() {
3049                table.insert("optional-dependencies", Item::Table(optional_deps));
3050            }
3051        }
3052
3053        if !self.dependency_groups.is_empty() {
3054            let mut dependency_groups = Table::new();
3055            for (extra, deps) in &self.dependency_groups {
3056                let deps = each_element_on_its_line_array(deps.iter().map(|dep| {
3057                    dep.to_toml(requires_python, dist_count_by_name)
3058                        .into_inline_table()
3059                }));
3060                if !deps.is_empty() {
3061                    dependency_groups.insert(extra.as_ref(), value(deps));
3062                }
3063            }
3064            if !dependency_groups.is_empty() {
3065                table.insert("dev-dependencies", Item::Table(dependency_groups));
3066            }
3067        }
3068
3069        if let Some(ref sdist) = self.sdist {
3070            table.insert("sdist", value(sdist.to_toml()?));
3071        }
3072
3073        if !self.wheels.is_empty() {
3074            let wheels = each_element_on_its_line_array(
3075                self.wheels
3076                    .iter()
3077                    .map(Wheel::to_toml)
3078                    .collect::<Result<Vec<_>, _>>()?
3079                    .into_iter(),
3080            );
3081            table.insert("wheels", value(wheels));
3082        }
3083
3084        // Write the package metadata, if non-empty.
3085        {
3086            let mut metadata_table = Table::new();
3087
3088            if !self.metadata.requires_dist.is_empty() {
3089                let requires_dist = self
3090                    .metadata
3091                    .requires_dist
3092                    .iter()
3093                    .map(|requirement| {
3094                        serde::Serialize::serialize(
3095                            &requirement,
3096                            toml_edit::ser::ValueSerializer::new(),
3097                        )
3098                    })
3099                    .collect::<Result<Vec<_>, _>>()?;
3100                let requires_dist = match requires_dist.as_slice() {
3101                    [] => Array::new(),
3102                    [requirement] => Array::from_iter([requirement]),
3103                    requires_dist => each_element_on_its_line_array(requires_dist.iter()),
3104                };
3105                metadata_table.insert("requires-dist", value(requires_dist));
3106            }
3107
3108            if !self.metadata.dependency_groups.is_empty() {
3109                let mut dependency_groups = Table::new();
3110                for (extra, deps) in &self.metadata.dependency_groups {
3111                    let deps = deps
3112                        .iter()
3113                        .map(|requirement| {
3114                            serde::Serialize::serialize(
3115                                &requirement,
3116                                toml_edit::ser::ValueSerializer::new(),
3117                            )
3118                        })
3119                        .collect::<Result<Vec<_>, _>>()?;
3120                    let deps = match deps.as_slice() {
3121                        [] => Array::new(),
3122                        [requirement] => Array::from_iter([requirement]),
3123                        deps => each_element_on_its_line_array(deps.iter()),
3124                    };
3125                    dependency_groups.insert(extra.as_ref(), value(deps));
3126                }
3127                if !dependency_groups.is_empty() {
3128                    metadata_table.insert("requires-dev", Item::Table(dependency_groups));
3129                }
3130            }
3131
3132            if !self.metadata.provides_extra.is_empty() {
3133                let provides_extras = self
3134                    .metadata
3135                    .provides_extra
3136                    .iter()
3137                    .map(|extra| {
3138                        serde::Serialize::serialize(&extra, toml_edit::ser::ValueSerializer::new())
3139                    })
3140                    .collect::<Result<Vec<_>, _>>()?;
3141                // This is just a list of names, so linebreaking it is excessive.
3142                let provides_extras = Array::from_iter(provides_extras);
3143                metadata_table.insert("provides-extras", value(provides_extras));
3144            }
3145
3146            if !metadata_table.is_empty() {
3147                table.insert("metadata", Item::Table(metadata_table));
3148            }
3149        }
3150
3151        Ok(table)
3152    }
3153
3154    fn find_best_wheel(&self, tag_policy: TagPolicy<'_>) -> Option<usize> {
3155        type WheelPriority<'lock> = (TagPriority, Option<&'lock BuildTag>);
3156
3157        let mut best: Option<(WheelPriority, usize)> = None;
3158        for (i, wheel) in self.wheels.iter().enumerate() {
3159            let TagCompatibility::Compatible(tag_priority) =
3160                wheel.filename.compatibility(tag_policy.tags())
3161            else {
3162                continue;
3163            };
3164            let build_tag = wheel.filename.build_tag();
3165            let wheel_priority = (tag_priority, build_tag);
3166            match best {
3167                None => {
3168                    best = Some((wheel_priority, i));
3169                }
3170                Some((best_priority, _)) => {
3171                    if wheel_priority > best_priority {
3172                        best = Some((wheel_priority, i));
3173                    }
3174                }
3175            }
3176        }
3177
3178        let best = best.map(|(_, i)| i);
3179        match tag_policy {
3180            TagPolicy::Required(_) => best,
3181            TagPolicy::Preferred(_) => best.or_else(|| self.wheels.first().map(|_| 0)),
3182        }
3183    }
3184
3185    /// Returns the [`PackageName`] of the package.
3186    pub fn name(&self) -> &PackageName {
3187        &self.id.name
3188    }
3189
3190    /// Returns the [`Version`] of the package.
3191    pub fn version(&self) -> Option<&Version> {
3192        self.id.version.as_ref()
3193    }
3194
3195    /// Returns the Git SHA of the package, if it is a Git source.
3196    pub fn git_sha(&self) -> Option<&GitOid> {
3197        match &self.id.source {
3198            Source::Git(_, git) => Some(&git.precise),
3199            _ => None,
3200        }
3201    }
3202
3203    /// Return the fork markers for this package, if any.
3204    pub fn fork_markers(&self) -> &[UniversalMarker] {
3205        self.fork_markers.as_slice()
3206    }
3207
3208    /// Returns the [`IndexUrl`] for the package, if it is a registry source.
3209    pub fn index(&self, root: &Path) -> Result<Option<IndexUrl>, LockError> {
3210        match &self.id.source {
3211            Source::Registry(RegistrySource::Url(url)) => {
3212                let index = IndexUrl::from(VerbatimUrl::from_url(
3213                    url.to_url().map_err(LockErrorKind::InvalidUrl)?,
3214                ));
3215                Ok(Some(index))
3216            }
3217            Source::Registry(RegistrySource::Path(path)) => {
3218                let index = IndexUrl::from(
3219                    VerbatimUrl::from_absolute_path(root.join(path))
3220                        .map_err(LockErrorKind::RegistryVerbatimUrl)?,
3221                );
3222                Ok(Some(index))
3223            }
3224            _ => Ok(None),
3225        }
3226    }
3227
3228    /// Returns all the hashes associated with this [`Package`].
3229    fn hashes(&self) -> HashDigests {
3230        let mut hashes = Vec::with_capacity(
3231            usize::from(self.sdist.as_ref().and_then(|sdist| sdist.hash()).is_some())
3232                + self
3233                    .wheels
3234                    .iter()
3235                    .map(|wheel| usize::from(wheel.hash.is_some()))
3236                    .sum::<usize>(),
3237        );
3238        if let Some(ref sdist) = self.sdist {
3239            if let Some(hash) = sdist.hash() {
3240                hashes.push(hash.0.clone());
3241            }
3242        }
3243        for wheel in &self.wheels {
3244            hashes.extend(wheel.hash.as_ref().map(|h| h.0.clone()));
3245            if let Some(zstd) = wheel.zstd.as_ref() {
3246                hashes.extend(zstd.hash.as_ref().map(|h| h.0.clone()));
3247            }
3248        }
3249        HashDigests::from(hashes)
3250    }
3251
3252    /// Returns the [`ResolvedRepositoryReference`] for the package, if it is a Git source.
3253    pub fn as_git_ref(&self) -> Result<Option<ResolvedRepositoryReference>, LockError> {
3254        match &self.id.source {
3255            Source::Git(url, git) => Ok(Some(ResolvedRepositoryReference {
3256                reference: RepositoryReference {
3257                    url: RepositoryUrl::new(&url.to_url().map_err(LockErrorKind::InvalidUrl)?),
3258                    reference: GitReference::from(git.kind.clone()),
3259                },
3260                sha: git.precise,
3261            })),
3262            _ => Ok(None),
3263        }
3264    }
3265
3266    /// Returns `true` if the package is a dynamic source tree.
3267    fn is_dynamic(&self) -> bool {
3268        self.id.version.is_none()
3269    }
3270
3271    /// Returns the extras the package provides, if any.
3272    pub fn provides_extras(&self) -> &[ExtraName] {
3273        &self.metadata.provides_extra
3274    }
3275
3276    /// Returns the dependency groups the package provides, if any.
3277    pub fn dependency_groups(&self) -> &BTreeMap<GroupName, BTreeSet<Requirement>> {
3278        &self.metadata.dependency_groups
3279    }
3280
3281    /// Returns the dependencies of the package.
3282    pub fn dependencies(&self) -> &[Dependency] {
3283        &self.dependencies
3284    }
3285
3286    /// Returns the optional dependencies of the package.
3287    pub fn optional_dependencies(&self) -> &BTreeMap<ExtraName, Vec<Dependency>> {
3288        &self.optional_dependencies
3289    }
3290
3291    /// Returns the resolved PEP 735 dependency groups of the package.
3292    pub fn resolved_dependency_groups(&self) -> &BTreeMap<GroupName, Vec<Dependency>> {
3293        &self.dependency_groups
3294    }
3295
3296    /// Returns an [`InstallTarget`] view for filtering decisions.
3297    pub fn as_install_target(&self) -> InstallTarget<'_> {
3298        InstallTarget {
3299            name: self.name(),
3300            is_local: self.id.source.is_local(),
3301        }
3302    }
3303}
3304
3305/// Attempts to construct a `VerbatimUrl` from the given normalized `Path`.
3306fn verbatim_url(path: &Path, id: &PackageId) -> Result<VerbatimUrl, LockError> {
3307    let url =
3308        VerbatimUrl::from_normalized_path(path).map_err(|err| LockErrorKind::VerbatimUrl {
3309            id: id.clone(),
3310            err,
3311        })?;
3312    Ok(url)
3313}
3314
3315/// Attempts to construct an absolute path from the given `Path`.
3316fn absolute_path(workspace_root: &Path, path: &Path) -> Result<PathBuf, LockError> {
3317    let path = uv_fs::normalize_absolute_path(&workspace_root.join(path))
3318        .map_err(LockErrorKind::AbsolutePath)?;
3319    Ok(path)
3320}
3321
3322#[derive(Clone, Debug, serde::Deserialize)]
3323#[serde(rename_all = "kebab-case")]
3324struct PackageWire {
3325    #[serde(flatten)]
3326    id: PackageId,
3327    #[serde(default)]
3328    metadata: PackageMetadata,
3329    #[serde(default)]
3330    sdist: Option<SourceDist>,
3331    #[serde(default)]
3332    wheels: Vec<Wheel>,
3333    #[serde(default, rename = "resolution-markers")]
3334    fork_markers: Vec<SimplifiedMarkerTree>,
3335    #[serde(default)]
3336    dependencies: Vec<DependencyWire>,
3337    #[serde(default)]
3338    optional_dependencies: BTreeMap<ExtraName, Vec<DependencyWire>>,
3339    #[serde(default, rename = "dev-dependencies", alias = "dependency-groups")]
3340    dependency_groups: BTreeMap<GroupName, Vec<DependencyWire>>,
3341}
3342
3343#[derive(Clone, Default, Debug, Eq, PartialEq, serde::Deserialize)]
3344#[serde(rename_all = "kebab-case")]
3345struct PackageMetadata {
3346    #[serde(default)]
3347    requires_dist: BTreeSet<Requirement>,
3348    #[serde(default, rename = "provides-extras")]
3349    provides_extra: Box<[ExtraName]>,
3350    #[serde(default, rename = "requires-dev", alias = "dependency-groups")]
3351    dependency_groups: BTreeMap<GroupName, BTreeSet<Requirement>>,
3352}
3353
3354impl PackageWire {
3355    fn unwire(
3356        self,
3357        requires_python: &RequiresPython,
3358        unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3359    ) -> Result<Package, LockError> {
3360        // Consistency check
3361        if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) {
3362            if let Some(version) = &self.id.version {
3363                for wheel in &self.wheels {
3364                    if *version != wheel.filename.version
3365                        && *version != wheel.filename.version.clone().without_local()
3366                    {
3367                        return Err(LockError::from(LockErrorKind::InconsistentVersions {
3368                            name: self.id.name,
3369                            version: version.clone(),
3370                            wheel: wheel.clone(),
3371                        }));
3372                    }
3373                }
3374                // We can't check the source dist version since it does not need to contain the version
3375                // in the filename.
3376            }
3377        }
3378
3379        let unwire_deps = |deps: Vec<DependencyWire>| -> Result<Vec<Dependency>, LockError> {
3380            deps.into_iter()
3381                .map(|dep| dep.unwire(requires_python, unambiguous_package_ids))
3382                .collect()
3383        };
3384
3385        Ok(Package {
3386            id: self.id,
3387            metadata: self.metadata,
3388            sdist: self.sdist,
3389            wheels: self.wheels,
3390            fork_markers: self
3391                .fork_markers
3392                .into_iter()
3393                .map(|simplified_marker| simplified_marker.into_marker(requires_python))
3394                .map(UniversalMarker::from_combined)
3395                .collect(),
3396            dependencies: unwire_deps(self.dependencies)?,
3397            optional_dependencies: self
3398                .optional_dependencies
3399                .into_iter()
3400                .map(|(extra, deps)| Ok((extra, unwire_deps(deps)?)))
3401                .collect::<Result<_, LockError>>()?,
3402            dependency_groups: self
3403                .dependency_groups
3404                .into_iter()
3405                .map(|(group, deps)| Ok((group, unwire_deps(deps)?)))
3406                .collect::<Result<_, LockError>>()?,
3407        })
3408    }
3409}
3410
3411/// Inside the lockfile, we match a dependency entry to a package entry through a key made up
3412/// of the name, the version and the source url.
3413#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3414#[serde(rename_all = "kebab-case")]
3415pub(crate) struct PackageId {
3416    pub(crate) name: PackageName,
3417    pub(crate) version: Option<Version>,
3418    source: Source,
3419}
3420
3421impl PackageId {
3422    fn from_annotated_dist(annotated_dist: &AnnotatedDist, root: &Path) -> Result<Self, LockError> {
3423        // Identify the source of the package.
3424        let source = Source::from_resolved_dist(&annotated_dist.dist, root)?;
3425        // Omit versions for dynamic source trees.
3426        let version = if source.is_source_tree()
3427            && annotated_dist
3428                .metadata
3429                .as_ref()
3430                .is_some_and(|metadata| metadata.dynamic)
3431        {
3432            None
3433        } else {
3434            Some(annotated_dist.version.clone())
3435        };
3436        let name = annotated_dist.name.clone();
3437        Ok(Self {
3438            name,
3439            version,
3440            source,
3441        })
3442    }
3443
3444    /// Writes this package ID inline into the table given.
3445    ///
3446    /// When a map is given, and if the package name in this ID is unambiguous
3447    /// (i.e., it has a count of 1 in the map), then the `version` and `source`
3448    /// fields are omitted. In all other cases, including when a map is not
3449    /// given, the `version` and `source` fields are written.
3450    fn to_toml(&self, dist_count_by_name: Option<&FxHashMap<PackageName, u64>>, table: &mut Table) {
3451        let count = dist_count_by_name.and_then(|map| map.get(&self.name).copied());
3452        table.insert("name", value(self.name.to_string()));
3453        if count.map(|count| count > 1).unwrap_or(true) {
3454            if let Some(version) = &self.version {
3455                table.insert("version", value(version.to_string()));
3456            }
3457            self.source.to_toml(table);
3458        }
3459    }
3460}
3461
3462impl Display for PackageId {
3463    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3464        if let Some(version) = &self.version {
3465            write!(f, "{}=={} @ {}", self.name, version, self.source)
3466        } else {
3467            write!(f, "{} @ {}", self.name, self.source)
3468        }
3469    }
3470}
3471
3472#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3473#[serde(rename_all = "kebab-case")]
3474struct PackageIdForDependency {
3475    name: PackageName,
3476    version: Option<Version>,
3477    source: Option<Source>,
3478}
3479
3480impl PackageIdForDependency {
3481    fn unwire(
3482        self,
3483        unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
3484    ) -> Result<PackageId, LockError> {
3485        let unambiguous_package_id = unambiguous_package_ids.get(&self.name);
3486        let source = self.source.map(Ok::<_, LockError>).unwrap_or_else(|| {
3487            let Some(package_id) = unambiguous_package_id else {
3488                return Err(LockErrorKind::MissingDependencySource {
3489                    name: self.name.clone(),
3490                }
3491                .into());
3492            };
3493            Ok(package_id.source.clone())
3494        })?;
3495        let version = if let Some(version) = self.version {
3496            Some(version)
3497        } else {
3498            if let Some(package_id) = unambiguous_package_id {
3499                package_id.version.clone()
3500            } else {
3501                // If the package is a source tree, assume that the missing `self.version` field is
3502                // indicative of a dynamic version.
3503                if source.is_source_tree() {
3504                    None
3505                } else {
3506                    return Err(LockErrorKind::MissingDependencyVersion {
3507                        name: self.name.clone(),
3508                    }
3509                    .into());
3510                }
3511            }
3512        };
3513        Ok(PackageId {
3514            name: self.name,
3515            version,
3516            source,
3517        })
3518    }
3519}
3520
3521impl From<PackageId> for PackageIdForDependency {
3522    fn from(id: PackageId) -> Self {
3523        Self {
3524            name: id.name,
3525            version: id.version,
3526            source: Some(id.source),
3527        }
3528    }
3529}
3530
3531/// A unique identifier to differentiate between different sources for the same version of a
3532/// package.
3533///
3534/// NOTE: Care should be taken when adding variants to this enum. Namely, new
3535/// variants should be added without changing the relative ordering of other
3536/// variants. Otherwise, this could cause the lockfile to have a different
3537/// canonical ordering of sources.
3538#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3539#[serde(try_from = "SourceWire")]
3540enum Source {
3541    /// A registry or `--find-links` index.
3542    Registry(RegistrySource),
3543    /// A Git repository.
3544    Git(UrlString, GitSource),
3545    /// A direct HTTP(S) URL.
3546    Direct(UrlString, DirectSource),
3547    /// A path to a local source or built archive.
3548    Path(Box<Path>),
3549    /// A path to a local directory.
3550    Directory(Box<Path>),
3551    /// A path to a local directory that should be installed as editable.
3552    Editable(Box<Path>),
3553    /// A path to a local directory that should not be built or installed.
3554    Virtual(Box<Path>),
3555}
3556
3557impl Source {
3558    fn from_resolved_dist(resolved_dist: &ResolvedDist, root: &Path) -> Result<Self, LockError> {
3559        match *resolved_dist {
3560            // We pass empty installed packages for locking.
3561            ResolvedDist::Installed { .. } => unreachable!(),
3562            ResolvedDist::Installable { ref dist, .. } => Self::from_dist(dist, root),
3563        }
3564    }
3565
3566    fn from_dist(dist: &Dist, root: &Path) -> Result<Self, LockError> {
3567        match *dist {
3568            Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, root),
3569            Dist::Source(ref source_dist) => Self::from_source_dist(source_dist, root),
3570        }
3571    }
3572
3573    fn from_built_dist(built_dist: &BuiltDist, root: &Path) -> Result<Self, LockError> {
3574        match *built_dist {
3575            BuiltDist::Registry(ref reg_dist) => Self::from_registry_built_dist(reg_dist, root),
3576            BuiltDist::DirectUrl(ref direct_dist) => Ok(Self::from_direct_built_dist(direct_dist)),
3577            BuiltDist::Path(ref path_dist) => Self::from_path_built_dist(path_dist, root),
3578        }
3579    }
3580
3581    fn from_source_dist(
3582        source_dist: &uv_distribution_types::SourceDist,
3583        root: &Path,
3584    ) -> Result<Self, LockError> {
3585        match *source_dist {
3586            uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
3587                Self::from_registry_source_dist(reg_dist, root)
3588            }
3589            uv_distribution_types::SourceDist::DirectUrl(ref direct_dist) => {
3590                Ok(Self::from_direct_source_dist(direct_dist))
3591            }
3592            uv_distribution_types::SourceDist::Git(ref git_dist) => {
3593                Ok(Self::from_git_dist(git_dist))
3594            }
3595            uv_distribution_types::SourceDist::Path(ref path_dist) => {
3596                Self::from_path_source_dist(path_dist, root)
3597            }
3598            uv_distribution_types::SourceDist::Directory(ref directory) => {
3599                Self::from_directory_source_dist(directory, root)
3600            }
3601        }
3602    }
3603
3604    fn from_registry_built_dist(
3605        reg_dist: &RegistryBuiltDist,
3606        root: &Path,
3607    ) -> Result<Self, LockError> {
3608        Self::from_index_url(&reg_dist.best_wheel().index, root)
3609    }
3610
3611    fn from_registry_source_dist(
3612        reg_dist: &RegistrySourceDist,
3613        root: &Path,
3614    ) -> Result<Self, LockError> {
3615        Self::from_index_url(&reg_dist.index, root)
3616    }
3617
3618    fn from_direct_built_dist(direct_dist: &DirectUrlBuiltDist) -> Self {
3619        Self::Direct(
3620            normalize_url(direct_dist.url.to_url()),
3621            DirectSource { subdirectory: None },
3622        )
3623    }
3624
3625    fn from_direct_source_dist(direct_dist: &DirectUrlSourceDist) -> Self {
3626        Self::Direct(
3627            normalize_url(direct_dist.url.to_url()),
3628            DirectSource {
3629                subdirectory: direct_dist.subdirectory.clone(),
3630            },
3631        )
3632    }
3633
3634    fn from_path_built_dist(path_dist: &PathBuiltDist, root: &Path) -> Result<Self, LockError> {
3635        let path = relative_to(&path_dist.install_path, root)
3636            .or_else(|_| std::path::absolute(&path_dist.install_path))
3637            .map_err(LockErrorKind::DistributionRelativePath)?;
3638        Ok(Self::Path(path.into_boxed_path()))
3639    }
3640
3641    fn from_path_source_dist(path_dist: &PathSourceDist, root: &Path) -> Result<Self, LockError> {
3642        let path = relative_to(&path_dist.install_path, root)
3643            .or_else(|_| std::path::absolute(&path_dist.install_path))
3644            .map_err(LockErrorKind::DistributionRelativePath)?;
3645        Ok(Self::Path(path.into_boxed_path()))
3646    }
3647
3648    fn from_directory_source_dist(
3649        directory_dist: &DirectorySourceDist,
3650        root: &Path,
3651    ) -> Result<Self, LockError> {
3652        let path = relative_to(&directory_dist.install_path, root)
3653            .or_else(|_| std::path::absolute(&directory_dist.install_path))
3654            .map_err(LockErrorKind::DistributionRelativePath)?;
3655        if directory_dist.editable.unwrap_or(false) {
3656            Ok(Self::Editable(path.into_boxed_path()))
3657        } else if directory_dist.r#virtual.unwrap_or(false) {
3658            Ok(Self::Virtual(path.into_boxed_path()))
3659        } else {
3660            Ok(Self::Directory(path.into_boxed_path()))
3661        }
3662    }
3663
3664    fn from_index_url(index_url: &IndexUrl, root: &Path) -> Result<Self, LockError> {
3665        match index_url {
3666            IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
3667                // Remove any sensitive credentials from the index URL.
3668                let redacted = index_url.without_credentials();
3669                let source = RegistrySource::Url(UrlString::from(redacted.as_ref()));
3670                Ok(Self::Registry(source))
3671            }
3672            IndexUrl::Path(url) => {
3673                let path = url
3674                    .to_file_path()
3675                    .map_err(|()| LockErrorKind::UrlToPath { url: url.to_url() })?;
3676                let path = relative_to(&path, root)
3677                    .or_else(|_| std::path::absolute(&path))
3678                    .map_err(LockErrorKind::IndexRelativePath)?;
3679                let source = RegistrySource::Path(path.into_boxed_path());
3680                Ok(Self::Registry(source))
3681            }
3682        }
3683    }
3684
3685    fn from_git_dist(git_dist: &GitSourceDist) -> Self {
3686        Self::Git(
3687            UrlString::from(locked_git_url(git_dist)),
3688            GitSource {
3689                kind: GitSourceKind::from(git_dist.git.reference().clone()),
3690                precise: git_dist.git.precise().unwrap_or_else(|| {
3691                    panic!("Git distribution is missing a precise hash: {git_dist}")
3692                }),
3693                subdirectory: git_dist.subdirectory.clone(),
3694                lfs: git_dist.git.lfs(),
3695            },
3696        )
3697    }
3698
3699    /// Returns `true` if the source should be considered immutable.
3700    ///
3701    /// We assume that registry sources are immutable. In other words, we expect that once a
3702    /// package-version is published to a registry, its metadata will not change.
3703    ///
3704    /// We also assume that Git sources are immutable, since a Git source encodes a specific commit.
3705    fn is_immutable(&self) -> bool {
3706        matches!(self, Self::Registry(..) | Self::Git(_, _))
3707    }
3708
3709    /// Returns `true` if the source is that of a wheel.
3710    fn is_wheel(&self) -> bool {
3711        match self {
3712            Self::Path(path) => {
3713                matches!(
3714                    DistExtension::from_path(path).ok(),
3715                    Some(DistExtension::Wheel)
3716                )
3717            }
3718            Self::Direct(url, _) => {
3719                matches!(
3720                    DistExtension::from_path(url.as_ref()).ok(),
3721                    Some(DistExtension::Wheel)
3722                )
3723            }
3724            Self::Directory(..) => false,
3725            Self::Editable(..) => false,
3726            Self::Virtual(..) => false,
3727            Self::Git(..) => false,
3728            Self::Registry(..) => false,
3729        }
3730    }
3731
3732    /// Returns `true` if the source is that of a source tree.
3733    fn is_source_tree(&self) -> bool {
3734        match self {
3735            Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => true,
3736            Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => false,
3737        }
3738    }
3739
3740    /// Returns the path to the source tree, if the source is a source tree.
3741    fn as_source_tree(&self) -> Option<&Path> {
3742        match self {
3743            Self::Directory(path) | Self::Editable(path) | Self::Virtual(path) => Some(path),
3744            Self::Path(..) | Self::Git(..) | Self::Registry(..) | Self::Direct(..) => None,
3745        }
3746    }
3747
3748    fn to_toml(&self, table: &mut Table) {
3749        let mut source_table = InlineTable::new();
3750        match self {
3751            Self::Registry(source) => match source {
3752                RegistrySource::Url(url) => {
3753                    source_table.insert("registry", Value::from(url.as_ref()));
3754                }
3755                RegistrySource::Path(path) => {
3756                    source_table.insert(
3757                        "registry",
3758                        Value::from(PortablePath::from(path).to_string()),
3759                    );
3760                }
3761            },
3762            Self::Git(url, _) => {
3763                source_table.insert("git", Value::from(url.as_ref()));
3764            }
3765            Self::Direct(url, DirectSource { subdirectory }) => {
3766                source_table.insert("url", Value::from(url.as_ref()));
3767                if let Some(ref subdirectory) = *subdirectory {
3768                    source_table.insert(
3769                        "subdirectory",
3770                        Value::from(PortablePath::from(subdirectory).to_string()),
3771                    );
3772                }
3773            }
3774            Self::Path(path) => {
3775                source_table.insert("path", Value::from(PortablePath::from(path).to_string()));
3776            }
3777            Self::Directory(path) => {
3778                source_table.insert(
3779                    "directory",
3780                    Value::from(PortablePath::from(path).to_string()),
3781                );
3782            }
3783            Self::Editable(path) => {
3784                source_table.insert(
3785                    "editable",
3786                    Value::from(PortablePath::from(path).to_string()),
3787                );
3788            }
3789            Self::Virtual(path) => {
3790                source_table.insert("virtual", Value::from(PortablePath::from(path).to_string()));
3791            }
3792        }
3793        table.insert("source", value(source_table));
3794    }
3795
3796    /// Check if a package is local by examining its source.
3797    pub(crate) fn is_local(&self) -> bool {
3798        matches!(
3799            self,
3800            Self::Path(_) | Self::Directory(_) | Self::Editable(_) | Self::Virtual(_)
3801        )
3802    }
3803}
3804
3805impl Display for Source {
3806    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3807        match self {
3808            Self::Registry(RegistrySource::Url(url)) | Self::Git(url, _) | Self::Direct(url, _) => {
3809                write!(f, "{}+{}", self.name(), url)
3810            }
3811            Self::Registry(RegistrySource::Path(path))
3812            | Self::Path(path)
3813            | Self::Directory(path)
3814            | Self::Editable(path)
3815            | Self::Virtual(path) => {
3816                write!(f, "{}+{}", self.name(), PortablePath::from(path))
3817            }
3818        }
3819    }
3820}
3821
3822impl Source {
3823    fn name(&self) -> &str {
3824        match self {
3825            Self::Registry(..) => "registry",
3826            Self::Git(..) => "git",
3827            Self::Direct(..) => "direct",
3828            Self::Path(..) => "path",
3829            Self::Directory(..) => "directory",
3830            Self::Editable(..) => "editable",
3831            Self::Virtual(..) => "virtual",
3832        }
3833    }
3834
3835    /// Returns `Some(true)` to indicate that the source kind _must_ include a
3836    /// hash.
3837    ///
3838    /// Returns `Some(false)` to indicate that the source kind _must not_
3839    /// include a hash.
3840    ///
3841    /// Returns `None` to indicate that the source kind _may_ include a hash.
3842    fn requires_hash(&self) -> Option<bool> {
3843        match self {
3844            Self::Registry(..) => None,
3845            Self::Direct(..) | Self::Path(..) => Some(true),
3846            Self::Git(..) | Self::Directory(..) | Self::Editable(..) | Self::Virtual(..) => {
3847                Some(false)
3848            }
3849        }
3850    }
3851}
3852
3853#[derive(Clone, Debug, serde::Deserialize)]
3854#[serde(untagged, rename_all = "kebab-case")]
3855enum SourceWire {
3856    Registry {
3857        registry: RegistrySourceWire,
3858    },
3859    Git {
3860        git: String,
3861    },
3862    Direct {
3863        url: UrlString,
3864        subdirectory: Option<PortablePathBuf>,
3865    },
3866    Path {
3867        path: PortablePathBuf,
3868    },
3869    Directory {
3870        directory: PortablePathBuf,
3871    },
3872    Editable {
3873        editable: PortablePathBuf,
3874    },
3875    Virtual {
3876        r#virtual: PortablePathBuf,
3877    },
3878}
3879
3880impl TryFrom<SourceWire> for Source {
3881    type Error = LockError;
3882
3883    fn try_from(wire: SourceWire) -> Result<Self, LockError> {
3884        #[allow(clippy::enum_glob_use)]
3885        use self::SourceWire::*;
3886
3887        match wire {
3888            Registry { registry } => Ok(Self::Registry(registry.into())),
3889            Git { git } => {
3890                let url = DisplaySafeUrl::parse(&git)
3891                    .map_err(|err| SourceParseError::InvalidUrl {
3892                        given: git.clone(),
3893                        err,
3894                    })
3895                    .map_err(LockErrorKind::InvalidGitSourceUrl)?;
3896
3897                let git_source = GitSource::from_url(&url)
3898                    .map_err(|err| match err {
3899                        GitSourceError::InvalidSha => SourceParseError::InvalidSha { given: git },
3900                        GitSourceError::MissingSha => SourceParseError::MissingSha { given: git },
3901                    })
3902                    .map_err(LockErrorKind::InvalidGitSourceUrl)?;
3903
3904                Ok(Self::Git(UrlString::from(url), git_source))
3905            }
3906            Direct { url, subdirectory } => Ok(Self::Direct(
3907                url,
3908                DirectSource {
3909                    subdirectory: subdirectory.map(Box::<std::path::Path>::from),
3910                },
3911            )),
3912            Path { path } => Ok(Self::Path(path.into())),
3913            Directory { directory } => Ok(Self::Directory(directory.into())),
3914            Editable { editable } => Ok(Self::Editable(editable.into())),
3915            Virtual { r#virtual } => Ok(Self::Virtual(r#virtual.into())),
3916        }
3917    }
3918}
3919
3920/// The source for a registry, which could be a URL or a relative path.
3921#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
3922enum RegistrySource {
3923    /// Ex) `https://pypi.org/simple`
3924    Url(UrlString),
3925    /// Ex) `../path/to/local/index`
3926    Path(Box<Path>),
3927}
3928
3929impl Display for RegistrySource {
3930    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
3931        match self {
3932            Self::Url(url) => write!(f, "{url}"),
3933            Self::Path(path) => write!(f, "{}", path.display()),
3934        }
3935    }
3936}
3937
3938#[derive(Clone, Debug)]
3939enum RegistrySourceWire {
3940    /// Ex) `https://pypi.org/simple`
3941    Url(UrlString),
3942    /// Ex) `../path/to/local/index`
3943    Path(PortablePathBuf),
3944}
3945
3946impl<'de> serde::de::Deserialize<'de> for RegistrySourceWire {
3947    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3948    where
3949        D: serde::de::Deserializer<'de>,
3950    {
3951        struct Visitor;
3952
3953        impl serde::de::Visitor<'_> for Visitor {
3954            type Value = RegistrySourceWire;
3955
3956            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
3957                formatter.write_str("a valid URL or a file path")
3958            }
3959
3960            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
3961            where
3962                E: serde::de::Error,
3963            {
3964                if split_scheme(value).is_some() {
3965                    Ok(
3966                        serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
3967                            value,
3968                        ))
3969                        .map(RegistrySourceWire::Url)?,
3970                    )
3971                } else {
3972                    Ok(
3973                        serde::Deserialize::deserialize(serde::de::value::StrDeserializer::new(
3974                            value,
3975                        ))
3976                        .map(RegistrySourceWire::Path)?,
3977                    )
3978                }
3979            }
3980        }
3981
3982        deserializer.deserialize_str(Visitor)
3983    }
3984}
3985
3986impl From<RegistrySourceWire> for RegistrySource {
3987    fn from(wire: RegistrySourceWire) -> Self {
3988        match wire {
3989            RegistrySourceWire::Url(url) => Self::Url(url),
3990            RegistrySourceWire::Path(path) => Self::Path(path.into()),
3991        }
3992    }
3993}
3994
3995#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
3996#[serde(rename_all = "kebab-case")]
3997struct DirectSource {
3998    subdirectory: Option<Box<Path>>,
3999}
4000
4001/// NOTE: Care should be taken when adding variants to this enum. Namely, new
4002/// variants should be added without changing the relative ordering of other
4003/// variants. Otherwise, this could cause the lockfile to have a different
4004/// canonical ordering of package entries.
4005#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
4006struct GitSource {
4007    precise: GitOid,
4008    subdirectory: Option<Box<Path>>,
4009    kind: GitSourceKind,
4010    lfs: GitLfs,
4011}
4012
4013/// An error that occurs when a source string could not be parsed.
4014#[derive(Clone, Debug, Eq, PartialEq)]
4015enum GitSourceError {
4016    InvalidSha,
4017    MissingSha,
4018}
4019
4020impl GitSource {
4021    /// Extracts a Git source reference from the query pairs and the hash
4022    /// fragment in the given URL.
4023    fn from_url(url: &Url) -> Result<Self, GitSourceError> {
4024        let mut kind = GitSourceKind::DefaultBranch;
4025        let mut subdirectory = None;
4026        let mut lfs = GitLfs::Disabled;
4027        for (key, val) in url.query_pairs() {
4028            match &*key {
4029                "tag" => kind = GitSourceKind::Tag(val.into_owned()),
4030                "branch" => kind = GitSourceKind::Branch(val.into_owned()),
4031                "rev" => kind = GitSourceKind::Rev(val.into_owned()),
4032                "subdirectory" => subdirectory = Some(PortablePathBuf::from(val.as_ref()).into()),
4033                "lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
4034                _ => {}
4035            }
4036        }
4037
4038        let precise = GitOid::from_str(url.fragment().ok_or(GitSourceError::MissingSha)?)
4039            .map_err(|_| GitSourceError::InvalidSha)?;
4040
4041        Ok(Self {
4042            precise,
4043            subdirectory,
4044            kind,
4045            lfs,
4046        })
4047    }
4048}
4049
4050#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, serde::Deserialize)]
4051#[serde(rename_all = "kebab-case")]
4052enum GitSourceKind {
4053    Tag(String),
4054    Branch(String),
4055    Rev(String),
4056    DefaultBranch,
4057}
4058
4059/// Inspired by: <https://discuss.python.org/t/lock-files-again-but-this-time-w-sdists/46593>
4060#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4061#[serde(rename_all = "kebab-case")]
4062struct SourceDistMetadata {
4063    /// A hash of the source distribution.
4064    hash: Option<Hash>,
4065    /// The size of the source distribution in bytes.
4066    ///
4067    /// This is only present for source distributions that come from registries.
4068    size: Option<u64>,
4069    /// The upload time of the source distribution.
4070    #[serde(alias = "upload_time")]
4071    upload_time: Option<Timestamp>,
4072}
4073
4074/// A URL or file path where the source dist that was
4075/// locked against was found. The location does not need to exist in the
4076/// future, so this should be treated as only a hint to where to look
4077/// and/or recording where the source dist file originally came from.
4078#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4079#[serde(from = "SourceDistWire")]
4080enum SourceDist {
4081    Url {
4082        url: UrlString,
4083        #[serde(flatten)]
4084        metadata: SourceDistMetadata,
4085    },
4086    Path {
4087        path: Box<Path>,
4088        #[serde(flatten)]
4089        metadata: SourceDistMetadata,
4090    },
4091    Metadata {
4092        #[serde(flatten)]
4093        metadata: SourceDistMetadata,
4094    },
4095}
4096
4097impl SourceDist {
4098    fn filename(&self) -> Option<Cow<'_, str>> {
4099        match self {
4100            Self::Metadata { .. } => None,
4101            Self::Url { url, .. } => url.filename().ok(),
4102            Self::Path { path, .. } => path.file_name().map(|filename| filename.to_string_lossy()),
4103        }
4104    }
4105
4106    fn url(&self) -> Option<&UrlString> {
4107        match self {
4108            Self::Metadata { .. } => None,
4109            Self::Url { url, .. } => Some(url),
4110            Self::Path { .. } => None,
4111        }
4112    }
4113
4114    pub(crate) fn hash(&self) -> Option<&Hash> {
4115        match self {
4116            Self::Metadata { metadata } => metadata.hash.as_ref(),
4117            Self::Url { metadata, .. } => metadata.hash.as_ref(),
4118            Self::Path { metadata, .. } => metadata.hash.as_ref(),
4119        }
4120    }
4121
4122    pub(crate) fn size(&self) -> Option<u64> {
4123        match self {
4124            Self::Metadata { metadata } => metadata.size,
4125            Self::Url { metadata, .. } => metadata.size,
4126            Self::Path { metadata, .. } => metadata.size,
4127        }
4128    }
4129
4130    pub(crate) fn upload_time(&self) -> Option<Timestamp> {
4131        match self {
4132            Self::Metadata { metadata } => metadata.upload_time,
4133            Self::Url { metadata, .. } => metadata.upload_time,
4134            Self::Path { metadata, .. } => metadata.upload_time,
4135        }
4136    }
4137}
4138
4139impl SourceDist {
4140    fn from_annotated_dist(
4141        id: &PackageId,
4142        annotated_dist: &AnnotatedDist,
4143    ) -> Result<Option<Self>, LockError> {
4144        match annotated_dist.dist {
4145            // We pass empty installed packages for locking.
4146            ResolvedDist::Installed { .. } => unreachable!(),
4147            ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4148                id,
4149                dist,
4150                annotated_dist.hashes.as_slice(),
4151                annotated_dist.index(),
4152            ),
4153        }
4154    }
4155
4156    fn from_dist(
4157        id: &PackageId,
4158        dist: &Dist,
4159        hashes: &[HashDigest],
4160        index: Option<&IndexUrl>,
4161    ) -> Result<Option<Self>, LockError> {
4162        match *dist {
4163            Dist::Built(BuiltDist::Registry(ref built_dist)) => {
4164                let Some(sdist) = built_dist.sdist.as_ref() else {
4165                    return Ok(None);
4166                };
4167                Self::from_registry_dist(sdist, index)
4168            }
4169            Dist::Built(_) => Ok(None),
4170            Dist::Source(ref source_dist) => Self::from_source_dist(id, source_dist, hashes, index),
4171        }
4172    }
4173
4174    fn from_source_dist(
4175        id: &PackageId,
4176        source_dist: &uv_distribution_types::SourceDist,
4177        hashes: &[HashDigest],
4178        index: Option<&IndexUrl>,
4179    ) -> Result<Option<Self>, LockError> {
4180        match *source_dist {
4181            uv_distribution_types::SourceDist::Registry(ref reg_dist) => {
4182                Self::from_registry_dist(reg_dist, index)
4183            }
4184            uv_distribution_types::SourceDist::DirectUrl(_) => {
4185                Self::from_direct_dist(id, hashes).map(Some)
4186            }
4187            uv_distribution_types::SourceDist::Path(_) => {
4188                Self::from_path_dist(id, hashes).map(Some)
4189            }
4190            // An actual sdist entry in the lockfile is only required when
4191            // it's from a registry or a direct URL. Otherwise, it's strictly
4192            // redundant with the information in all other kinds of `source`.
4193            uv_distribution_types::SourceDist::Git(_)
4194            | uv_distribution_types::SourceDist::Directory(_) => Ok(None),
4195        }
4196    }
4197
4198    fn from_registry_dist(
4199        reg_dist: &RegistrySourceDist,
4200        index: Option<&IndexUrl>,
4201    ) -> Result<Option<Self>, LockError> {
4202        // Reject distributions from registries that don't match the index URL, as can occur with
4203        // `--find-links`.
4204        if index.is_none_or(|index| *index != reg_dist.index) {
4205            return Ok(None);
4206        }
4207
4208        match &reg_dist.index {
4209            IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4210                let url = normalize_file_location(&reg_dist.file.url)
4211                    .map_err(LockErrorKind::InvalidUrl)
4212                    .map_err(LockError::from)?;
4213                let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4214                let size = reg_dist.file.size;
4215                let upload_time = reg_dist
4216                    .file
4217                    .upload_time_utc_ms
4218                    .map(Timestamp::from_millisecond)
4219                    .transpose()
4220                    .map_err(LockErrorKind::InvalidTimestamp)?;
4221                Ok(Some(Self::Url {
4222                    url,
4223                    metadata: SourceDistMetadata {
4224                        hash,
4225                        size,
4226                        upload_time,
4227                    },
4228                }))
4229            }
4230            IndexUrl::Path(path) => {
4231                let index_path = path
4232                    .to_file_path()
4233                    .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4234                let url = reg_dist
4235                    .file
4236                    .url
4237                    .to_url()
4238                    .map_err(LockErrorKind::InvalidUrl)?;
4239
4240                if url.scheme() == "file" {
4241                    let reg_dist_path = url
4242                        .to_file_path()
4243                        .map_err(|()| LockErrorKind::UrlToPath { url })?;
4244                    let path = relative_to(&reg_dist_path, index_path)
4245                        .or_else(|_| std::path::absolute(&reg_dist_path))
4246                        .map_err(LockErrorKind::DistributionRelativePath)?
4247                        .into_boxed_path();
4248                    let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4249                    let size = reg_dist.file.size;
4250                    let upload_time = reg_dist
4251                        .file
4252                        .upload_time_utc_ms
4253                        .map(Timestamp::from_millisecond)
4254                        .transpose()
4255                        .map_err(LockErrorKind::InvalidTimestamp)?;
4256                    Ok(Some(Self::Path {
4257                        path,
4258                        metadata: SourceDistMetadata {
4259                            hash,
4260                            size,
4261                            upload_time,
4262                        },
4263                    }))
4264                } else {
4265                    let url = normalize_file_location(&reg_dist.file.url)
4266                        .map_err(LockErrorKind::InvalidUrl)
4267                        .map_err(LockError::from)?;
4268                    let hash = reg_dist.file.hashes.iter().max().cloned().map(Hash::from);
4269                    let size = reg_dist.file.size;
4270                    let upload_time = reg_dist
4271                        .file
4272                        .upload_time_utc_ms
4273                        .map(Timestamp::from_millisecond)
4274                        .transpose()
4275                        .map_err(LockErrorKind::InvalidTimestamp)?;
4276                    Ok(Some(Self::Url {
4277                        url,
4278                        metadata: SourceDistMetadata {
4279                            hash,
4280                            size,
4281                            upload_time,
4282                        },
4283                    }))
4284                }
4285            }
4286        }
4287    }
4288
4289    fn from_direct_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4290        let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4291            let kind = LockErrorKind::Hash {
4292                id: id.clone(),
4293                artifact_type: "direct URL source distribution",
4294                expected: true,
4295            };
4296            return Err(kind.into());
4297        };
4298        Ok(Self::Metadata {
4299            metadata: SourceDistMetadata {
4300                hash: Some(hash),
4301                size: None,
4302                upload_time: None,
4303            },
4304        })
4305    }
4306
4307    fn from_path_dist(id: &PackageId, hashes: &[HashDigest]) -> Result<Self, LockError> {
4308        let Some(hash) = hashes.iter().max().cloned().map(Hash::from) else {
4309            let kind = LockErrorKind::Hash {
4310                id: id.clone(),
4311                artifact_type: "path source distribution",
4312                expected: true,
4313            };
4314            return Err(kind.into());
4315        };
4316        Ok(Self::Metadata {
4317            metadata: SourceDistMetadata {
4318                hash: Some(hash),
4319                size: None,
4320                upload_time: None,
4321            },
4322        })
4323    }
4324}
4325
4326#[derive(Clone, Debug, serde::Deserialize)]
4327#[serde(untagged, rename_all = "kebab-case")]
4328enum SourceDistWire {
4329    Url {
4330        url: UrlString,
4331        #[serde(flatten)]
4332        metadata: SourceDistMetadata,
4333    },
4334    Path {
4335        path: PortablePathBuf,
4336        #[serde(flatten)]
4337        metadata: SourceDistMetadata,
4338    },
4339    Metadata {
4340        #[serde(flatten)]
4341        metadata: SourceDistMetadata,
4342    },
4343}
4344
4345impl SourceDist {
4346    /// Returns the TOML representation of this source distribution.
4347    fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
4348        let mut table = InlineTable::new();
4349        match self {
4350            Self::Metadata { .. } => {}
4351            Self::Url { url, .. } => {
4352                table.insert("url", Value::from(url.as_ref()));
4353            }
4354            Self::Path { path, .. } => {
4355                table.insert("path", Value::from(PortablePath::from(path).to_string()));
4356            }
4357        }
4358        if let Some(hash) = self.hash() {
4359            table.insert("hash", Value::from(hash.to_string()));
4360        }
4361        if let Some(size) = self.size() {
4362            table.insert(
4363                "size",
4364                toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4365            );
4366        }
4367        if let Some(upload_time) = self.upload_time() {
4368            table.insert("upload-time", Value::from(upload_time.to_string()));
4369        }
4370        Ok(table)
4371    }
4372}
4373
4374impl From<SourceDistWire> for SourceDist {
4375    fn from(wire: SourceDistWire) -> Self {
4376        match wire {
4377            SourceDistWire::Url { url, metadata } => Self::Url { url, metadata },
4378            SourceDistWire::Path { path, metadata } => Self::Path {
4379                path: path.into(),
4380                metadata,
4381            },
4382            SourceDistWire::Metadata { metadata } => Self::Metadata { metadata },
4383        }
4384    }
4385}
4386
4387impl From<GitReference> for GitSourceKind {
4388    fn from(value: GitReference) -> Self {
4389        match value {
4390            GitReference::Branch(branch) => Self::Branch(branch),
4391            GitReference::Tag(tag) => Self::Tag(tag),
4392            GitReference::BranchOrTag(rev) => Self::Rev(rev),
4393            GitReference::BranchOrTagOrCommit(rev) => Self::Rev(rev),
4394            GitReference::NamedRef(rev) => Self::Rev(rev),
4395            GitReference::DefaultBranch => Self::DefaultBranch,
4396        }
4397    }
4398}
4399
4400impl From<GitSourceKind> for GitReference {
4401    fn from(value: GitSourceKind) -> Self {
4402        match value {
4403            GitSourceKind::Branch(branch) => Self::Branch(branch),
4404            GitSourceKind::Tag(tag) => Self::Tag(tag),
4405            GitSourceKind::Rev(rev) => Self::from_rev(rev),
4406            GitSourceKind::DefaultBranch => Self::DefaultBranch,
4407        }
4408    }
4409}
4410
4411/// Construct the lockfile-compatible [`DisplaySafeUrl`] for a [`GitSourceDist`].
4412fn locked_git_url(git_dist: &GitSourceDist) -> DisplaySafeUrl {
4413    let mut url = git_dist.git.repository().clone();
4414
4415    // Remove the credentials.
4416    url.remove_credentials();
4417
4418    // Clear out any existing state.
4419    url.set_fragment(None);
4420    url.set_query(None);
4421
4422    // Put the subdirectory in the query.
4423    if let Some(subdirectory) = git_dist
4424        .subdirectory
4425        .as_deref()
4426        .map(PortablePath::from)
4427        .as_ref()
4428        .map(PortablePath::to_string)
4429    {
4430        url.query_pairs_mut()
4431            .append_pair("subdirectory", &subdirectory);
4432    }
4433
4434    // Put lfs=true in the package source git url only when explicitly enabled.
4435    if git_dist.git.lfs().enabled() {
4436        url.query_pairs_mut().append_pair("lfs", "true");
4437    }
4438
4439    // Put the requested reference in the query.
4440    match git_dist.git.reference() {
4441        GitReference::Branch(branch) => {
4442            url.query_pairs_mut().append_pair("branch", branch.as_str());
4443        }
4444        GitReference::Tag(tag) => {
4445            url.query_pairs_mut().append_pair("tag", tag.as_str());
4446        }
4447        GitReference::BranchOrTag(rev)
4448        | GitReference::BranchOrTagOrCommit(rev)
4449        | GitReference::NamedRef(rev) => {
4450            url.query_pairs_mut().append_pair("rev", rev.as_str());
4451        }
4452        GitReference::DefaultBranch => {}
4453    }
4454
4455    // Put the precise commit in the fragment.
4456    url.set_fragment(
4457        git_dist
4458            .git
4459            .precise()
4460            .as_ref()
4461            .map(GitOid::to_string)
4462            .as_deref(),
4463    );
4464
4465    url
4466}
4467
4468#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4469struct ZstdWheel {
4470    hash: Option<Hash>,
4471    size: Option<u64>,
4472}
4473
4474/// Inspired by: <https://discuss.python.org/t/lock-files-again-but-this-time-w-sdists/46593>
4475#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4476#[serde(try_from = "WheelWire")]
4477struct Wheel {
4478    /// A URL or file path (via `file://`) where the wheel that was locked
4479    /// against was found. The location does not need to exist in the future,
4480    /// so this should be treated as only a hint to where to look and/or
4481    /// recording where the wheel file originally came from.
4482    url: WheelWireSource,
4483    /// A hash of the built distribution.
4484    ///
4485    /// This is only present for wheels that come from registries and direct
4486    /// URLs. Wheels from git or path dependencies do not have hashes
4487    /// associated with them.
4488    hash: Option<Hash>,
4489    /// The size of the built distribution in bytes.
4490    ///
4491    /// This is only present for wheels that come from registries.
4492    size: Option<u64>,
4493    /// The upload time of the built distribution.
4494    ///
4495    /// This is only present for wheels that come from registries.
4496    upload_time: Option<Timestamp>,
4497    /// The filename of the wheel.
4498    ///
4499    /// This isn't part of the wire format since it's redundant with the
4500    /// URL. But we do use it for various things, and thus compute it at
4501    /// deserialization time. Not being able to extract a wheel filename from a
4502    /// wheel URL is thus a deserialization error.
4503    filename: WheelFilename,
4504    /// The zstandard-compressed wheel metadata, if any.
4505    zstd: Option<ZstdWheel>,
4506}
4507
4508impl Wheel {
4509    fn from_annotated_dist(annotated_dist: &AnnotatedDist) -> Result<Vec<Self>, LockError> {
4510        match annotated_dist.dist {
4511            // We pass empty installed packages for locking.
4512            ResolvedDist::Installed { .. } => unreachable!(),
4513            ResolvedDist::Installable { ref dist, .. } => Self::from_dist(
4514                dist,
4515                annotated_dist.hashes.as_slice(),
4516                annotated_dist.index(),
4517            ),
4518        }
4519    }
4520
4521    fn from_dist(
4522        dist: &Dist,
4523        hashes: &[HashDigest],
4524        index: Option<&IndexUrl>,
4525    ) -> Result<Vec<Self>, LockError> {
4526        match *dist {
4527            Dist::Built(ref built_dist) => Self::from_built_dist(built_dist, hashes, index),
4528            Dist::Source(uv_distribution_types::SourceDist::Registry(ref source_dist)) => {
4529                source_dist
4530                    .wheels
4531                    .iter()
4532                    .filter(|wheel| {
4533                        // Reject distributions from registries that don't match the index URL, as can occur with
4534                        // `--find-links`.
4535                        index.is_some_and(|index| *index == wheel.index)
4536                    })
4537                    .map(Self::from_registry_wheel)
4538                    .collect()
4539            }
4540            Dist::Source(_) => Ok(vec![]),
4541        }
4542    }
4543
4544    fn from_built_dist(
4545        built_dist: &BuiltDist,
4546        hashes: &[HashDigest],
4547        index: Option<&IndexUrl>,
4548    ) -> Result<Vec<Self>, LockError> {
4549        match *built_dist {
4550            BuiltDist::Registry(ref reg_dist) => Self::from_registry_dist(reg_dist, index),
4551            BuiltDist::DirectUrl(ref direct_dist) => {
4552                Ok(vec![Self::from_direct_dist(direct_dist, hashes)])
4553            }
4554            BuiltDist::Path(ref path_dist) => Ok(vec![Self::from_path_dist(path_dist, hashes)]),
4555        }
4556    }
4557
4558    fn from_registry_dist(
4559        reg_dist: &RegistryBuiltDist,
4560        index: Option<&IndexUrl>,
4561    ) -> Result<Vec<Self>, LockError> {
4562        reg_dist
4563            .wheels
4564            .iter()
4565            .filter(|wheel| {
4566                // Reject distributions from registries that don't match the index URL, as can occur with
4567                // `--find-links`.
4568                index.is_some_and(|index| *index == wheel.index)
4569            })
4570            .map(Self::from_registry_wheel)
4571            .collect()
4572    }
4573
4574    fn from_registry_wheel(wheel: &RegistryBuiltWheel) -> Result<Self, LockError> {
4575        let url = match &wheel.index {
4576            IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
4577                let url = normalize_file_location(&wheel.file.url)
4578                    .map_err(LockErrorKind::InvalidUrl)
4579                    .map_err(LockError::from)?;
4580                WheelWireSource::Url { url }
4581            }
4582            IndexUrl::Path(path) => {
4583                let index_path = path
4584                    .to_file_path()
4585                    .map_err(|()| LockErrorKind::UrlToPath { url: path.to_url() })?;
4586                let wheel_url = wheel.file.url.to_url().map_err(LockErrorKind::InvalidUrl)?;
4587
4588                if wheel_url.scheme() == "file" {
4589                    let wheel_path = wheel_url
4590                        .to_file_path()
4591                        .map_err(|()| LockErrorKind::UrlToPath { url: wheel_url })?;
4592                    let path = relative_to(&wheel_path, index_path)
4593                        .or_else(|_| std::path::absolute(&wheel_path))
4594                        .map_err(LockErrorKind::DistributionRelativePath)?
4595                        .into_boxed_path();
4596                    WheelWireSource::Path { path }
4597                } else {
4598                    let url = normalize_file_location(&wheel.file.url)
4599                        .map_err(LockErrorKind::InvalidUrl)
4600                        .map_err(LockError::from)?;
4601                    WheelWireSource::Url { url }
4602                }
4603            }
4604        };
4605        let filename = wheel.filename.clone();
4606        let hash = wheel.file.hashes.iter().max().cloned().map(Hash::from);
4607        let size = wheel.file.size;
4608        let upload_time = wheel
4609            .file
4610            .upload_time_utc_ms
4611            .map(Timestamp::from_millisecond)
4612            .transpose()
4613            .map_err(LockErrorKind::InvalidTimestamp)?;
4614        let zstd = wheel.file.zstd.as_ref().map(|zstd| ZstdWheel {
4615            hash: zstd.hashes.iter().max().cloned().map(Hash::from),
4616            size: zstd.size,
4617        });
4618        Ok(Self {
4619            url,
4620            hash,
4621            size,
4622            upload_time,
4623            filename,
4624            zstd,
4625        })
4626    }
4627
4628    fn from_direct_dist(direct_dist: &DirectUrlBuiltDist, hashes: &[HashDigest]) -> Self {
4629        Self {
4630            url: WheelWireSource::Url {
4631                url: normalize_url(direct_dist.url.to_url()),
4632            },
4633            hash: hashes.iter().max().cloned().map(Hash::from),
4634            size: None,
4635            upload_time: None,
4636            filename: direct_dist.filename.clone(),
4637            zstd: None,
4638        }
4639    }
4640
4641    fn from_path_dist(path_dist: &PathBuiltDist, hashes: &[HashDigest]) -> Self {
4642        Self {
4643            url: WheelWireSource::Filename {
4644                filename: path_dist.filename.clone(),
4645            },
4646            hash: hashes.iter().max().cloned().map(Hash::from),
4647            size: None,
4648            upload_time: None,
4649            filename: path_dist.filename.clone(),
4650            zstd: None,
4651        }
4652    }
4653
4654    pub(crate) fn to_registry_wheel(
4655        &self,
4656        source: &RegistrySource,
4657        root: &Path,
4658    ) -> Result<RegistryBuiltWheel, LockError> {
4659        let filename: WheelFilename = self.filename.clone();
4660
4661        match source {
4662            RegistrySource::Url(url) => {
4663                let file_location = match &self.url {
4664                    WheelWireSource::Url { url: file_url } => {
4665                        FileLocation::AbsoluteUrl(file_url.clone())
4666                    }
4667                    WheelWireSource::Path { .. } | WheelWireSource::Filename { .. } => {
4668                        return Err(LockErrorKind::MissingUrl {
4669                            name: filename.name,
4670                            version: filename.version,
4671                        }
4672                        .into());
4673                    }
4674                };
4675                let file = Box::new(uv_distribution_types::File {
4676                    dist_info_metadata: false,
4677                    filename: SmallString::from(filename.to_string()),
4678                    hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
4679                    requires_python: None,
4680                    size: self.size,
4681                    upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
4682                    url: file_location,
4683                    yanked: None,
4684                    zstd: self
4685                        .zstd
4686                        .as_ref()
4687                        .map(|zstd| uv_distribution_types::Zstd {
4688                            hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
4689                            size: zstd.size,
4690                        })
4691                        .map(Box::new),
4692                });
4693                let index = IndexUrl::from(VerbatimUrl::from_url(
4694                    url.to_url().map_err(LockErrorKind::InvalidUrl)?,
4695                ));
4696                Ok(RegistryBuiltWheel {
4697                    filename,
4698                    file,
4699                    index,
4700                })
4701            }
4702            RegistrySource::Path(index_path) => {
4703                let file_location = match &self.url {
4704                    WheelWireSource::Url { url: file_url } => {
4705                        FileLocation::AbsoluteUrl(file_url.clone())
4706                    }
4707                    WheelWireSource::Path { path: file_path } => {
4708                        let file_path = root.join(index_path).join(file_path);
4709                        let file_url =
4710                            DisplaySafeUrl::from_file_path(&file_path).map_err(|()| {
4711                                LockErrorKind::PathToUrl {
4712                                    path: file_path.into_boxed_path(),
4713                                }
4714                            })?;
4715                        FileLocation::AbsoluteUrl(UrlString::from(file_url))
4716                    }
4717                    WheelWireSource::Filename { .. } => {
4718                        return Err(LockErrorKind::MissingPath {
4719                            name: filename.name,
4720                            version: filename.version,
4721                        }
4722                        .into());
4723                    }
4724                };
4725                let file = Box::new(uv_distribution_types::File {
4726                    dist_info_metadata: false,
4727                    filename: SmallString::from(filename.to_string()),
4728                    hashes: self.hash.iter().map(|h| h.0.clone()).collect(),
4729                    requires_python: None,
4730                    size: self.size,
4731                    upload_time_utc_ms: self.upload_time.map(Timestamp::as_millisecond),
4732                    url: file_location,
4733                    yanked: None,
4734                    zstd: self
4735                        .zstd
4736                        .as_ref()
4737                        .map(|zstd| uv_distribution_types::Zstd {
4738                            hashes: zstd.hash.iter().map(|h| h.0.clone()).collect(),
4739                            size: zstd.size,
4740                        })
4741                        .map(Box::new),
4742                });
4743                let index = IndexUrl::from(
4744                    VerbatimUrl::from_absolute_path(root.join(index_path))
4745                        .map_err(LockErrorKind::RegistryVerbatimUrl)?,
4746                );
4747                Ok(RegistryBuiltWheel {
4748                    filename,
4749                    file,
4750                    index,
4751                })
4752            }
4753        }
4754    }
4755}
4756
4757#[derive(Clone, Debug, serde::Deserialize)]
4758#[serde(rename_all = "kebab-case")]
4759struct WheelWire {
4760    #[serde(flatten)]
4761    url: WheelWireSource,
4762    /// A hash of the built distribution.
4763    ///
4764    /// This is only present for wheels that come from registries and direct
4765    /// URLs. Wheels from git or path dependencies do not have hashes
4766    /// associated with them.
4767    hash: Option<Hash>,
4768    /// The size of the built distribution in bytes.
4769    ///
4770    /// This is only present for wheels that come from registries.
4771    size: Option<u64>,
4772    /// The upload time of the built distribution.
4773    ///
4774    /// This is only present for wheels that come from registries.
4775    #[serde(alias = "upload_time")]
4776    upload_time: Option<Timestamp>,
4777    /// The zstandard-compressed wheel metadata, if any.
4778    #[serde(alias = "zstd")]
4779    zstd: Option<ZstdWheel>,
4780}
4781
4782#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
4783#[serde(untagged, rename_all = "kebab-case")]
4784enum WheelWireSource {
4785    /// Used for all wheels that come from remote sources.
4786    Url {
4787        /// A URL where the wheel that was locked against was found. The location
4788        /// does not need to exist in the future, so this should be treated as
4789        /// only a hint to where to look and/or recording where the wheel file
4790        /// originally came from.
4791        url: UrlString,
4792    },
4793    /// Used for wheels that come from local registries (like `--find-links`).
4794    Path {
4795        /// The path to the wheel, relative to the index.
4796        path: Box<Path>,
4797    },
4798    /// Used for path wheels.
4799    ///
4800    /// We only store the filename for path wheel, since we can't store a relative path in the url
4801    Filename {
4802        /// We duplicate the filename since a lot of code relies on having the filename on the
4803        /// wheel entry.
4804        filename: WheelFilename,
4805    },
4806}
4807
4808impl Wheel {
4809    /// Returns the TOML representation of this wheel.
4810    fn to_toml(&self) -> Result<InlineTable, toml_edit::ser::Error> {
4811        let mut table = InlineTable::new();
4812        match &self.url {
4813            WheelWireSource::Url { url } => {
4814                table.insert("url", Value::from(url.as_ref()));
4815            }
4816            WheelWireSource::Path { path } => {
4817                table.insert("path", Value::from(PortablePath::from(path).to_string()));
4818            }
4819            WheelWireSource::Filename { filename } => {
4820                table.insert("filename", Value::from(filename.to_string()));
4821            }
4822        }
4823        if let Some(ref hash) = self.hash {
4824            table.insert("hash", Value::from(hash.to_string()));
4825        }
4826        if let Some(size) = self.size {
4827            table.insert(
4828                "size",
4829                toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4830            );
4831        }
4832        if let Some(upload_time) = self.upload_time {
4833            table.insert("upload-time", Value::from(upload_time.to_string()));
4834        }
4835        if let Some(zstd) = &self.zstd {
4836            let mut inner = InlineTable::new();
4837            if let Some(ref hash) = zstd.hash {
4838                inner.insert("hash", Value::from(hash.to_string()));
4839            }
4840            if let Some(size) = zstd.size {
4841                inner.insert(
4842                    "size",
4843                    toml_edit::ser::ValueSerializer::new().serialize_u64(size)?,
4844                );
4845            }
4846            table.insert("zstd", Value::from(inner));
4847        }
4848        Ok(table)
4849    }
4850}
4851
4852impl TryFrom<WheelWire> for Wheel {
4853    type Error = String;
4854
4855    fn try_from(wire: WheelWire) -> Result<Self, String> {
4856        let filename = match &wire.url {
4857            WheelWireSource::Url { url } => {
4858                let filename = url.filename().map_err(|err| err.to_string())?;
4859                filename.parse::<WheelFilename>().map_err(|err| {
4860                    format!("failed to parse `{filename}` as wheel filename: {err}")
4861                })?
4862            }
4863            WheelWireSource::Path { path } => {
4864                let filename = path
4865                    .file_name()
4866                    .and_then(|file_name| file_name.to_str())
4867                    .ok_or_else(|| {
4868                        format!("path `{}` has no filename component", path.display())
4869                    })?;
4870                filename.parse::<WheelFilename>().map_err(|err| {
4871                    format!("failed to parse `{filename}` as wheel filename: {err}")
4872                })?
4873            }
4874            WheelWireSource::Filename { filename } => filename.clone(),
4875        };
4876
4877        Ok(Self {
4878            url: wire.url,
4879            hash: wire.hash,
4880            size: wire.size,
4881            upload_time: wire.upload_time,
4882            zstd: wire.zstd,
4883            filename,
4884        })
4885    }
4886}
4887
4888/// A single dependency of a package in a lockfile.
4889#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
4890pub struct Dependency {
4891    package_id: PackageId,
4892    extra: BTreeSet<ExtraName>,
4893    /// A marker simplified from the PEP 508 marker in `complexified_marker`
4894    /// by assuming `requires-python` is satisfied. So if
4895    /// `requires-python = '>=3.8'`, then
4896    /// `python_version >= '3.8' and python_version < '3.12'`
4897    /// gets simplified to `python_version < '3.12'`.
4898    ///
4899    /// Generally speaking, this marker should not be exposed to
4900    /// anything outside this module unless it's for a specialized use
4901    /// case. But specifically, it should never be used to evaluate
4902    /// against a marker environment or for disjointness checks or any
4903    /// other kind of marker algebra.
4904    ///
4905    /// It exists because there are some cases where we do actually
4906    /// want to compare markers in their "simplified" form. For
4907    /// example, when collapsing the extras on duplicate dependencies.
4908    /// Even if a dependency has different complexified markers,
4909    /// they might have identical markers once simplified. And since
4910    /// `requires-python` applies to the entire lock file, it's
4911    /// acceptable to do comparisons on the simplified form.
4912    simplified_marker: SimplifiedMarkerTree,
4913    /// The "complexified" marker is a universal marker whose PEP 508
4914    /// marker can stand on its own independent of `requires-python`.
4915    /// It can be safely used for any kind of marker algebra.
4916    complexified_marker: UniversalMarker,
4917}
4918
4919impl Dependency {
4920    fn new(
4921        requires_python: &RequiresPython,
4922        package_id: PackageId,
4923        extra: BTreeSet<ExtraName>,
4924        complexified_marker: UniversalMarker,
4925    ) -> Self {
4926        let simplified_marker =
4927            SimplifiedMarkerTree::new(requires_python, complexified_marker.combined());
4928        let complexified_marker = simplified_marker.into_marker(requires_python);
4929        Self {
4930            package_id,
4931            extra,
4932            simplified_marker,
4933            complexified_marker: UniversalMarker::from_combined(complexified_marker),
4934        }
4935    }
4936
4937    fn from_annotated_dist(
4938        requires_python: &RequiresPython,
4939        annotated_dist: &AnnotatedDist,
4940        complexified_marker: UniversalMarker,
4941        root: &Path,
4942    ) -> Result<Self, LockError> {
4943        let package_id = PackageId::from_annotated_dist(annotated_dist, root)?;
4944        let extra = annotated_dist.extra.iter().cloned().collect();
4945        Ok(Self::new(
4946            requires_python,
4947            package_id,
4948            extra,
4949            complexified_marker,
4950        ))
4951    }
4952
4953    /// Returns the TOML representation of this dependency.
4954    fn to_toml(
4955        &self,
4956        _requires_python: &RequiresPython,
4957        dist_count_by_name: &FxHashMap<PackageName, u64>,
4958    ) -> Table {
4959        let mut table = Table::new();
4960        self.package_id
4961            .to_toml(Some(dist_count_by_name), &mut table);
4962        if !self.extra.is_empty() {
4963            let extra_array = self
4964                .extra
4965                .iter()
4966                .map(ToString::to_string)
4967                .collect::<Array>();
4968            table.insert("extra", value(extra_array));
4969        }
4970        if let Some(marker) = self.simplified_marker.try_to_string() {
4971            table.insert("marker", value(marker));
4972        }
4973
4974        table
4975    }
4976
4977    /// Returns the package name of this dependency.
4978    pub fn package_name(&self) -> &PackageName {
4979        &self.package_id.name
4980    }
4981
4982    /// Returns the extras specified on this dependency.
4983    pub fn extra(&self) -> &BTreeSet<ExtraName> {
4984        &self.extra
4985    }
4986}
4987
4988impl Display for Dependency {
4989    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
4990        match (self.extra.is_empty(), self.package_id.version.as_ref()) {
4991            (true, Some(version)) => write!(f, "{}=={}", self.package_id.name, version),
4992            (true, None) => write!(f, "{}", self.package_id.name),
4993            (false, Some(version)) => write!(
4994                f,
4995                "{}[{}]=={}",
4996                self.package_id.name,
4997                self.extra.iter().join(","),
4998                version
4999            ),
5000            (false, None) => write!(
5001                f,
5002                "{}[{}]",
5003                self.package_id.name,
5004                self.extra.iter().join(",")
5005            ),
5006        }
5007    }
5008}
5009
5010/// A single dependency of a package in a lockfile.
5011#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)]
5012#[serde(rename_all = "kebab-case")]
5013struct DependencyWire {
5014    #[serde(flatten)]
5015    package_id: PackageIdForDependency,
5016    #[serde(default)]
5017    extra: BTreeSet<ExtraName>,
5018    #[serde(default)]
5019    marker: SimplifiedMarkerTree,
5020}
5021
5022impl DependencyWire {
5023    fn unwire(
5024        self,
5025        requires_python: &RequiresPython,
5026        unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
5027    ) -> Result<Dependency, LockError> {
5028        let complexified_marker = self.marker.into_marker(requires_python);
5029        Ok(Dependency {
5030            package_id: self.package_id.unwire(unambiguous_package_ids)?,
5031            extra: self.extra,
5032            simplified_marker: self.marker,
5033            complexified_marker: UniversalMarker::from_combined(complexified_marker),
5034        })
5035    }
5036}
5037
5038/// A single hash for a distribution artifact in a lockfile.
5039///
5040/// A hash is encoded as a single TOML string in the format
5041/// `{algorithm}:{digest}`.
5042#[derive(Clone, Debug, PartialEq, Eq)]
5043struct Hash(HashDigest);
5044
5045impl From<HashDigest> for Hash {
5046    fn from(hd: HashDigest) -> Self {
5047        Self(hd)
5048    }
5049}
5050
5051impl FromStr for Hash {
5052    type Err = HashParseError;
5053
5054    fn from_str(s: &str) -> Result<Self, HashParseError> {
5055        let (algorithm, digest) = s.split_once(':').ok_or(HashParseError(
5056            "expected '{algorithm}:{digest}', but found no ':' in hash digest",
5057        ))?;
5058        let algorithm = algorithm
5059            .parse()
5060            .map_err(|_| HashParseError("unrecognized hash algorithm"))?;
5061        Ok(Self(HashDigest {
5062            algorithm,
5063            digest: digest.into(),
5064        }))
5065    }
5066}
5067
5068impl Display for Hash {
5069    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
5070        write!(f, "{}:{}", self.0.algorithm, self.0.digest)
5071    }
5072}
5073
5074impl<'de> serde::Deserialize<'de> for Hash {
5075    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5076    where
5077        D: serde::de::Deserializer<'de>,
5078    {
5079        struct Visitor;
5080
5081        impl serde::de::Visitor<'_> for Visitor {
5082            type Value = Hash;
5083
5084            fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
5085                f.write_str("a string")
5086            }
5087
5088            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
5089                Hash::from_str(v).map_err(serde::de::Error::custom)
5090            }
5091        }
5092
5093        deserializer.deserialize_str(Visitor)
5094    }
5095}
5096
5097impl From<Hash> for Hashes {
5098    fn from(value: Hash) -> Self {
5099        match value.0.algorithm {
5100            HashAlgorithm::Md5 => Self {
5101                md5: Some(value.0.digest),
5102                sha256: None,
5103                sha384: None,
5104                sha512: None,
5105                blake2b: None,
5106            },
5107            HashAlgorithm::Sha256 => Self {
5108                md5: None,
5109                sha256: Some(value.0.digest),
5110                sha384: None,
5111                sha512: None,
5112                blake2b: None,
5113            },
5114            HashAlgorithm::Sha384 => Self {
5115                md5: None,
5116                sha256: None,
5117                sha384: Some(value.0.digest),
5118                sha512: None,
5119                blake2b: None,
5120            },
5121            HashAlgorithm::Sha512 => Self {
5122                md5: None,
5123                sha256: None,
5124                sha384: None,
5125                sha512: Some(value.0.digest),
5126                blake2b: None,
5127            },
5128            HashAlgorithm::Blake2b => Self {
5129                md5: None,
5130                sha256: None,
5131                sha384: None,
5132                sha512: None,
5133                blake2b: Some(value.0.digest),
5134            },
5135        }
5136    }
5137}
5138
5139/// Convert a [`FileLocation`] into a normalized [`UrlString`].
5140fn normalize_file_location(location: &FileLocation) -> Result<UrlString, ToUrlError> {
5141    match location {
5142        FileLocation::AbsoluteUrl(absolute) => Ok(absolute.without_fragment().into_owned()),
5143        FileLocation::RelativeUrl(_, _) => Ok(normalize_url(location.to_url()?)),
5144    }
5145}
5146
5147/// Convert a [`DisplaySafeUrl`] into a normalized [`UrlString`] by removing the fragment.
5148fn normalize_url(mut url: DisplaySafeUrl) -> UrlString {
5149    url.set_fragment(None);
5150    UrlString::from(url)
5151}
5152
5153/// Normalize a [`Requirement`], which could come from a lockfile, a `pyproject.toml`, etc.
5154///
5155/// Performs the following steps:
5156///
5157/// 1. Removes any sensitive credentials.
5158/// 2. Ensures that the lock and install paths are appropriately framed with respect to the
5159///    current [`Workspace`].
5160/// 3. Removes the `origin` field, which is only used in `requirements.txt`.
5161/// 4. Simplifies the markers using the provided [`RequiresPython`] instance.
5162fn normalize_requirement(
5163    mut requirement: Requirement,
5164    root: &Path,
5165    requires_python: &RequiresPython,
5166) -> Result<Requirement, LockError> {
5167    // Sort the extras and groups for consistency.
5168    requirement.extras.sort();
5169    requirement.groups.sort();
5170
5171    // Normalize the requirement source.
5172    match requirement.source {
5173        RequirementSource::Git {
5174            git,
5175            subdirectory,
5176            url: _,
5177        } => {
5178            // Reconstruct the Git URL.
5179            let git = {
5180                let mut repository = git.repository().clone();
5181
5182                // Remove the credentials.
5183                repository.remove_credentials();
5184
5185                // Remove the fragment and query from the URL; they're already present in the source.
5186                repository.set_fragment(None);
5187                repository.set_query(None);
5188
5189                GitUrl::from_fields(
5190                    repository,
5191                    git.reference().clone(),
5192                    git.precise(),
5193                    git.lfs(),
5194                )?
5195            };
5196
5197            // Reconstruct the PEP 508 URL from the underlying data.
5198            let url = DisplaySafeUrl::from(ParsedGitUrl {
5199                url: git.clone(),
5200                subdirectory: subdirectory.clone(),
5201            });
5202
5203            Ok(Requirement {
5204                name: requirement.name,
5205                extras: requirement.extras,
5206                groups: requirement.groups,
5207                marker: requires_python.simplify_markers(requirement.marker),
5208                source: RequirementSource::Git {
5209                    git,
5210                    subdirectory,
5211                    url: VerbatimUrl::from_url(url),
5212                },
5213                origin: None,
5214            })
5215        }
5216        RequirementSource::Path {
5217            install_path,
5218            ext,
5219            url: _,
5220        } => {
5221            let install_path =
5222                uv_fs::normalize_path_buf(root.join(&install_path)).into_boxed_path();
5223            let url = VerbatimUrl::from_normalized_path(&install_path)
5224                .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5225
5226            Ok(Requirement {
5227                name: requirement.name,
5228                extras: requirement.extras,
5229                groups: requirement.groups,
5230                marker: requires_python.simplify_markers(requirement.marker),
5231                source: RequirementSource::Path {
5232                    install_path,
5233                    ext,
5234                    url,
5235                },
5236                origin: None,
5237            })
5238        }
5239        RequirementSource::Directory {
5240            install_path,
5241            editable,
5242            r#virtual,
5243            url: _,
5244        } => {
5245            let install_path =
5246                uv_fs::normalize_path_buf(root.join(&install_path)).into_boxed_path();
5247            let url = VerbatimUrl::from_normalized_path(&install_path)
5248                .map_err(LockErrorKind::RequirementVerbatimUrl)?;
5249
5250            Ok(Requirement {
5251                name: requirement.name,
5252                extras: requirement.extras,
5253                groups: requirement.groups,
5254                marker: requires_python.simplify_markers(requirement.marker),
5255                source: RequirementSource::Directory {
5256                    install_path,
5257                    editable: Some(editable.unwrap_or(false)),
5258                    r#virtual: Some(r#virtual.unwrap_or(false)),
5259                    url,
5260                },
5261                origin: None,
5262            })
5263        }
5264        RequirementSource::Registry {
5265            specifier,
5266            index,
5267            conflict,
5268        } => {
5269            // Round-trip the index to remove anything apart from the URL.
5270            let index = index
5271                .map(|index| index.url.into_url())
5272                .map(|mut index| {
5273                    index.remove_credentials();
5274                    index
5275                })
5276                .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index))));
5277            Ok(Requirement {
5278                name: requirement.name,
5279                extras: requirement.extras,
5280                groups: requirement.groups,
5281                marker: requires_python.simplify_markers(requirement.marker),
5282                source: RequirementSource::Registry {
5283                    specifier,
5284                    index,
5285                    conflict,
5286                },
5287                origin: None,
5288            })
5289        }
5290        RequirementSource::Url {
5291            mut location,
5292            subdirectory,
5293            ext,
5294            url: _,
5295        } => {
5296            // Remove the credentials.
5297            location.remove_credentials();
5298
5299            // Remove the fragment from the URL; it's already present in the source.
5300            location.set_fragment(None);
5301
5302            // Reconstruct the PEP 508 URL from the underlying data.
5303            let url = DisplaySafeUrl::from(ParsedArchiveUrl {
5304                url: location.clone(),
5305                subdirectory: subdirectory.clone(),
5306                ext,
5307            });
5308
5309            Ok(Requirement {
5310                name: requirement.name,
5311                extras: requirement.extras,
5312                groups: requirement.groups,
5313                marker: requires_python.simplify_markers(requirement.marker),
5314                source: RequirementSource::Url {
5315                    location,
5316                    subdirectory,
5317                    ext,
5318                    url: VerbatimUrl::from_url(url),
5319                },
5320                origin: None,
5321            })
5322        }
5323    }
5324}
5325
5326#[derive(Debug)]
5327pub struct LockError {
5328    kind: Box<LockErrorKind>,
5329    hint: Option<WheelTagHint>,
5330}
5331
5332impl std::error::Error for LockError {
5333    fn source(&self) -> Option<&(dyn Error + 'static)> {
5334        self.kind.source()
5335    }
5336}
5337
5338impl std::fmt::Display for LockError {
5339    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5340        write!(f, "{}", self.kind)?;
5341        if let Some(hint) = &self.hint {
5342            write!(f, "\n\n{hint}")?;
5343        }
5344        Ok(())
5345    }
5346}
5347
5348impl LockError {
5349    /// Returns true if the [`LockError`] is a resolver error.
5350    pub fn is_resolution(&self) -> bool {
5351        matches!(&*self.kind, LockErrorKind::Resolution { .. })
5352    }
5353}
5354
5355impl<E> From<E> for LockError
5356where
5357    LockErrorKind: From<E>,
5358{
5359    fn from(err: E) -> Self {
5360        Self {
5361            kind: Box::new(LockErrorKind::from(err)),
5362            hint: None,
5363        }
5364    }
5365}
5366
5367#[derive(Debug, Clone, PartialEq, Eq)]
5368#[allow(clippy::enum_variant_names)]
5369enum WheelTagHint {
5370    /// None of the available wheels for a package have a compatible Python language tag (e.g.,
5371    /// `cp310` in `cp310-abi3-manylinux_2_17_x86_64.whl`).
5372    LanguageTags {
5373        package: PackageName,
5374        version: Option<Version>,
5375        tags: BTreeSet<LanguageTag>,
5376        best: Option<LanguageTag>,
5377    },
5378    /// None of the available wheels for a package have a compatible ABI tag (e.g., `abi3` in
5379    /// `cp310-abi3-manylinux_2_17_x86_64.whl`).
5380    AbiTags {
5381        package: PackageName,
5382        version: Option<Version>,
5383        tags: BTreeSet<AbiTag>,
5384        best: Option<AbiTag>,
5385    },
5386    /// None of the available wheels for a package have a compatible platform tag (e.g.,
5387    /// `manylinux_2_17_x86_64` in `cp310-abi3-manylinux_2_17_x86_64.whl`).
5388    PlatformTags {
5389        package: PackageName,
5390        version: Option<Version>,
5391        tags: BTreeSet<PlatformTag>,
5392        best: Option<PlatformTag>,
5393        markers: MarkerEnvironment,
5394    },
5395}
5396
5397impl WheelTagHint {
5398    /// Generate a [`WheelTagHint`] from the given (incompatible) wheels.
5399    fn from_wheels(
5400        name: &PackageName,
5401        version: Option<&Version>,
5402        filenames: &[&WheelFilename],
5403        tags: &Tags,
5404        markers: &MarkerEnvironment,
5405    ) -> Option<Self> {
5406        let incompatibility = filenames
5407            .iter()
5408            .map(|filename| {
5409                tags.compatibility(
5410                    filename.python_tags(),
5411                    filename.abi_tags(),
5412                    filename.platform_tags(),
5413                )
5414            })
5415            .max()?;
5416        match incompatibility {
5417            TagCompatibility::Incompatible(IncompatibleTag::Python) => {
5418                let best = tags.python_tag();
5419                let tags = Self::python_tags(filenames.iter().copied()).collect::<BTreeSet<_>>();
5420                if tags.is_empty() {
5421                    None
5422                } else {
5423                    Some(Self::LanguageTags {
5424                        package: name.clone(),
5425                        version: version.cloned(),
5426                        tags,
5427                        best,
5428                    })
5429                }
5430            }
5431            TagCompatibility::Incompatible(IncompatibleTag::Abi) => {
5432                let best = tags.abi_tag();
5433                let tags = Self::abi_tags(filenames.iter().copied())
5434                    // Ignore `none`, which is universally compatible.
5435                    //
5436                    // As an example, `none` can appear here if we're solving for Python 3.13, and
5437                    // the distribution includes a wheel for `cp312-none-macosx_11_0_arm64`.
5438                    //
5439                    // In that case, the wheel isn't compatible, but when solving for Python 3.13,
5440                    // the `cp312` Python tag _can_ be compatible (e.g., for `cp312-abi3-macosx_11_0_arm64.whl`),
5441                    // so this is considered an ABI incompatibility rather than Python incompatibility.
5442                    .filter(|tag| *tag != AbiTag::None)
5443                    .collect::<BTreeSet<_>>();
5444                if tags.is_empty() {
5445                    None
5446                } else {
5447                    Some(Self::AbiTags {
5448                        package: name.clone(),
5449                        version: version.cloned(),
5450                        tags,
5451                        best,
5452                    })
5453                }
5454            }
5455            TagCompatibility::Incompatible(IncompatibleTag::Platform) => {
5456                let best = tags.platform_tag().cloned();
5457                let incompatible_tags = Self::platform_tags(filenames.iter().copied(), tags)
5458                    .cloned()
5459                    .collect::<BTreeSet<_>>();
5460                if incompatible_tags.is_empty() {
5461                    None
5462                } else {
5463                    Some(Self::PlatformTags {
5464                        package: name.clone(),
5465                        version: version.cloned(),
5466                        tags: incompatible_tags,
5467                        best,
5468                        markers: markers.clone(),
5469                    })
5470                }
5471            }
5472            _ => None,
5473        }
5474    }
5475
5476    /// Returns an iterator over the compatible Python tags of the available wheels.
5477    fn python_tags<'a>(
5478        filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5479    ) -> impl Iterator<Item = LanguageTag> + 'a {
5480        filenames.flat_map(WheelFilename::python_tags).copied()
5481    }
5482
5483    /// Returns an iterator over the compatible Python tags of the available wheels.
5484    fn abi_tags<'a>(
5485        filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5486    ) -> impl Iterator<Item = AbiTag> + 'a {
5487        filenames.flat_map(WheelFilename::abi_tags).copied()
5488    }
5489
5490    /// Returns the set of platform tags for the distribution that are ABI-compatible with the given
5491    /// tags.
5492    fn platform_tags<'a>(
5493        filenames: impl Iterator<Item = &'a WheelFilename> + 'a,
5494        tags: &'a Tags,
5495    ) -> impl Iterator<Item = &'a PlatformTag> + 'a {
5496        filenames.flat_map(move |filename| {
5497            if filename.python_tags().iter().any(|wheel_py| {
5498                filename
5499                    .abi_tags()
5500                    .iter()
5501                    .any(|wheel_abi| tags.is_compatible_abi(*wheel_py, *wheel_abi))
5502            }) {
5503                filename.platform_tags().iter()
5504            } else {
5505                [].iter()
5506            }
5507        })
5508    }
5509
5510    fn suggest_environment_marker(markers: &MarkerEnvironment) -> String {
5511        let sys_platform = markers.sys_platform();
5512        let platform_machine = markers.platform_machine();
5513
5514        // Generate the marker string based on actual environment values
5515        if platform_machine.is_empty() {
5516            format!("sys_platform == '{sys_platform}'")
5517        } else {
5518            format!("sys_platform == '{sys_platform}' and platform_machine == '{platform_machine}'")
5519        }
5520    }
5521}
5522
5523impl std::fmt::Display for WheelTagHint {
5524    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5525        match self {
5526            Self::LanguageTags {
5527                package,
5528                version,
5529                tags,
5530                best,
5531            } => {
5532                if let Some(best) = best {
5533                    let s = if tags.len() == 1 { "" } else { "s" };
5534                    let best = if let Some(pretty) = best.pretty() {
5535                        format!("{} (`{}`)", pretty.cyan(), best.cyan())
5536                    } else {
5537                        format!("{}", best.cyan())
5538                    };
5539                    if let Some(version) = version {
5540                        write!(
5541                            f,
5542                            "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python implementation tag{s}: {}",
5543                            "hint".bold().cyan(),
5544                            ":".bold(),
5545                            best,
5546                            package.cyan(),
5547                            format!("v{version}").cyan(),
5548                            tags.iter()
5549                                .map(|tag| format!("`{}`", tag.cyan()))
5550                                .join(", "),
5551                        )
5552                    } else {
5553                        write!(
5554                            f,
5555                            "{}{} You're using {}, but `{}` only has wheels with the following Python implementation tag{s}: {}",
5556                            "hint".bold().cyan(),
5557                            ":".bold(),
5558                            best,
5559                            package.cyan(),
5560                            tags.iter()
5561                                .map(|tag| format!("`{}`", tag.cyan()))
5562                                .join(", "),
5563                        )
5564                    }
5565                } else {
5566                    let s = if tags.len() == 1 { "" } else { "s" };
5567                    if let Some(version) = version {
5568                        write!(
5569                            f,
5570                            "{}{} Wheels are available for `{}` ({}) with the following Python implementation tag{s}: {}",
5571                            "hint".bold().cyan(),
5572                            ":".bold(),
5573                            package.cyan(),
5574                            format!("v{version}").cyan(),
5575                            tags.iter()
5576                                .map(|tag| format!("`{}`", tag.cyan()))
5577                                .join(", "),
5578                        )
5579                    } else {
5580                        write!(
5581                            f,
5582                            "{}{} Wheels are available for `{}` with the following Python implementation tag{s}: {}",
5583                            "hint".bold().cyan(),
5584                            ":".bold(),
5585                            package.cyan(),
5586                            tags.iter()
5587                                .map(|tag| format!("`{}`", tag.cyan()))
5588                                .join(", "),
5589                        )
5590                    }
5591                }
5592            }
5593            Self::AbiTags {
5594                package,
5595                version,
5596                tags,
5597                best,
5598            } => {
5599                if let Some(best) = best {
5600                    let s = if tags.len() == 1 { "" } else { "s" };
5601                    let best = if let Some(pretty) = best.pretty() {
5602                        format!("{} (`{}`)", pretty.cyan(), best.cyan())
5603                    } else {
5604                        format!("{}", best.cyan())
5605                    };
5606                    if let Some(version) = version {
5607                        write!(
5608                            f,
5609                            "{}{} You're using {}, but `{}` ({}) only has wheels with the following Python ABI tag{s}: {}",
5610                            "hint".bold().cyan(),
5611                            ":".bold(),
5612                            best,
5613                            package.cyan(),
5614                            format!("v{version}").cyan(),
5615                            tags.iter()
5616                                .map(|tag| format!("`{}`", tag.cyan()))
5617                                .join(", "),
5618                        )
5619                    } else {
5620                        write!(
5621                            f,
5622                            "{}{} You're using {}, but `{}` only has wheels with the following Python ABI tag{s}: {}",
5623                            "hint".bold().cyan(),
5624                            ":".bold(),
5625                            best,
5626                            package.cyan(),
5627                            tags.iter()
5628                                .map(|tag| format!("`{}`", tag.cyan()))
5629                                .join(", "),
5630                        )
5631                    }
5632                } else {
5633                    let s = if tags.len() == 1 { "" } else { "s" };
5634                    if let Some(version) = version {
5635                        write!(
5636                            f,
5637                            "{}{} Wheels are available for `{}` ({}) with the following Python ABI tag{s}: {}",
5638                            "hint".bold().cyan(),
5639                            ":".bold(),
5640                            package.cyan(),
5641                            format!("v{version}").cyan(),
5642                            tags.iter()
5643                                .map(|tag| format!("`{}`", tag.cyan()))
5644                                .join(", "),
5645                        )
5646                    } else {
5647                        write!(
5648                            f,
5649                            "{}{} Wheels are available for `{}` with the following Python ABI tag{s}: {}",
5650                            "hint".bold().cyan(),
5651                            ":".bold(),
5652                            package.cyan(),
5653                            tags.iter()
5654                                .map(|tag| format!("`{}`", tag.cyan()))
5655                                .join(", "),
5656                        )
5657                    }
5658                }
5659            }
5660            Self::PlatformTags {
5661                package,
5662                version,
5663                tags,
5664                best,
5665                markers,
5666            } => {
5667                let s = if tags.len() == 1 { "" } else { "s" };
5668                if let Some(best) = best {
5669                    let example_marker = Self::suggest_environment_marker(markers);
5670                    let best = if let Some(pretty) = best.pretty() {
5671                        format!("{} (`{}`)", pretty.cyan(), best.cyan())
5672                    } else {
5673                        format!("`{}`", best.cyan())
5674                    };
5675                    let package_ref = if let Some(version) = version {
5676                        format!("`{}` ({})", package.cyan(), format!("v{version}").cyan())
5677                    } else {
5678                        format!("`{}`", package.cyan())
5679                    };
5680                    write!(
5681                        f,
5682                        "{}{} 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",
5683                        "hint".bold().cyan(),
5684                        ":".bold(),
5685                        best,
5686                        package_ref,
5687                        tags.iter()
5688                            .map(|tag| format!("`{}`", tag.cyan()))
5689                            .join(", "),
5690                        format!("\"{example_marker}\"").cyan(),
5691                        "tool.uv.required-environments".green()
5692                    )
5693                } else {
5694                    if let Some(version) = version {
5695                        write!(
5696                            f,
5697                            "{}{} Wheels are available for `{}` ({}) on the following platform{s}: {}",
5698                            "hint".bold().cyan(),
5699                            ":".bold(),
5700                            package.cyan(),
5701                            format!("v{version}").cyan(),
5702                            tags.iter()
5703                                .map(|tag| format!("`{}`", tag.cyan()))
5704                                .join(", "),
5705                        )
5706                    } else {
5707                        write!(
5708                            f,
5709                            "{}{} Wheels are available for `{}` on the following platform{s}: {}",
5710                            "hint".bold().cyan(),
5711                            ":".bold(),
5712                            package.cyan(),
5713                            tags.iter()
5714                                .map(|tag| format!("`{}`", tag.cyan()))
5715                                .join(", "),
5716                        )
5717                    }
5718                }
5719            }
5720        }
5721    }
5722}
5723
5724/// An error that occurs when generating a `Lock` data structure.
5725///
5726/// These errors are sometimes the result of possible programming bugs.
5727/// For example, if there are two or more duplicative distributions given
5728/// to `Lock::new`, then an error is returned. It's likely that the fault
5729/// is with the caller somewhere in such cases.
5730#[derive(Debug, thiserror::Error)]
5731enum LockErrorKind {
5732    /// An error that occurs when multiple packages with the same
5733    /// ID were found.
5734    #[error("Found duplicate package `{id}`", id = id.cyan())]
5735    DuplicatePackage {
5736        /// The ID of the conflicting package.
5737        id: PackageId,
5738    },
5739    /// An error that occurs when there are multiple dependencies for the
5740    /// same package that have identical identifiers.
5741    #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = id.cyan(), dependency = dependency.cyan())]
5742    DuplicateDependency {
5743        /// The ID of the package for which a duplicate dependency was
5744        /// found.
5745        id: PackageId,
5746        /// The ID of the conflicting dependency.
5747        dependency: Dependency,
5748    },
5749    /// An error that occurs when there are multiple dependencies for the
5750    /// same package that have identical identifiers, as part of the
5751    /// that package's optional dependencies.
5752    #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}[{extra}]").cyan(), dependency = dependency.cyan())]
5753    DuplicateOptionalDependency {
5754        /// The ID of the package for which a duplicate dependency was
5755        /// found.
5756        id: PackageId,
5757        /// The name of the extra.
5758        extra: ExtraName,
5759        /// The ID of the conflicting dependency.
5760        dependency: Dependency,
5761    },
5762    /// An error that occurs when there are multiple dependencies for the
5763    /// same package that have identical identifiers, as part of the
5764    /// that package's development dependencies.
5765    #[error("For package `{id}`, found duplicate dependency `{dependency}`", id = format!("{id}:{group}").cyan(), dependency = dependency.cyan())]
5766    DuplicateDevDependency {
5767        /// The ID of the package for which a duplicate dependency was
5768        /// found.
5769        id: PackageId,
5770        /// The name of the dev dependency group.
5771        group: GroupName,
5772        /// The ID of the conflicting dependency.
5773        dependency: Dependency,
5774    },
5775    /// An error that occurs when the URL to a file for a wheel or
5776    /// source dist could not be converted to a structured `url::Url`.
5777    #[error(transparent)]
5778    InvalidUrl(
5779        /// The underlying error that occurred. This includes the
5780        /// errant URL in its error message.
5781        #[from]
5782        ToUrlError,
5783    ),
5784    /// An error that occurs when the extension can't be determined
5785    /// for a given wheel or source distribution.
5786    #[error("Failed to parse file extension for `{id}`; expected one of: {err}", id = id.cyan())]
5787    MissingExtension {
5788        /// The filename that was expected to have an extension.
5789        id: PackageId,
5790        /// The list of valid extensions that were expected.
5791        err: ExtensionError,
5792    },
5793    /// Failed to parse a Git source URL.
5794    #[error("Failed to parse Git URL")]
5795    InvalidGitSourceUrl(
5796        /// The underlying error that occurred. This includes the
5797        /// errant URL in the message.
5798        #[source]
5799        SourceParseError,
5800    ),
5801    #[error("Failed to parse timestamp")]
5802    InvalidTimestamp(
5803        /// The underlying error that occurred. This includes the
5804        /// errant timestamp in the message.
5805        #[source]
5806        jiff::Error,
5807    ),
5808    /// An error that occurs when there's an unrecognized dependency.
5809    ///
5810    /// That is, a dependency for a package that isn't in the lockfile.
5811    #[error("For package `{id}`, found dependency `{dependency}` with no locked package", id = id.cyan(), dependency = dependency.cyan())]
5812    UnrecognizedDependency {
5813        /// The ID of the package that has an unrecognized dependency.
5814        id: PackageId,
5815        /// The ID of the dependency that doesn't have a corresponding package
5816        /// entry.
5817        dependency: Dependency,
5818    },
5819    /// An error that occurs when a hash is expected (or not) for a particular
5820    /// artifact, but one was not found (or was).
5821    #[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" })]
5822    Hash {
5823        /// The ID of the package that has a missing hash.
5824        id: PackageId,
5825        /// The specific type of artifact, e.g., "source package"
5826        /// or "wheel".
5827        artifact_type: &'static str,
5828        /// When true, a hash is expected to be present.
5829        expected: bool,
5830    },
5831    /// An error that occurs when a package is included with an extra name,
5832    /// but no corresponding base package (i.e., without the extra) exists.
5833    #[error("Found package `{id}` with extra `{extra}` but no base package", id = id.cyan(), extra = extra.cyan())]
5834    MissingExtraBase {
5835        /// The ID of the package that has a missing base.
5836        id: PackageId,
5837        /// The extra name that was found.
5838        extra: ExtraName,
5839    },
5840    /// An error that occurs when a package is included with a development
5841    /// dependency group, but no corresponding base package (i.e., without
5842    /// the group) exists.
5843    #[error("Found package `{id}` with development dependency group `{group}` but no base package", id = id.cyan())]
5844    MissingDevBase {
5845        /// The ID of the package that has a missing base.
5846        id: PackageId,
5847        /// The development dependency group that was found.
5848        group: GroupName,
5849    },
5850    /// An error that occurs from an invalid lockfile where a wheel comes from a non-wheel source
5851    /// such as a directory.
5852    #[error("Wheels cannot come from {source_type} sources")]
5853    InvalidWheelSource {
5854        /// The ID of the distribution that has a missing base.
5855        id: PackageId,
5856        /// The kind of the invalid source.
5857        source_type: &'static str,
5858    },
5859    /// An error that occurs when a distribution indicates that it is sourced from a remote
5860    /// registry, but is missing a URL.
5861    #[error("Found registry distribution `{name}` ({version}) without a valid URL", name = name.cyan(), version = format!("v{version}").cyan())]
5862    MissingUrl {
5863        /// The name of the distribution that is missing a URL.
5864        name: PackageName,
5865        /// The version of the distribution that is missing a URL.
5866        version: Version,
5867    },
5868    /// An error that occurs when a distribution indicates that it is sourced from a local registry,
5869    /// but is missing a path.
5870    #[error("Found registry distribution `{name}` ({version}) without a valid path", name = name.cyan(), version = format!("v{version}").cyan())]
5871    MissingPath {
5872        /// The name of the distribution that is missing a path.
5873        name: PackageName,
5874        /// The version of the distribution that is missing a path.
5875        version: Version,
5876    },
5877    /// An error that occurs when a distribution indicates that it is sourced from a registry, but
5878    /// is missing a filename.
5879    #[error("Found registry distribution `{id}` without a valid filename", id = id.cyan())]
5880    MissingFilename {
5881        /// The ID of the distribution that is missing a filename.
5882        id: PackageId,
5883    },
5884    /// An error that occurs when a distribution is included with neither wheels nor a source
5885    /// distribution.
5886    #[error("Distribution `{id}` can't be installed because it doesn't have a source distribution or wheel for the current platform", id = id.cyan())]
5887    NeitherSourceDistNorWheel {
5888        /// The ID of the distribution.
5889        id: PackageId,
5890    },
5891    /// An error that occurs when a distribution is marked as both `--no-binary` and `--no-build`.
5892    #[error("Distribution `{id}` can't be installed because it is marked as both `--no-binary` and `--no-build`", id = id.cyan())]
5893    NoBinaryNoBuild {
5894        /// The ID of the distribution.
5895        id: PackageId,
5896    },
5897    /// An error that occurs when a distribution is marked as `--no-binary`, but no source
5898    /// distribution is available.
5899    #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but has no source distribution", id = id.cyan())]
5900    NoBinary {
5901        /// The ID of the distribution.
5902        id: PackageId,
5903    },
5904    /// An error that occurs when a distribution is marked as `--no-build`, but no binary
5905    /// distribution is available.
5906    #[error("Distribution `{id}` can't be installed because it is marked as `--no-build` but has no binary distribution", id = id.cyan())]
5907    NoBuild {
5908        /// The ID of the distribution.
5909        id: PackageId,
5910    },
5911    /// An error that occurs when a wheel-only distribution is incompatible with the current
5912    /// platform.
5913    #[error("Distribution `{id}` can't be installed because the binary distribution is incompatible with the current platform", id = id.cyan())]
5914    IncompatibleWheelOnly {
5915        /// The ID of the distribution.
5916        id: PackageId,
5917    },
5918    /// An error that occurs when a wheel-only source is marked as `--no-binary`.
5919    #[error("Distribution `{id}` can't be installed because it is marked as `--no-binary` but is itself a binary distribution", id = id.cyan())]
5920    NoBinaryWheelOnly {
5921        /// The ID of the distribution.
5922        id: PackageId,
5923    },
5924    /// An error that occurs when converting between URLs and paths.
5925    #[error("Found dependency `{id}` with no locked distribution", id = id.cyan())]
5926    VerbatimUrl {
5927        /// The ID of the distribution that has a missing base.
5928        id: PackageId,
5929        /// The inner error we forward.
5930        #[source]
5931        err: VerbatimUrlError,
5932    },
5933    /// An error that occurs when parsing an existing requirement.
5934    #[error("Could not compute relative path between workspace and distribution")]
5935    DistributionRelativePath(
5936        /// The inner error we forward.
5937        #[source]
5938        io::Error,
5939    ),
5940    /// An error that occurs when converting an index URL to a relative path
5941    #[error("Could not compute relative path between workspace and index")]
5942    IndexRelativePath(
5943        /// The inner error we forward.
5944        #[source]
5945        io::Error,
5946    ),
5947    /// An error that occurs when converting a lockfile path from relative to absolute.
5948    #[error("Could not compute absolute path from workspace root and lockfile path")]
5949    AbsolutePath(
5950        /// The inner error we forward.
5951        #[source]
5952        io::Error,
5953    ),
5954    /// An error that occurs when an ambiguous `package.dependency` is
5955    /// missing a `version` field.
5956    #[error("Dependency `{name}` has missing `version` field but has more than one matching package", name = name.cyan())]
5957    MissingDependencyVersion {
5958        /// The name of the dependency that is missing a `version` field.
5959        name: PackageName,
5960    },
5961    /// An error that occurs when an ambiguous `package.dependency` is
5962    /// missing a `source` field.
5963    #[error("Dependency `{name}` has missing `source` field but has more than one matching package", name = name.cyan())]
5964    MissingDependencySource {
5965        /// The name of the dependency that is missing a `source` field.
5966        name: PackageName,
5967    },
5968    /// An error that occurs when parsing an existing requirement.
5969    #[error("Could not compute relative path between workspace and requirement")]
5970    RequirementRelativePath(
5971        /// The inner error we forward.
5972        #[source]
5973        io::Error,
5974    ),
5975    /// An error that occurs when parsing an existing requirement.
5976    #[error("Could not convert between URL and path")]
5977    RequirementVerbatimUrl(
5978        /// The inner error we forward.
5979        #[source]
5980        VerbatimUrlError,
5981    ),
5982    /// An error that occurs when parsing a registry's index URL.
5983    #[error("Could not convert between URL and path")]
5984    RegistryVerbatimUrl(
5985        /// The inner error we forward.
5986        #[source]
5987        VerbatimUrlError,
5988    ),
5989    /// An error that occurs when converting a path to a URL.
5990    #[error("Failed to convert path to URL: {path}", path = path.display().cyan())]
5991    PathToUrl { path: Box<Path> },
5992    /// An error that occurs when converting a URL to a path
5993    #[error("Failed to convert URL to path: {url}", url = url.cyan())]
5994    UrlToPath { url: DisplaySafeUrl },
5995    /// An error that occurs when multiple packages with the same
5996    /// name were found when identifying the root packages.
5997    #[error("Found multiple packages matching `{name}`", name = name.cyan())]
5998    MultipleRootPackages {
5999        /// The ID of the package.
6000        name: PackageName,
6001    },
6002    /// An error that occurs when a root package can't be found.
6003    #[error("Could not find root package `{name}`", name = name.cyan())]
6004    MissingRootPackage {
6005        /// The ID of the package.
6006        name: PackageName,
6007    },
6008    /// An error that occurs when resolving metadata for a package.
6009    #[error("Failed to generate package metadata for `{id}`", id = id.cyan())]
6010    Resolution {
6011        /// The ID of the distribution that failed to resolve.
6012        id: PackageId,
6013        /// The inner error we forward.
6014        #[source]
6015        err: uv_distribution::Error,
6016    },
6017    /// A package has inconsistent versions in a single entry
6018    // Using name instead of id since the version in the id is part of the conflict.
6019    #[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())]
6020    InconsistentVersions {
6021        /// The name of the package with the inconsistent entry.
6022        name: PackageName,
6023        /// The version of the package with the inconsistent entry.
6024        version: Version,
6025        /// The wheel with the inconsistent version.
6026        wheel: Wheel,
6027    },
6028    #[error(
6029        "Found conflicting extras `{package1}[{extra1}]` \
6030         and `{package2}[{extra2}]` enabled simultaneously"
6031    )]
6032    ConflictingExtra {
6033        package1: PackageName,
6034        extra1: ExtraName,
6035        package2: PackageName,
6036        extra2: ExtraName,
6037    },
6038    #[error(transparent)]
6039    GitUrlParse(#[from] GitUrlParseError),
6040    #[error("Failed to read `{path}`")]
6041    UnreadablePyprojectToml {
6042        path: PathBuf,
6043        #[source]
6044        err: std::io::Error,
6045    },
6046    #[error("Failed to parse `{path}`")]
6047    InvalidPyprojectToml {
6048        path: PathBuf,
6049        #[source]
6050        err: toml::de::Error,
6051    },
6052    /// An error that occurs when a workspace member has a non-local source.
6053    #[error("Workspace member `{id}` has non-local source", id = id.cyan())]
6054    NonLocalWorkspaceMember {
6055        /// The ID of the workspace member with an invalid source.
6056        id: PackageId,
6057    },
6058}
6059
6060/// An error that occurs when a source string could not be parsed.
6061#[derive(Debug, thiserror::Error)]
6062enum SourceParseError {
6063    /// An error that occurs when the URL in the source is invalid.
6064    #[error("Invalid URL in source `{given}`")]
6065    InvalidUrl {
6066        /// The source string given.
6067        given: String,
6068        /// The URL parse error.
6069        #[source]
6070        err: DisplaySafeUrlError,
6071    },
6072    /// An error that occurs when a Git URL is missing a precise commit SHA.
6073    #[error("Missing SHA in source `{given}`")]
6074    MissingSha {
6075        /// The source string given.
6076        given: String,
6077    },
6078    /// An error that occurs when a Git URL has an invalid SHA.
6079    #[error("Invalid SHA in source `{given}`")]
6080    InvalidSha {
6081        /// The source string given.
6082        given: String,
6083    },
6084}
6085
6086/// An error that occurs when a hash digest could not be parsed.
6087#[derive(Clone, Debug, Eq, PartialEq)]
6088struct HashParseError(&'static str);
6089
6090impl std::error::Error for HashParseError {}
6091
6092impl Display for HashParseError {
6093    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
6094        Display::fmt(self.0, f)
6095    }
6096}
6097
6098/// Format an array so that each element is on its own line and has a trailing comma.
6099///
6100/// Example:
6101///
6102/// ```toml
6103/// dependencies = [
6104///     { name = "idna" },
6105///     { name = "sniffio" },
6106/// ]
6107/// ```
6108fn each_element_on_its_line_array(elements: impl Iterator<Item = impl Into<Value>>) -> Array {
6109    let mut array = elements
6110        .map(|item| {
6111            let mut value = item.into();
6112            // Each dependency is on its own line and indented.
6113            value.decor_mut().set_prefix("\n    ");
6114            value
6115        })
6116        .collect::<Array>();
6117    // With a trailing comma, inserting another entry doesn't change the preceding line,
6118    // reducing the diff noise.
6119    array.set_trailing_comma(true);
6120    // The line break between the last element's comma and the closing square bracket.
6121    array.set_trailing("\n");
6122    array
6123}
6124
6125/// Returns the simplified string-ified version of each marker given.
6126///
6127/// Note that the marker strings returned will include conflict markers if they
6128/// are present.
6129fn simplified_universal_markers(
6130    markers: &[UniversalMarker],
6131    requires_python: &RequiresPython,
6132) -> Vec<String> {
6133    let mut pep508_only = vec![];
6134    let mut seen = FxHashSet::default();
6135    for marker in markers {
6136        let simplified =
6137            SimplifiedMarkerTree::new(requires_python, marker.pep508()).as_simplified_marker_tree();
6138        if seen.insert(simplified) {
6139            pep508_only.push(simplified);
6140        }
6141    }
6142    let any_overlap = pep508_only
6143        .iter()
6144        .tuple_combinations()
6145        .any(|(&marker1, &marker2)| !marker1.is_disjoint(marker2));
6146    let markers = if !any_overlap {
6147        pep508_only
6148    } else {
6149        markers
6150            .iter()
6151            .map(|marker| {
6152                SimplifiedMarkerTree::new(requires_python, marker.combined())
6153                    .as_simplified_marker_tree()
6154            })
6155            .collect()
6156    };
6157    markers
6158        .into_iter()
6159        .filter_map(MarkerTree::try_to_string)
6160        .collect()
6161}
6162
6163#[cfg(test)]
6164mod tests {
6165    use uv_warnings::anstream;
6166
6167    use super::*;
6168
6169    /// Assert a given display snapshot, stripping ANSI color codes.
6170    macro_rules! assert_stripped_snapshot {
6171        ($expr:expr, @$snapshot:literal) => {{
6172            let expr = format!("{}", $expr);
6173            let expr = format!("{}", anstream::adapter::strip_str(&expr));
6174            insta::assert_snapshot!(expr, @$snapshot);
6175        }};
6176    }
6177
6178    #[test]
6179    fn missing_dependency_source_unambiguous() {
6180        let data = r#"
6181version = 1
6182requires-python = ">=3.12"
6183
6184[[package]]
6185name = "a"
6186version = "0.1.0"
6187source = { registry = "https://pypi.org/simple" }
6188sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6189
6190[[package]]
6191name = "b"
6192version = "0.1.0"
6193source = { registry = "https://pypi.org/simple" }
6194sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6195
6196[[package.dependencies]]
6197name = "a"
6198version = "0.1.0"
6199"#;
6200        let result: Result<Lock, _> = toml::from_str(data);
6201        insta::assert_debug_snapshot!(result);
6202    }
6203
6204    #[test]
6205    fn missing_dependency_version_unambiguous() {
6206        let data = r#"
6207version = 1
6208requires-python = ">=3.12"
6209
6210[[package]]
6211name = "a"
6212version = "0.1.0"
6213source = { registry = "https://pypi.org/simple" }
6214sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6215
6216[[package]]
6217name = "b"
6218version = "0.1.0"
6219source = { registry = "https://pypi.org/simple" }
6220sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6221
6222[[package.dependencies]]
6223name = "a"
6224source = { registry = "https://pypi.org/simple" }
6225"#;
6226        let result: Result<Lock, _> = toml::from_str(data);
6227        insta::assert_debug_snapshot!(result);
6228    }
6229
6230    #[test]
6231    fn missing_dependency_source_version_unambiguous() {
6232        let data = r#"
6233version = 1
6234requires-python = ">=3.12"
6235
6236[[package]]
6237name = "a"
6238version = "0.1.0"
6239source = { registry = "https://pypi.org/simple" }
6240sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6241
6242[[package]]
6243name = "b"
6244version = "0.1.0"
6245source = { registry = "https://pypi.org/simple" }
6246sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6247
6248[[package.dependencies]]
6249name = "a"
6250"#;
6251        let result: Result<Lock, _> = toml::from_str(data);
6252        insta::assert_debug_snapshot!(result);
6253    }
6254
6255    #[test]
6256    fn missing_dependency_source_ambiguous() {
6257        let data = r#"
6258version = 1
6259requires-python = ">=3.12"
6260
6261[[package]]
6262name = "a"
6263version = "0.1.0"
6264source = { registry = "https://pypi.org/simple" }
6265sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6266
6267[[package]]
6268name = "a"
6269version = "0.1.1"
6270source = { registry = "https://pypi.org/simple" }
6271sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6272
6273[[package]]
6274name = "b"
6275version = "0.1.0"
6276source = { registry = "https://pypi.org/simple" }
6277sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6278
6279[[package.dependencies]]
6280name = "a"
6281version = "0.1.0"
6282"#;
6283        let result = toml::from_str::<Lock>(data).unwrap_err();
6284        assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6285    }
6286
6287    #[test]
6288    fn missing_dependency_version_ambiguous() {
6289        let data = r#"
6290version = 1
6291requires-python = ">=3.12"
6292
6293[[package]]
6294name = "a"
6295version = "0.1.0"
6296source = { registry = "https://pypi.org/simple" }
6297sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6298
6299[[package]]
6300name = "a"
6301version = "0.1.1"
6302source = { registry = "https://pypi.org/simple" }
6303sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6304
6305[[package]]
6306name = "b"
6307version = "0.1.0"
6308source = { registry = "https://pypi.org/simple" }
6309sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6310
6311[[package.dependencies]]
6312name = "a"
6313source = { registry = "https://pypi.org/simple" }
6314"#;
6315        let result = toml::from_str::<Lock>(data).unwrap_err();
6316        assert_stripped_snapshot!(result, @"Dependency `a` has missing `version` field but has more than one matching package");
6317    }
6318
6319    #[test]
6320    fn missing_dependency_source_version_ambiguous() {
6321        let data = r#"
6322version = 1
6323requires-python = ">=3.12"
6324
6325[[package]]
6326name = "a"
6327version = "0.1.0"
6328source = { registry = "https://pypi.org/simple" }
6329sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6330
6331[[package]]
6332name = "a"
6333version = "0.1.1"
6334source = { registry = "https://pypi.org/simple" }
6335sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6336
6337[[package]]
6338name = "b"
6339version = "0.1.0"
6340source = { registry = "https://pypi.org/simple" }
6341sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6342
6343[[package.dependencies]]
6344name = "a"
6345"#;
6346        let result = toml::from_str::<Lock>(data).unwrap_err();
6347        assert_stripped_snapshot!(result, @"Dependency `a` has missing `source` field but has more than one matching package");
6348    }
6349
6350    #[test]
6351    fn missing_dependency_version_dynamic() {
6352        let data = r#"
6353version = 1
6354requires-python = ">=3.12"
6355
6356[[package]]
6357name = "a"
6358source = { editable = "path/to/a" }
6359
6360[[package]]
6361name = "a"
6362version = "0.1.1"
6363source = { registry = "https://pypi.org/simple" }
6364sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6365
6366[[package]]
6367name = "b"
6368version = "0.1.0"
6369source = { registry = "https://pypi.org/simple" }
6370sdist = { url = "https://example.com", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 0 }
6371
6372[[package.dependencies]]
6373name = "a"
6374source = { editable = "path/to/a" }
6375"#;
6376        let result = toml::from_str::<Lock>(data);
6377        insta::assert_debug_snapshot!(result);
6378    }
6379
6380    #[test]
6381    fn hash_optional_missing() {
6382        let data = r#"
6383version = 1
6384requires-python = ">=3.12"
6385
6386[[package]]
6387name = "anyio"
6388version = "4.3.0"
6389source = { registry = "https://pypi.org/simple" }
6390wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl" }]
6391"#;
6392        let result: Result<Lock, _> = toml::from_str(data);
6393        insta::assert_debug_snapshot!(result);
6394    }
6395
6396    #[test]
6397    fn hash_optional_present() {
6398        let data = r#"
6399version = 1
6400requires-python = ">=3.12"
6401
6402[[package]]
6403name = "anyio"
6404version = "4.3.0"
6405source = { registry = "https://pypi.org/simple" }
6406wheels = [{ url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6407"#;
6408        let result: Result<Lock, _> = toml::from_str(data);
6409        insta::assert_debug_snapshot!(result);
6410    }
6411
6412    #[test]
6413    fn hash_required_present() {
6414        let data = r#"
6415version = 1
6416requires-python = ">=3.12"
6417
6418[[package]]
6419name = "anyio"
6420version = "4.3.0"
6421source = { path = "file:///foo/bar" }
6422wheels = [{ url = "file:///foo/bar/anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8" }]
6423"#;
6424        let result: Result<Lock, _> = toml::from_str(data);
6425        insta::assert_debug_snapshot!(result);
6426    }
6427
6428    #[test]
6429    fn source_direct_no_subdir() {
6430        let data = r#"
6431version = 1
6432requires-python = ">=3.12"
6433
6434[[package]]
6435name = "anyio"
6436version = "4.3.0"
6437source = { url = "https://burntsushi.net" }
6438"#;
6439        let result: Result<Lock, _> = toml::from_str(data);
6440        insta::assert_debug_snapshot!(result);
6441    }
6442
6443    #[test]
6444    fn source_direct_has_subdir() {
6445        let data = r#"
6446version = 1
6447requires-python = ">=3.12"
6448
6449[[package]]
6450name = "anyio"
6451version = "4.3.0"
6452source = { url = "https://burntsushi.net", subdirectory = "wat/foo/bar" }
6453"#;
6454        let result: Result<Lock, _> = toml::from_str(data);
6455        insta::assert_debug_snapshot!(result);
6456    }
6457
6458    #[test]
6459    fn source_directory() {
6460        let data = r#"
6461version = 1
6462requires-python = ">=3.12"
6463
6464[[package]]
6465name = "anyio"
6466version = "4.3.0"
6467source = { directory = "path/to/dir" }
6468"#;
6469        let result: Result<Lock, _> = toml::from_str(data);
6470        insta::assert_debug_snapshot!(result);
6471    }
6472
6473    #[test]
6474    fn source_editable() {
6475        let data = r#"
6476version = 1
6477requires-python = ">=3.12"
6478
6479[[package]]
6480name = "anyio"
6481version = "4.3.0"
6482source = { editable = "path/to/dir" }
6483"#;
6484        let result: Result<Lock, _> = toml::from_str(data);
6485        insta::assert_debug_snapshot!(result);
6486    }
6487}