Add source distribution support to uv-build crate (#6896)

## Summary

Just exposes the correct PEP 517 hooks.
This commit is contained in:
Charlie Marsh 2024-09-02 14:14:49 -04:00 committed by GitHub
parent ea0a0db7a7
commit 42a4d80a63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 81 additions and 50 deletions

View file

@ -79,8 +79,8 @@ static DEFAULT_BACKEND: LazyLock<Pep517Backend> = LazyLock::new(|| Pep517Backend
pub enum Error { pub enum Error {
#[error(transparent)] #[error(transparent)]
Io(#[from] io::Error), Io(#[from] io::Error),
#[error("Invalid source distribution: {0}")] #[error("{} does not appear to be a Python project, as neither `pyproject.toml` nor `setup.py` is present in the directory", _0.simplified_display())]
InvalidSourceDist(String), InvalidSourceDist(PathBuf),
#[error("Invalid `pyproject.toml`")] #[error("Invalid `pyproject.toml`")]
InvalidPyprojectToml(#[from] toml::de::Error), InvalidPyprojectToml(#[from] toml::de::Error),
#[error("Editable installs with setup.py legacy builds are unsupported, please specify a build backend in pyproject.toml")] #[error("Editable installs with setup.py legacy builds are unsupported, please specify a build backend in pyproject.toml")]
@ -353,7 +353,7 @@ pub struct SourceBuildContext {
default_resolution: Rc<Mutex<Option<Resolution>>>, default_resolution: Rc<Mutex<Option<Resolution>>>,
} }
/// Holds the state through a series of PEP 517 frontend to backend calls or a single setup.py /// Holds the state through a series of PEP 517 frontend to backend calls or a single `setup.py`
/// invocation. /// invocation.
/// ///
/// This keeps both the temp dir and the result of a potential `prepare_metadata_for_build_wheel` /// This keeps both the temp dir and the result of a potential `prepare_metadata_for_build_wheel`
@ -595,8 +595,7 @@ impl SourceBuild {
// We require either a `pyproject.toml` or a `setup.py` file at the top level. // We require either a `pyproject.toml` or a `setup.py` file at the top level.
if !source_tree.join("setup.py").is_file() { if !source_tree.join("setup.py").is_file() {
return Err(Box::new(Error::InvalidSourceDist( return Err(Box::new(Error::InvalidSourceDist(
"The archive contains neither a `pyproject.toml` nor a `setup.py` file at the top level" source_tree.to_path_buf(),
.to_string(),
))); )));
} }
@ -713,19 +712,19 @@ impl SourceBuild {
Ok(self.metadata_directory.clone()) Ok(self.metadata_directory.clone())
} }
/// Build a source distribution from an archive (`.zip` or `.tar.gz`), return the location of the /// Build a distribution from an archive (`.zip` or `.tar.gz`) or source tree, and return the
/// built wheel. /// location of the built distribution.
/// ///
/// The location will be inside `temp_dir`, i.e. you must use the wheel before dropping the temp /// The location will be inside `temp_dir`, i.e., you must use the distribution before dropping
/// dir. /// the temporary directory.
/// ///
/// <https://packaging.python.org/en/latest/specifications/source-distribution-format/> /// <https://packaging.python.org/en/latest/specifications/source-distribution-format/>
#[instrument(skip_all, fields(version_id = self.version_id))] #[instrument(skip_all, fields(version_id = self.version_id))]
pub async fn build_wheel(&self, wheel_dir: &Path) -> Result<String, Error> { pub async fn build(&self, wheel_dir: &Path) -> Result<String, Error> {
// The build scripts run with the extracted root as cwd, so they need the absolute path. // The build scripts run with the extracted root as cwd, so they need the absolute path.
let wheel_dir = fs::canonicalize(wheel_dir)?; let wheel_dir = std::path::absolute(wheel_dir)?;
// Prevent clashes from two uv processes building wheels in parallel. // Prevent clashes from two uv processes building distributions in parallel.
let tmp_dir = tempdir_in(&wheel_dir)?; let tmp_dir = tempdir_in(&wheel_dir)?;
let filename = self let filename = self
.pep517_build(tmp_dir.path(), &self.pep517_backend) .pep517_build(tmp_dir.path(), &self.pep517_backend)
@ -737,33 +736,59 @@ impl SourceBuild {
Ok(filename) Ok(filename)
} }
/// Perform a PEP 517 build for a wheel or source distribution (sdist).
async fn pep517_build( async fn pep517_build(
&self, &self,
wheel_dir: &Path, output_dir: &Path,
pep517_backend: &Pep517Backend, pep517_backend: &Pep517Backend,
) -> Result<String, Error> { ) -> Result<String, Error> {
let metadata_directory = self
.metadata_directory
.as_deref()
.map_or("None".to_string(), |path| {
format!(r#""{}""#, path.escape_for_python())
});
// Write the hook output to a file so that we can read it back reliably. // Write the hook output to a file so that we can read it back reliably.
let outfile = self let outfile = self
.temp_dir .temp_dir
.path() .path()
.join(format!("build_{}.txt", self.build_kind)); .join(format!("build_{}.txt", self.build_kind));
// Construct the appropriate build script based on the build kind.
let script = match self.build_kind {
BuildKind::Sdist => {
debug!(
r#"Calling `{}.build_{}("{}", {})`"#,
pep517_backend.backend,
self.build_kind,
output_dir.escape_for_python(),
self.config_settings.escape_for_python(),
);
formatdoc! {
r#"
{}
sdist_filename = backend.build_{}("{}", {})
with open("{}", "w") as fp:
fp.write(sdist_filename)
"#,
pep517_backend.backend_import(),
self.build_kind,
output_dir.escape_for_python(),
self.config_settings.escape_for_python(),
outfile.escape_for_python()
}
}
BuildKind::Wheel | BuildKind::Editable => {
let metadata_directory = self
.metadata_directory
.as_deref()
.map_or("None".to_string(), |path| {
format!(r#""{}""#, path.escape_for_python())
});
debug!( debug!(
r#"Calling `{}.build_{}("{}", {}, {})`"#, r#"Calling `{}.build_{}("{}", {}, {})`"#,
pep517_backend.backend, pep517_backend.backend,
self.build_kind, self.build_kind,
wheel_dir.escape_for_python(), output_dir.escape_for_python(),
self.config_settings.escape_for_python(), self.config_settings.escape_for_python(),
metadata_directory, metadata_directory,
); );
let script = formatdoc! { formatdoc! {
r#" r#"
{} {}
@ -773,11 +798,14 @@ impl SourceBuild {
"#, "#,
pep517_backend.backend_import(), pep517_backend.backend_import(),
self.build_kind, self.build_kind,
wheel_dir.escape_for_python(), output_dir.escape_for_python(),
self.config_settings.escape_for_python(), self.config_settings.escape_for_python(),
metadata_directory, metadata_directory,
outfile.escape_for_python() outfile.escape_for_python()
}
}
}; };
let span = info_span!( let span = info_span!(
"run_python_script", "run_python_script",
script=format!("build_{}", self.build_kind), script=format!("build_{}", self.build_kind),
@ -797,8 +825,8 @@ impl SourceBuild {
if !output.status.success() { if !output.status.success() {
return Err(Error::from_command_output( return Err(Error::from_command_output(
format!( format!(
"Build backend failed to build wheel through `build_{}()`", "Build backend failed to build {} through `build_{}()`",
self.build_kind self.build_kind, self.build_kind,
), ),
&output, &output,
&self.version_id, &self.version_id,
@ -806,11 +834,11 @@ impl SourceBuild {
} }
let distribution_filename = fs::read_to_string(&outfile)?; let distribution_filename = fs::read_to_string(&outfile)?;
if !wheel_dir.join(&distribution_filename).is_file() { if !output_dir.join(&distribution_filename).is_file() {
return Err(Error::from_command_output( return Err(Error::from_command_output(
format!( format!(
"Build backend failed to produce wheel through `build_{}()`: `{distribution_filename}` not found", "Build backend failed to produce {} through `build_{}()`: `{distribution_filename}` not found",
self.build_kind self.build_kind, self.build_kind,
), ),
&output, &output,
&self.version_id, &self.version_id,
@ -826,7 +854,7 @@ impl SourceBuildTrait for SourceBuild {
} }
async fn wheel<'a>(&'a self, wheel_dir: &'a Path) -> anyhow::Result<String> { async fn wheel<'a>(&'a self, wheel_dir: &'a Path) -> anyhow::Result<String> {
Ok(self.build_wheel(wheel_dir).await?) Ok(self.build(wheel_dir).await?)
} }
} }

View file

@ -6,10 +6,12 @@ use crate::{PackageNameSpecifier, PackageNameSpecifiers};
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum BuildKind { pub enum BuildKind {
/// A regular PEP 517 wheel build /// A PEP 517 wheel build.
#[default] #[default]
Wheel, Wheel,
/// A PEP 660 editable installation wheel build /// A PEP 517 source distribution build.
Sdist,
/// A PEP 660 editable installation wheel build.
Editable, Editable,
} }
@ -17,6 +19,7 @@ impl Display for BuildKind {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Wheel => f.write_str("wheel"), Self::Wheel => f.write_str("wheel"),
Self::Sdist => f.write_str("sdist"),
Self::Editable => f.write_str("editable"), Self::Editable => f.write_str("editable"),
} }
} }

View file

@ -110,5 +110,5 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
concurrency.builds, concurrency.builds,
) )
.await?; .await?;
Ok(wheel_dir.join(builder.build_wheel(&wheel_dir).await?)) Ok(wheel_dir.join(builder.build(&wheel_dir).await?))
} }