Implement editable installs in dev command (#566)

First step, sufficient to run
```shell
cargo run --bin puffin-dev -- build --editable -w target/editables/ scripts/editable-installs/poetry_editable/
```
and check the wheel to confirm its working. Tests will be added with the
pip-sync integration.
This commit is contained in:
konsti 2023-12-12 15:45:55 +01:00 committed by GitHub
parent c25d5240f1
commit 7c1dd71f66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 33 deletions

View File

@ -51,6 +51,8 @@ pub enum Error {
InvalidSourceDist(String), InvalidSourceDist(String),
#[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")]
EditableSetupPy,
#[error("Failed to install requirements from {0}")] #[error("Failed to install requirements from {0}")]
RequirementsInstall(&'static str, #[source] anyhow::Error), RequirementsInstall(&'static str, #[source] anyhow::Error),
#[error("Failed to create temporary virtual environment")] #[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 /// Uses an [`Arc`] internally, clone freely
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct SourceBuildContext { pub struct SourceBuildContext {
@ -212,6 +232,8 @@ pub struct SourceBuild {
metadata_directory: Option<PathBuf>, metadata_directory: Option<PathBuf>,
/// Package id such as `foo-1.2.3`, for error reporting /// Package id such as `foo-1.2.3`, for error reporting
package_id: String, package_id: String,
/// Whether we do a regular PEP 517 build or an PEP 660 editable build
build_kind: BuildKind,
} }
impl SourceBuild { impl SourceBuild {
@ -226,6 +248,7 @@ impl SourceBuild {
build_context: &impl BuildContext, build_context: &impl BuildContext,
source_build_context: SourceBuildContext, source_build_context: SourceBuildContext,
source_dist: &str, source_dist: &str,
build_kind: BuildKind,
) -> Result<SourceBuild, Error> { ) -> Result<SourceBuild, Error> {
let temp_dir = tempdir()?; let temp_dir = tempdir()?;
@ -324,6 +347,7 @@ impl SourceBuild {
pep517_backend, pep517_backend,
build_context, build_context,
source_dist, source_dist,
build_kind,
) )
.await?; .await?;
} else if !source_tree.join("setup.py").is_file() { } else if !source_tree.join("setup.py").is_file() {
@ -338,6 +362,7 @@ impl SourceBuild {
source_tree, source_tree,
pep517_backend, pep517_backend,
venv, venv,
build_kind,
metadata_directory: None, metadata_directory: None,
package_id: source_dist.to_string(), package_id: source_dist.to_string(),
}) })
@ -423,16 +448,16 @@ impl SourceBuild {
if let Some(pep517_backend) = &self.pep517_backend { if let Some(pep517_backend) = &self.pep517_backend {
// Prevent clashes from two puffin processes building wheels in parallel. // Prevent clashes from two puffin processes building wheels 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(), pep517_backend).await?;
.pep517_build_wheel(tmp_dir.path(), pep517_backend)
.await?;
let from = tmp_dir.path().join(&filename); let from = tmp_dir.path().join(&filename);
let to = wheel_dir.join(&filename); let to = wheel_dir.join(&filename);
fs_err::rename(from, to)?; fs_err::rename(from, to)?;
Ok(filename) Ok(filename)
} else { } else {
if self.build_kind != BuildKind::Wheel {
return Err(Error::EditableSetupPy);
}
// We checked earlier that setup.py exists. // We checked earlier that setup.py exists.
let python_interpreter = self.venv.python_executable(); let python_interpreter = self.venv.python_executable();
let output = Command::new(&python_interpreter) let output = Command::new(&python_interpreter)
@ -468,7 +493,7 @@ impl SourceBuild {
} }
} }
async fn pep517_build_wheel( async fn pep517_build(
&self, &self,
wheel_dir: &Path, wheel_dir: &Path,
pep517_backend: &Pep517Backend, pep517_backend: &Pep517Backend,
@ -480,18 +505,18 @@ impl SourceBuild {
format!(r#""{}""#, escape_path_for_python(path)) format!(r#""{}""#, escape_path_for_python(path))
}); });
debug!( debug!(
"Calling `{}.build_wheel(metadata_directory={})`", "Calling `{}.build_{}(metadata_directory={})`",
pep517_backend.backend, metadata_directory pep517_backend.backend, self.build_kind, metadata_directory
); );
let escaped_wheel_dir = escape_path_for_python(wheel_dir); let escaped_wheel_dir = escape_path_for_python(wheel_dir);
let script = formatdoc! { let script = formatdoc! {
r#"{} r#"{}
print(backend.build_wheel("{}", metadata_directory={})) print(backend.build_{}("{}", metadata_directory={}))
"#, pep517_backend.backend_import(), escaped_wheel_dir, metadata_directory "#, pep517_backend.backend_import(), self.build_kind, escaped_wheel_dir, metadata_directory
}; };
let span = info_span!( let span = info_span!(
"run_python_script", "run_python_script",
name="build_wheel", name=format!("build_{}", self.build_kind),
python_version = %self.venv.interpreter().version() python_version = %self.venv.interpreter().version()
); );
let output = let output =
@ -499,7 +524,10 @@ impl SourceBuild {
drop(span); drop(span);
if !output.status.success() { if !output.status.success() {
return Err(Error::from_command_output( 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, &output,
&self.package_id, &self.package_id,
)); ));
@ -510,8 +538,10 @@ impl SourceBuild {
distribution_filename.filter(|wheel| wheel_dir.join(wheel).is_file()) distribution_filename.filter(|wheel| wheel_dir.join(wheel).is_file())
else { else {
return Err(Error::from_command_output( return Err(Error::from_command_output(
"Build backend did not return the wheel filename through `build_wheel()`" format!(
.to_string(), "Build backend failed to build wheel through `build_{}()`",
self.build_kind
),
&output, &output,
&self.package_id, &self.package_id,
)); ));
@ -533,23 +563,24 @@ async fn create_pep517_build_environment(
pep517_backend: &Pep517Backend, pep517_backend: &Pep517Backend,
build_context: &impl BuildContext, build_context: &impl BuildContext,
package_id: &str, package_id: &str,
build_kind: BuildKind,
) -> Result<(), Error> { ) -> Result<(), Error> {
debug!( debug!(
"Calling `{}.get_requires_for_build_wheel()`", "Calling `{}.get_requires_for_build_{}()`",
pep517_backend.backend pep517_backend.backend, build_kind
); );
let script = formatdoc! { let script = formatdoc! {
r#" r#"
{} {}
import json import json
get_requires_for_build_wheel = getattr(backend, "get_requires_for_build_wheel", None) get_requires_for_build = getattr(backend, "get_requires_for_build_{}", None)
if get_requires_for_build_wheel: if get_requires_for_build:
requires = get_requires_for_build_wheel() requires = get_requires_for_build()
else: else:
requires = [] requires = []
print(json.dumps(requires)) print(json.dumps(requires))
"#, pep517_backend.backend_import() "#, pep517_backend.backend_import(), build_kind
}; };
let span = info_span!( let span = info_span!(
"get_requires_for_build_wheel", "get_requires_for_build_wheel",
@ -560,8 +591,7 @@ async fn create_pep517_build_environment(
drop(span); drop(span);
if !output.status.success() { if !output.status.success() {
return Err(Error::from_command_output( return Err(Error::from_command_output(
"Build backend failed to determine extra requires with `get_requires_for_build_wheel`" format!("Build backend failed to determine extra requires with `build_{build_kind}()`",),
.to_string(),
&output, &output,
package_id, package_id,
)); ));
@ -577,7 +607,7 @@ async fn create_pep517_build_environment(
let extra_requires: Vec<Requirement> = extra_requires.map_err(|err| { let extra_requires: Vec<Requirement> = extra_requires.map_err(|err| {
Error::from_command_output( Error::from_command_output(
format!( 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, &output,
package_id, package_id,

View File

@ -6,6 +6,7 @@ use clap::Parser;
use fs_err as fs; use fs_err as fs;
use platform_host::Platform; use platform_host::Platform;
use puffin_build::{BuildKind, SourceBuild, SourceBuildContext};
use puffin_cache::{Cache, CacheArgs}; use puffin_cache::{Cache, CacheArgs};
use puffin_client::RegistryClientBuilder; use puffin_client::RegistryClientBuilder;
use puffin_dispatch::BuildDispatch; use puffin_dispatch::BuildDispatch;
@ -26,6 +27,10 @@ pub(crate) struct BuildArgs {
sdist: PathBuf, sdist: PathBuf,
/// The subdirectory to build within the source distribution. /// The subdirectory to build within the source distribution.
subdirectory: Option<PathBuf>, subdirectory: Option<PathBuf>,
/// 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)] #[command(flatten)]
cache_args: CacheArgs, cache_args: CacheArgs,
} }
@ -38,6 +43,11 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
} else { } else {
env::current_dir()? env::current_dir()?
}; };
let build_kind = if args.editable {
BuildKind::Editable
} else {
BuildKind::Wheel
};
let cache = Cache::try_from(args.cache_args)?; let cache = Cache::try_from(args.cache_args)?;
@ -52,14 +62,16 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
false, false,
IndexUrls::default(), IndexUrls::default(),
); );
let wheel = build_dispatch let builder = SourceBuild::setup(
.build_source( &args.sdist,
&args.sdist, args.subdirectory.as_deref(),
args.subdirectory.as_deref(), build_dispatch.interpreter(),
&wheel_dir, &build_dispatch,
// Good enough for the dev command SourceBuildContext::default(),
&args.sdist.display().to_string(), // Good enough for the dev command
) &args.sdist.display().to_string(),
.await?; build_kind,
Ok(wheel_dir.join(wheel)) )
.await?;
Ok(wheel_dir.join(builder.build(&wheel_dir).await?))
} }

View File

@ -13,7 +13,7 @@ use tracing::{debug, instrument};
use distribution_types::{CachedDist, Metadata}; use distribution_types::{CachedDist, Metadata};
use pep508_rs::Requirement; use pep508_rs::Requirement;
use platform_tags::Tags; use platform_tags::Tags;
use puffin_build::{SourceBuild, SourceBuildContext}; use puffin_build::{BuildKind, SourceBuild, SourceBuildContext};
use puffin_cache::Cache; use puffin_cache::Cache;
use puffin_client::RegistryClient; use puffin_client::RegistryClient;
use puffin_installer::{Downloader, InstallPlan, Installer, Reinstall}; use puffin_installer::{Downloader, InstallPlan, Installer, Reinstall};
@ -230,6 +230,7 @@ impl BuildContext for BuildDispatch {
self, self,
self.source_build_context.clone(), self.source_build_context.clone(),
source_dist, source_dist,
BuildKind::Wheel,
) )
.await?; .await?;
Ok(builder.build(wheel_dir).await?) Ok(builder.build(wheel_dir).await?)