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_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
52static 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#[derive(Deserialize, Debug)]
63#[serde(rename_all = "kebab-case")]
64struct PyProjectToml {
65 build_system: Option<BuildSystem>,
67 project: Option<Project>,
69 tool: Option<Tool>,
71}
72
73#[derive(Deserialize, Debug)]
78#[serde(rename_all = "kebab-case")]
79struct Project {
80 name: PackageName,
82 version: Option<Version>,
84 dynamic: Option<Vec<String>>,
87}
88
89#[derive(Deserialize, Debug)]
91#[serde(rename_all = "kebab-case")]
92struct BuildSystem {
93 requires: Vec<uv_pep508::Requirement<VerbatimParsedUrl>>,
95 build_backend: Option<String>,
97 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
163struct Pep517Backend {
164 backend: String,
169 requirements: Vec<Requirement>,
171 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 '"'.to_string()
190 + &path.replace('\\', "\\\\").replace('"', "\\\"")
191 + &'"'.to_string()
192 })
193 .join(", ");
194
195 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 self.backend.split(':').next() == Some("setuptools.build_meta")
214 }
215}
216
217#[derive(Debug, Default, Clone)]
219pub struct SourceBuildContext {
220 default_resolution: Rc<Mutex<Option<Resolution>>>,
222}
223
224pub struct SourceBuild {
230 temp_dir: TempDir,
231 source_tree: PathBuf,
232 config_settings: ConfigSettings,
233 pep517_backend: Pep517Backend,
235 project: Option<Project>,
237 venv: PythonEnvironment,
239 metadata_directory: Option<PathBuf>,
249 package_name: Option<PackageName>,
251 package_version: Option<Version>,
253 version_id: Option<String>,
256 build_kind: BuildKind,
258 level: BuildOutput,
260 modified_path: OsString,
262 environment_variables: FxHashMap<OsString, OsString>,
264 runner: PythonRunner,
266}
267
268impl SourceBuild {
269 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 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 package_name.clone().unwrap(),
343 ))
344 }
345 requirement => Ok(requirement),
346 }
347 })
348 .map_ok(Requirement::from)
349 .collect::<Result<Vec<_>, _>>()?;
350
351 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 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 let user_path = environment_variables.remove(&OsString::from(EnvVars::PATH));
400
401 let os_path = env::var_os(EnvVars::PATH);
403
404 let modified_path = if let Some(user_path) = user_path {
406 match os_path {
407 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 None => Some(user_path),
415 }
416 } else {
417 os_path
418 };
419
420 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 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 async fn acquire_lock(&self) -> Result<Option<LockedFile>, Error> {
480 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 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 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 let requirements = if let Some(name) = pyproject_toml
575 .project
576 .as_ref()
577 .map(|project| &project.name)
578 .or(package_name)
579 .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 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 pyproject_toml.project.is_none()
628 && !source_tree.join("setup.py").is_file()
629 && !source_tree.join("setup.cfg").is_file()
630 {
631 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 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 Ok((DEFAULT_BACKEND.clone(), None))
672 }
673 Err(err) => Err(Box::new(err.into())),
674 }
675 }
676
677 pub async fn get_metadata_without_build(&mut self) -> Result<Option<PathBuf>, Error> {
680 if let Some(metadata_dir) = &self.metadata_directory {
682 return Ok(Some(metadata_dir.clone()));
683 }
684
685 let _lock = self.acquire_lock().await?;
687
688 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 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 #[instrument(skip_all, fields(version_id = self.version_id))]
797 pub async fn build(&self, wheel_dir: &Path) -> Result<String, Error> {
798 let wheel_dir = std::path::absolute(wheel_dir)?;
800 let filename = self.pep517_build(&wheel_dir).await?;
801 Ok(filename)
802 }
803
804 async fn pep517_build(&self, output_dir: &Path) -> Result<String, Error> {
806 let _lock = self.acquire_lock().await?;
808
809 let outfile = self
811 .temp_dir
812 .path()
813 .join(format!("build_{}.txt", self.build_kind));
814
815 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
937async 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 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 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 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 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#[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 fn new(concurrency: usize, level: BuildOutput) -> Self {
1112 Self {
1113 control: Semaphore::new(concurrency),
1114 level,
1115 }
1116 }
1117
1118 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 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 .env(EnvVars::PYTHONIOENCODING, "utf-8:backslashreplace")
1164 .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 let mut stdout_buf = Vec::with_capacity(1024);
1176 let mut stderr_buf = Vec::with_capacity(1024);
1177
1178 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 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 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 Stderr,
1216 Debug,
1218 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}