Skip to main content

uv_build_frontend/
lib.rs

1//! Build wheels from source distributions.
2//!
3//! <https://packaging.python.org/en/latest/specifications/source-distribution-format/>
4
5mod error;
6mod pipreqs;
7
8use std::borrow::Cow;
9use std::ffi::OsString;
10use std::fmt::Formatter;
11use std::fmt::Write;
12use std::io;
13use std::path::{Path, PathBuf};
14use std::process::ExitStatus;
15use std::rc::Rc;
16use std::str::FromStr;
17use std::sync::LazyLock;
18use std::{env, iter};
19
20use fs_err as fs;
21use indoc::formatdoc;
22use itertools::Itertools;
23use rustc_hash::FxHashMap;
24use serde::de::{self, IntoDeserializer, SeqAccess, Visitor, value};
25use serde::{Deserialize, Deserializer};
26use tempfile::TempDir;
27use tokio::io::AsyncBufReadExt;
28use tokio::process::Command;
29use tokio::sync::{Mutex, Semaphore};
30use tracing::{Instrument, debug, info_span, instrument, warn};
31use uv_auth::CredentialsCache;
32use uv_cache_key::cache_digest;
33use uv_configuration::{BuildKind, BuildOutput, NoSources};
34use uv_distribution::BuildRequires;
35use uv_distribution_types::{
36    ConfigSettings, ExtraBuildRequirement, ExtraBuildRequires, IndexLocations, Requirement,
37    Resolution,
38};
39use uv_fs::{LockedFile, LockedFileMode};
40use uv_fs::{PythonExt, Simplified};
41use uv_normalize::PackageName;
42use uv_pep440::Version;
43use uv_pypi_types::VerbatimParsedUrl;
44use uv_python::{Interpreter, PythonEnvironment};
45use uv_static::EnvVars;
46use uv_types::{AnyErrorBuild, BuildContext, BuildIsolation, BuildStack, SourceBuildTrait};
47use uv_warnings::warn_user_once;
48use uv_workspace::WorkspaceCache;
49
50pub use crate::error::{Error, MissingHeaderCause};
51
52/// The default backend to use when PEP 517 is used without a `build-system` section.
53static DEFAULT_BACKEND: LazyLock<Pep517Backend> = LazyLock::new(|| Pep517Backend {
54    backend: "setuptools.build_meta:__legacy__".to_string(),
55    backend_path: None,
56    requirements: vec![Requirement::from(
57        uv_pep508::Requirement::from_str("setuptools >= 40.8.0").unwrap(),
58    )],
59});
60
61/// A `pyproject.toml` as specified in PEP 517.
62#[derive(Deserialize, Debug)]
63#[serde(rename_all = "kebab-case")]
64struct PyProjectToml {
65    /// Build-related data
66    build_system: Option<BuildSystem>,
67    /// Project metadata
68    project: Option<Project>,
69    /// Tool configuration
70    tool: Option<Tool>,
71}
72
73/// The `[project]` section of a pyproject.toml as specified in PEP 621.
74///
75/// This representation only includes a subset of the fields defined in PEP 621 necessary for
76/// informing wheel builds.
77#[derive(Deserialize, Debug)]
78#[serde(rename_all = "kebab-case")]
79struct Project {
80    /// The name of the project
81    name: PackageName,
82    /// The version of the project as supported by PEP 440
83    version: Option<Version>,
84    /// Specifies which fields listed by PEP 621 were intentionally unspecified so another tool
85    /// can/will provide such metadata dynamically.
86    dynamic: Option<Vec<String>>,
87}
88
89/// The `[build-system]` section of a pyproject.toml as specified in PEP 517.
90#[derive(Deserialize, Debug)]
91#[serde(rename_all = "kebab-case")]
92struct BuildSystem {
93    /// PEP 508 dependencies required to execute the build system.
94    requires: Vec<uv_pep508::Requirement<VerbatimParsedUrl>>,
95    /// A string naming a Python object that will be used to perform the build.
96    build_backend: Option<String>,
97    /// Specify that their backend code is hosted in-tree, this key contains a list of directories.
98    backend_path: Option<BackendPath>,
99}
100
101#[derive(Deserialize, Debug)]
102#[serde(rename_all = "kebab-case")]
103struct Tool {
104    uv: Option<ToolUv>,
105}
106
107#[derive(Deserialize, Debug)]
108#[serde(rename_all = "kebab-case")]
109struct ToolUv {
110    workspace: Option<de::IgnoredAny>,
111}
112
113impl BackendPath {
114    /// Return an iterator over the paths in the backend path.
115    fn iter(&self) -> impl Iterator<Item = &str> {
116        self.0.iter().map(String::as_str)
117    }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
121struct BackendPath(Vec<String>);
122
123impl<'de> Deserialize<'de> for BackendPath {
124    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
125    where
126        D: Deserializer<'de>,
127    {
128        struct StringOrVec;
129
130        impl<'de> Visitor<'de> for StringOrVec {
131            type Value = Vec<String>;
132
133            fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
134                formatter.write_str("list of strings")
135            }
136
137            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
138            where
139                E: de::Error,
140            {
141                // Allow exactly `backend-path = "."`, as used in `flit_core==2.3.0`.
142                if s == "." {
143                    Ok(vec![".".to_string()])
144                } else {
145                    Err(de::Error::invalid_value(de::Unexpected::Str(s), &self))
146                }
147            }
148
149            fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
150            where
151                S: SeqAccess<'de>,
152            {
153                Deserialize::deserialize(value::SeqAccessDeserializer::new(seq))
154            }
155        }
156
157        deserializer.deserialize_any(StringOrVec).map(BackendPath)
158    }
159}
160
161/// `[build-backend]` from pyproject.toml
162#[derive(Debug, Clone, PartialEq, Eq)]
163struct Pep517Backend {
164    /// The build backend string such as `setuptools.build_meta:__legacy__` or `maturin` from
165    /// `build-backend.backend` in pyproject.toml
166    ///
167    /// <https://peps.python.org/pep-0517/#build-wheel>
168    backend: String,
169    /// `build-backend.requirements` in pyproject.toml
170    requirements: Vec<Requirement>,
171    /// <https://peps.python.org/pep-0517/#in-tree-build-backends>
172    backend_path: Option<BackendPath>,
173}
174
175impl Pep517Backend {
176    fn backend_import(&self) -> String {
177        let import = if let Some((path, object)) = self.backend.split_once(':') {
178            format!("from {path} import {object} as backend")
179        } else {
180            format!("import {} as backend", self.backend)
181        };
182
183        let backend_path_encoded = self
184            .backend_path
185            .iter()
186            .flat_map(BackendPath::iter)
187            .map(|path| {
188                // Turn into properly escaped python string
189                '"'.to_string()
190                    + &path.replace('\\', "\\\\").replace('"', "\\\"")
191                    + &'"'.to_string()
192            })
193            .join(", ");
194
195        // > Projects can specify that their backend code is hosted in-tree by including the
196        // > backend-path key in pyproject.toml. This key contains a list of directories, which the
197        // > frontend will add to the start of sys.path when loading the backend, and running the
198        // > backend hooks.
199        formatdoc! {r#"
200            import sys
201
202            if sys.path[0] == "":
203                sys.path.pop(0)
204
205            sys.path = [{backend_path}] + sys.path
206
207            {import}
208        "#, backend_path = backend_path_encoded}
209    }
210
211    fn is_setuptools(&self) -> bool {
212        // either `setuptools.build_meta` or `setuptools.build_meta:__legacy__`
213        self.backend.split(':').next() == Some("setuptools.build_meta")
214    }
215}
216
217/// Uses an [`Rc`] internally, clone freely.
218#[derive(Debug, Default, Clone)]
219pub struct SourceBuildContext {
220    /// An in-memory resolution of the default backend's requirements for PEP 517 builds.
221    default_resolution: Rc<Mutex<Option<Resolution>>>,
222}
223
224/// Holds the state through a series of PEP 517 frontend to backend calls or a single `setup.py`
225/// invocation.
226///
227/// This keeps both the temp dir and the result of a potential `prepare_metadata_for_build_wheel`
228/// call which changes how we call `build_wheel`.
229pub struct SourceBuild {
230    temp_dir: TempDir,
231    source_tree: PathBuf,
232    config_settings: ConfigSettings,
233    /// If performing a PEP 517 build, the backend to use.
234    pep517_backend: Pep517Backend,
235    /// The PEP 621 project metadata, if any.
236    project: Option<Project>,
237    /// The virtual environment in which to build the source distribution.
238    venv: PythonEnvironment,
239    /// Populated if `prepare_metadata_for_build_wheel` was called.
240    ///
241    /// > If the build frontend has previously called `prepare_metadata_for_build_wheel` and depends
242    /// > on the wheel resulting from this call to have metadata matching this earlier call, then
243    /// > it should provide the path to the created .dist-info directory as the `metadata_directory`
244    /// > argument. If this argument is provided, then `build_wheel` MUST produce a wheel with
245    /// > identical metadata. The directory passed in by the build frontend MUST be identical to the
246    /// > directory created by `prepare_metadata_for_build_wheel`, including any unrecognized files
247    /// > it created.
248    metadata_directory: Option<PathBuf>,
249    /// The name of the package, if known.
250    package_name: Option<PackageName>,
251    /// The version of the package, if known.
252    package_version: Option<Version>,
253    /// Distribution identifier, e.g., `foo-1.2.3`. Used for error reporting if the name and
254    /// version are unknown.
255    version_id: Option<String>,
256    /// Whether we do a regular PEP 517 build or an PEP 660 editable build
257    build_kind: BuildKind,
258    /// Whether to send build output to `stderr` or `tracing`, etc.
259    level: BuildOutput,
260    /// Modified PATH that contains the `venv_bin`, `user_path` and `system_path` variables in that order
261    modified_path: OsString,
262    /// Environment variables to be passed in during metadata or wheel building
263    environment_variables: FxHashMap<OsString, OsString>,
264    /// Runner for Python scripts.
265    runner: PythonRunner,
266}
267
268impl SourceBuild {
269    /// Create a virtual environment in which to build a source distribution, extracting the
270    /// contents from an archive if necessary.
271    ///
272    /// `source_dist` is for error reporting only.
273    pub async fn setup(
274        source: &Path,
275        subdirectory: Option<&Path>,
276        install_path: &Path,
277        fallback_package_name: Option<&PackageName>,
278        fallback_package_version: Option<&Version>,
279        interpreter: &Interpreter,
280        build_context: &impl BuildContext,
281        source_build_context: SourceBuildContext,
282        version_id: Option<&str>,
283        locations: &IndexLocations,
284        no_sources: NoSources,
285        workspace_cache: &WorkspaceCache,
286        config_settings: ConfigSettings,
287        build_isolation: BuildIsolation<'_>,
288        extra_build_requires: &ExtraBuildRequires,
289        build_stack: &BuildStack,
290        build_kind: BuildKind,
291        mut environment_variables: FxHashMap<OsString, OsString>,
292        level: BuildOutput,
293        concurrent_builds: usize,
294        credentials_cache: &CredentialsCache,
295    ) -> Result<Self, Error> {
296        let temp_dir = build_context.cache().venv_dir()?;
297
298        let source_tree = if let Some(subdir) = subdirectory {
299            source.join(subdir)
300        } else {
301            source.to_path_buf()
302        };
303
304        // Check if we have a PEP 517 build backend.
305        let (pep517_backend, project) = Self::extract_pep517_backend(
306            &source_tree,
307            install_path,
308            fallback_package_name,
309            locations,
310            &no_sources,
311            workspace_cache,
312            credentials_cache,
313        )
314        .await
315        .map_err(|err| *err)?;
316
317        let package_name = project
318            .as_ref()
319            .map(|project| &project.name)
320            .or(fallback_package_name)
321            .cloned();
322        let package_version = project
323            .as_ref()
324            .and_then(|project| project.version.as_ref())
325            .or(fallback_package_version)
326            .cloned();
327
328        let extra_build_dependencies = package_name
329            .as_ref()
330            .and_then(|name| extra_build_requires.get(name).cloned())
331            .unwrap_or_default()
332            .into_iter()
333            .map(|requirement| {
334                match requirement {
335                    ExtraBuildRequirement {
336                        requirement,
337                        match_runtime: true,
338                    } if requirement.source.is_empty() => {
339                        Err(Error::UnmatchedRuntime(
340                            requirement.name.clone(),
341                            // SAFETY: if `package_name` is `None`, the iterator is empty.
342                            package_name.clone().unwrap(),
343                        ))
344                    }
345                    requirement => Ok(requirement),
346                }
347            })
348            .map_ok(Requirement::from)
349            .collect::<Result<Vec<_>, _>>()?;
350
351        // Create a virtual environment, or install into the shared environment if requested.
352        let venv = if let Some(venv) = build_isolation.shared_environment(package_name.as_ref()) {
353            venv.clone()
354        } else {
355            uv_virtualenv::create_venv(
356                temp_dir.path(),
357                interpreter.clone(),
358                uv_virtualenv::Prompt::None,
359                false,
360                uv_virtualenv::OnExisting::Remove(
361                    uv_virtualenv::RemovalReason::TemporaryEnvironment,
362                ),
363                false,
364                false,
365                false,
366            )?
367        };
368
369        // Set up the build environment. If build isolation is disabled, we assume the build
370        // environment is already setup.
371        if build_isolation.is_isolated(package_name.as_ref()) {
372            debug!("Resolving build requirements");
373
374            let dependency_sources = if extra_build_dependencies.is_empty() {
375                "`build-system.requires`"
376            } else {
377                "`build-system.requires` and `extra-build-dependencies`"
378            };
379
380            let resolved_requirements = Self::get_resolved_requirements(
381                build_context,
382                source_build_context,
383                &pep517_backend,
384                extra_build_dependencies,
385                build_stack,
386            )
387            .await?;
388
389            build_context
390                .install(&resolved_requirements, &venv, build_stack)
391                .await
392                .map_err(|err| Error::RequirementsInstall(dependency_sources, err.into()))?;
393        } else {
394            debug!("Proceeding without build isolation");
395        }
396
397        // Figure out what the modified path should be, and remove the PATH variable from the
398        // environment variables if it's there.
399        let user_path = environment_variables.remove(&OsString::from(EnvVars::PATH));
400
401        // See if there is an OS PATH variable.
402        let os_path = env::var_os(EnvVars::PATH);
403
404        // Prepend the user supplied PATH to the existing OS PATH
405        let modified_path = if let Some(user_path) = user_path {
406            match os_path {
407                // Prepend the user supplied PATH to the existing PATH
408                Some(env_path) => {
409                    let user_path = PathBuf::from(user_path);
410                    let new_path = env::split_paths(&user_path).chain(env::split_paths(&env_path));
411                    Some(env::join_paths(new_path).map_err(Error::BuildScriptPath)?)
412                }
413                // Use the user supplied PATH
414                None => Some(user_path),
415            }
416        } else {
417            os_path
418        };
419
420        // Prepend the venv bin directory to the modified path
421        let modified_path = if let Some(path) = modified_path {
422            let venv_path = iter::once(venv.scripts().to_path_buf()).chain(env::split_paths(&path));
423            env::join_paths(venv_path).map_err(Error::BuildScriptPath)?
424        } else {
425            OsString::from(venv.scripts())
426        };
427
428        // Create the PEP 517 build environment. If build isolation is disabled, we assume the build
429        // environment is already setup.
430        let runner = PythonRunner::new(concurrent_builds, level);
431        if build_isolation.is_isolated(package_name.as_ref()) {
432            debug!("Creating PEP 517 build environment");
433
434            create_pep517_build_environment(
435                &runner,
436                &source_tree,
437                install_path,
438                &venv,
439                &pep517_backend,
440                build_context,
441                package_name.as_ref(),
442                package_version.as_ref(),
443                version_id,
444                locations,
445                no_sources,
446                workspace_cache,
447                build_stack,
448                build_kind,
449                level,
450                &config_settings,
451                &environment_variables,
452                &modified_path,
453                &temp_dir,
454                credentials_cache,
455            )
456            .await?;
457        }
458
459        Ok(Self {
460            temp_dir,
461            source_tree,
462            pep517_backend,
463            project,
464            venv,
465            build_kind,
466            level,
467            config_settings,
468            metadata_directory: None,
469            package_name,
470            package_version,
471            version_id: version_id.map(ToString::to_string),
472            environment_variables,
473            modified_path,
474            runner,
475        })
476    }
477
478    /// Acquire a lock on the source tree, if necessary.
479    async fn acquire_lock(&self) -> Result<Option<LockedFile>, Error> {
480        // Depending on the command, setuptools puts `*.egg-info`, `build/`, and `dist/` in the
481        // source tree, and concurrent invocations of setuptools using the same source dir can
482        // stomp on each other. We need to lock something to fix that, but we don't want to dump a
483        // `.lock` file into the source tree that the user will need to .gitignore. Take a global
484        // proxy lock instead.
485        let mut source_tree_lock = None;
486        if self.pep517_backend.is_setuptools() {
487            debug!("Locking the source tree for setuptools");
488            let canonical_source_path = self.source_tree.canonicalize()?;
489            let lock_path = env::temp_dir().join(format!(
490                "uv-setuptools-{}.lock",
491                cache_digest(&canonical_source_path)
492            ));
493            source_tree_lock = LockedFile::acquire(
494                lock_path,
495                LockedFileMode::Exclusive,
496                self.source_tree.to_string_lossy(),
497            )
498            .await
499            .inspect_err(|err| {
500                warn!("Failed to acquire build lock: {err}");
501            })
502            .ok();
503        }
504        Ok(source_tree_lock)
505    }
506
507    async fn get_resolved_requirements(
508        build_context: &impl BuildContext,
509        source_build_context: SourceBuildContext,
510        pep517_backend: &Pep517Backend,
511        extra_build_dependencies: Vec<Requirement>,
512        build_stack: &BuildStack,
513    ) -> Result<Resolution, Error> {
514        Ok(
515            if pep517_backend.requirements == DEFAULT_BACKEND.requirements
516                && extra_build_dependencies.is_empty()
517            {
518                let mut resolution = source_build_context.default_resolution.lock().await;
519                if let Some(resolved_requirements) = &*resolution {
520                    resolved_requirements.clone()
521                } else {
522                    let resolved_requirements = build_context
523                        .resolve(&DEFAULT_BACKEND.requirements, build_stack)
524                        .await
525                        .map_err(|err| {
526                            Error::RequirementsResolve("`setup.py` build", err.into())
527                        })?;
528                    *resolution = Some(resolved_requirements.clone());
529                    resolved_requirements
530                }
531            } else {
532                let (requirements, dependency_sources) = if extra_build_dependencies.is_empty() {
533                    (
534                        Cow::Borrowed(&pep517_backend.requirements),
535                        "`build-system.requires`",
536                    )
537                } else {
538                    // If there are extra build dependencies, we need to resolve them together with
539                    // the backend requirements.
540                    let mut requirements = pep517_backend.requirements.clone();
541                    requirements.extend(extra_build_dependencies);
542                    (
543                        Cow::Owned(requirements),
544                        "`build-system.requires` and `extra-build-dependencies`",
545                    )
546                };
547                build_context
548                    .resolve(&requirements, build_stack)
549                    .await
550                    .map_err(|err| Error::RequirementsResolve(dependency_sources, err.into()))?
551            },
552        )
553    }
554
555    /// Extract the PEP 517 backend from the `pyproject.toml` or `setup.py` file.
556    async fn extract_pep517_backend(
557        source_tree: &Path,
558        install_path: &Path,
559        package_name: Option<&PackageName>,
560        locations: &IndexLocations,
561        no_sources: &NoSources,
562        workspace_cache: &WorkspaceCache,
563        credentials_cache: &CredentialsCache,
564    ) -> Result<(Pep517Backend, Option<Project>), Box<Error>> {
565        match fs::read_to_string(source_tree.join("pyproject.toml")) {
566            Ok(toml) => {
567                let pyproject_toml = toml_edit::Document::from_str(&toml)
568                    .map_err(Error::InvalidPyprojectTomlSyntax)?;
569                let pyproject_toml = PyProjectToml::deserialize(pyproject_toml.into_deserializer())
570                    .map_err(Error::InvalidPyprojectTomlSchema)?;
571
572                let backend = if let Some(build_system) = pyproject_toml.build_system {
573                    // If necessary, lower the requirements.
574                    let requirements = if let Some(name) = pyproject_toml
575                        .project
576                        .as_ref()
577                        .map(|project| &project.name)
578                        .or(package_name)
579                        // If sources are disabled, there's nothing to do here
580                        .filter(|_| !no_sources.all())
581                    {
582                        let build_requires = uv_pypi_types::BuildRequires {
583                            name: Some(name.clone()),
584                            requires_dist: build_system.requires,
585                        };
586                        let build_requires = BuildRequires::from_project_maybe_workspace(
587                            build_requires,
588                            install_path,
589                            locations,
590                            no_sources,
591                            workspace_cache,
592                            credentials_cache,
593                        )
594                        .await
595                        .map_err(Error::Lowering)?;
596                        build_requires.requires_dist
597                    } else {
598                        build_system
599                            .requires
600                            .into_iter()
601                            .map(Requirement::from)
602                            .collect()
603                    };
604
605                    Pep517Backend {
606                        // If `build-backend` is missing, inject the legacy setuptools backend, but
607                        // retain the `requires`, to match `pip` and `build`. Note that while PEP 517
608                        // says that in this case we "should revert to the legacy behaviour of running
609                        // `setup.py` (either directly, or by implicitly invoking the
610                        // `setuptools.build_meta:__legacy__` backend)", we found that in practice, only
611                        // the legacy setuptools backend is allowed. See also:
612                        // https://github.com/pypa/build/blob/de5b44b0c28c598524832dff685a98d5a5148c44/src/build/__init__.py#L114-L118
613                        backend: build_system
614                            .build_backend
615                            .unwrap_or_else(|| "setuptools.build_meta:__legacy__".to_string()),
616                        backend_path: build_system.backend_path,
617                        requirements,
618                    }
619                } else {
620                    // If a `pyproject.toml` is present, but `[build-system]` is missing, proceed
621                    // with a PEP 517 build using the default backend (`setuptools`), to match `pip`
622                    // and `build`.
623                    //
624                    // If there is no build system defined and there is no metadata source for
625                    // `setuptools`, warn. The build will succeed, but the metadata will be
626                    // incomplete (for example, the package name will be `UNKNOWN`).
627                    if pyproject_toml.project.is_none()
628                        && !source_tree.join("setup.py").is_file()
629                        && !source_tree.join("setup.cfg").is_file()
630                    {
631                        // Give a specific hint for `uv pip install .` in a workspace root.
632                        let looks_like_workspace_root = pyproject_toml
633                            .tool
634                            .as_ref()
635                            .and_then(|tool| tool.uv.as_ref())
636                            .and_then(|tool| tool.workspace.as_ref())
637                            .is_some();
638                        if looks_like_workspace_root {
639                            warn_user_once!(
640                                "`{}` appears to be a workspace root without a Python project; \
641                                consider using `uv sync` to install the workspace, or add a \
642                                `[build-system]` table to `pyproject.toml`",
643                                source_tree.simplified_display().cyan(),
644                            );
645                        } else {
646                            warn_user_once!(
647                                "`{}` does not appear to be a Python project, as the `pyproject.toml` \
648                                does not include a `[build-system]` table, and neither `setup.py` \
649                                nor `setup.cfg` are present in the directory",
650                                source_tree.simplified_display().cyan(),
651                            );
652                        }
653                    }
654
655                    DEFAULT_BACKEND.clone()
656                };
657                Ok((backend, pyproject_toml.project))
658            }
659            Err(err) if err.kind() == io::ErrorKind::NotFound => {
660                // We require either a `pyproject.toml` or a `setup.py` file at the top level.
661                if !source_tree.join("setup.py").is_file() {
662                    return Err(Box::new(Error::InvalidSourceDist(
663                        source_tree.to_path_buf(),
664                    )));
665                }
666
667                // If no `pyproject.toml` is present, by default, proceed with a PEP 517 build using
668                // the default backend, to match `build`. `pip` uses `setup.py` directly in this
669                // case, but plans to make PEP 517 builds the default in the future.
670                // See: https://github.com/pypa/pip/issues/9175.
671                Ok((DEFAULT_BACKEND.clone(), None))
672            }
673            Err(err) => Err(Box::new(err.into())),
674        }
675    }
676
677    /// Try calling `prepare_metadata_for_build_wheel` to get the metadata without executing the
678    /// actual build.
679    pub async fn get_metadata_without_build(&mut self) -> Result<Option<PathBuf>, Error> {
680        // We've already called this method; return the existing result.
681        if let Some(metadata_dir) = &self.metadata_directory {
682            return Ok(Some(metadata_dir.clone()));
683        }
684
685        // Lock the source tree, if necessary.
686        let _lock = self.acquire_lock().await?;
687
688        // Hatch allows for highly dynamic customization of metadata via hooks. In such cases, Hatch
689        // can't uphold the PEP 517 contract, in that the metadata Hatch would return by
690        // `prepare_metadata_for_build_wheel` isn't guaranteed to match that of the built wheel.
691        //
692        // Hatch disables `prepare_metadata_for_build_wheel` entirely for pip. We'll instead disable
693        // it on our end when metadata is defined as "dynamic" in the pyproject.toml, which should
694        // allow us to leverage the hook in _most_ cases while still avoiding incorrect metadata for
695        // the remaining cases.
696        //
697        // This heuristic will have false positives (i.e., there will be some Hatch projects for
698        // which we could have safely called `prepare_metadata_for_build_wheel`, despite having
699        // dynamic metadata). However, false positives are preferable to false negatives, since
700        // this is just an optimization.
701        //
702        // See: https://github.com/astral-sh/uv/issues/2130
703        if self.pep517_backend.backend == "hatchling.build" {
704            if self
705                .project
706                .as_ref()
707                .and_then(|project| project.dynamic.as_ref())
708                .is_some_and(|dynamic| {
709                    dynamic
710                        .iter()
711                        .any(|field| field == "dependencies" || field == "optional-dependencies")
712                })
713            {
714                return Ok(None);
715            }
716        }
717
718        let metadata_directory = self.temp_dir.path().join("metadata_directory");
719        fs::create_dir(&metadata_directory)?;
720
721        // Write the hook output to a file so that we can read it back reliably.
722        let outfile = self.temp_dir.path().join(format!(
723            "prepare_metadata_for_build_{}.txt",
724            self.build_kind
725        ));
726
727        debug!(
728            "Calling `{}.prepare_metadata_for_build_{}()`",
729            self.pep517_backend.backend, self.build_kind,
730        );
731        let script = formatdoc! {
732            r#"
733            {}
734            import json
735
736            prepare_metadata_for_build = getattr(backend, "prepare_metadata_for_build_{}", None)
737            if prepare_metadata_for_build:
738                dirname = prepare_metadata_for_build("{}", {})
739            else:
740                dirname = None
741
742            with open("{}", "w") as fp:
743                fp.write(dirname or "")
744            "#,
745            self.pep517_backend.backend_import(),
746            self.build_kind,
747            escape_path_for_python(&metadata_directory),
748            self.config_settings.escape_for_python(),
749            outfile.escape_for_python(),
750        };
751        let span = info_span!(
752            "run_python_script",
753            script = format!("prepare_metadata_for_build_{}", self.build_kind),
754            version_id = self.version_id,
755        );
756        let output = self
757            .runner
758            .run_script(
759                &self.venv,
760                &script,
761                &self.source_tree,
762                &self.environment_variables,
763                &self.modified_path,
764            )
765            .instrument(span)
766            .await?;
767        if !output.status.success() {
768            return Err(Error::from_command_output(
769                format!(
770                    "Call to `{}.prepare_metadata_for_build_{}` failed",
771                    self.pep517_backend.backend, self.build_kind
772                ),
773                &output,
774                self.level,
775                self.package_name.as_ref(),
776                self.package_version.as_ref(),
777                self.version_id.as_deref(),
778            ));
779        }
780
781        let dirname = fs::read_to_string(&outfile)?;
782        if dirname.is_empty() {
783            return Ok(None);
784        }
785        self.metadata_directory = Some(metadata_directory.join(dirname));
786        Ok(self.metadata_directory.clone())
787    }
788
789    /// Build a distribution from an archive (`.zip` or `.tar.gz`) or source tree, and return the
790    /// location of the built distribution.
791    ///
792    /// The location will be inside `temp_dir`, i.e., you must use the distribution before dropping
793    /// the temporary directory.
794    ///
795    /// <https://packaging.python.org/en/latest/specifications/source-distribution-format/>
796    #[instrument(skip_all, fields(version_id = self.version_id))]
797    pub async fn build(&self, wheel_dir: &Path) -> Result<String, Error> {
798        // The build scripts run with the extracted root as cwd, so they need the absolute path.
799        let wheel_dir = std::path::absolute(wheel_dir)?;
800        let filename = self.pep517_build(&wheel_dir).await?;
801        Ok(filename)
802    }
803
804    /// Perform a PEP 517 build for a wheel or source distribution (sdist).
805    async fn pep517_build(&self, output_dir: &Path) -> Result<String, Error> {
806        // Lock the source tree, if necessary.
807        let _lock = self.acquire_lock().await?;
808
809        // Write the hook output to a file so that we can read it back reliably.
810        let outfile = self
811            .temp_dir
812            .path()
813            .join(format!("build_{}.txt", self.build_kind));
814
815        // Construct the appropriate build script based on the build kind.
816        let script = match self.build_kind {
817            BuildKind::Sdist => {
818                debug!(
819                    r#"Calling `{}.build_{}("{}", {})`"#,
820                    self.pep517_backend.backend,
821                    self.build_kind,
822                    output_dir.escape_for_python(),
823                    self.config_settings.escape_for_python(),
824                );
825                formatdoc! {
826                    r#"
827                    {}
828
829                    sdist_filename = backend.build_{}("{}", {})
830                    with open("{}", "w") as fp:
831                        fp.write(sdist_filename)
832                    "#,
833                    self.pep517_backend.backend_import(),
834                    self.build_kind,
835                    output_dir.escape_for_python(),
836                    self.config_settings.escape_for_python(),
837                    outfile.escape_for_python()
838                }
839            }
840            BuildKind::Wheel | BuildKind::Editable => {
841                let metadata_directory = self
842                    .metadata_directory
843                    .as_deref()
844                    .map_or("None".to_string(), |path| {
845                        format!(r#""{}""#, path.escape_for_python())
846                    });
847                debug!(
848                    r#"Calling `{}.build_{}("{}", {}, {})`"#,
849                    self.pep517_backend.backend,
850                    self.build_kind,
851                    output_dir.escape_for_python(),
852                    self.config_settings.escape_for_python(),
853                    metadata_directory,
854                );
855                formatdoc! {
856                    r#"
857                    {}
858
859                    wheel_filename = backend.build_{}("{}", {}, {})
860                    with open("{}", "w") as fp:
861                        fp.write(wheel_filename)
862                    "#,
863                    self.pep517_backend.backend_import(),
864                    self.build_kind,
865                    output_dir.escape_for_python(),
866                    self.config_settings.escape_for_python(),
867                    metadata_directory,
868                    outfile.escape_for_python()
869                }
870            }
871        };
872
873        let span = info_span!(
874            "run_python_script",
875            script = format!("build_{}", self.build_kind),
876            version_id = self.version_id,
877        );
878        let output = self
879            .runner
880            .run_script(
881                &self.venv,
882                &script,
883                &self.source_tree,
884                &self.environment_variables,
885                &self.modified_path,
886            )
887            .instrument(span)
888            .await?;
889        if !output.status.success() {
890            return Err(Error::from_command_output(
891                format!(
892                    "Call to `{}.build_{}` failed",
893                    self.pep517_backend.backend, self.build_kind
894                ),
895                &output,
896                self.level,
897                self.package_name.as_ref(),
898                self.package_version.as_ref(),
899                self.version_id.as_deref(),
900            ));
901        }
902
903        let distribution_filename = fs::read_to_string(&outfile)?;
904        if !output_dir.join(&distribution_filename).is_file() {
905            return Err(Error::from_command_output(
906                format!(
907                    "Call to `{}.build_{}` failed",
908                    self.pep517_backend.backend, self.build_kind
909                ),
910                &output,
911                self.level,
912                self.package_name.as_ref(),
913                self.package_version.as_ref(),
914                self.version_id.as_deref(),
915            ));
916        }
917        Ok(distribution_filename)
918    }
919}
920
921impl SourceBuildTrait for SourceBuild {
922    async fn metadata(&mut self) -> Result<Option<PathBuf>, AnyErrorBuild> {
923        Ok(self.get_metadata_without_build().await?)
924    }
925
926    async fn wheel<'a>(&'a self, wheel_dir: &'a Path) -> Result<String, AnyErrorBuild> {
927        Ok(self.build(wheel_dir).await?)
928    }
929}
930
931fn escape_path_for_python(path: &Path) -> String {
932    path.to_string_lossy()
933        .replace('\\', "\\\\")
934        .replace('"', "\\\"")
935}
936
937/// Not a method because we call it before the builder is completely initialized
938async fn create_pep517_build_environment(
939    runner: &PythonRunner,
940    source_tree: &Path,
941    install_path: &Path,
942    venv: &PythonEnvironment,
943    pep517_backend: &Pep517Backend,
944    build_context: &impl BuildContext,
945    package_name: Option<&PackageName>,
946    package_version: Option<&Version>,
947    version_id: Option<&str>,
948    locations: &IndexLocations,
949    no_sources: NoSources,
950    workspace_cache: &WorkspaceCache,
951    build_stack: &BuildStack,
952    build_kind: BuildKind,
953    level: BuildOutput,
954    config_settings: &ConfigSettings,
955    environment_variables: &FxHashMap<OsString, OsString>,
956    modified_path: &OsString,
957    temp_dir: &TempDir,
958    credentials_cache: &CredentialsCache,
959) -> Result<(), Error> {
960    // Write the hook output to a file so that we can read it back reliably.
961    let outfile = temp_dir
962        .path()
963        .join(format!("get_requires_for_build_{build_kind}.txt"));
964
965    debug!(
966        "Calling `{}.get_requires_for_build_{}()`",
967        pep517_backend.backend, build_kind
968    );
969
970    let script = formatdoc! {
971        r#"
972            {}
973            import json
974
975            get_requires_for_build = getattr(backend, "get_requires_for_build_{}", None)
976            if get_requires_for_build:
977                requires = get_requires_for_build({})
978            else:
979                requires = []
980
981            with open("{}", "w") as fp:
982                json.dump(requires, fp)
983        "#,
984        pep517_backend.backend_import(),
985        build_kind,
986        config_settings.escape_for_python(),
987        outfile.escape_for_python()
988    };
989    let span = info_span!(
990        "run_python_script",
991        script = format!("get_requires_for_build_{}", build_kind),
992        version_id = version_id,
993    );
994    let output = runner
995        .run_script(
996            venv,
997            &script,
998            source_tree,
999            environment_variables,
1000            modified_path,
1001        )
1002        .instrument(span)
1003        .await?;
1004    if !output.status.success() {
1005        return Err(Error::from_command_output(
1006            format!(
1007                "Call to `{}.build_{}` failed",
1008                pep517_backend.backend, build_kind
1009            ),
1010            &output,
1011            level,
1012            package_name,
1013            package_version,
1014            version_id,
1015        ));
1016    }
1017
1018    // Read and deserialize the requirements from the output file.
1019    let read_requires_result = fs_err::read(&outfile)
1020        .map_err(|err| err.to_string())
1021        .and_then(|contents| serde_json::from_slice(&contents).map_err(|err| err.to_string()));
1022    let extra_requires: Vec<uv_pep508::Requirement<VerbatimParsedUrl>> = match read_requires_result
1023    {
1024        Ok(extra_requires) => extra_requires,
1025        Err(err) => {
1026            return Err(Error::from_command_output(
1027                format!(
1028                    "Call to `{}.get_requires_for_build_{}` failed: {}",
1029                    pep517_backend.backend, build_kind, err
1030                ),
1031                &output,
1032                level,
1033                package_name,
1034                package_version,
1035                version_id,
1036            ));
1037        }
1038    };
1039
1040    // If necessary, lower the requirements.
1041    let extra_requires = if no_sources.all() {
1042        extra_requires.into_iter().map(Requirement::from).collect()
1043    } else {
1044        let build_requires = uv_pypi_types::BuildRequires {
1045            name: package_name.cloned(),
1046            requires_dist: extra_requires,
1047        };
1048        let build_requires = BuildRequires::from_project_maybe_workspace(
1049            build_requires,
1050            install_path,
1051            locations,
1052            &no_sources,
1053            workspace_cache,
1054            credentials_cache,
1055        )
1056        .await
1057        .map_err(Error::Lowering)?;
1058        build_requires.requires_dist
1059    };
1060
1061    // Some packages (such as tqdm 4.66.1) list only extra requires that have already been part of
1062    // the pyproject.toml requires (in this case, `wheel`). We can skip doing the whole resolution
1063    // and installation again.
1064    // TODO(konstin): Do we still need this when we have a fast resolver?
1065    if extra_requires
1066        .iter()
1067        .any(|req| !pep517_backend.requirements.contains(req))
1068    {
1069        debug!("Installing extra requirements for build backend");
1070        let requirements: Vec<_> = pep517_backend
1071            .requirements
1072            .iter()
1073            .cloned()
1074            .chain(extra_requires)
1075            .collect();
1076        let resolution = build_context
1077            .resolve(&requirements, build_stack)
1078            .await
1079            .map_err(|err| {
1080                Error::RequirementsResolve("`build-system.requires`", AnyErrorBuild::from(err))
1081            })?;
1082
1083        build_context
1084            .install(&resolution, venv, build_stack)
1085            .await
1086            .map_err(|err| {
1087                Error::RequirementsInstall("`build-system.requires`", AnyErrorBuild::from(err))
1088            })?;
1089    }
1090
1091    Ok(())
1092}
1093
1094/// A runner that manages the execution of external python processes with a
1095/// concurrency limit.
1096#[derive(Debug)]
1097struct PythonRunner {
1098    control: Semaphore,
1099    level: BuildOutput,
1100}
1101
1102#[derive(Debug)]
1103struct PythonRunnerOutput {
1104    stdout: Vec<String>,
1105    stderr: Vec<String>,
1106    status: ExitStatus,
1107}
1108
1109impl PythonRunner {
1110    /// Create a `PythonRunner` with the provided concurrency limit and output level.
1111    fn new(concurrency: usize, level: BuildOutput) -> Self {
1112        Self {
1113            control: Semaphore::new(concurrency),
1114            level,
1115        }
1116    }
1117
1118    /// Spawn a process that runs a python script in the provided environment.
1119    ///
1120    /// If the concurrency limit has been reached this method will wait until a pending
1121    /// script completes before spawning this one.
1122    ///
1123    /// Note: It is the caller's responsibility to create an informative span.
1124    async fn run_script(
1125        &self,
1126        venv: &PythonEnvironment,
1127        script: &str,
1128        source_tree: &Path,
1129        environment_variables: &FxHashMap<OsString, OsString>,
1130        modified_path: &OsString,
1131    ) -> Result<PythonRunnerOutput, Error> {
1132        /// Read lines from a reader and store them in a buffer.
1133        async fn read_from(
1134            mut reader: tokio::io::Split<tokio::io::BufReader<impl tokio::io::AsyncRead + Unpin>>,
1135            mut printer: Printer,
1136            buffer: &mut Vec<String>,
1137        ) -> io::Result<()> {
1138            loop {
1139                match reader.next_segment().await? {
1140                    Some(line_buf) => {
1141                        let line_buf = line_buf.strip_suffix(b"\r").unwrap_or(&line_buf);
1142                        let line = String::from_utf8_lossy(line_buf).into();
1143                        let _ = write!(printer, "{line}");
1144                        buffer.push(line);
1145                    }
1146                    None => return Ok(()),
1147                }
1148            }
1149        }
1150
1151        let _permit = self.control.acquire().await.unwrap();
1152
1153        let mut child = Command::new(venv.python_executable())
1154            .args(["-c", script])
1155            .current_dir(source_tree.simplified())
1156            .envs(environment_variables)
1157            .env(EnvVars::PATH, modified_path)
1158            .env(EnvVars::VIRTUAL_ENV, venv.root())
1159            // NOTE: it would be nice to get colored output from build backends,
1160            // but setting CLICOLOR_FORCE=1 changes the output of underlying
1161            // tools, which might mess with wrappers trying to parse their
1162            // output.
1163            .env(EnvVars::PYTHONIOENCODING, "utf-8:backslashreplace")
1164            // Remove potentially-sensitive environment variables.
1165            .env_remove(EnvVars::PYX_API_KEY)
1166            .env_remove(EnvVars::UV_API_KEY)
1167            .env_remove(EnvVars::PYX_AUTH_TOKEN)
1168            .env_remove(EnvVars::UV_AUTH_TOKEN)
1169            .stdout(std::process::Stdio::piped())
1170            .stderr(std::process::Stdio::piped())
1171            .spawn()
1172            .map_err(|err| Error::CommandFailed(venv.python_executable().to_path_buf(), err))?;
1173
1174        // Create buffers to capture `stdout` and `stderr`.
1175        let mut stdout_buf = Vec::with_capacity(1024);
1176        let mut stderr_buf = Vec::with_capacity(1024);
1177
1178        // Create separate readers for `stdout` and `stderr`.
1179        let stdout_reader = tokio::io::BufReader::new(child.stdout.take().unwrap()).split(b'\n');
1180        let stderr_reader = tokio::io::BufReader::new(child.stderr.take().unwrap()).split(b'\n');
1181
1182        // Asynchronously read from the in-memory pipes.
1183        let printer = Printer::from(self.level);
1184        let result = tokio::join!(
1185            read_from(stdout_reader, printer, &mut stdout_buf),
1186            read_from(stderr_reader, printer, &mut stderr_buf),
1187        );
1188        match result {
1189            (Ok(()), Ok(())) => {}
1190            (Err(err), _) | (_, Err(err)) => {
1191                return Err(Error::CommandFailed(
1192                    venv.python_executable().to_path_buf(),
1193                    err,
1194                ));
1195            }
1196        }
1197
1198        // Wait for the child process to finish.
1199        let status = child
1200            .wait()
1201            .await
1202            .map_err(|err| Error::CommandFailed(venv.python_executable().to_path_buf(), err))?;
1203
1204        Ok(PythonRunnerOutput {
1205            stdout: stdout_buf,
1206            stderr: stderr_buf,
1207            status,
1208        })
1209    }
1210}
1211
1212#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1213pub enum Printer {
1214    /// Send the build backend output to `stderr`.
1215    Stderr,
1216    /// Send the build backend output to `tracing`.
1217    Debug,
1218    /// Hide the build backend output.
1219    Quiet,
1220}
1221
1222impl From<BuildOutput> for Printer {
1223    fn from(output: BuildOutput) -> Self {
1224        match output {
1225            BuildOutput::Stderr => Self::Stderr,
1226            BuildOutput::Debug => Self::Debug,
1227            BuildOutput::Quiet => Self::Quiet,
1228        }
1229    }
1230}
1231
1232impl Write for Printer {
1233    fn write_str(&mut self, s: &str) -> std::fmt::Result {
1234        match self {
1235            Self::Stderr => {
1236                anstream::eprintln!("{s}");
1237            }
1238            Self::Debug => {
1239                debug!("{s}");
1240            }
1241            Self::Quiet => {}
1242        }
1243        Ok(())
1244    }
1245}