diff --git a/Changelog.md b/Changelog.md index 6a2978b1b..a964b6ac4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Add support for PEP 660 editable installs in [#648](https://github.com/PyO3/maturin/pull/648) + ## [0.11.5] - 2021-10-13 * Fixed module documentation missing bug of pyo3 bindings in [#639](https://github.com/PyO3/maturin/pull/639) diff --git a/maturin/__init__.py b/maturin/__init__.py index 3d7a11834..ee796252e 100644 --- a/maturin/__init__.py +++ b/maturin/__init__.py @@ -26,10 +26,14 @@ def get_config() -> Dict[str, str]: # noinspection PyUnusedLocal -def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): +def _build_wheel( + wheel_directory, config_settings=None, metadata_directory=None, editable=False +): # PEP 517 specifies that only `sys.executable` points to the correct # python interpreter command = ["maturin", "pep517", "build-wheel", "-i", sys.executable] + if editable: + command.append("--editable") print("Running `{}`".format(" ".join(command))) sys.stdout.flush() @@ -48,6 +52,11 @@ def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): return filename +# noinspection PyUnusedLocal +def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): + return _build_wheel(wheel_directory, config_settings, metadata_directory) + + # noinspection PyUnusedLocal def build_sdist(sdist_directory, config_settings=None): command = ["maturin", "pep517", "write-sdist", "--sdist-directory", sdist_directory] @@ -74,6 +83,17 @@ def get_requires_for_build_wheel(config_settings=None): return [] +# noinspection PyUnusedLocal +def build_editable(wheel_directory, config_settings=None, metadata_directory=None): + return _build_wheel( + wheel_directory, config_settings, metadata_directory, editable=True + ) + + +# Requirements to build an editable are the same as for a wheel +get_requires_for_build_editable = get_requires_for_build_wheel + + # noinspection PyUnusedLocal def get_requires_for_build_sdist(config_settings=None): return [] @@ -122,3 +142,7 @@ def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): sys.stdout.flush() output = output.decode(errors="replace") return output.strip().splitlines()[-1] + + +# Metadata for editable are the same as for a wheel +prepare_metadata_for_build_editable = prepare_metadata_for_build_wheel diff --git a/src/build_context.rs b/src/build_context.rs index e0abb8bbb..7abd556d4 100644 --- a/src/build_context.rs +++ b/src/build_context.rs @@ -172,6 +172,8 @@ pub struct BuildContext { pub cargo_metadata: Metadata, /// Whether to use universal2 or use the native macOS tag (off) pub universal2: bool, + /// Build editable wheels + pub editable: bool, } /// The wheel file location and its Python version tag (e.g. `py3`). @@ -244,6 +246,13 @@ impl BuildContext { Ok(policy) } + fn add_pth(&self, writer: &mut WheelWriter) -> Result<()> { + if self.editable { + writer.add_pth(&self.project_layout, &self.metadata21)?; + } + Ok(()) + } + fn write_binding_wheel_abi3( &self, artifact: &Path, @@ -267,6 +276,8 @@ impl BuildContext { ) .context("Failed to add the files to the wheel")?; + self.add_pth(&mut writer)?; + let wheel_path = writer.finish()?; Ok((wheel_path, format!("cp{}{}", major, min_minor))) } @@ -322,6 +333,8 @@ impl BuildContext { ) .context("Failed to add the files to the wheel")?; + self.add_pth(&mut writer)?; + let wheel_path = writer.finish()?; Ok(( wheel_path, @@ -398,10 +411,10 @@ impl BuildContext { .target .get_universal_tags(platform_tag, self.universal2); - let mut builder = WheelWriter::new(&tag, &self.out, &self.metadata21, &tags)?; + let mut writer = WheelWriter::new(&tag, &self.out, &self.metadata21, &tags)?; write_cffi_module( - &mut builder, + &mut writer, &self.project_layout, self.manifest_path.parent().unwrap(), &self.module_name, @@ -410,7 +423,9 @@ impl BuildContext { false, )?; - let wheel_path = builder.finish()?; + self.add_pth(&mut writer)?; + + let wheel_path = writer.finish()?; Ok((wheel_path, "py3".to_string())) } @@ -440,7 +455,7 @@ impl BuildContext { bail!("Defining entrypoints and working with a binary doesn't mix well"); } - let mut builder = WheelWriter::new(&tag, &self.out, &self.metadata21, &tags)?; + let mut writer = WheelWriter::new(&tag, &self.out, &self.metadata21, &tags)?; match self.project_layout { ProjectLayout::Mixed { @@ -448,7 +463,7 @@ impl BuildContext { ref extension_name, .. } => { - write_python_part(&mut builder, python_module, extension_name) + write_python_part(&mut writer, python_module, extension_name) .context("Failed to add the python module to the package")?; } ProjectLayout::PureRust { .. } => {} @@ -459,9 +474,11 @@ impl BuildContext { let bin_name = artifact .file_name() .expect("Couldn't get the filename from the binary produced by cargo"); - write_bin(&mut builder, artifact, &self.metadata21, bin_name)?; + write_bin(&mut writer, artifact, &self.metadata21, bin_name)?; - let wheel_path = builder.finish()?; + self.add_pth(&mut writer)?; + + let wheel_path = writer.finish()?; Ok((wheel_path, "py3".to_string())) } diff --git a/src/build_options.rs b/src/build_options.rs index 75dc8b477..33d6b1f25 100644 --- a/src/build_options.rs +++ b/src/build_options.rs @@ -106,7 +106,12 @@ impl Default for BuildOptions { impl BuildOptions { /// Tries to fill the missing metadata for a BuildContext by querying cargo and python - pub fn into_build_context(self, release: bool, strip: bool) -> Result { + pub fn into_build_context( + self, + release: bool, + strip: bool, + editable: bool, + ) -> Result { let manifest_file = &self.manifest_path; if !manifest_file.exists() { let current_dir = @@ -303,6 +308,7 @@ impl BuildOptions { interpreter, cargo_metadata, universal2, + editable, }) } } @@ -789,7 +795,7 @@ mod test { let mut options = BuildOptions::default(); options.cargo_extra_args.push("--features log".to_string()); options.bindings = Some("bin".to_string()); - let context = options.into_build_context(false, false).unwrap(); + let context = options.into_build_context(false, false, false).unwrap(); assert_eq!(context.cargo_extra_args, vec!["--features", "log"]) } diff --git a/src/develop.rs b/src/develop.rs index 42ad78b51..b2c51e14e 100644 --- a/src/develop.rs +++ b/src/develop.rs @@ -64,7 +64,7 @@ pub fn develop( universal2: false, }; - let build_context = build_options.into_build_context(release, strip)?; + let build_context = build_options.into_build_context(release, strip, false)?; let interpreter = PythonInterpreter::check_executable(python, &target, &build_context.bridge)? .ok_or_else(|| { diff --git a/src/main.rs b/src/main.rs index cf728c36a..d9876af1d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -323,6 +323,9 @@ enum Pep517Command { /// Strip the library for minimum file size #[structopt(long)] strip: bool, + /// Build editable wheels + #[structopt(long)] + editable: bool, }, /// The implementation of build_sdist #[structopt(name = "write-sdist")] @@ -356,7 +359,7 @@ fn pep517(subcommand: Pep517Command) -> Result<()> { build_options.interpreter.as_ref(), Some(version) if version.len() == 1 )); - let context = build_options.into_build_context(true, strip)?; + let context = build_options.into_build_context(true, strip, false)?; // Since afaik all other PEP 517 backends also return linux tagged wheels, we do so too let tags = match context.bridge { @@ -384,8 +387,9 @@ fn pep517(subcommand: Pep517Command) -> Result<()> { Pep517Command::BuildWheel { build_options, strip, + editable, } => { - let build_context = build_options.into_build_context(true, strip)?; + let build_context = build_options.into_build_context(true, strip, editable)?; let wheels = build_context.build_wheels()?; assert_eq!(wheels.len(), 1); println!("{}", wheels[0].0.to_str().unwrap()); @@ -507,7 +511,7 @@ fn run() -> Result<()> { strip, no_sdist, } => { - let build_context = build.into_build_context(release, strip)?; + let build_context = build.into_build_context(release, strip, false)?; if !no_sdist { build_context.build_source_distribution()?; } @@ -521,7 +525,7 @@ fn run() -> Result<()> { no_strip, no_sdist, } => { - let build_context = build.into_build_context(!debug, !no_strip)?; + let build_context = build.into_build_context(!debug, !no_strip, false)?; if !build_context.release { eprintln!("⚠️ Warning: You're publishing debug wheels"); diff --git a/src/module_writer.rs b/src/module_writer.rs index 8ea835a10..2fa054bb6 100644 --- a/src/module_writer.rs +++ b/src/module_writer.rs @@ -268,6 +268,29 @@ impl WheelWriter { Ok(builder) } + /// Add a pth file to wheel root for editable installs + pub fn add_pth( + &mut self, + project_layout: &ProjectLayout, + metadata21: &Metadata21, + ) -> Result<()> { + match project_layout { + ProjectLayout::Mixed { + ref python_module, .. + } => { + let absolute_path = python_module.canonicalize()?; + if let Some(python_path) = absolute_path.parent().and_then(|p| p.to_str()) { + let name = metadata21.get_distribution_escaped(); + self.add_bytes(format!("{}.pth", name), python_path.as_bytes())?; + } else { + println!("⚠ source code path contains non-Unicode sequences, editable installs may not work."); + } + } + ProjectLayout::PureRust { .. } => {} + } + Ok(()) + } + /// Creates the record file and finishes the zip pub fn finish(mut self) -> Result { let compression_method = if cfg!(feature = "faster-tests") { diff --git a/tests/common/errors.rs b/tests/common/errors.rs index 5852e7b32..92545ee74 100644 --- a/tests/common/errors.rs +++ b/tests/common/errors.rs @@ -13,7 +13,7 @@ pub fn abi3_without_version() -> Result<()> { ]; let options = BuildOptions::from_iter_safe(cli)?; - let result = options.into_build_context(false, cfg!(feature = "faster-tests")); + let result = options.into_build_context(false, cfg!(feature = "faster-tests"), false); if let Err(err) = result { assert_eq!(err.to_string(), "You have selected the `abi3` feature but not a minimum version (e.g. the `abi3-py36` feature). \ @@ -40,7 +40,7 @@ pub fn pyo3_no_extension_module() -> Result<()> { let options = BuildOptions::from_iter_safe(cli)?; let result = options - .into_build_context(false, cfg!(feature = "faster-tests"))? + .into_build_context(false, cfg!(feature = "faster-tests"), false)? .build_wheels(); if let Err(err) = result { if !(err @@ -71,7 +71,7 @@ pub fn locked_doesnt_build_without_cargo_lock() -> Result<()> { "-i=python", ]; let options = BuildOptions::from_iter_safe(cli)?; - let result = options.into_build_context(false, cfg!(feature = "faster-tests")); + let result = options.into_build_context(false, cfg!(feature = "faster-tests"), false); if let Err(err) = result { let err_string = err .source() diff --git a/tests/common/integration.rs b/tests/common/integration.rs index 73c542f40..82a6a73c0 100644 --- a/tests/common/integration.rs +++ b/tests/common/integration.rs @@ -32,7 +32,7 @@ pub fn test_integration(package: impl AsRef, bindings: Option) -> let options: BuildOptions = BuildOptions::from_iter_safe(cli)?; - let build_context = options.into_build_context(false, cfg!(feature = "faster-tests"))?; + let build_context = options.into_build_context(false, cfg!(feature = "faster-tests"), false)?; let wheels = build_context.build_wheels()?; let test_name = package @@ -184,7 +184,7 @@ pub fn test_integration_conda(package: impl AsRef, bindings: Option = vec![]; diff --git a/tests/common/other.rs b/tests/common/other.rs index 267951d42..e959f16f8 100644 --- a/tests/common/other.rs +++ b/tests/common/other.rs @@ -54,7 +54,7 @@ pub fn test_musl() -> Result { "linux", ])?; - let build_context = options.into_build_context(false, cfg!(feature = "faster-tests"))?; + let build_context = options.into_build_context(false, cfg!(feature = "faster-tests"), false)?; let built_lib = build_context .manifest_path .parent() @@ -91,7 +91,7 @@ pub fn test_workspace_cargo_lock() -> Result<()> { "linux", ])?; - let build_context = options.into_build_context(false, cfg!(feature = "faster-tests"))?; + let build_context = options.into_build_context(false, cfg!(feature = "faster-tests"), false)?; let source_distribution = build_context.build_source_distribution()?; assert!(source_distribution.is_some());