1mod 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
53static 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#[derive(Deserialize, Debug)]
64#[serde(rename_all = "kebab-case")]
65struct PyProjectToml {
66 build_system: Option<BuildSystem>,
68 project: Option<Project>,
70 tool: Option<Tool>,
72}
73
74#[derive(Deserialize, Debug)]
79#[serde(rename_all = "kebab-case")]
80struct Project {
81 name: PackageName,
83 version: Option<Version>,
85 dynamic: Option<Vec<String>>,
88}
89
90#[derive(Deserialize, Debug)]
92#[serde(rename_all = "kebab-case")]
93struct BuildSystem {
94 requires: Vec<uv_pep508::Requirement<VerbatimParsedUrl>>,
96 build_backend: Option<String>,
98 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
164struct Pep517Backend {
165 backend: String,
170 requirements: Vec<Requirement>,
172 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 '"'.to_string()
191 + &path.replace('\\', "\\\\").replace('"', "\\\"")
192 + &'"'.to_string()
193 })
194 .join(", ");
195
196 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 self.backend.split(':').next() == Some("setuptools.build_meta")
215 }
216}
217
218#[derive(Debug, Default, Clone)]
220pub struct SourceBuildContext {
221 default_resolution: Rc<Mutex<Option<Resolution>>>,
223}
224
225pub struct SourceBuild {
231 temp_dir: TempDir,
232 source_tree: PathBuf,
233 config_settings: ConfigSettings,
234 pep517_backend: Pep517Backend,
236 project: Option<Project>,
238 venv: PythonEnvironment,
240 metadata_directory: Option<PathBuf>,
250 package_name: Option<PackageName>,
252 package_version: Option<Version>,
254 version_id: Option<String>,
257 build_kind: BuildKind,
259 level: BuildOutput,
261 modified_path: OsString,
263 environment_variables: FxHashMap<OsString, OsString>,
265 runner: PythonRunner,
267}
268
269impl SourceBuild {
270 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 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 package_name.clone().unwrap(),
345 ))
346 }
347 requirement => Ok(requirement),
348 }
349 })
350 .map_ok(Requirement::from)
351 .collect::<Result<Vec<_>, _>>()?;
352
353 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 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 let user_path = environment_variables.remove(&OsString::from(EnvVars::PATH));
403
404 let os_path = env::var_os(EnvVars::PATH);
406
407 let modified_path = if let Some(user_path) = user_path {
409 match os_path {
410 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 None => Some(user_path),
418 }
419 } else {
420 os_path
421 };
422
423 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 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 async fn acquire_lock(&self) -> Result<Option<LockedFile>, Error> {
483 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 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 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 let requirements = if let Some(name) = pyproject_toml
578 .project
579 .as_ref()
580 .map(|project| &project.name)
581 .or(package_name)
582 .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 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 pyproject_toml.project.is_none()
631 && !source_tree.join("setup.py").is_file()
632 && !source_tree.join("setup.cfg").is_file()
633 {
634 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 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 Ok((DEFAULT_BACKEND.clone(), None))
675 }
676 Err(err) => Err(Box::new(err.into())),
677 }
678 }
679
680 pub async fn get_metadata_without_build(&mut self) -> Result<Option<PathBuf>, Error> {
683 if let Some(metadata_dir) = &self.metadata_directory {
685 return Ok(Some(metadata_dir.clone()));
686 }
687
688 let _lock = self.acquire_lock().await?;
690
691 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 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 #[instrument(skip_all, fields(version_id = self.version_id))]
800 pub async fn build(&self, wheel_dir: &Path) -> Result<String, Error> {
801 let wheel_dir = std::path::absolute(wheel_dir)?;
803 let filename = self.pep517_build(&wheel_dir).await?;
804 Ok(filename)
805 }
806
807 async fn pep517_build(&self, output_dir: &Path) -> Result<String, Error> {
809 let _lock = self.acquire_lock().await?;
811
812 let outfile = self
814 .temp_dir
815 .path()
816 .join(format!("build_{}.txt", self.build_kind));
817
818 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
940async 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 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 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 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 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#[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 fn new(concurrency: usize, level: BuildOutput) -> Self {
1115 Self {
1116 control: Semaphore::new(concurrency),
1117 level,
1118 }
1119 }
1120
1121 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 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 .env(EnvVars::PYTHONIOENCODING, "utf-8:backslashreplace")
1167 .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 let mut stdout_buf = Vec::with_capacity(1024);
1179 let mut stderr_buf = Vec::with_capacity(1024);
1180
1181 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 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 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 Stderr,
1219 Debug,
1221 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}