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