Skip to main content

uv_distribution/metadata/
mod.rs

1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3
4use thiserror::Error;
5
6use uv_auth::CredentialsCache;
7use uv_cache::Cache;
8use uv_configuration::NoSources;
9use uv_distribution_types::{GitDirectorySourceUrl, IndexLocations, Requirement};
10use uv_normalize::{ExtraName, GroupName, PackageName};
11use uv_pep440::{Version, VersionSpecifiers};
12use uv_pypi_types::{HashDigests, ResolutionMetadata};
13use uv_workspace::dependency_groups::DependencyGroupError;
14use uv_workspace::{WorkspaceCache, WorkspaceError};
15
16pub use crate::metadata::build_requires::{BuildRequires, LoweredExtraBuildDependencies};
17pub use crate::metadata::dependency_groups::SourcedDependencyGroups;
18pub use crate::metadata::lowering::LoweredRequirement;
19pub use crate::metadata::lowering::LoweringError;
20pub use crate::metadata::requires_dist::{FlatRequiresDist, RequiresDist};
21
22mod build_requires;
23mod dependency_groups;
24mod lowering;
25mod requires_dist;
26
27#[derive(Debug, Error)]
28pub enum MetadataError {
29    #[error(transparent)]
30    Workspace(#[from] WorkspaceError),
31    #[error(transparent)]
32    DependencyGroup(#[from] DependencyGroupError),
33    #[error("No `pyproject.toml` found at: {0}")]
34    MissingPyprojectToml(PathBuf),
35    #[error("Failed to parse entry: `{0}`")]
36    LoweringError(PackageName, #[source] Box<LoweringError>),
37    #[error("Failed to parse entry in group `{0}`: `{1}`")]
38    GroupLoweringError(GroupName, PackageName, #[source] Box<LoweringError>),
39    #[error(
40        "Source entry for `{0}` only applies to extra `{1}`, but the `{1}` extra does not exist. When an extra is present on a source (e.g., `extra = \"{1}\"`), the relevant package must be included in the `project.optional-dependencies` section for that extra (e.g., `project.optional-dependencies = {{ \"{1}\" = [\"{0}\"] }}`)."
41    )]
42    MissingSourceExtra(PackageName, ExtraName),
43    #[error(
44        "Source entry for `{0}` only applies to extra `{1}`, but `{0}` was not found under the `project.optional-dependencies` section for that extra. When an extra is present on a source (e.g., `extra = \"{1}\"`), the relevant package must be included in the `project.optional-dependencies` section for that extra (e.g., `project.optional-dependencies = {{ \"{1}\" = [\"{0}\"] }}`)."
45    )]
46    IncompleteSourceExtra(PackageName, ExtraName),
47    #[error(
48        "Source entry for `{0}` only applies to dependency group `{1}`, but the `{1}` group does not exist. When a group is present on a source (e.g., `group = \"{1}\"`), the relevant package must be included in the `dependency-groups` section for that extra (e.g., `dependency-groups = {{ \"{1}\" = [\"{0}\"] }}`)."
49    )]
50    MissingSourceGroup(PackageName, GroupName),
51    #[error(
52        "Source entry for `{0}` only applies to dependency group `{1}`, but `{0}` was not found under the `dependency-groups` section for that group. When a group is present on a source (e.g., `group = \"{1}\"`), the relevant package must be included in the `dependency-groups` section for that extra (e.g., `dependency-groups = {{ \"{1}\" = [\"{0}\"] }}`)."
53    )]
54    IncompleteSourceGroup(PackageName, GroupName),
55}
56
57impl uv_errors::Hinted for MetadataError {
58    fn hints(&self) -> uv_errors::Hints<'_> {
59        match self {
60            Self::LoweringError(_, err) | Self::GroupLoweringError(_, _, err) => err.hints(),
61            _ => uv_errors::Hints::none(),
62        }
63    }
64}
65
66#[derive(Debug, Clone)]
67pub struct Metadata {
68    // Mandatory fields
69    pub name: PackageName,
70    pub version: Version,
71    // Optional fields
72    pub requires_dist: Box<[Requirement]>,
73    pub requires_python: Option<VersionSpecifiers>,
74    pub provides_extra: Box<[ExtraName]>,
75    pub dependency_groups: BTreeMap<GroupName, Box<[Requirement]>>,
76    pub dynamic: bool,
77}
78
79impl Metadata {
80    /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive
81    /// dependencies.
82    pub(crate) fn from_metadata23(metadata: ResolutionMetadata) -> Self {
83        // This route handles package metadata rather than explicit user input.
84        // Write local dependency paths relative to the lockfile.
85        Self::from_resolution_metadata(metadata).with_force_relative(true)
86    }
87
88    /// Lower metadata selected from `tool.uv.dependency-metadata`.
89    pub(crate) fn from_dependency_metadata(metadata: ResolutionMetadata) -> Self {
90        // Respect the relative/absolute path preference in user-provided metadata overrides.
91        Self::from_resolution_metadata(metadata)
92    }
93
94    /// Lower package metadata without selecting an output path policy.
95    fn from_resolution_metadata(metadata: ResolutionMetadata) -> Self {
96        Self {
97            name: metadata.name,
98            version: metadata.version,
99            requires_dist: Box::into_iter(metadata.requires_dist)
100                .map(Requirement::from)
101                .collect(),
102            requires_python: metadata.requires_python,
103            provides_extra: metadata.provides_extra,
104            dependency_groups: BTreeMap::default(),
105            dynamic: metadata.dynamic,
106        }
107    }
108
109    /// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory
110    /// dependencies.
111    pub async fn from_workspace(
112        metadata: ResolutionMetadata,
113        install_path: &Path,
114        git_source: Option<&GitWorkspaceMember<'_>>,
115        locations: &IndexLocations,
116        sources: NoSources,
117        editable: bool,
118        cache: &Cache,
119        workspace_cache: &WorkspaceCache,
120        credentials_cache: &CredentialsCache,
121    ) -> Result<Self, MetadataError> {
122        // Lower the requirements.
123        let requires_dist = uv_pypi_types::RequiresDist {
124            name: metadata.name,
125            requires_dist: metadata.requires_dist,
126            provides_extra: metadata.provides_extra,
127            dynamic: metadata.dynamic,
128        };
129        let RequiresDist {
130            name,
131            requires_dist,
132            provides_extra,
133            dependency_groups,
134            dynamic,
135        } = RequiresDist::from_project_maybe_workspace(
136            requires_dist,
137            install_path,
138            git_source,
139            locations,
140            sources,
141            editable,
142            cache,
143            workspace_cache,
144            credentials_cache,
145        )
146        .await?;
147
148        // Combine with the remaining metadata.
149        Ok(Self {
150            name,
151            version: metadata.version,
152            requires_dist,
153            requires_python: metadata.requires_python,
154            provides_extra,
155            dependency_groups,
156            dynamic,
157        })
158    }
159
160    /// Set whether local dependency sources should be represented by relative paths.
161    ///
162    /// Disabling this restores each URL's original path spelling preference.
163    #[must_use]
164    pub fn with_force_relative(mut self, force_relative: bool) -> Self {
165        for requirement in self.requires_dist.iter_mut().chain(
166            self.dependency_groups
167                .values_mut()
168                .flat_map(|requirements| requirements.iter_mut()),
169        ) {
170            requirement.set_force_relative(force_relative);
171        }
172
173        self
174    }
175}
176
177/// The metadata associated with an archive.
178#[derive(Debug, Clone)]
179pub struct ArchiveMetadata {
180    /// The [`Metadata`] for the underlying distribution.
181    pub metadata: Metadata,
182    /// Hashes computed from the source or built archive.
183    pub hashes: HashDigests,
184}
185
186impl ArchiveMetadata {
187    /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive
188    /// dependencies.
189    pub fn from_metadata23(metadata: ResolutionMetadata) -> Self {
190        Self {
191            metadata: Metadata::from_metadata23(metadata),
192            hashes: HashDigests::empty(),
193        }
194    }
195}
196
197impl From<Metadata> for ArchiveMetadata {
198    fn from(metadata: Metadata) -> Self {
199        Self {
200            metadata,
201            hashes: HashDigests::empty(),
202        }
203    }
204}
205
206/// A workspace member from a checked-out Git repo.
207#[derive(Debug, Clone)]
208pub struct GitWorkspaceMember<'a> {
209    /// The root of the checkout, which may be the root of the workspace or may be above the
210    /// workspace root.
211    pub fetch_root: &'a Path,
212    pub git_source: &'a GitDirectorySourceUrl<'a>,
213}