use anyhow::{Context, Result, bail};
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, BuildOptions, CargoOptions, DevelopOptions, PathWriter, PythonInterpreter, Target,
TargetTriple, VirtualWriter, develop, find_path_deps, unpack_sdist, write_dist_info,
};
#[cfg(feature = "schemars")]
use maturin::{GenerateJsonSchemaOptions, generate_json_schema};
#[cfg(feature = "scaffolding")]
use maturin::{GenerateProjectOptions, ci::GenerateCI, init_project, new_project};
#[cfg(feature = "upload")]
use maturin::{PublishOpt, upload_ui};
use std::env;
use std::path::PathBuf;
use std::str::FromStr;
use tracing::{debug, instrument};
use tracing_subscriber::filter::Directive;
use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Debug, Parser)]
#[command(
version,
name = env!("CARGO_PKG_NAME"),
display_order = 1,
after_help = "Visit https://maturin.rs to learn more about maturin.",
styles = cargo_options::styles(),
)]
struct Opt {
#[arg(global = true, action = clap::ArgAction::Count, long, short)]
verbose: u8,
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Parser)]
enum Command {
#[command(name = "build", alias = "b")]
Build {
#[arg(short = 'r', long, help_heading = heading::COMPILATION_OPTIONS, conflicts_with = "profile")]
release: bool,
#[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<bool>,
#[arg(long)]
sdist: bool,
#[command(flatten)]
build: BuildOptions,
},
#[cfg(feature = "upload")]
#[command(name = "publish")]
Publish {
#[arg(long, conflicts_with = "profile")]
debug: bool,
#[arg(long = "no-strip")]
no_strip: bool,
#[arg(long = "no-sdist")]
no_sdist: bool,
#[command(flatten)]
publish: PublishOpt,
#[command(flatten)]
build: BuildOptions,
},
#[command(name = "list-python")]
ListPython {
#[arg(long)]
target: Option<TargetTriple>,
},
#[command(name = "develop", alias = "dev")]
Develop(DevelopOptions),
#[command(name = "sdist")]
SDist {
#[arg(short = 'm', long = "manifest-path")]
manifest_path: Option<PathBuf>,
#[arg(short, long)]
out: Option<PathBuf>,
},
#[cfg(feature = "scaffolding")]
#[command(name = "init")]
InitProject {
path: Option<String>,
#[command(flatten)]
options: GenerateProjectOptions,
},
#[cfg(feature = "scaffolding")]
#[command(name = "new")]
NewProject {
path: String,
#[command(flatten)]
options: GenerateProjectOptions,
},
#[cfg(feature = "scaffolding")]
#[command(name = "generate-ci")]
GenerateCI(GenerateCI),
#[cfg(feature = "upload")]
#[command(name = "upload")]
Upload {
#[command(flatten)]
publish: PublishOpt,
#[arg(value_name = "FILE")]
files: Vec<PathBuf>,
},
#[command(subcommand)]
Pep517(Pep517Command),
#[cfg(feature = "cli-completion")]
#[command(name = "completions", hide = true)]
Completions {
#[arg(value_name = "SHELL")]
shell: clap_complete_command::Shell,
},
#[cfg(feature = "zig")]
#[command(subcommand, hide = true)]
Zig(Zig),
#[cfg(feature = "schemars")]
#[command(name = "generate-json-schema", hide = true)]
GenerateJsonSchema(GenerateJsonSchemaOptions),
}
#[derive(Debug, Subcommand)]
#[command(name = "pep517", hide = true)]
enum Pep517Command {
#[command(name = "write-dist-info")]
WriteDistInfo {
#[command(flatten)]
build_options: BuildOptions,
#[arg(long = "metadata-directory")]
metadata_directory: PathBuf,
#[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<bool>,
},
#[command(name = "build-wheel")]
BuildWheel {
#[command(flatten)]
build_options: BuildOptions,
#[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<bool>,
#[arg(long)]
editable: bool,
},
#[command(name = "write-sdist")]
WriteSDist {
#[arg(long = "sdist-directory")]
sdist_directory: PathBuf,
#[arg(short = 'm', long = "manifest-path", value_name = "PATH")]
manifest_path: Option<PathBuf>,
},
}
fn detect_venv(target: &Target) -> Result<PathBuf> {
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) => {
}
};
let current_dir = env::current_dir().context("Failed to detect current 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 <path/to/wheel>` instead."
)
}
fn pep517(subcommand: Pep517Command) -> Result<()> {
match subcommand {
Pep517Command::WriteDistInfo {
build_options,
metadata_directory,
strip,
} => {
assert_eq!(build_options.interpreter.len(), 1);
let mut context = build_options
.into_build_context()
.strip(strip)
.editable(false)
.build()?;
if context.cargo_options.profile.is_none() {
context.cargo_options.profile = Some("release".to_string());
}
let mut writer =
VirtualWriter::new(PathWriter::from_path(metadata_directory), Override::empty());
let dist_info_dir = write_dist_info(
&mut writer,
&context.project_layout.project_root,
&context.metadata24,
&context.tags_from_bridge()?,
)?;
writer.finish()?;
println!("{}", dist_info_dir.display());
}
Pep517Command::BuildWheel {
build_options,
strip,
editable,
} => {
let mut build_context = build_options
.into_build_context()
.strip(strip)
.editable(editable)
.build()?;
if build_context.cargo_options.profile.is_none() {
build_context.cargo_options.profile = Some("release".to_string());
}
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 {
out: Some(sdist_directory),
cargo: CargoOptions {
manifest_path,
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")]
{
let mut args = env::args();
let program_path = PathBuf::from(args.next().expect("no program path"));
let program_name = program_path.file_stem().expect("no program name");
if program_name.eq_ignore_ascii_case("ar") {
let zig = Zig::Ar {
args: args.collect(),
};
zig.execute()?;
return Ok(());
} else if program_name.eq_ignore_ascii_case("lib") {
let zig = Zig::Lib {
args: args.collect(),
};
zig.execute()?;
return Ok(());
} else if program_name.to_string_lossy().ends_with("dlltool") {
let zig = Zig::Dlltool {
args: args.collect(),
};
zig.execute()?;
return Ok(());
} else if program_name.eq_ignore_ascii_case("install_name_tool") {
cargo_zigbuild::macos::install_name_tool::execute(args)?;
return Ok(());
}
}
#[cfg(not(feature = "wild"))]
let opt = Opt::parse();
#[cfg(feature = "wild")]
let opt = Opt::parse_from(wild::args_os());
setup_logging(opt.verbose)?;
match opt.command {
Command::Build {
mut build,
release,
strip,
sdist,
} => {
if release {
build.profile = Some("release".to_string());
}
let _sdist_tmp;
if sdist {
let sdist_path = build_sdist(&build, strip)?;
let (tmp, cargo_toml) = unpack_sdist(&sdist_path)?;
_sdist_tmp = Some(tmp);
eprintln!(
"📦 Building wheels from source distribution at {}",
cargo_toml.parent().unwrap().display()
);
build.cargo.manifest_path = Some(cargo_toml);
} else {
_sdist_tmp = None;
}
let build_context = build
.into_build_context()
.strip(strip)
.editable(false)
.build()?;
let wheels = build_context.build_wheels()?;
assert!(!wheels.is_empty());
}
#[cfg(feature = "upload")]
Command::Publish {
mut build,
mut publish,
debug,
no_strip,
no_sdist,
} => {
if debug {
build.profile = Some("dev".to_string());
}
let _sdist_tmp;
let mut sdist_path = None;
if !no_sdist {
let path = build_sdist(&build, Some(!no_strip))?;
let (tmp, cargo_toml) = unpack_sdist(&path)?;
_sdist_tmp = Some(tmp);
eprintln!(
"📦 Building wheels from source distribution at {}",
cargo_toml.parent().unwrap().display()
);
build.cargo.manifest_path = Some(cargo_toml);
sdist_path = Some(path);
} else {
_sdist_tmp = None;
}
let mut build_context = build
.into_build_context()
.strip(Some(!no_strip))
.editable(false)
.build()?;
let profile = build_context
.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::<Vec<_>>();
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())?;
PythonInterpreter::find_by_target(&target, None, None)
} else {
let target = Target::from_target_triple(None)?;
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 } => {
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); let build_options = BuildOptions {
out,
cargo: CargoOptions {
manifest_path,
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)?,
#[cfg(feature = "scaffolding")]
Command::InitProject { path, options } => init_project(path, options)?,
#[cfg(feature = "scaffolding")]
Command::NewProject { path, options } => new_project(path, options)?,
#[cfg(feature = "scaffolding")]
Command::GenerateCI(generate_ci) => generate_ci.execute()?,
#[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)?
}
#[cfg(feature = "cli-completion")]
Command::Completions { shell } => {
shell.generate(&mut Opt::command(), &mut std::io::stdout());
}
#[cfg(feature = "zig")]
Command::Zig(subcommand) => {
subcommand
.execute()
.context("Failed to run zig linker wrapper")?;
}
#[cfg(feature = "schemars")]
Command::GenerateJsonSchema(args) => generate_json_schema(args)?,
}
Ok(())
}
fn build_sdist(build: &BuildOptions, strip: Option<bool>) -> Result<PathBuf> {
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)
}
#[cfg(not(debug_assertions))]
fn setup_panic_hook() {
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
eprintln!("\n===================================================================");
eprintln!("maturin has panicked. This is a bug in maturin. Please report this");
eprintln!("at https://github.com/PyO3/maturin/issues/new/choose.");
eprintln!("If you can reliably reproduce this panic, include the");
eprintln!("reproduction steps and re-run with the RUST_BACKTRACE=1 environment");
eprintln!("variable set and include the backtrace in your report.");
eprintln!();
eprintln!("Platform: {} {}", env::consts::OS, env::consts::ARCH);
eprintln!("Version: {}", env!("CARGO_PKG_VERSION"));
eprintln!("Args: {}", env::args().collect::<Vec<_>>().join(" "));
eprintln!();
default_hook(panic_info);
std::process::exit(101);
}));
}
fn setup_logging(verbose: u8) -> Result<()> {
let default_directive = match verbose {
0..=1 => tracing::level_filters::LevelFilter::OFF.into(),
2 => Directive::from_str("debug").unwrap(),
3.. => Directive::from_str("trace").unwrap(),
};
let filter = EnvFilter::builder()
.with_default_directive(default_directive)
.from_env()
.context("Invalid RUST_LOG directives")?;
let logger = tracing_subscriber::fmt::layer()
.compact()
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE);
tracing_subscriber::registry()
.with(logger.with_filter(filter))
.init();
Ok(())
}
fn main() {
#[cfg(not(debug_assertions))]
setup_panic_hook();
if let Err(e) = run() {
eprintln!("💥 maturin failed");
for cause in e.chain() {
eprintln!(" Caused by: {cause}");
}
std::process::exit(1);
}
}