diff --git a/crates/puffin-build/src/lib.rs b/crates/puffin-build/src/lib.rs index 711789c8f..d94a28792 100644 --- a/crates/puffin-build/src/lib.rs +++ b/crates/puffin-build/src/lib.rs @@ -51,6 +51,8 @@ pub enum Error { InvalidSourceDist(String), #[error("Invalid pyproject.toml")] InvalidPyprojectToml(#[from] toml::de::Error), + #[error("Editable installs with setup.py legacy builds are unsupported, please specify a build backend in pyproject.toml")] + EditableSetupPy, #[error("Failed to install requirements from {0}")] RequirementsInstall(&'static str, #[source] anyhow::Error), #[error("Failed to create temporary virtual environment")] @@ -181,6 +183,24 @@ impl Pep517Backend { } } +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum BuildKind { + /// A regular PEP 517 wheel build + #[default] + Wheel, + /// A PEP 660 editable installation wheel build + Editable, +} + +impl Display for BuildKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + BuildKind::Wheel => f.write_str("wheel"), + BuildKind::Editable => f.write_str("editable"), + } + } +} + /// Uses an [`Arc`] internally, clone freely #[derive(Debug, Default, Clone)] pub struct SourceBuildContext { @@ -212,6 +232,8 @@ pub struct SourceBuild { metadata_directory: Option, /// Package id such as `foo-1.2.3`, for error reporting package_id: String, + /// Whether we do a regular PEP 517 build or an PEP 660 editable build + build_kind: BuildKind, } impl SourceBuild { @@ -226,6 +248,7 @@ impl SourceBuild { build_context: &impl BuildContext, source_build_context: SourceBuildContext, source_dist: &str, + build_kind: BuildKind, ) -> Result { let temp_dir = tempdir()?; @@ -324,6 +347,7 @@ impl SourceBuild { pep517_backend, build_context, source_dist, + build_kind, ) .await?; } else if !source_tree.join("setup.py").is_file() { @@ -338,6 +362,7 @@ impl SourceBuild { source_tree, pep517_backend, venv, + build_kind, metadata_directory: None, package_id: source_dist.to_string(), }) @@ -423,16 +448,16 @@ impl SourceBuild { if let Some(pep517_backend) = &self.pep517_backend { // Prevent clashes from two puffin processes building wheels in parallel. let tmp_dir = tempdir_in(&wheel_dir)?; - let filename = self - .pep517_build_wheel(tmp_dir.path(), pep517_backend) - .await?; + let filename = self.pep517_build(tmp_dir.path(), pep517_backend).await?; let from = tmp_dir.path().join(&filename); let to = wheel_dir.join(&filename); fs_err::rename(from, to)?; - Ok(filename) } else { + if self.build_kind != BuildKind::Wheel { + return Err(Error::EditableSetupPy); + } // We checked earlier that setup.py exists. let python_interpreter = self.venv.python_executable(); let output = Command::new(&python_interpreter) @@ -468,7 +493,7 @@ impl SourceBuild { } } - async fn pep517_build_wheel( + async fn pep517_build( &self, wheel_dir: &Path, pep517_backend: &Pep517Backend, @@ -480,18 +505,18 @@ impl SourceBuild { format!(r#""{}""#, escape_path_for_python(path)) }); debug!( - "Calling `{}.build_wheel(metadata_directory={})`", - pep517_backend.backend, metadata_directory + "Calling `{}.build_{}(metadata_directory={})`", + pep517_backend.backend, self.build_kind, metadata_directory ); let escaped_wheel_dir = escape_path_for_python(wheel_dir); let script = formatdoc! { r#"{} - print(backend.build_wheel("{}", metadata_directory={})) - "#, pep517_backend.backend_import(), escaped_wheel_dir, metadata_directory + print(backend.build_{}("{}", metadata_directory={})) + "#, pep517_backend.backend_import(), self.build_kind, escaped_wheel_dir, metadata_directory }; let span = info_span!( "run_python_script", - name="build_wheel", + name=format!("build_{}", self.build_kind), python_version = %self.venv.interpreter().version() ); let output = @@ -499,7 +524,10 @@ impl SourceBuild { drop(span); if !output.status.success() { return Err(Error::from_command_output( - "Build backend failed to build wheel through `build_wheel()`".to_string(), + format!( + "Build backend failed to build wheel through `build_{}()`", + self.build_kind + ), &output, &self.package_id, )); @@ -510,8 +538,10 @@ impl SourceBuild { distribution_filename.filter(|wheel| wheel_dir.join(wheel).is_file()) else { return Err(Error::from_command_output( - "Build backend did not return the wheel filename through `build_wheel()`" - .to_string(), + format!( + "Build backend failed to build wheel through `build_{}()`", + self.build_kind + ), &output, &self.package_id, )); @@ -533,23 +563,24 @@ async fn create_pep517_build_environment( pep517_backend: &Pep517Backend, build_context: &impl BuildContext, package_id: &str, + build_kind: BuildKind, ) -> Result<(), Error> { debug!( - "Calling `{}.get_requires_for_build_wheel()`", - pep517_backend.backend + "Calling `{}.get_requires_for_build_{}()`", + pep517_backend.backend, build_kind ); let script = formatdoc! { r#" {} import json - get_requires_for_build_wheel = getattr(backend, "get_requires_for_build_wheel", None) - if get_requires_for_build_wheel: - requires = get_requires_for_build_wheel() + get_requires_for_build = getattr(backend, "get_requires_for_build_{}", None) + if get_requires_for_build: + requires = get_requires_for_build() else: requires = [] print(json.dumps(requires)) - "#, pep517_backend.backend_import() + "#, pep517_backend.backend_import(), build_kind }; let span = info_span!( "get_requires_for_build_wheel", @@ -560,8 +591,7 @@ async fn create_pep517_build_environment( drop(span); if !output.status.success() { return Err(Error::from_command_output( - "Build backend failed to determine extra requires with `get_requires_for_build_wheel`" - .to_string(), + format!("Build backend failed to determine extra requires with `build_{build_kind}()`",), &output, package_id, )); @@ -577,7 +607,7 @@ async fn create_pep517_build_environment( let extra_requires: Vec = extra_requires.map_err(|err| { Error::from_command_output( format!( - "Build backend failed to return extra requires with `get_requires_for_build_wheel`: {err}" + "Build backend failed to return extra requires with `get_requires_for_build_{build_kind}`: {err}" ), &output, package_id, diff --git a/crates/puffin-dev/src/build.rs b/crates/puffin-dev/src/build.rs index 3071f8443..9f0e6201b 100644 --- a/crates/puffin-dev/src/build.rs +++ b/crates/puffin-dev/src/build.rs @@ -6,6 +6,7 @@ use clap::Parser; use fs_err as fs; use platform_host::Platform; +use puffin_build::{BuildKind, SourceBuild, SourceBuildContext}; use puffin_cache::{Cache, CacheArgs}; use puffin_client::RegistryClientBuilder; use puffin_dispatch::BuildDispatch; @@ -26,6 +27,10 @@ pub(crate) struct BuildArgs { sdist: PathBuf, /// The subdirectory to build within the source distribution. subdirectory: Option, + /// You can edit the python sources of an editable install and the changes will be used without + /// the need to reinstall it. + #[clap(short, long)] + editable: bool, #[command(flatten)] cache_args: CacheArgs, } @@ -38,6 +43,11 @@ pub(crate) async fn build(args: BuildArgs) -> Result { } else { env::current_dir()? }; + let build_kind = if args.editable { + BuildKind::Editable + } else { + BuildKind::Wheel + }; let cache = Cache::try_from(args.cache_args)?; @@ -52,14 +62,16 @@ pub(crate) async fn build(args: BuildArgs) -> Result { false, IndexUrls::default(), ); - let wheel = build_dispatch - .build_source( - &args.sdist, - args.subdirectory.as_deref(), - &wheel_dir, - // Good enough for the dev command - &args.sdist.display().to_string(), - ) - .await?; - Ok(wheel_dir.join(wheel)) + let builder = SourceBuild::setup( + &args.sdist, + args.subdirectory.as_deref(), + build_dispatch.interpreter(), + &build_dispatch, + SourceBuildContext::default(), + // Good enough for the dev command + &args.sdist.display().to_string(), + build_kind, + ) + .await?; + Ok(wheel_dir.join(builder.build(&wheel_dir).await?)) } diff --git a/crates/puffin-dispatch/src/lib.rs b/crates/puffin-dispatch/src/lib.rs index b5108a514..7cb3d4931 100644 --- a/crates/puffin-dispatch/src/lib.rs +++ b/crates/puffin-dispatch/src/lib.rs @@ -13,7 +13,7 @@ use tracing::{debug, instrument}; use distribution_types::{CachedDist, Metadata}; use pep508_rs::Requirement; use platform_tags::Tags; -use puffin_build::{SourceBuild, SourceBuildContext}; +use puffin_build::{BuildKind, SourceBuild, SourceBuildContext}; use puffin_cache::Cache; use puffin_client::RegistryClient; use puffin_installer::{Downloader, InstallPlan, Installer, Reinstall}; @@ -230,6 +230,7 @@ impl BuildContext for BuildDispatch { self, self.source_build_context.clone(), source_dist, + BuildKind::Wheel, ) .await?; Ok(builder.build(wheel_dir).await?)