diff --git a/src/binding_generator/bin_binding.rs b/src/binding_generator/bin_binding.rs new file mode 100644 index 000000000..a5eda0ae9 --- /dev/null +++ b/src/binding_generator/bin_binding.rs @@ -0,0 +1,154 @@ +use std::collections::HashMap; +use std::path::Path; +use std::str::FromStr as _; + +use anyhow::Context; +use anyhow::Result; +use pep508_rs::Requirement; +use tempfile::TempDir; + +use crate::BuildArtifact; +use crate::BuildContext; +use crate::Metadata24; +use crate::PythonInterpreter; +use crate::archive_source::ArchiveSource; +use crate::archive_source::GeneratedSourceData; + +use super::BindingGenerator; +use super::GeneratorOutput; + +/// A generator for producing bin (and wasm) bindings. +pub struct BinBindingGenerator<'m> { + metadata: &'m mut Metadata24, +} + +impl<'m> BinBindingGenerator<'m> { + pub fn new(metadata: &'m mut Metadata24) -> Self { + Self { metadata } + } +} + +impl<'m> BindingGenerator for BinBindingGenerator<'m> { + fn generate_bindings( + &mut self, + context: &BuildContext, + _interpreter: Option<&PythonInterpreter>, + artifact: &BuildArtifact, + _module: &Path, + _temp_dir: &TempDir, + ) -> Result { + // I wouldn't know of any case where this would be the wrong (and neither do + // I know a better alternative) + let bin_name = artifact + .path + .file_name() + .context("Couldn't get the filename from the binary produced by cargo")? + .to_str() + .context("binary produced by cargo has non-utf8 filename")? + .to_string(); + let scripts_dir = self.metadata.get_data_dir().join("scripts"); + let artifact_target = scripts_dir.join(&bin_name); + + let mut additional_files = None; + if context.target.is_wasi() { + update_entry_points(self.metadata, &bin_name)?; + + let mut files = HashMap::new(); + files.insert( + Path::new(&self.metadata.get_distribution_escaped()) + .join(bin_name.replace('-', "_")) + .with_extension("py"), + ArchiveSource::Generated(GeneratedSourceData { + data: generate_wasm_launcher(&bin_name).into(), + executable: false, + }), + ); + additional_files = Some(files); + } + + Ok(GeneratorOutput { + artifact_target, + artifact_source_override: None, + additional_files, + }) + } +} + +/// Adds a wrapper script that starts the wasm binary through wasmtime. +pub fn generate_wasm_launcher(bin_name: &str) -> String { + format!( + r#"from pathlib import Path + +from wasmtime import Store, Module, Engine, WasiConfig, Linker + +import sysconfig + +def main(): + # The actual executable + program_location = Path(sysconfig.get_path("scripts")).joinpath("{bin_name}") + # wasmtime-py boilerplate + engine = Engine() + store = Store(engine) + # TODO: is there an option to just get the default of the wasmtime cli here? + wasi = WasiConfig() + wasi.inherit_argv() + wasi.inherit_env() + wasi.inherit_stdout() + wasi.inherit_stderr() + wasi.inherit_stdin() + # TODO: Find a real solution here. Maybe there's an always allow callback? + # Even fancier would be something configurable in pyproject.toml + wasi.preopen_dir(".", ".") + store.set_wasi(wasi) + linker = Linker(engine) + linker.define_wasi() + module = Module.from_file(store.engine, str(program_location)) + linking1 = linker.instantiate(store, module) + # TODO: this is taken from https://docs.wasmtime.dev/api/wasmtime/struct.Linker.html#method.get_default + # is this always correct? + start = linking1.exports(store).get("") or linking1.exports(store)["_start"] + start(store) + +if __name__ == '__main__': + main() + "# + ) +} + +/// Insert wasm launcher scripts as entrypoints and the wasmtime dependency +fn update_entry_points(metadata24: &mut Metadata24, bin_name: &str) -> Result<()> { + let distribution_name = metadata24.get_distribution_escaped(); + let console_scripts = metadata24 + .entry_points + .entry("console_scripts".to_string()) + .or_default(); + + // From https://packaging.python.org/en/latest/specifications/entry-points/ + // > The name may contain any characters except =, but it cannot start or end with any + // > whitespace character, or start with [. For new entry points, it is recommended to + // > use only letters, numbers, underscores, dots and dashes (regex [\w.-]+). + // All of these rules are already enforced by cargo: + // https://github.com/rust-lang/cargo/blob/58a961314437258065e23cb6316dfc121d96fb71/src/cargo/util/restricted_names.rs#L39-L84 + // i.e. we don't need to do any bin name validation here anymore + let base_name = bin_name + .strip_suffix(".wasm") + .context("No .wasm suffix in wasi binary")?; + console_scripts.insert( + base_name.to_string(), + format!("{distribution_name}.{}:main", base_name.replace('-', "_")), + ); + + // Add our wasmtime default version if the user didn't provide one + if !metadata24 + .requires_dist + .iter() + .any(|requirement| requirement.name.as_ref() == "wasmtime") + { + // Having the wasmtime version hardcoded is not ideal, it's easy enough to overwrite + metadata24 + .requires_dist + .push(Requirement::from_str("wasmtime>=11.0.0,<12.0.0").unwrap()); + } + + Ok(()) +} diff --git a/src/binding_generator/cffi_binding.rs b/src/binding_generator/cffi_binding.rs index 571f500c4..572247457 100644 --- a/src/binding_generator/cffi_binding.rs +++ b/src/binding_generator/cffi_binding.rs @@ -33,7 +33,7 @@ pub struct CffiBindingGenerator {} impl BindingGenerator for CffiBindingGenerator { fn generate_bindings( - &self, + &mut self, context: &BuildContext, interpreter: Option<&PythonInterpreter>, _artifact: &BuildArtifact, diff --git a/src/binding_generator/mod.rs b/src/binding_generator/mod.rs index a040d0e75..409323a7f 100644 --- a/src/binding_generator/mod.rs +++ b/src/binding_generator/mod.rs @@ -1,3 +1,4 @@ +use std::borrow::Borrow; use std::collections::HashMap; use std::io; use std::io::Write as _; @@ -16,31 +17,31 @@ use tracing::debug; use crate::BuildArtifact; use crate::BuildContext; -use crate::Metadata24; use crate::ModuleWriter; use crate::PythonInterpreter; use crate::archive_source::ArchiveSource; use crate::module_writer::ModuleWriterExt; +#[cfg(unix)] use crate::module_writer::default_permission; use crate::module_writer::write_python_part; +mod bin_binding; mod cffi_binding; mod pyo3_binding; mod uniffi_binding; -mod wasm_binding; +pub use bin_binding::BinBindingGenerator; pub use cffi_binding::CffiBindingGenerator; pub use pyo3_binding::Pyo3BindingGenerator; pub use uniffi_binding::UniFfiBindingGenerator; -pub use wasm_binding::write_wasm_launcher; -///A trait to generate the binding files to be included in the built module +/// A trait to generate the binding files to be included in the built module /// /// This trait is used to generate the support files necessary to build a python /// module for any [crate::BridgeModel] pub(crate) trait BindingGenerator { fn generate_bindings( - &self, + &mut self, context: &BuildContext, interpreter: Option<&PythonInterpreter>, artifact: &BuildArtifact, @@ -83,20 +84,24 @@ pub(crate) struct GeneratorOutput { /// 2. everything else => install to archive/build as normal /// /// Note: Writing the pth to the archive is handled by [BuildContext], not here -pub fn generate_binding( +pub fn generate_binding( writer: &mut impl ModuleWriter, - generator: &impl BindingGenerator, + generator: &mut impl BindingGenerator, context: &BuildContext, interpreter: Option<&PythonInterpreter>, - artifact: &BuildArtifact, -) -> Result<()> { + artifacts: &[A], +) -> Result<()> +where + A: Borrow, +{ // 1. Install the python files if !context.editable { write_python_part( writer, &context.project_layout, context.pyproject_toml.as_ref(), - )?; + ) + .context("Failed to add the python module to the package")?; } let base_path = context @@ -115,82 +120,85 @@ pub fn generate_binding( None => PathBuf::from(&context.project_layout.extension_name), }; - let temp_dir = tempdir()?; - let GeneratorOutput { - artifact_target, - artifact_source_override, - additional_files, - } = generator.generate_bindings(context, interpreter, artifact, &module, &temp_dir)?; - - match (context.editable, &base_path) { - (true, Some(base_path)) => { - let target = base_path.join(&artifact_target); - debug!("Removing previously built module {}", target.display()); - fs::create_dir_all(target.parent().unwrap())?; - // Remove existing so file to avoid triggering SIGSEV in running process - // See https://github.com/PyO3/maturin/issues/758 - let _ = fs::remove_file(&target); - let source = artifact_source_override.unwrap_or_else(|| artifact.path.clone()); - - // 2. Install the artifact - debug!("Installing {} from {}", target.display(), source.display()); - fs::copy(&source, &target).with_context(|| { - format!( - "Failed to copy {} to {}", - source.display(), - target.display(), - ) - })?; - - // 3. Install additional files - if let Some(additional_files) = additional_files { - for (target, source) in additional_files { - let target = base_path.join(target); - fs::create_dir_all(target.parent().unwrap())?; - debug!("Generating file {}", target.display()); - let mut options = File::options(); - options.write(true).create(true).truncate(true); - #[cfg(unix)] - { - options.mode(default_permission(source.executable())); - } + for artifact in artifacts { + let artifact = artifact.borrow(); + let temp_dir = tempdir()?; + let GeneratorOutput { + artifact_target, + artifact_source_override, + additional_files, + } = generator.generate_bindings(context, interpreter, artifact, &module, &temp_dir)?; + + match (context.editable, &base_path) { + (true, Some(base_path)) => { + let target = base_path.join(&artifact_target); + debug!("Removing previously built module {}", target.display()); + fs::create_dir_all(target.parent().unwrap())?; + // Remove existing so file to avoid triggering SIGSEV in running process + // See https://github.com/PyO3/maturin/issues/758 + let _ = fs::remove_file(&target); + let source = artifact_source_override.unwrap_or_else(|| artifact.path.clone()); + + // 2a. Install the artifact + debug!("Installing {} from {}", target.display(), source.display()); + fs::copy(&source, &target).with_context(|| { + format!( + "Failed to copy {} to {}", + source.display(), + target.display(), + ) + })?; + + // 3a. Install additional files + if let Some(additional_files) = additional_files { + for (target, source) in additional_files { + let target = base_path.join(target); + fs::create_dir_all(target.parent().unwrap())?; + debug!("Generating file {}", target.display()); + let mut options = File::options(); + options.write(true).create(true).truncate(true); + #[cfg(unix)] + { + options.mode(default_permission(source.executable())); + } - let mut file = options.open(&target)?; - match source { - ArchiveSource::Generated(source) => file.write_all(&source.data)?, - ArchiveSource::File(source) => { - let mut source = File::options().read(true).open(source.path)?; - io::copy(&mut source, &mut file)?; + let mut file = options.open(&target)?; + match source { + ArchiveSource::Generated(source) => file.write_all(&source.data)?, + ArchiveSource::File(source) => { + let mut source = File::options().read(true).open(source.path)?; + io::copy(&mut source, &mut file)?; + } } } } } - } - _ => { - // 2. Install the artifact - let source = artifact_source_override.unwrap_or_else(|| artifact.path.clone()); - debug!( - "Adding to archive {} from {}", - artifact_target.display(), - source.display() - ); - writer.add_file(artifact_target, source, true)?; - - // 3. Install additional files - if let Some(additional_files) = additional_files { - for (target, source) in additional_files { - debug!("Generating archive entry {}", target.display()); - match source { - ArchiveSource::Generated(source) => { - writer.add_bytes( - target, - None, - source.data.as_slice(), - source.executable, - )?; - } - ArchiveSource::File(source) => { - writer.add_file(target, source.path, source.executable)?; + _ => { + // 2b. Install the artifact + let source = artifact_source_override.unwrap_or_else(|| artifact.path.clone()); + debug!( + "Adding to archive {} from {}", + artifact_target.display(), + source.display() + ); + writer.add_file(artifact_target, source, true)?; + + // 3b. Install additional files + if let Some(additional_files) = additional_files { + for (target, source) in additional_files { + debug!("Generating archive entry {}", target.display()); + match source { + ArchiveSource::Generated(source) => { + writer.add_bytes( + target, + None, + source.data.as_slice(), + source.executable, + )?; + } + ArchiveSource::File(source) => { + writer.add_file(target, source.path, source.executable)?; + } } } } @@ -214,22 +222,3 @@ pub fn generate_binding( Ok(()) } - -/// Adds a data directory with a scripts directory with the binary inside it -pub fn write_bin( - writer: &mut impl ModuleWriter, - artifact: &Path, - metadata: &Metadata24, - bin_name: &str, -) -> Result<()> { - let data_dir = PathBuf::from(format!( - "{}-{}.data", - &metadata.get_distribution_escaped(), - &metadata.version - )) - .join("scripts"); - - // We can't use add_file since we need to mark the file as executable - writer.add_file(data_dir.join(bin_name), artifact, true)?; - Ok(()) -} diff --git a/src/binding_generator/pyo3_binding.rs b/src/binding_generator/pyo3_binding.rs index 73dd29ec1..4cc3a1b06 100644 --- a/src/binding_generator/pyo3_binding.rs +++ b/src/binding_generator/pyo3_binding.rs @@ -36,7 +36,7 @@ impl Pyo3BindingGenerator { impl BindingGenerator for Pyo3BindingGenerator { fn generate_bindings( - &self, + &mut self, context: &BuildContext, interpreter: Option<&PythonInterpreter>, artifact: &BuildArtifact, diff --git a/src/binding_generator/uniffi_binding.rs b/src/binding_generator/uniffi_binding.rs index 4a2281572..5e9d19247 100644 --- a/src/binding_generator/uniffi_binding.rs +++ b/src/binding_generator/uniffi_binding.rs @@ -28,7 +28,7 @@ pub struct UniFfiBindingGenerator {} impl BindingGenerator for UniFfiBindingGenerator { fn generate_bindings( - &self, + &mut self, context: &BuildContext, _interpreter: Option<&PythonInterpreter>, artifact: &BuildArtifact, diff --git a/src/binding_generator/wasm_binding.rs b/src/binding_generator/wasm_binding.rs deleted file mode 100644 index ecf1f6adc..000000000 --- a/src/binding_generator/wasm_binding.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::path::Path; - -use anyhow::Result; - -use crate::Metadata24; -use crate::ModuleWriter; - -/// Adds a wrapper script that start the wasm binary through wasmtime. -/// -/// Note that the wasm binary needs to be written separately by [write_bin] -pub fn write_wasm_launcher( - writer: &mut impl ModuleWriter, - metadata: &Metadata24, - bin_name: &str, -) -> Result<()> { - let entrypoint_script = format!( - r#"from pathlib import Path - -from wasmtime import Store, Module, Engine, WasiConfig, Linker - -import sysconfig - -def main(): - # The actual executable - program_location = Path(sysconfig.get_path("scripts")).joinpath("{bin_name}") - # wasmtime-py boilerplate - engine = Engine() - store = Store(engine) - # TODO: is there an option to just get the default of the wasmtime cli here? - wasi = WasiConfig() - wasi.inherit_argv() - wasi.inherit_env() - wasi.inherit_stdout() - wasi.inherit_stderr() - wasi.inherit_stdin() - # TODO: Find a real solution here. Maybe there's an always allow callback? - # Even fancier would be something configurable in pyproject.toml - wasi.preopen_dir(".", ".") - store.set_wasi(wasi) - linker = Linker(engine) - linker.define_wasi() - module = Module.from_file(store.engine, str(program_location)) - linking1 = linker.instantiate(store, module) - # TODO: this is taken from https://docs.wasmtime.dev/api/wasmtime/struct.Linker.html#method.get_default - # is this always correct? - start = linking1.exports(store).get("") or linking1.exports(store)["_start"] - start(store) - -if __name__ == '__main__': - main() - "# - ); - - let launcher_path = Path::new(&metadata.get_distribution_escaped()) - .join(bin_name.replace('-', "_")) - .with_extension("py"); - writer.add_bytes(&launcher_path, None, entrypoint_script.as_bytes(), true)?; - Ok(()) -} diff --git a/src/build_context.rs b/src/build_context.rs index 77b08cfc1..10af19a24 100644 --- a/src/build_context.rs +++ b/src/build_context.rs @@ -1,14 +1,14 @@ use crate::auditwheel::{AuditWheelMode, get_policy_and_libs, patchelf, relpath}; use crate::auditwheel::{PlatformTag, Policy}; use crate::binding_generator::{ - CffiBindingGenerator, Pyo3BindingGenerator, UniFfiBindingGenerator, generate_binding, - write_bin, write_wasm_launcher, + BinBindingGenerator, CffiBindingGenerator, Pyo3BindingGenerator, UniFfiBindingGenerator, + generate_binding, }; use crate::bridge::Abi3Version; use crate::build_options::CargoOptions; use crate::compile::{CompileTarget, warn_missing_py_init}; use crate::compression::CompressionOptions; -use crate::module_writer::{ModuleWriterExt, WheelWriter, add_data, write_python_part}; +use crate::module_writer::{ModuleWriterExt, WheelWriter, add_data}; use crate::project_layout::ProjectLayout; use crate::source_distribution::source_distribution; use crate::target::validate_wheel_filename_for_pypi; @@ -22,71 +22,18 @@ use cargo_metadata::CrateType; use cargo_metadata::Metadata; use fs_err as fs; use ignore::overrides::{Override, OverrideBuilder}; -use indexmap::IndexMap; use lddtree::Library; use normpath::PathExt; -use pep508_rs::Requirement; use platform_info::*; use sha2::{Digest, Sha256}; +use std::borrow::Borrow; use std::collections::{BTreeMap, HashSet}; use std::env; use std::io; use std::path::{Path, PathBuf}; -use std::str::FromStr; use tracing::instrument; use zip::DateTime; -/// Insert wasm launcher scripts as entrypoints and the wasmtime dependency -fn bin_wasi_helper( - artifacts_and_files: &[(&BuildArtifact, String)], - mut metadata24: Metadata24, -) -> Result { - eprintln!("⚠️ Warning: wasi support is experimental"); - // escaped can contain [\w\d.], but i don't know how we'd handle dots correctly here - if metadata24.get_distribution_escaped().contains('.') { - bail!( - "Can't build wasm wheel if there is a dot in the name ('{}')", - metadata24.get_distribution_escaped() - ) - } - if !metadata24.entry_points.is_empty() { - bail!("You can't define entrypoints yourself for a binary project"); - } - - let mut console_scripts = IndexMap::new(); - for (_, bin_name) in artifacts_and_files { - let base_name = bin_name - .strip_suffix(".wasm") - .context("No .wasm suffix in wasi binary")?; - console_scripts.insert( - base_name.to_string(), - format!( - "{}.{}:main", - metadata24.get_distribution_escaped(), - base_name.replace('-', "_") - ), - ); - } - - metadata24 - .entry_points - .insert("console_scripts".to_string(), console_scripts); - - // Add our wasmtime default version if the user didn't provide one - if !metadata24 - .requires_dist - .iter() - .any(|requirement| requirement.name.as_ref() == "wasmtime") - { - // Having the wasmtime version hardcoded is not ideal, it's easy enough to overwrite - metadata24 - .requires_dist - .push(Requirement::from_str("wasmtime>=11.0.0,<12.0.0").unwrap()); - } - - Ok(metadata24) -} - /// Contains all the metadata required to build the crate #[derive(Clone)] pub struct BuildContext { @@ -361,9 +308,13 @@ impl BuildContext { } /// Add library search paths in Cargo target directory rpath when building in editable mode - fn add_rpath(&self, artifacts: &[&BuildArtifact]) -> Result<()> { + fn add_rpath(&self, artifacts: &[A]) -> Result<()> + where + A: Borrow, + { if self.editable && self.target.is_linux() && !artifacts.is_empty() { for artifact in artifacts { + let artifact = artifact.borrow(); if artifact.linked_paths.is_empty() { continue; } @@ -387,12 +338,15 @@ impl BuildContext { Ok(()) } - fn add_external_libs( + fn add_external_libs( &self, writer: &mut WheelWriter, - artifacts: &[&BuildArtifact], + artifacts: &[A], ext_libs: &[Vec], - ) -> Result<()> { + ) -> Result<()> + where + A: Borrow, + { if self.editable { return self.add_rpath(artifacts); } @@ -472,6 +426,7 @@ impl BuildContext { } for (artifact, artifact_ext_libs) in artifacts.iter().zip(ext_libs) { + let artifact = artifact.borrow(); let artifact_deps: HashSet<_> = artifact_ext_libs.iter().map(|lib| &lib.name).collect(); let replacements = soname_map .iter() @@ -528,6 +483,7 @@ impl BuildContext { _ => PathBuf::from(&self.module_name), }; for artifact in artifacts { + let artifact = artifact.borrow(); let mut new_rpaths = patchelf::get_rpath(&artifact.path)?; // TODO: clean existing rpath entries if it's not pointed to a location within the wheel // See https://github.com/pypa/auditwheel/blob/353c24250d66951d5ac7e60b97471a6da76c123f/src/auditwheel/repair.py#L160 @@ -761,21 +717,19 @@ impl BuildContext { let mut writer = WheelWriter::new( &tag, &self.out, - &self.project_layout.project_root, &self.metadata24, - std::slice::from_ref(&tag), self.excludes(Format::Wheel)?, file_options, )?; self.add_external_libs(&mut writer, &[&artifact], &[ext_libs])?; - let generator = Pyo3BindingGenerator::new(true); + let mut generator = Pyo3BindingGenerator::new(true); generate_binding( &mut writer, - &generator, + &mut generator, self, self.interpreter.first(), - &artifact, + &[&artifact], ) .context("Failed to add the files to the wheel")?; @@ -785,7 +739,11 @@ impl BuildContext { &self.metadata24, self.project_layout.data.as_deref(), )?; - let wheel_path = writer.finish(&self.metadata24)?; + let wheel_path = writer.finish( + &self.metadata24, + &self.project_layout.project_root, + std::slice::from_ref(&tag), + )?; Ok((wheel_path, format!("cp{major}{min_minor}"))) } @@ -842,21 +800,19 @@ impl BuildContext { let mut writer = WheelWriter::new( &tag, &self.out, - &self.project_layout.project_root, &self.metadata24, - std::slice::from_ref(&tag), self.excludes(Format::Wheel)?, file_options, )?; self.add_external_libs(&mut writer, &[&artifact], &[ext_libs])?; - let generator = Pyo3BindingGenerator::new(false); + let mut generator = Pyo3BindingGenerator::new(false); generate_binding( &mut writer, - &generator, + &mut generator, self, Some(python_interpreter), - &artifact, + &[&artifact], ) .context("Failed to add the files to the wheel")?; @@ -866,7 +822,11 @@ impl BuildContext { &self.metadata24, self.project_layout.data.as_deref(), )?; - let wheel_path = writer.finish(&self.metadata24)?; + let wheel_path = writer.finish( + &self.metadata24, + &self.project_layout.project_root, + std::slice::from_ref(&tag), + )?; Ok(( wheel_path, format!("cp{}{}", python_interpreter.major, python_interpreter.minor), @@ -968,21 +928,19 @@ impl BuildContext { let mut writer = WheelWriter::new( &tag, &self.out, - &self.project_layout.project_root, &self.metadata24, - &tags, self.excludes(Format::Wheel)?, file_options, )?; self.add_external_libs(&mut writer, &[&artifact], &[ext_libs])?; - let generator = CffiBindingGenerator::default(); + let mut generator = CffiBindingGenerator::default(); generate_binding( &mut writer, - &generator, + &mut generator, self, self.interpreter.first(), - &artifact, + &[&artifact], )?; self.add_pth(&mut writer)?; @@ -991,7 +949,8 @@ impl BuildContext { &self.metadata24, self.project_layout.data.as_deref(), )?; - let wheel_path = writer.finish(&self.metadata24)?; + let wheel_path = + writer.finish(&self.metadata24, &self.project_layout.project_root, &tags)?; Ok((wheel_path, "py3".to_string())) } @@ -1041,21 +1000,19 @@ impl BuildContext { let mut writer = WheelWriter::new( &tag, &self.out, - &self.project_layout.project_root, &self.metadata24, - &tags, self.excludes(Format::Wheel)?, file_options, )?; self.add_external_libs(&mut writer, &[&artifact], &[ext_libs])?; - let generator = UniFfiBindingGenerator::default(); + let mut generator = UniFfiBindingGenerator::default(); generate_binding( &mut writer, - &generator, + &mut generator, self, self.interpreter.first(), - &artifact, + &[&artifact], )?; self.add_pth(&mut writer)?; @@ -1064,7 +1021,8 @@ impl BuildContext { &self.metadata24, self.project_layout.data.as_deref(), )?; - let wheel_path = writer.finish(&self.metadata24)?; + let wheel_path = + writer.finish(&self.metadata24, &self.project_layout.project_root, &tags)?; Ok((wheel_path, "py3".to_string())) } @@ -1093,6 +1051,31 @@ impl BuildContext { platform_tags: &[PlatformTag], ext_libs: &[Vec], ) -> Result { + if !self.metadata24.scripts.is_empty() { + bail!("Defining scripts and working with a binary doesn't mix well"); + } + + if self.target.is_wasi() { + eprintln!("⚠️ Warning: wasi support is experimental"); + // escaped can contain [\w\d.], but i don't know how we'd handle dots correctly here + if self.metadata24.get_distribution_escaped().contains('.') { + bail!( + "Can't build wasm wheel if there is a dot in the name ('{}')", + self.metadata24.get_distribution_escaped() + ) + } + + if !self.metadata24.entry_points.is_empty() { + bail!("You can't define entrypoints yourself for a binary project"); + } + + if self.project_layout.python_module.is_some() { + // TODO: Can we have python code and the wasm launchers coexisting + // without clashes? + bail!("Sorry, adding python code to a wasm binary is currently not supported") + } + } + let (tag, tags) = match (self.bridge(), python_interpreter) { (BridgeModel::Bin(None), _) => self.get_universal_tags(platform_tags)?, (BridgeModel::Bin(Some(..)), Some(python_interpreter)) => { @@ -1102,39 +1085,7 @@ impl BuildContext { _ => unreachable!(), }; - if !self.metadata24.scripts.is_empty() { - bail!("Defining scripts and working with a binary doesn't mix well"); - } - - let mut artifacts_and_files = Vec::new(); - for artifact in artifacts { - // I wouldn't know of any case where this would be the wrong (and neither do - // I know a better alternative) - let bin_name = artifact - .path - .file_name() - .context("Couldn't get the filename from the binary produced by cargo")? - .to_str() - .context("binary produced by cargo has non-utf8 filename")? - .to_string(); - - // From https://packaging.python.org/en/latest/specifications/entry-points/ - // > The name may contain any characters except =, but it cannot start or end with any - // > whitespace character, or start with [. For new entry points, it is recommended to - // > use only letters, numbers, underscores, dots and dashes (regex [\w.-]+). - // All of these rules are already enforced by cargo: - // https://github.com/rust-lang/cargo/blob/58a961314437258065e23cb6316dfc121d96fb71/src/cargo/util/restricted_names.rs#L39-L84 - // i.e. we don't need to do any bin name validation here anymore - - artifacts_and_files.push((artifact, bin_name)) - } - - let metadata24 = if self.target.is_wasi() { - bin_wasi_helper(&artifacts_and_files, self.metadata24.clone())? - } else { - self.metadata24.clone() - }; - + let mut metadata24 = self.metadata24.clone(); let file_options = self .compression .get_file_options() @@ -1142,44 +1093,30 @@ impl BuildContext { let mut writer = WheelWriter::new( &tag, &self.out, - &self.project_layout.project_root, &metadata24, - &tags, self.excludes(Format::Wheel)?, file_options, )?; - if self.project_layout.python_module.is_some() && self.target.is_wasi() { - // TODO: Can we have python code and the wasm launchers coexisting - // without clashes? - bail!("Sorry, adding python code to a wasm binary is currently not supported") - } - if !self.editable { - write_python_part( - &mut writer, - &self.project_layout, - self.pyproject_toml.as_ref(), - ) - .context("Failed to add the python module to the package")?; - } + self.add_external_libs(&mut writer, artifacts, ext_libs)?; - let mut artifacts_ref = Vec::with_capacity(artifacts.len()); - for (artifact, bin_name) in &artifacts_and_files { - artifacts_ref.push(*artifact); - write_bin(&mut writer, &artifact.path, &self.metadata24, bin_name)?; - if self.target.is_wasi() { - write_wasm_launcher(&mut writer, &self.metadata24, bin_name)?; - } - } - self.add_external_libs(&mut writer, &artifacts_ref, ext_libs)?; + let mut generator = BinBindingGenerator::new(&mut metadata24); + generate_binding( + &mut writer, + &mut generator, + self, + self.interpreter.first(), + artifacts, + ) + .context("Failed to add the files to the wheel")?; self.add_pth(&mut writer)?; add_data( &mut writer, - &self.metadata24, + &metadata24, self.project_layout.data.as_deref(), )?; - let wheel_path = writer.finish(&self.metadata24)?; + let wheel_path = writer.finish(&metadata24, &self.project_layout.project_root, &tags)?; Ok((wheel_path, "py3".to_string())) } diff --git a/src/module_writer/wheel_writer.rs b/src/module_writer/wheel_writer.rs index fc92443fc..6ebca0824 100644 --- a/src/module_writer/wheel_writer.rs +++ b/src/module_writer/wheel_writer.rs @@ -89,9 +89,7 @@ impl WheelWriter { pub fn new( tag: &str, wheel_dir: &Path, - pyproject_dir: &Path, metadata24: &Metadata24, - tags: &[String], excludes: Override, file_options: SimpleFileOptions, ) -> Result { @@ -104,7 +102,7 @@ impl WheelWriter { let file = File::create(wheel_path)?; - let mut builder = WheelWriter { + let builder = WheelWriter { zip: ZipWriter::new(file), record: BTreeMap::new(), file_tracker: FileTracker::default(), @@ -113,8 +111,6 @@ impl WheelWriter { target_exclusion_warning_emitted: false, }; - write_dist_info(&mut builder, pyproject_dir, metadata24, tags)?; - Ok(builder) } @@ -155,7 +151,14 @@ impl WheelWriter { } /// Creates the record file and finishes the zip - pub fn finish(mut self, metadata24: &Metadata24) -> Result { + pub fn finish( + mut self, + metadata24: &Metadata24, + pyproject_dir: &Path, + tags: &[String], + ) -> Result { + write_dist_info(&mut self, pyproject_dir, metadata24, tags)?; + let options = self .file_options .unix_permissions(default_permission(false)); @@ -199,14 +202,12 @@ mod tests { let writer = WheelWriter::new( "no compression", tmp_dir.path(), - tmp_dir.path(), &metadata, - &[], Override::empty(), compression_options.get_file_options(), )?; - writer.finish(&metadata)?; + writer.finish(&metadata, tmp_dir.path(), &[])?; tmp_dir.close()?; Ok(())