From 8e0389e2fd03e2a63f40ed691c74ba8662ded2f6 Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 19 Nov 2024 22:52:11 +0100 Subject: [PATCH] Build backend: Build editable (#9230) Support for editable installs. This is a simple PEP 660 implementation. --- crates/uv-build-backend/src/lib.rs | 59 ++++++++++++++++++++++- crates/uv/src/commands/build_backend.rs | 53 ++++++++++++++++----- crates/uv/tests/it/build_backend.rs | 63 +++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 14 deletions(-) diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index 9fea3b8f45fd..ddaae8998b29 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -357,8 +357,6 @@ pub fn build_wheel( entry.file_type(), )); } - - entry.path(); } // Add the license files @@ -416,6 +414,63 @@ pub fn build_wheel( Ok(filename) } +/// Build a wheel from the source tree and place it in the output directory. +pub fn build_editable( + source_tree: &Path, + wheel_dir: &Path, + metadata_directory: Option<&Path>, + uv_version: &str, +) -> Result { + let contents = fs_err::read_to_string(source_tree.join("pyproject.toml"))?; + let pyproject_toml = PyProjectToml::parse(&contents)?; + pyproject_toml.check_build_system("1.0.0+test"); + + check_metadata_directory(source_tree, metadata_directory, &pyproject_toml)?; + + let filename = WheelFilename { + name: pyproject_toml.name().clone(), + version: pyproject_toml.version().clone(), + build_tag: None, + python_tag: vec!["py3".to_string()], + abi_tag: vec!["none".to_string()], + platform_tag: vec!["any".to_string()], + }; + + let wheel_path = wheel_dir.join(filename.to_string()); + debug!("Writing wheel at {}", wheel_path.user_display()); + let mut wheel_writer = ZipDirectoryWriter::new_wheel(File::create(&wheel_path)?); + + debug!("Adding pth file to {}", wheel_path.user_display()); + let module_root = pyproject_toml + .wheel_settings() + .and_then(|wheel_settings| wheel_settings.module_root.as_deref()) + .unwrap_or_else(|| Path::new("src")); + if module_root.is_absolute() { + return Err(Error::AbsoluteModuleRoot(module_root.to_path_buf())); + } + let src_root = source_tree.join(module_root); + let module_root = src_root.join(pyproject_toml.name().as_dist_info_name().as_ref()); + if !module_root.join("__init__.py").is_file() { + return Err(Error::MissingModule(module_root)); + } + wheel_writer.write_bytes( + &format!("{}.pth", pyproject_toml.name().as_dist_info_name()), + src_root.as_os_str().as_encoded_bytes(), + )?; + + debug!("Adding metadata files to: `{}`", wheel_path.user_display()); + let dist_info_dir = write_dist_info( + &mut wheel_writer, + &pyproject_toml, + &filename, + source_tree, + uv_version, + )?; + wheel_writer.close(&dist_info_dir)?; + + Ok(filename) +} + /// Add the files and directories matching from the source tree matching any of the globs in the /// wheel subdirectory. fn wheel_subdir_from_globs( diff --git a/crates/uv/src/commands/build_backend.rs b/crates/uv/src/commands/build_backend.rs index 954450b791e8..b23c17855f9e 100644 --- a/crates/uv/src/commands/build_backend.rs +++ b/crates/uv/src/commands/build_backend.rs @@ -1,11 +1,13 @@ #![allow(clippy::print_stdout)] use crate::commands::ExitStatus; -use anyhow::Result; +use anyhow::{Context, Result}; use std::env; +use std::io::Write; use std::path::Path; use uv_build_backend::SourceDistSettings; +/// PEP 517 hook to build a source distribution. pub(crate) fn build_sdist(sdist_directory: &Path) -> Result { let filename = uv_build_backend::build_source_dist( &env::current_dir()?, @@ -13,9 +15,12 @@ pub(crate) fn build_sdist(sdist_directory: &Path) -> Result { SourceDistSettings::default(), uv_version::version(), )?; - println!("{filename}"); + // Tell the build frontend about the name of the artifact we built + writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; Ok(ExitStatus::Success) } + +/// PEP 517 hook to build a wheel. pub(crate) fn build_wheel( wheel_directory: &Path, metadata_directory: Option<&Path>, @@ -26,38 +31,62 @@ pub(crate) fn build_wheel( metadata_directory, uv_version::version(), )?; - println!("{filename}"); + // Tell the build frontend about the name of the artifact we built + writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; Ok(ExitStatus::Success) } +/// PEP 660 hook to build a wheel. pub(crate) fn build_editable( - _wheel_directory: &Path, - _metadata_directory: Option<&Path>, + wheel_directory: &Path, + metadata_directory: Option<&Path>, ) -> Result { - todo!() + let filename = uv_build_backend::build_editable( + &env::current_dir()?, + wheel_directory, + metadata_directory, + uv_version::version(), + )?; + // Tell the build frontend about the name of the artifact we built + writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; + Ok(ExitStatus::Success) } +/// Not used from Python code, exists for symmetry with PEP 517. pub(crate) fn get_requires_for_build_sdist() -> Result { - todo!() + unimplemented!("uv does not support extra requires") } +/// Not used from Python code, exists for symmetry with PEP 517. pub(crate) fn get_requires_for_build_wheel() -> Result { - todo!() + unimplemented!("uv does not support extra requires") } + +/// PEP 517 hook to just emit metadata through `.dist-info`. pub(crate) fn prepare_metadata_for_build_wheel(metadata_directory: &Path) -> Result { let filename = uv_build_backend::metadata( &env::current_dir()?, metadata_directory, uv_version::version(), )?; - println!("{filename}"); + // Tell the build frontend about the name of the artifact we built + writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; Ok(ExitStatus::Success) } +/// Not used from Python code, exists for symmetry with PEP 660. pub(crate) fn get_requires_for_build_editable() -> Result { - todo!() + unimplemented!("uv does not support extra requires") } -pub(crate) fn prepare_metadata_for_build_editable(_wheel_directory: &Path) -> Result { - todo!() +/// PEP 660 hook to just emit metadata through `.dist-info`. +pub(crate) fn prepare_metadata_for_build_editable(metadata_directory: &Path) -> Result { + let filename = uv_build_backend::metadata( + &env::current_dir()?, + metadata_directory, + uv_version::version(), + )?; + // Tell the build frontend about the name of the artifact we built + writeln!(&mut std::io::stdout(), "{filename}").context("stdout is closed")?; + Ok(ExitStatus::Success) } diff --git a/crates/uv/tests/it/build_backend.rs b/crates/uv/tests/it/build_backend.rs index 36ac0cf8cee2..932b29ec6063 100644 --- a/crates/uv/tests/it/build_backend.rs +++ b/crates/uv/tests/it/build_backend.rs @@ -7,6 +7,7 @@ use indoc::indoc; use std::env; use std::io::BufReader; use std::path::Path; +use std::process::Command; use tempfile::TempDir; use uv_static::EnvVars; @@ -142,3 +143,65 @@ fn built_by_uv_direct() -> Result<()> { Ok(()) } + +/// Test that editables work. +/// +/// We can't test end-to-end here including the PEP 517 bridge code since we don't have a uv wheel, +/// so we call the build backend directly. +#[test] +fn built_by_uv_editable() -> Result<()> { + let context = TestContext::new("3.12"); + let built_by_uv = Path::new("../../scripts/packages/built-by-uv"); + + // Without the editable, pytest fails. + context.pip_install().arg("pytest").assert().success(); + Command::new(context.interpreter()) + .arg("-m") + .arg("pytest") + .current_dir(built_by_uv) + .assert() + .failure(); + + // Build and install the editable. Normally, this should be one step with the editable never + // been seen, but we have to split it for the test. + let wheel_dir = TempDir::new()?; + uv_snapshot!(context + .build_backend() + .arg("build-wheel") + .arg(wheel_dir.path()) + .current_dir(built_by_uv), @r###" + success: true + exit_code: 0 + ----- stdout ----- + built_by_uv-0.1.0-py3-none-any.whl + + ----- stderr ----- + "###); + context + .pip_install() + .arg(wheel_dir.path().join("built_by_uv-0.1.0-py3-none-any.whl")) + .assert() + .success(); + + drop(wheel_dir); + + // Now, pytest passes. + uv_snapshot!(Command::new(context.interpreter()) + .arg("-m") + .arg("pytest") + // Avoid showing absolute paths + .arg("--no-header") + // Otherwise, the header has a different length on windows + .arg("--quiet") + .current_dir(built_by_uv), @r###" + success: true + exit_code: 0 + ----- stdout ----- + .. [100%] + 2 passed in [TIME] + + ----- stderr ----- + "###); + + Ok(()) +}