Skip to main content

uv_distribution/metadata/
build_requires.rs

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