pub(crate) mod install_backend;
use crate::PlatformTag;
use crate::PythonInterpreter;
use crate::Target;
use crate::auditwheel::AuditWheelMode;
use crate::build_options::CargoOptions;
use crate::compression::CompressionOptions;
use crate::target::detect_arch_from_python;
use crate::{
BuildContext, BuildOptions, BuildOrchestrator, OutputOptions, PlatformOptions, PythonOptions,
};
use anyhow::{Context, Result, anyhow, bail, ensure};
use cargo_options::heading;
use fs_err as fs;
use install_backend::{InstallBackend, check_pip_exists, find_uv_bin, find_uv_python, is_uv_venv};
use once_cell::sync::Lazy;
use regex::Regex;
use std::path::Path;
use std::path::PathBuf;
use std::str;
use tempfile::TempDir;
use tracing::{debug, instrument};
use url::Url;
#[derive(Debug, clap::Parser)]
pub struct DevelopOptions {
#[arg(
short = 'b',
long = "bindings",
alias = "binding-crate",
value_parser = ["pyo3", "pyo3-ffi", "cffi", "uniffi", "bin"]
)]
pub bindings: Option<String>,
#[arg(short = 'r', long, help_heading = heading::COMPILATION_OPTIONS, conflicts_with = "profile")]
pub release: bool,
#[arg(long)]
pub strip: bool,
#[arg(
short = 'E',
long,
value_delimiter = ',',
action = clap::ArgAction::Append
)]
pub extras: Vec<String>,
#[arg(
short = 'G',
long,
value_delimiter = ',',
action = clap::ArgAction::Append
)]
pub group: Vec<String>,
#[arg(long)]
pub skip_install: bool,
#[arg(long)]
pub pip_path: Option<PathBuf>,
#[command(flatten)]
pub cargo_options: CargoOptions,
#[arg(long)]
pub uv: bool,
#[command(flatten)]
pub compression: CompressionOptions,
#[arg(long)]
pub generate_stubs: bool,
}
#[instrument(skip_all)]
fn install_dependencies(
build_context: &BuildContext,
extras: &[String],
groups: &[String],
python: &Path,
venv_dir: &Path,
install_backend: &InstallBackend,
) -> Result<()> {
if !build_context.project.metadata24.requires_dist.is_empty() {
let mut extra_names = Vec::with_capacity(extras.len());
for extra in extras {
extra_names.push(
pep508_rs::ExtraName::new(extra.clone())
.with_context(|| format!("invalid extra name: {extra}"))?,
);
}
let mut args = vec!["install".to_string()];
args.extend(
build_context
.project
.metadata24
.requires_dist
.iter()
.map(|x| {
let mut pkg = x.clone();
pkg.marker = pkg.marker.simplify_extras(&extra_names);
pkg.to_string()
}),
);
let status = install_backend
.make_command(python)
.args(&args)
.env("VIRTUAL_ENV", venv_dir)
.status()
.with_context(|| format!("Failed to run {} install", install_backend.name()))?;
if !status.success() {
bail!(
r#"{} install finished with "{}""#,
install_backend.name(),
status
)
}
}
let effective_groups = if groups.is_empty() {
let has_dev_group = build_context
.project
.pyproject_toml
.as_ref()
.and_then(|p| p.dependency_groups.as_ref())
.is_some_and(|dg| dg.0.contains_key("dev"));
if has_dev_group {
vec!["dev".to_string()]
} else {
Vec::new()
}
} else {
groups.to_vec()
};
if !effective_groups.is_empty() {
let mut args = vec!["install".to_string()];
for group in &effective_groups {
args.push("--group".to_string());
args.push(group.clone());
}
let status = install_backend
.make_command(python)
.args(&args)
.env("VIRTUAL_ENV", venv_dir)
.status()
.with_context(|| format!("Failed to run {} install --group", install_backend.name()))?;
if !status.success() {
bail!(
r#"{} install --group finished with "{}""#,
install_backend.name(),
status
)
}
}
Ok(())
}
#[instrument(skip_all, fields(wheel_filename = %wheel_filename.display()))]
fn install_wheel(
build_context: &BuildContext,
python: &Path,
venv_dir: &Path,
wheel_filename: &Path,
install_backend: &InstallBackend,
) -> Result<()> {
let mut cmd = install_backend.make_command(python);
let output = cmd
.args(["install", "--no-deps", "--force-reinstall"])
.arg(dunce::simplified(wheel_filename))
.env("VIRTUAL_ENV", venv_dir)
.output()
.context(format!(
"{} install failed (ran {:?} with {:?})",
install_backend.name(),
cmd.get_program(),
&cmd.get_args().collect::<Vec<_>>(),
))?;
if !output.status.success() {
bail!(
"{} install in {} failed running {:?}: {}\n--- Stdout:\n{}\n--- Stderr:\n{}\n---\n",
install_backend.name(),
venv_dir.display(),
&cmd.get_args().collect::<Vec<_>>(),
output.status,
String::from_utf8_lossy(&output.stdout).trim(),
String::from_utf8_lossy(&output.stderr).trim(),
);
}
if !output.stderr.is_empty() && install_backend.stderr_indicates_problem() {
eprintln!(
"⚠️ Warning: {} raised a warning running {:?}:\n{}",
install_backend.name(),
&cmd.get_args().collect::<Vec<_>>(),
String::from_utf8_lossy(&output.stderr).trim(),
);
}
if let Err(err) = configure_as_editable(build_context, python, install_backend) {
eprintln!("⚠️ Warning: failed to set package as editable: {err}");
}
Ok(())
}
#[instrument(skip_all)]
fn configure_as_editable(
build_context: &BuildContext,
python: &Path,
install_backend: &InstallBackend,
) -> Result<()> {
println!("✏️ Setting installed package as editable");
install_backend.check_supports_show_files(python)?;
let mut cmd = install_backend.make_command(python);
let cmd = cmd.args(["show", "--files", &build_context.project.metadata24.name]);
debug!("running {:?}", cmd);
let output = cmd.output()?;
ensure!(output.status.success(), "failed to list package files");
if let Some(direct_url_path) = parse_direct_url_path(&String::from_utf8_lossy(&output.stdout))?
{
let project_dir = build_context
.project
.pyproject_toml_path
.parent()
.ok_or_else(|| anyhow!("failed to get project directory"))?;
let uri = Url::from_file_path(project_dir)
.map_err(|_| anyhow!("failed to convert project directory to file URL"))?;
let content = format!("{{\"dir_info\": {{\"editable\": true}}, \"url\": \"{uri}\"}}");
fs::write(direct_url_path, content)?;
}
Ok(())
}
fn parse_direct_url_path(pip_show_output: &str) -> Result<Option<PathBuf>> {
static LOCATION_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"Location: ([^\r\n]*)").unwrap());
static DIRECT_URL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r" (.*direct_url.json)").unwrap());
if let Some(Some(location)) = LOCATION_RE.captures(pip_show_output).map(|c| c.get(1))
&& let Some(Some(direct_url_path)) =
DIRECT_URL_RE.captures(pip_show_output).map(|c| c.get(1))
{
return Ok(Some(
PathBuf::from(location.as_str()).join(direct_url_path.as_str()),
));
}
Ok(None)
}
#[allow(clippy::too_many_arguments)]
pub fn develop(develop_options: DevelopOptions, venv_dir: &Path) -> Result<()> {
let DevelopOptions {
bindings,
release,
strip,
extras,
group,
skip_install,
pip_path,
mut cargo_options,
uv,
compression,
generate_stubs,
} = develop_options;
compression.validate();
if release {
cargo_options.profile = Some("release".to_string());
}
let mut target_triple = cargo_options.target.clone();
let target = Target::from_target_triple(cargo_options.target.as_ref())?;
let python = target.get_venv_python(venv_dir);
if !target.user_specified
&& let Some(detected_target) = detect_arch_from_python(&python, &target)
{
target_triple = Some(detected_target);
}
let wheel_dir = TempDir::new().context("Failed to create temporary directory")?;
let build_options = BuildOptions {
python: PythonOptions {
interpreter: vec![python.clone()],
find_interpreter: false,
bindings,
},
platform: PlatformOptions {
platform_tag: vec![PlatformTag::Linux],
auditwheel: Some(AuditWheelMode::Skip),
skip_auditwheel: false,
#[cfg(feature = "zig")]
zig: false,
},
output: OutputOptions {
out: Some(wheel_dir.path().to_path_buf()),
include_debuginfo: !strip && target.is_windows(),
sbom_include: Vec::new(),
},
cargo: CargoOptions {
target: target_triple,
..cargo_options
},
compression,
generate_stubs,
};
let build_context = build_options
.into_build_context()
.strip(if strip { Some(true) } else { None })
.editable(true)
.build()?;
if build_context
.project
.pyproject_toml
.as_ref()
.is_some_and(|p| !p.warn_invalid_version_info())
{
bail!(
"Cannot build without valid version information. \
You need to specify either `project.version` or `project.dynamic = [\"version\"]` in pyproject.toml."
);
}
let interpreter =
PythonInterpreter::check_executable(&python, &target, build_context.project.bridge())?
.ok_or_else(|| {
anyhow!("Expected `python` to be a python interpreter inside a virtualenv ಠ_ಠ")
})?;
let uv_venv = is_uv_venv(venv_dir);
let uv_info = if uv || uv_venv {
match find_uv_python(&interpreter.executable).or_else(|_| find_uv_bin()) {
Ok(uv_info) => Some(Ok(uv_info)),
Err(e) => {
if uv {
Some(Err(e))
} else {
None
}
}
}
} else {
None
};
let install_backend = if let Some(uv_info) = uv_info {
let (uv_path, uv_args) = uv_info?;
InstallBackend::Uv {
path: uv_path,
args: uv_args,
}
} else {
check_pip_exists(&interpreter.executable, pip_path.as_ref())
.context("Failed to find pip (if working with a uv venv try `maturin develop --uv`)")?;
InstallBackend::Pip {
path: pip_path.clone(),
}
};
if !skip_install {
install_dependencies(
&build_context,
&extras,
&group,
&python,
venv_dir,
&install_backend,
)?;
}
let orchestrator = BuildOrchestrator::new(&build_context);
let wheels = orchestrator.build_wheels()?;
if !skip_install {
for (filename, _supported_version) in wheels.iter() {
install_wheel(
&build_context,
&python,
venv_dir,
filename,
&install_backend,
)?;
eprintln!(
"🛠 Installed {}-{}",
build_context.project.metadata24.name, build_context.project.metadata24.version
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::parse_direct_url_path;
#[test]
#[cfg(not(target_os = "windows"))]
fn test_parse_direct_url() {
let example_with_direct_url = "\
Name: my-project
Version: 0.1.0
Location: /foo bar/venv/lib/pythonABC/site-packages
Editable project location: /tmp/temporary.whl
Files:
my_project-0.1.0+abc123de.dist-info/INSTALLER
my_project-0.1.0+abc123de.dist-info/METADATA
my_project-0.1.0+abc123de.dist-info/RECORD
my_project-0.1.0+abc123de.dist-info/REQUESTED
my_project-0.1.0+abc123de.dist-info/WHEEL
my_project-0.1.0+abc123de.dist-info/direct_url.json
my_project-0.1.0+abc123de.dist-info/entry_points.txt
my_project.pth
";
let expected_path = PathBuf::from(
"/foo bar/venv/lib/pythonABC/site-packages/my_project-0.1.0+abc123de.dist-info/direct_url.json",
);
assert_eq!(
parse_direct_url_path(example_with_direct_url).unwrap(),
Some(expected_path)
);
let example_without_direct_url = "\
Name: my-project
Version: 0.1.0
Location: /foo bar/venv/lib/pythonABC/site-packages
Files:
my_project-0.1.0+abc123de.dist-info/INSTALLER
my_project-0.1.0+abc123de.dist-info/METADATA
my_project-0.1.0+abc123de.dist-info/RECORD
my_project-0.1.0+abc123de.dist-info/REQUESTED
my_project-0.1.0+abc123de.dist-info/WHEEL
my_project-0.1.0+abc123de.dist-info/entry_points.txt
my_project.pth
";
assert_eq!(
parse_direct_url_path(example_without_direct_url).unwrap(),
None
);
}
#[test]
#[cfg(target_os = "windows")]
fn test_parse_direct_url_windows() {
let example_with_direct_url_windows = "\
Name: my-project\r
Version: 0.1.0\r
Location: C:\\foo bar\\venv\\Lib\\site-packages\r
Files:\r
my_project-0.1.0+abc123de.dist-info\\INSTALLER\r
my_project-0.1.0+abc123de.dist-info\\METADATA\r
my_project-0.1.0+abc123de.dist-info\\RECORD\r
my_project-0.1.0+abc123de.dist-info\\REQUESTED\r
my_project-0.1.0+abc123de.dist-info\\WHEEL\r
my_project-0.1.0+abc123de.dist-info\\direct_url.json\r
my_project-0.1.0+abc123de.dist-info\\entry_points.txt\r
my_project.pth\r
";
let expected_path = PathBuf::from(
"C:\\foo bar\\venv\\Lib\\site-packages\\my_project-0.1.0+abc123de.dist-info\\direct_url.json",
);
assert_eq!(
parse_direct_url_path(example_with_direct_url_windows).unwrap(),
Some(expected_path)
);
}
}