uv_distribution/metadata/
build_requires.rs

1use std::collections::BTreeMap;
2use std::path::Path;
3use uv_auth::CredentialsCache;
4use uv_configuration::SourceStrategy;
5use uv_distribution_types::{
6    ExtraBuildRequirement, ExtraBuildRequires, IndexLocations, Requirement,
7};
8use uv_normalize::PackageName;
9use uv_workspace::pyproject::{ExtraBuildDependencies, ExtraBuildDependency, ToolUvSources};
10use uv_workspace::{
11    DiscoveryOptions, MemberDiscovery, ProjectWorkspace, Workspace, WorkspaceCache,
12};
13
14use crate::metadata::{LoweredRequirement, MetadataError};
15
16/// Lowered requirements from a `[build-system.requires]` field in a `pyproject.toml` file.
17#[derive(Debug, Clone)]
18pub struct BuildRequires {
19    pub name: Option<PackageName>,
20    pub requires_dist: Vec<Requirement>,
21}
22
23impl BuildRequires {
24    /// Lower without considering `tool.uv` in `pyproject.toml`, used for index and other archive
25    /// dependencies.
26    pub fn from_metadata23(metadata: uv_pypi_types::BuildRequires) -> Self {
27        Self {
28            name: metadata.name,
29            requires_dist: metadata
30                .requires_dist
31                .into_iter()
32                .map(Requirement::from)
33                .collect(),
34        }
35    }
36
37    /// Lower by considering `tool.uv` in `pyproject.toml` if present, used for Git and directory
38    /// dependencies.
39    pub async fn from_project_maybe_workspace(
40        metadata: uv_pypi_types::BuildRequires,
41        install_path: &Path,
42        locations: &IndexLocations,
43        sources: SourceStrategy,
44        cache: &WorkspaceCache,
45        credentials_cache: &CredentialsCache,
46    ) -> Result<Self, MetadataError> {
47        let discovery = match sources {
48            SourceStrategy::Enabled => DiscoveryOptions::default(),
49            SourceStrategy::Disabled => DiscoveryOptions {
50                members: MemberDiscovery::None,
51                ..Default::default()
52            },
53        };
54        let Some(project_workspace) =
55            ProjectWorkspace::from_maybe_project_root(install_path, &discovery, cache).await?
56        else {
57            return Ok(Self::from_metadata23(metadata));
58        };
59
60        Self::from_project_workspace(
61            metadata,
62            &project_workspace,
63            locations,
64            sources,
65            credentials_cache,
66        )
67    }
68
69    /// Lower the `build-system.requires` field from a `pyproject.toml` file.
70    pub fn from_project_workspace(
71        metadata: uv_pypi_types::BuildRequires,
72        project_workspace: &ProjectWorkspace,
73        locations: &IndexLocations,
74        source_strategy: SourceStrategy,
75        credentials_cache: &CredentialsCache,
76    ) -> Result<Self, MetadataError> {
77        // Collect any `tool.uv.index` entries.
78        let empty = vec![];
79        let project_indexes = match source_strategy {
80            SourceStrategy::Enabled => project_workspace
81                .current_project()
82                .pyproject_toml()
83                .tool
84                .as_ref()
85                .and_then(|tool| tool.uv.as_ref())
86                .and_then(|uv| uv.index.as_deref())
87                .unwrap_or(&empty),
88            SourceStrategy::Disabled => &empty,
89        };
90
91        // Collect any `tool.uv.sources` and `tool.uv.dev_dependencies` from `pyproject.toml`.
92        let empty = BTreeMap::default();
93        let project_sources = match source_strategy {
94            SourceStrategy::Enabled => project_workspace
95                .current_project()
96                .pyproject_toml()
97                .tool
98                .as_ref()
99                .and_then(|tool| tool.uv.as_ref())
100                .and_then(|uv| uv.sources.as_ref())
101                .map(ToolUvSources::inner)
102                .unwrap_or(&empty),
103            SourceStrategy::Disabled => &empty,
104        };
105
106        // Lower the requirements.
107        let requires_dist = metadata.requires_dist.into_iter();
108        let requires_dist = match source_strategy {
109            SourceStrategy::Enabled => requires_dist
110                .flat_map(|requirement| {
111                    let requirement_name = requirement.name.clone();
112                    let extra = requirement.marker.top_level_extra_name();
113                    let group = None;
114                    LoweredRequirement::from_requirement(
115                        requirement,
116                        metadata.name.as_ref(),
117                        project_workspace.project_root(),
118                        project_sources,
119                        project_indexes,
120                        extra.as_deref(),
121                        group,
122                        locations,
123                        project_workspace.workspace(),
124                        None,
125                        credentials_cache,
126                    )
127                    .map(move |requirement| match requirement {
128                        Ok(requirement) => Ok(requirement.into_inner()),
129                        Err(err) => Err(MetadataError::LoweringError(
130                            requirement_name.clone(),
131                            Box::new(err),
132                        )),
133                    })
134                })
135                .collect::<Result<Vec<_>, _>>()?,
136            SourceStrategy::Disabled => requires_dist.into_iter().map(Requirement::from).collect(),
137        };
138
139        Ok(Self {
140            name: metadata.name,
141            requires_dist,
142        })
143    }
144
145    /// Lower the `build-system.requires` field from a `pyproject.toml` file.
146    pub fn from_workspace(
147        metadata: uv_pypi_types::BuildRequires,
148        workspace: &Workspace,
149        locations: &IndexLocations,
150        source_strategy: SourceStrategy,
151        credentials_cache: &CredentialsCache,
152    ) -> Result<Self, MetadataError> {
153        // Collect any `tool.uv.index` entries.
154        let empty = vec![];
155        let project_indexes = match source_strategy {
156            SourceStrategy::Enabled => workspace
157                .pyproject_toml()
158                .tool
159                .as_ref()
160                .and_then(|tool| tool.uv.as_ref())
161                .and_then(|uv| uv.index.as_deref())
162                .unwrap_or(&empty),
163            SourceStrategy::Disabled => &empty,
164        };
165
166        // Collect any `tool.uv.sources` and `tool.uv.dev_dependencies` from `pyproject.toml`.
167        let empty = BTreeMap::default();
168        let project_sources = match source_strategy {
169            SourceStrategy::Enabled => workspace
170                .pyproject_toml()
171                .tool
172                .as_ref()
173                .and_then(|tool| tool.uv.as_ref())
174                .and_then(|uv| uv.sources.as_ref())
175                .map(ToolUvSources::inner)
176                .unwrap_or(&empty),
177            SourceStrategy::Disabled => &empty,
178        };
179
180        // Lower the requirements.
181        let requires_dist = metadata.requires_dist.into_iter();
182        let requires_dist = match source_strategy {
183            SourceStrategy::Enabled => requires_dist
184                .flat_map(|requirement| {
185                    let requirement_name = requirement.name.clone();
186                    let extra = requirement.marker.top_level_extra_name();
187                    let group = None;
188                    LoweredRequirement::from_requirement(
189                        requirement,
190                        None,
191                        workspace.install_path(),
192                        project_sources,
193                        project_indexes,
194                        extra.as_deref(),
195                        group,
196                        locations,
197                        workspace,
198                        None,
199                        credentials_cache,
200                    )
201                    .map(move |requirement| match requirement {
202                        Ok(requirement) => Ok(requirement.into_inner()),
203                        Err(err) => Err(MetadataError::LoweringError(
204                            requirement_name.clone(),
205                            Box::new(err),
206                        )),
207                    })
208                })
209                .collect::<Result<Vec<_>, _>>()?,
210            SourceStrategy::Disabled => requires_dist.into_iter().map(Requirement::from).collect(),
211        };
212
213        Ok(Self {
214            name: metadata.name,
215            requires_dist,
216        })
217    }
218}
219
220/// Lowered extra build dependencies.
221///
222/// This is a wrapper around [`ExtraBuildRequires`] that provides methods to lower
223/// [`ExtraBuildDependencies`] from a workspace context or from already lowered dependencies.
224#[derive(Debug, Clone, Default)]
225pub struct LoweredExtraBuildDependencies(ExtraBuildRequires);
226
227impl LoweredExtraBuildDependencies {
228    /// Return the [`ExtraBuildRequires`] that this was lowered into.
229    pub fn into_inner(self) -> ExtraBuildRequires {
230        self.0
231    }
232
233    /// Create from a workspace, lowering the extra build dependencies.
234    pub fn from_workspace(
235        extra_build_dependencies: ExtraBuildDependencies,
236        workspace: &Workspace,
237        index_locations: &IndexLocations,
238        source_strategy: SourceStrategy,
239        credentials_cache: &CredentialsCache,
240    ) -> Result<Self, MetadataError> {
241        match source_strategy {
242            SourceStrategy::Enabled => {
243                // Collect project sources and indexes
244                let project_indexes = workspace
245                    .pyproject_toml()
246                    .tool
247                    .as_ref()
248                    .and_then(|tool| tool.uv.as_ref())
249                    .and_then(|uv| uv.index.as_deref())
250                    .unwrap_or(&[]);
251
252                let empty_sources = BTreeMap::default();
253                let project_sources = workspace
254                    .pyproject_toml()
255                    .tool
256                    .as_ref()
257                    .and_then(|tool| tool.uv.as_ref())
258                    .and_then(|uv| uv.sources.as_ref())
259                    .map(ToolUvSources::inner)
260                    .unwrap_or(&empty_sources);
261
262                // Lower each package's extra build dependencies
263                let mut build_requires = ExtraBuildRequires::default();
264                for (package_name, requirements) in extra_build_dependencies {
265                    let lowered: Vec<ExtraBuildRequirement> = requirements
266                        .into_iter()
267                        .flat_map(
268                            |ExtraBuildDependency {
269                                 requirement,
270                                 match_runtime,
271                             }| {
272                                let requirement_name = requirement.name.clone();
273                                let extra = requirement.marker.top_level_extra_name();
274                                let group = None;
275                                LoweredRequirement::from_requirement(
276                                    requirement,
277                                    None,
278                                    workspace.install_path(),
279                                    project_sources,
280                                    project_indexes,
281                                    extra.as_deref(),
282                                    group,
283                                    index_locations,
284                                    workspace,
285                                    None,
286                                    credentials_cache,
287                                )
288                                .map(move |requirement| {
289                                    match requirement {
290                                        Ok(requirement) => Ok(ExtraBuildRequirement {
291                                            requirement: requirement.into_inner(),
292                                            match_runtime,
293                                        }),
294                                        Err(err) => Err(MetadataError::LoweringError(
295                                            requirement_name.clone(),
296                                            Box::new(err),
297                                        )),
298                                    }
299                                })
300                            },
301                        )
302                        .collect::<Result<Vec<_>, _>>()?;
303                    build_requires.insert(package_name, lowered);
304                }
305                Ok(Self(build_requires))
306            }
307            SourceStrategy::Disabled => Ok(Self::from_non_lowered(extra_build_dependencies)),
308        }
309    }
310
311    /// Create from lowered dependencies (for non-workspace contexts, like scripts).
312    pub fn from_lowered(extra_build_dependencies: ExtraBuildRequires) -> Self {
313        Self(extra_build_dependencies)
314    }
315
316    /// Create from unlowered dependencies (e.g., for contexts in the pip CLI).
317    pub fn from_non_lowered(extra_build_dependencies: ExtraBuildDependencies) -> Self {
318        Self(
319            extra_build_dependencies
320                .into_iter()
321                .map(|(name, requirements)| {
322                    (
323                        name,
324                        requirements
325                            .into_iter()
326                            .map(
327                                |ExtraBuildDependency {
328                                     requirement,
329                                     match_runtime,
330                                 }| {
331                                    ExtraBuildRequirement {
332                                        requirement: requirement.into(),
333                                        match_runtime,
334                                    }
335                                },
336                            )
337                            .collect::<Vec<_>>(),
338                    )
339                })
340                .collect(),
341        )
342    }
343}