diff --git a/src/build_options.rs b/src/build_options.rs index fdb1b320d..f19843216 100644 --- a/src/build_options.rs +++ b/src/build_options.rs @@ -153,16 +153,16 @@ mod tests { use super::*; use crate::bridge::{Abi3Version, PyO3, PyO3Crate, find_bridge}; use crate::python_interpreter::InterpreterResolver; + use crate::test_utils::test_crate_path; use crate::{BridgeModel, Target}; use cargo_metadata::MetadataCommand; use insta::assert_snapshot; use pretty_assertions::assert_eq; - use std::path::Path; #[test] fn test_find_bridge_pyo3() { let pyo3_mixed = MetadataCommand::new() - .manifest_path(Path::new("test-crates/pyo3-mixed").join("Cargo.toml")) + .manifest_path(test_crate_path("pyo3-mixed").join("Cargo.toml")) .exec() .unwrap(); @@ -181,7 +181,7 @@ mod tests { use crate::bridge::{PyO3Metadata, PyO3VersionMetadata}; let pyo3_pure = MetadataCommand::new() - .manifest_path(Path::new("test-crates/pyo3-pure").join("Cargo.toml")) + .manifest_path(test_crate_path("pyo3-pure").join("Cargo.toml")) .exec() .unwrap(); @@ -207,14 +207,14 @@ mod tests { #[test] fn test_find_bridge_pyo3_feature() { let pyo3_pure = MetadataCommand::new() - .manifest_path(Path::new("test-crates/pyo3-feature").join("Cargo.toml")) + .manifest_path(test_crate_path("pyo3-feature").join("Cargo.toml")) .exec() .unwrap(); assert!(find_bridge(&pyo3_pure, None, None).is_err()); let pyo3_pure = MetadataCommand::new() - .manifest_path(Path::new("test-crates/pyo3-feature").join("Cargo.toml")) + .manifest_path(test_crate_path("pyo3-feature").join("Cargo.toml")) .other_options(vec!["--features=pyo3".to_string()]) .exec() .unwrap(); @@ -228,7 +228,7 @@ mod tests { #[test] fn test_find_bridge_cffi() { let cffi_pure = MetadataCommand::new() - .manifest_path(Path::new("test-crates/cffi-pure").join("Cargo.toml")) + .manifest_path(test_crate_path("cffi-pure").join("Cargo.toml")) .exec() .unwrap(); @@ -247,7 +247,7 @@ mod tests { #[test] fn test_find_bridge_bin() { let hello_world = MetadataCommand::new() - .manifest_path(Path::new("test-crates/hello-world").join("Cargo.toml")) + .manifest_path(test_crate_path("hello-world").join("Cargo.toml")) .exec() .unwrap(); @@ -263,7 +263,7 @@ mod tests { assert!(find_bridge(&hello_world, Some("pyo3"), None).is_err()); let pyo3_bin = MetadataCommand::new() - .manifest_path(Path::new("test-crates/pyo3-bin").join("Cargo.toml")) + .manifest_path(test_crate_path("pyo3-bin").join("Cargo.toml")) .exec() .unwrap(); assert!(matches!( diff --git a/src/commands/build.rs b/src/commands/build.rs new file mode 100644 index 000000000..ab9f8d363 --- /dev/null +++ b/src/commands/build.rs @@ -0,0 +1,39 @@ +use crate::commands::StripOption; +use crate::commands::utils::unpack_sdist_for_build; +use anyhow::Result; +use maturin::BuildOptions; + +pub fn build( + mut build: BuildOptions, + release: bool, + strip_opt: StripOption, + sdist: bool, + pgo: bool, +) -> Result<()> { + let strip = strip_opt.strip; + // set profile to release if specified; `--release` and `--profile` are mutually exclusive + if release { + build.profile = Some("release".to_string()); + } + // Keep tempdir alive for the duration of the build + let _sdist_tmp; + let sdist_pyproject_path; + if sdist { + let (_, unpacked) = unpack_sdist_for_build(&mut build, strip)?; + sdist_pyproject_path = unpacked.pyproject_toml_path.clone(); + _sdist_tmp = Some(unpacked); + } else { + _sdist_tmp = None; + sdist_pyproject_path = None; + } + let build_context = build + .into_build_context() + .strip(strip) + .editable(false) + .pyproject_toml_path(sdist_pyproject_path) + .pgo(pgo) + .build()?; + let wheels = build_context.build_wheels()?; + assert!(!wheels.is_empty()); + Ok(()) +} diff --git a/src/commands/develop.rs b/src/commands/develop.rs new file mode 100644 index 000000000..cf37ff84f --- /dev/null +++ b/src/commands/develop.rs @@ -0,0 +1,59 @@ +use anyhow::{Context, Result, bail}; +use maturin::{DevelopOptions, Target, develop}; +use std::env; +use std::path::PathBuf; +use tracing::debug; + +pub fn develop_cmd(develop_options: DevelopOptions) -> Result<()> { + let target = Target::from_target_triple(develop_options.cargo_options.target.as_ref())?; + let venv_dir = detect_venv(&target)?; + develop(develop_options, &venv_dir)?; + Ok(()) +} + +fn detect_venv(target: &Target) -> Result { + match (env::var_os("VIRTUAL_ENV"), env::var_os("CONDA_PREFIX")) { + (Some(dir), None) => return Ok(PathBuf::from(dir)), + (None, Some(dir)) => return Ok(PathBuf::from(dir)), + (Some(venv), Some(conda)) if venv == conda => return Ok(PathBuf::from(venv)), + (Some(_), Some(_)) => { + bail!("Both VIRTUAL_ENV and CONDA_PREFIX are set. Please unset one of them") + } + (None, None) => { + // No env var, try finding .venv + } + }; + + let current_dir = env::current_dir().context("Failed to detect current directory ಠ_ಠ")?; + // .venv in the current or any parent directory + for dir in current_dir.ancestors() { + let dot_venv = dir.join(".venv"); + if dot_venv.is_dir() { + if !dot_venv.join("pyvenv.cfg").is_file() { + bail!( + "Expected {} to be a virtual environment, but pyvenv.cfg is missing", + dot_venv.display() + ); + } + let python = target.get_venv_python(&dot_venv); + if !python.is_file() { + bail!( + "Your virtualenv at {} is broken. It contains a pyvenv.cfg but no python at {}", + dot_venv.display(), + python.display() + ); + } + debug!("Found a virtualenv named .venv at {}", dot_venv.display()); + return Ok(dot_venv); + } + } + + bail!( + "Couldn't find a virtualenv or conda environment, but you need one to use this command. \ + For maturin to find your virtualenv you need to either set VIRTUAL_ENV (through activate), \ + set CONDA_PREFIX (through conda activate) or have a virtualenv called .venv in the current \ + or any parent folder. \ + See https://virtualenv.pypa.io/en/latest/index.html on how to use virtualenv or \ + use `maturin build` and `pip install ` instead." + ) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 000000000..607c2db43 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,104 @@ +#[cfg(feature = "zig")] +use anyhow::Context; +use anyhow::Result; +#[cfg(feature = "schemars")] +use maturin::GenerateJsonSchemaOptions; +#[cfg(feature = "upload")] +use maturin::PublishOpt; +use maturin::{BridgeModel, Target, TargetTriple}; +#[cfg(feature = "scaffolding")] +use maturin::{GenerateProjectOptions, ci::GenerateCI}; +#[cfg(feature = "upload")] +use std::path::PathBuf; + +pub mod build; +pub mod develop; +pub mod pep517; +#[cfg(feature = "upload")] +pub mod publish; +pub mod sdist; +pub mod utils; + +/// Shared `--strip` CLI option used by multiple commands. +#[derive(Debug, clap::Args, Clone, Copy)] +pub struct StripOption { + /// Strip the library for minimum file size. + /// Can be set to `true` or `false`, or used as a flag (`--strip` implies `true`). + #[arg( + long, + env = "MATURIN_STRIP", + // `--strip` without a value is treated as `--strip true` + default_missing_value = "true", + num_args = 0..=1, + require_equals = false + )] + pub strip: Option, +} + +/// Generate shell completions +#[cfg(feature = "cli-completion")] +pub fn completions(shell: clap_complete_command::Shell, cmd: &mut clap::Command) { + shell.generate(cmd, &mut std::io::stdout()); +} + +/// Search and list the available python installations +pub fn list_python(target: Option) -> Result<()> { + let found = if target.is_some() { + let target = Target::from_target_triple(target.as_ref())?; + maturin::PythonInterpreter::lookup_target(&target, None, None) + } else { + let target = Target::from_target_triple(None)?; + // We don't know the targeted bindings yet, so we use the most lenient + maturin::PythonInterpreter::find_all(&target, &BridgeModel::Cffi, None)? + }; + eprintln!("🐍 {} python interpreter found:", found.len()); + for interpreter in found { + eprintln!(" - {interpreter}"); + } + Ok(()) +} + +/// Create a new cargo project in an existing directory +#[cfg(feature = "scaffolding")] +pub fn init_project(path: Option, options: GenerateProjectOptions) -> Result<()> { + maturin::init_project(path, options) +} + +/// Create a new cargo project +#[cfg(feature = "scaffolding")] +pub fn new_project(path: String, options: GenerateProjectOptions) -> Result<()> { + maturin::new_project(path, options) +} + +/// Generate CI configuration +#[cfg(feature = "scaffolding")] +pub fn generate_ci(generate_ci: GenerateCI) -> Result<()> { + generate_ci.execute() +} + +/// Generate the JSON schema for the `pyproject.toml` file. +#[cfg(feature = "schemars")] +pub fn generate_json_schema(args: GenerateJsonSchemaOptions) -> Result<()> { + maturin::generate_json_schema(args) +} + +/// Upload python packages to pypi +#[cfg(feature = "upload")] +pub fn upload(mut publish: PublishOpt, files: Vec) -> Result<()> { + if files.is_empty() { + eprintln!("⚠️ Warning: No files given, exiting."); + return Ok(()); + } + publish.non_interactive_on_ci(); + + maturin::upload_ui(&files, &publish)?; + Ok(()) +} + +/// Zig linker wrapper +#[cfg(feature = "zig")] +pub fn zig(subcommand: cargo_zigbuild::Zig) -> Result<()> { + subcommand + .execute() + .context("Failed to run zig linker wrapper") +} diff --git a/src/commands/pep517.rs b/src/commands/pep517.rs new file mode 100644 index 000000000..412441107 --- /dev/null +++ b/src/commands/pep517.rs @@ -0,0 +1,135 @@ +use anyhow::{Context, Result}; +use clap::Subcommand; +use ignore::overrides::Override; +use maturin::{ + BuildContext, BuildOptions, CargoOptions, OutputOptions, PathWriter, VirtualWriter, + write_dist_info, +}; +use std::path::PathBuf; + +use crate::commands::StripOption; + +/// Backend for the PEP 517 integration. Not for human consumption +/// +/// The commands are meant to be called from the python PEP 517 +#[derive(Debug, Subcommand)] +#[command(name = "pep517", hide = true)] +pub enum Pep517Command { + /// The implementation of prepare_metadata_for_build_wheel + #[command(name = "write-dist-info")] + WriteDistInfo { + #[command(flatten)] + build_options: BuildOptions, + /// The metadata_directory argument to prepare_metadata_for_build_wheel + #[arg(long = "metadata-directory")] + metadata_directory: PathBuf, + #[command(flatten)] + strip_opt: StripOption, + }, + #[command(name = "build-wheel")] + /// Implementation of build_wheel + BuildWheel { + #[command(flatten)] + build_options: BuildOptions, + #[command(flatten)] + strip_opt: StripOption, + /// Build editable wheels + #[arg(long)] + editable: bool, + }, + /// The implementation of build_sdist + #[command(name = "write-sdist")] + WriteSDist { + /// The sdist_directory argument to build_sdist + #[arg(long = "sdist-directory")] + sdist_directory: PathBuf, + #[arg(short = 'm', long = "manifest-path", value_name = "PATH")] + /// The path to the Cargo.toml + manifest_path: Option, + }, +} + +/// Dispatches into the native implementations of the PEP 517 functions +/// +/// The last line of stdout is used as return value from the python part of the implementation +pub fn pep517(subcommand: Pep517Command) -> Result<()> { + // PEP 517 builds default to release profile. + fn ensure_release_profile(context: &mut BuildContext) { + if context.project.cargo_options.profile.is_none() { + context.project.cargo_options.profile = Some("release".to_string()); + } + } + + match subcommand { + Pep517Command::WriteDistInfo { + build_options, + metadata_directory, + strip_opt, + } => { + assert_eq!(build_options.python.interpreter.len(), 1); + let mut context = build_options + .into_build_context() + .strip(strip_opt.strip) + .editable(false) + .build()?; + ensure_release_profile(&mut context); + + let mut writer = + VirtualWriter::new(PathWriter::from_path(metadata_directory), Override::empty()); + let dist_info_dir = write_dist_info( + &mut writer, + &context.project.project_layout.project_root, + &context.project.metadata24, + &context.tags_from_bridge()?, + )?; + writer.finish()?; + println!("{}", dist_info_dir.display()); + } + Pep517Command::BuildWheel { + build_options, + strip_opt, + editable, + } => { + let mut build_context = build_options + .into_build_context() + .strip(strip_opt.strip) + .editable(editable) + .build()?; + ensure_release_profile(&mut build_context); + let wheels = build_context.build_wheels()?; + assert_eq!(wheels.len(), 1); + println!("{}", wheels[0].0.to_str().unwrap()); + } + Pep517Command::WriteSDist { + sdist_directory, + manifest_path, + } => { + let build_options = BuildOptions { + output: OutputOptions { + out: Some(sdist_directory), + ..Default::default() + }, + cargo: CargoOptions { + manifest_path, + // Enable all features to ensure all optional path dependencies are packaged + // into source distribution + all_features: true, + ..Default::default() + }, + ..Default::default() + }; + let build_context = build_options + .into_build_context() + .strip(Some(false)) + .editable(false) + .sdist_only(true) + .build()?; + let (path, _) = build_context + .build_source_distribution()? + .context("Failed to build source distribution, pyproject.toml not found")?; + println!("{}", path.file_name().unwrap().to_str().unwrap()); + } + }; + + Ok(()) +} diff --git a/src/commands/publish.rs b/src/commands/publish.rs new file mode 100644 index 000000000..6d8c75164 --- /dev/null +++ b/src/commands/publish.rs @@ -0,0 +1,63 @@ +use crate::commands::utils::unpack_sdist_for_build; +use anyhow::Result; +use maturin::{BuildOptions, PublishOpt, upload_ui}; + +pub fn publish( + mut build: BuildOptions, + mut publish: PublishOpt, + debug: bool, + no_strip: bool, + no_sdist: bool, + pgo: bool, +) -> Result<()> { + // set profile to dev if specified; `--debug` and `--profile` are mutually exclusive + // + // do it here to take precedence over pyproject.toml profile setting + if debug { + build.profile = Some("dev".to_string()); + } + + // Keep tempdir alive for the duration of the build + let _sdist_tmp; + let mut sdist_path = None; + let sdist_pyproject_path; + if !no_sdist { + let (path, unpacked) = unpack_sdist_for_build(&mut build, Some(!no_strip))?; + sdist_pyproject_path = unpacked.pyproject_toml_path.clone(); + _sdist_tmp = Some(unpacked); + sdist_path = Some(path); + } else { + _sdist_tmp = None; + sdist_pyproject_path = None; + } + + let mut build_context = build + .into_build_context() + .strip(Some(!no_strip)) + .editable(false) + .pyproject_toml_path(sdist_pyproject_path) + .pgo(pgo) + .build()?; + + // ensure profile always set when publishing + // (respect pyproject.toml if set) + // don't need to check `debug` here, set above to take precedence if set + let profile = build_context + .project + .cargo_options + .profile + .get_or_insert_with(|| "release".to_string()); + if profile == "dev" { + eprintln!("⚠️ Warning: You're publishing debug wheels"); + } + + let mut wheels = build_context.build_wheels()?; + if let Some(sdist_path) = sdist_path { + wheels.push((sdist_path, "source".to_string())); + } + + let items = wheels.into_iter().map(|wheel| wheel.0).collect::>(); + publish.non_interactive_on_ci(); + upload_ui(&items, &publish)?; + Ok(()) +} diff --git a/src/commands/sdist.rs b/src/commands/sdist.rs new file mode 100644 index 000000000..2e20c6b84 --- /dev/null +++ b/src/commands/sdist.rs @@ -0,0 +1,46 @@ +use anyhow::{Context, Result}; +use maturin::{BuildOptions, CargoOptions, OutputOptions, find_path_deps}; +use std::path::PathBuf; + +pub fn sdist(manifest_path: Option, out: Option) -> Result<()> { + // Get cargo metadata to check for path dependencies + let cargo_metadata_result = cargo_metadata::MetadataCommand::new() + .cargo_path("cargo") + .manifest_path( + manifest_path + .as_deref() + .unwrap_or_else(|| std::path::Path::new("Cargo.toml")), + ) + .verbose(true) + .exec(); + + let has_path_deps = cargo_metadata_result + .ok() + .and_then(|metadata| find_path_deps(&metadata).ok()) + .map(|path_deps| !path_deps.is_empty()) + .unwrap_or(false); // If we can't get metadata, don't force all features + let build_options = BuildOptions { + output: OutputOptions { + out, + ..Default::default() + }, + cargo: CargoOptions { + manifest_path, + // Only enable all features when we have local path dependencies + // to ensure they are packaged into source distribution + all_features: has_path_deps, + ..Default::default() + }, + ..Default::default() + }; + let build_context = build_options + .into_build_context() + .strip(Some(false)) + .editable(false) + .sdist_only(true) + .build()?; + build_context + .build_source_distribution()? + .context("Failed to build source distribution, pyproject.toml not found")?; + Ok(()) +} diff --git a/src/commands/utils.rs b/src/commands/utils.rs new file mode 100644 index 000000000..a002a60bd --- /dev/null +++ b/src/commands/utils.rs @@ -0,0 +1,59 @@ +use anyhow::{Context, Result}; +use maturin::{BuildOptions, UnpackedSdist, unpack_sdist}; +use std::path::PathBuf; + +/// Result of unpacking an sdist for wheel building +pub struct UnpackedBuild { + /// Must be kept alive for the duration of the build + _tmpdir: tempfile::TempDir, + pub pyproject_toml_path: Option, +} + +/// Build a source distribution from the given build options, returning the sdist path. +pub fn build_sdist(build: &BuildOptions, strip: Option) -> Result { + let sdist_context = build + .clone() + .into_build_context() + .strip(strip) + .editable(false) + .sdist_only(true) + .build()?; + let (sdist_path, _) = sdist_context + .build_source_distribution()? + .context("Failed to build source distribution, pyproject.toml not found")?; + Ok(sdist_path) +} + +/// Build an sdist, unpack it, and point build options at the unpacked source. +/// +/// Returns the sdist path and the temporary directory holding the unpacked tree. +pub fn unpack_sdist_for_build( + build: &mut BuildOptions, + strip: Option, +) -> Result<(PathBuf, UnpackedBuild)> { + let sdist_path = build_sdist(build, strip)?; + // Preserve the original output directory so that wheels built + // from the unpacked sdist still land in the user-visible + // `target/wheels` (or the explicit `--out` directory) instead + // of the temporary directory's target. + if build.output.out.is_none() { + build.output.out = sdist_path.parent().map(PathBuf::from); + } + let UnpackedSdist { + tmpdir, + cargo_toml, + pyproject_toml, + } = unpack_sdist(&sdist_path)?; + eprintln!( + "📦 Building wheels from source distribution at {}", + cargo_toml.parent().unwrap().display() + ); + build.cargo.manifest_path = Some(cargo_toml); + Ok(( + sdist_path, + UnpackedBuild { + _tmpdir: tmpdir, + pyproject_toml_path: Some(pyproject_toml), + }, + )) +} diff --git a/src/lib.rs b/src/lib.rs index 55d960360..e3662dbe0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,8 @@ mod python_interpreter; mod sbom; mod source_distribution; mod target; +#[cfg(test)] +mod test_utils; #[cfg(feature = "upload")] mod upload; pub(crate) mod util; diff --git a/src/main.rs b/src/main.rs index 74fc0ed85..6d4ad265a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,47 +3,30 @@ //! //! Run with --help for usage information -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result}; use cargo_options::heading; #[cfg(feature = "zig")] use cargo_zigbuild::Zig; #[cfg(feature = "cli-completion")] use clap::CommandFactory; -use clap::{Parser, Subcommand}; -use ignore::overrides::Override; -use maturin::{ - BridgeModel, BuildContext, BuildOptions, CargoOptions, DevelopOptions, OutputOptions, - PathWriter, Target, TargetTriple, UnpackedSdist, VirtualWriter, develop, find_path_deps, - unpack_sdist, write_dist_info, -}; +use clap::Parser; #[cfg(feature = "schemars")] -use maturin::{GenerateJsonSchemaOptions, generate_json_schema}; -#[cfg(feature = "scaffolding")] -use maturin::{GenerateProjectOptions, ci::GenerateCI, init_project, new_project}; +use maturin::GenerateJsonSchemaOptions; #[cfg(feature = "upload")] -use maturin::{PublishOpt, upload_ui}; +use maturin::PublishOpt; +use maturin::{BuildOptions, DevelopOptions, TargetTriple}; +#[cfg(feature = "scaffolding")] +use maturin::{GenerateProjectOptions, ci::GenerateCI}; use std::env; use std::path::PathBuf; use std::str::FromStr; -use tracing::{debug, instrument}; +use tracing::instrument; use tracing_subscriber::filter::Directive; use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt}; -/// Shared `--strip` CLI option used by multiple commands. -#[derive(Debug, clap::Args)] -struct StripOption { - /// Strip the library for minimum file size. - /// Can be set to `true` or `false`, or used as a flag (`--strip` implies `true`). - #[arg( - long, - env = "MATURIN_STRIP", - // `--strip` without a value is treated as `--strip true` - default_missing_value = "true", - num_args = 0..=1, - require_equals = false - )] - strip: Option, -} +mod commands; +use crate::commands::StripOption; +use crate::commands::pep517::Pep517Command; #[derive(Debug, Parser)] #[command( @@ -202,178 +185,6 @@ enum Command { GenerateJsonSchema(GenerateJsonSchemaOptions), } -/// Backend for the PEP 517 integration. Not for human consumption -/// -/// The commands are meant to be called from the python PEP 517 -#[derive(Debug, Subcommand)] -#[command(name = "pep517", hide = true)] -enum Pep517Command { - /// The implementation of prepare_metadata_for_build_wheel - #[command(name = "write-dist-info")] - WriteDistInfo { - #[command(flatten)] - build_options: BuildOptions, - /// The metadata_directory argument to prepare_metadata_for_build_wheel - #[arg(long = "metadata-directory")] - metadata_directory: PathBuf, - #[command(flatten)] - strip_opt: StripOption, - }, - #[command(name = "build-wheel")] - /// Implementation of build_wheel - BuildWheel { - #[command(flatten)] - build_options: BuildOptions, - #[command(flatten)] - strip_opt: StripOption, - /// Build editable wheels - #[arg(long)] - editable: bool, - }, - /// The implementation of build_sdist - #[command(name = "write-sdist")] - WriteSDist { - /// The sdist_directory argument to build_sdist - #[arg(long = "sdist-directory")] - sdist_directory: PathBuf, - #[arg(short = 'm', long = "manifest-path", value_name = "PATH")] - /// The path to the Cargo.toml - manifest_path: Option, - }, -} - -fn detect_venv(target: &Target) -> Result { - match (env::var_os("VIRTUAL_ENV"), env::var_os("CONDA_PREFIX")) { - (Some(dir), None) => return Ok(PathBuf::from(dir)), - (None, Some(dir)) => return Ok(PathBuf::from(dir)), - (Some(venv), Some(conda)) if venv == conda => return Ok(PathBuf::from(venv)), - (Some(_), Some(_)) => { - bail!("Both VIRTUAL_ENV and CONDA_PREFIX are set. Please unset one of them") - } - (None, None) => { - // No env var, try finding .venv - } - }; - - let current_dir = env::current_dir().context("Failed to detect current directory ಠ_ಠ")?; - // .venv in the current or any parent directory - for dir in current_dir.ancestors() { - let dot_venv = dir.join(".venv"); - if dot_venv.is_dir() { - if !dot_venv.join("pyvenv.cfg").is_file() { - bail!( - "Expected {} to be a virtual environment, but pyvenv.cfg is missing", - dot_venv.display() - ); - } - let python = target.get_venv_python(&dot_venv); - if !python.is_file() { - bail!( - "Your virtualenv at {} is broken. It contains a pyvenv.cfg but no python at {}", - dot_venv.display(), - python.display() - ); - } - debug!("Found a virtualenv named .venv at {}", dot_venv.display()); - return Ok(dot_venv); - } - } - - bail!( - "Couldn't find a virtualenv or conda environment, but you need one to use this command. \ - For maturin to find your virtualenv you need to either set VIRTUAL_ENV (through activate), \ - set CONDA_PREFIX (through conda activate) or have a virtualenv called .venv in the current \ - or any parent folder. \ - See https://virtualenv.pypa.io/en/latest/index.html on how to use virtualenv or \ - use `maturin build` and `pip install ` instead." - ) -} - -/// Dispatches into the native implementations of the PEP 517 functions -/// -/// The last line of stdout is used as return value from the python part of the implementation -fn pep517(subcommand: Pep517Command) -> Result<()> { - // PEP 517 builds default to release profile. - fn ensure_release_profile(context: &mut BuildContext) { - if context.project.cargo_options.profile.is_none() { - context.project.cargo_options.profile = Some("release".to_string()); - } - } - - match subcommand { - Pep517Command::WriteDistInfo { - build_options, - metadata_directory, - strip_opt, - } => { - assert_eq!(build_options.python.interpreter.len(), 1); - let mut context = build_options - .into_build_context() - .strip(strip_opt.strip) - .editable(false) - .build()?; - ensure_release_profile(&mut context); - - let mut writer = - VirtualWriter::new(PathWriter::from_path(metadata_directory), Override::empty()); - let dist_info_dir = write_dist_info( - &mut writer, - &context.project.project_layout.project_root, - &context.project.metadata24, - &context.tags_from_bridge()?, - )?; - writer.finish()?; - println!("{}", dist_info_dir.display()); - } - Pep517Command::BuildWheel { - build_options, - strip_opt, - editable, - } => { - let mut build_context = build_options - .into_build_context() - .strip(strip_opt.strip) - .editable(editable) - .build()?; - ensure_release_profile(&mut build_context); - let wheels = build_context.build_wheels()?; - assert_eq!(wheels.len(), 1); - println!("{}", wheels[0].0.to_str().unwrap()); - } - Pep517Command::WriteSDist { - sdist_directory, - manifest_path, - } => { - let build_options = BuildOptions { - output: OutputOptions { - out: Some(sdist_directory), - ..Default::default() - }, - cargo: CargoOptions { - manifest_path, - // Enable all features to ensure all optional path dependencies are packaged - // into source distribution - all_features: true, - ..Default::default() - }, - ..Default::default() - }; - let build_context = build_options - .into_build_context() - .strip(Some(false)) - .editable(false) - .sdist_only(true) - .build()?; - let (path, _) = build_context - .build_source_distribution()? - .context("Failed to build source distribution, pyproject.toml not found")?; - println!("{}", path.file_name().unwrap().to_str().unwrap()); - } - }; - - Ok(()) -} - #[instrument] fn run() -> Result<()> { #[cfg(feature = "zig")] @@ -416,247 +227,46 @@ fn run() -> Result<()> { match opt.command { Command::Build { - mut build, + build, release, strip_opt, sdist, pgo, - } => { - let strip = strip_opt.strip; - // set profile to release if specified; `--release` and `--profile` are mutually exclusive - if release { - build.profile = Some("release".to_string()); - } - // Keep tempdir alive for the duration of the build - let _sdist_tmp; - let sdist_pyproject_path; - if sdist { - let (_, unpacked) = unpack_sdist_for_build(&mut build, strip)?; - _sdist_tmp = Some(unpacked._tmpdir); - sdist_pyproject_path = unpacked.pyproject_toml_path; - } else { - _sdist_tmp = None; - sdist_pyproject_path = None; - } - let build_context = build - .into_build_context() - .strip(strip) - .editable(false) - .pyproject_toml_path(sdist_pyproject_path) - .pgo(pgo) - .build()?; - let wheels = build_context.build_wheels()?; - assert!(!wheels.is_empty()); - } + } => commands::build::build(build, release, strip_opt, sdist, pgo)?, #[cfg(feature = "upload")] Command::Publish { - mut build, - mut publish, + build, + publish, debug, no_strip, no_sdist, pgo, - } => { - // set profile to dev if specified; `--debug` and `--profile` are mutually exclusive - // - // do it here to take precedence over pyproject.toml profile setting - if debug { - build.profile = Some("dev".to_string()); - } - - // Keep tempdir alive for the duration of the build - let _sdist_tmp; - let mut sdist_path = None; - let sdist_pyproject_path; - if !no_sdist { - let (path, unpacked) = unpack_sdist_for_build(&mut build, Some(!no_strip))?; - _sdist_tmp = Some(unpacked._tmpdir); - sdist_pyproject_path = unpacked.pyproject_toml_path; - sdist_path = Some(path); - } else { - _sdist_tmp = None; - sdist_pyproject_path = None; - } - - let mut build_context = build - .into_build_context() - .strip(Some(!no_strip)) - .editable(false) - .pyproject_toml_path(sdist_pyproject_path) - .pgo(pgo) - .build()?; - - // ensure profile always set when publishing - // (respect pyproject.toml if set) - // don't need to check `debug` here, set above to take precedence if set - let profile = build_context - .project - .cargo_options - .profile - .get_or_insert_with(|| "release".to_string()); - if profile == "dev" { - eprintln!("⚠️ Warning: You're publishing debug wheels"); - } - - let mut wheels = build_context.build_wheels()?; - if let Some(sdist_path) = sdist_path { - wheels.push((sdist_path, "source".to_string())); - } - - let items = wheels.into_iter().map(|wheel| wheel.0).collect::>(); - publish.non_interactive_on_ci(); - upload_ui(&items, &publish)? - } - Command::ListPython { target } => { - let found = if target.is_some() { - let target = Target::from_target_triple(target.as_ref())?; - maturin::PythonInterpreter::lookup_target(&target, None, None) - } else { - let target = Target::from_target_triple(None)?; - // We don't know the targeted bindings yet, so we use the most lenient - maturin::PythonInterpreter::find_all(&target, &BridgeModel::Cffi, None)? - }; - eprintln!("🐍 {} python interpreter found:", found.len()); - for interpreter in found { - eprintln!(" - {interpreter}"); - } - } - Command::Develop(develop_options) => { - let target = Target::from_target_triple(develop_options.cargo_options.target.as_ref())?; - let venv_dir = detect_venv(&target)?; - develop(develop_options, &venv_dir)?; - } - Command::SDist { manifest_path, out } => { - // Get cargo metadata to check for path dependencies - let cargo_metadata_result = cargo_metadata::MetadataCommand::new() - .cargo_path("cargo") - .manifest_path( - manifest_path - .as_deref() - .unwrap_or_else(|| std::path::Path::new("Cargo.toml")), - ) - .verbose(true) - .exec(); - - let has_path_deps = cargo_metadata_result - .ok() - .and_then(|metadata| find_path_deps(&metadata).ok()) - .map(|path_deps| !path_deps.is_empty()) - .unwrap_or(false); // If we can't get metadata, don't force all features - let build_options = BuildOptions { - output: OutputOptions { - out, - ..Default::default() - }, - cargo: CargoOptions { - manifest_path, - // Only enable all features when we have local path dependencies - // to ensure they are packaged into source distribution - all_features: has_path_deps, - ..Default::default() - }, - ..Default::default() - }; - let build_context = build_options - .into_build_context() - .strip(Some(false)) - .editable(false) - .sdist_only(true) - .build()?; - build_context - .build_source_distribution()? - .context("Failed to build source distribution, pyproject.toml not found")?; - } - Command::Pep517(subcommand) => pep517(subcommand)?, + } => commands::publish::publish(build, publish, debug, no_strip, no_sdist, pgo)?, + Command::ListPython { target } => commands::list_python(target)?, + Command::Develop(develop_options) => commands::develop::develop_cmd(develop_options)?, + Command::SDist { manifest_path, out } => commands::sdist::sdist(manifest_path, out)?, + Command::Pep517(subcommand) => commands::pep517::pep517(subcommand)?, #[cfg(feature = "scaffolding")] - Command::InitProject { path, options } => init_project(path, options)?, + Command::InitProject { path, options } => commands::init_project(path, options)?, #[cfg(feature = "scaffolding")] - Command::NewProject { path, options } => new_project(path, options)?, + Command::NewProject { path, options } => commands::new_project(path, options)?, #[cfg(feature = "scaffolding")] - Command::GenerateCI(generate_ci) => generate_ci.execute()?, + Command::GenerateCI(generate_ci) => commands::generate_ci(generate_ci)?, #[cfg(feature = "upload")] - Command::Upload { mut publish, files } => { - if files.is_empty() { - eprintln!("⚠️ Warning: No files given, exiting."); - return Ok(()); - } - publish.non_interactive_on_ci(); - - upload_ui(&files, &publish)? - } + Command::Upload { publish, files } => commands::upload(publish, files)?, #[cfg(feature = "cli-completion")] Command::Completions { shell } => { - shell.generate(&mut Opt::command(), &mut std::io::stdout()); + commands::completions(shell, &mut Opt::command()); } #[cfg(feature = "zig")] - Command::Zig(subcommand) => { - subcommand - .execute() - .context("Failed to run zig linker wrapper")?; - } + Command::Zig(subcommand) => commands::zig(subcommand)?, #[cfg(feature = "schemars")] - Command::GenerateJsonSchema(args) => generate_json_schema(args)?, + Command::GenerateJsonSchema(args) => commands::generate_json_schema(args)?, } Ok(()) } -/// Build a source distribution from the given build options, returning the sdist path. -fn build_sdist(build: &BuildOptions, strip: Option) -> Result { - let sdist_context = build - .clone() - .into_build_context() - .strip(strip) - .editable(false) - .sdist_only(true) - .build()?; - let (sdist_path, _) = sdist_context - .build_source_distribution()? - .context("Failed to build source distribution, pyproject.toml not found")?; - Ok(sdist_path) -} - -/// Result of unpacking an sdist for wheel building -struct UnpackedBuild { - /// Must be kept alive for the duration of the build - _tmpdir: tempfile::TempDir, - pyproject_toml_path: Option, -} - -/// Build an sdist, unpack it, and point build options at the unpacked source. -/// -/// Returns the sdist path and the temporary directory holding the unpacked tree. -fn unpack_sdist_for_build( - build: &mut BuildOptions, - strip: Option, -) -> Result<(PathBuf, UnpackedBuild)> { - let sdist_path = build_sdist(build, strip)?; - // Preserve the original output directory so that wheels built - // from the unpacked sdist still land in the user-visible - // `target/wheels` (or the explicit `--out` directory) instead - // of the temporary directory's target. - if build.output.out.is_none() { - build.output.out = sdist_path.parent().map(PathBuf::from); - } - let UnpackedSdist { - tmpdir, - cargo_toml, - pyproject_toml, - } = unpack_sdist(&sdist_path)?; - eprintln!( - "📦 Building wheels from source distribution at {}", - cargo_toml.parent().unwrap().display() - ); - build.cargo.manifest_path = Some(cargo_toml); - Ok(( - sdist_path, - UnpackedBuild { - _tmpdir: tmpdir, - pyproject_toml_path: Some(pyproject_toml), - }, - )) -} - #[cfg(not(debug_assertions))] fn setup_panic_hook() { let default_hook = std::panic::take_hook(); diff --git a/src/metadata.rs b/src/metadata.rs index b09ee6acc..68f1ab3d5 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -842,6 +842,7 @@ fn fold_header(text: &str) -> String { #[cfg(test)] mod tests { use super::*; + use crate::test_utils::test_crate_path; use cargo_metadata::MetadataCommand; use expect_test::{Expect, expect}; use indoc::indoc; @@ -966,7 +967,7 @@ A test project #[test] fn test_merge_metadata_from_pyproject_toml() { - let manifest_dir = PathBuf::from("test-crates").join("pyo3-pure"); + let manifest_dir = test_crate_path("pyo3-pure"); let cargo_metadata = MetadataCommand::new() .manifest_path(manifest_dir.join("Cargo.toml")) .exec() @@ -982,7 +983,7 @@ A test project ); assert_eq!( metadata.description, - Some(fs_err::read_to_string("test-crates/pyo3-pure/README.md").unwrap()) + Some(fs_err::read_to_string(manifest_dir.join("README.md")).unwrap()) ); assert_eq!(metadata.classifiers, &["Programming Language :: Rust"]); assert_eq!( @@ -1015,7 +1016,7 @@ A test project #[test] fn test_merge_metadata_from_pyproject_toml_with_customized_python_source_dir() { - let manifest_dir = PathBuf::from("test-crates").join("pyo3-mixed-py-subdir"); + let manifest_dir = test_crate_path("pyo3-mixed-py-subdir"); let cargo_metadata = MetadataCommand::new() .manifest_path(manifest_dir.join("Cargo.toml")) .exec() @@ -1036,7 +1037,7 @@ A test project #[test] fn test_implicit_readme() { - let manifest_dir = PathBuf::from("test-crates").join("pyo3-mixed"); + let manifest_dir = test_crate_path("pyo3-mixed"); let cargo_metadata = MetadataCommand::new() .manifest_path(manifest_dir.join("Cargo.toml")) .exec() @@ -1051,7 +1052,7 @@ A test project #[test] fn test_pep639() { - let manifest_dir = PathBuf::from("test-crates").join("pyo3-mixed"); + let manifest_dir = test_crate_path("pyo3-mixed"); let cargo_metadata = MetadataCommand::new() .manifest_path(manifest_dir.join("Cargo.toml")) .exec() @@ -1068,7 +1069,7 @@ A test project #[test] fn test_merge_metadata_from_pyproject_dynamic_license_test() { - let manifest_dir = PathBuf::from("test-crates").join("license-test"); + let manifest_dir = test_crate_path("license-test"); let cargo_metadata = MetadataCommand::new() .manifest_path(manifest_dir.join("Cargo.toml")) .exec() diff --git a/src/pyproject_toml.rs b/src/pyproject_toml.rs index cc152c261..d6a2b6237 100644 --- a/src/pyproject_toml.rs +++ b/src/pyproject_toml.rs @@ -773,6 +773,7 @@ impl PyProjectToml { #[cfg(test)] mod tests { + use crate::test_utils::test_crate_path; use crate::{ PyProjectToml, pyproject_toml::{ @@ -852,7 +853,8 @@ mod tests { #[test] fn test_warn_missing_maturin_version() { - let with_constraint = PyProjectToml::new("test-crates/pyo3-pure/pyproject.toml").unwrap(); + let with_constraint = + PyProjectToml::new(test_crate_path("pyo3-pure").join("pyproject.toml")).unwrap(); assert!(with_constraint.warn_bad_maturin_version()); let without_constraint_dir = TempDir::new().unwrap(); let pyproject_file = without_constraint_dir.path().join("pyproject.toml"); diff --git a/src/sbom.rs b/src/sbom.rs index 6f0f3f8a4..4535d1a3b 100644 --- a/src/sbom.rs +++ b/src/sbom.rs @@ -75,7 +75,8 @@ pub fn generate_sbom_data( #[cfg(not(feature = "sbom"))] { - let _ = rust_sbom_enabled; + _ = project; + _ = rust_sbom_enabled; Ok(None) } } diff --git a/src/test_utils.rs b/src/test_utils.rs new file mode 100644 index 000000000..5d5ebf212 --- /dev/null +++ b/src/test_utils.rs @@ -0,0 +1,8 @@ +use std::path::{Path, PathBuf}; + +/// Resolve the path to a test crate. +pub fn test_crate_path(name: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("test-crates") + .join(name) +}