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