uv_distribution/metadata/
build_requires.rs

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