diff --git a/Cargo.lock b/Cargo.lock index 9272134e2..fc98be7eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -759,6 +759,101 @@ dependencies = [ "autocfg", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1104,6 +1199,7 @@ dependencies = [ "pyproject-toml", "python-pkginfo", "regex", + "rstest", "rustc_version", "rustls", "rustls-pemfile", @@ -1418,6 +1514,12 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.29" @@ -1649,6 +1751,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "relative-path" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc" + [[package]] name = "rfc2047-decoder" version = "0.2.2" @@ -1677,6 +1785,35 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.48", + "unicode-ident", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -1941,6 +2078,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.13.1" diff --git a/Cargo.toml b/Cargo.toml index 53ef92610..c993529cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,7 @@ pretty_assertions = { version = "1.3.0", optional = true } [dev-dependencies] expect-test = "1.4.1" +rstest = "0.18.2" indoc = "2.0.3" pretty_assertions = "1.3.0" rustversion = "1.0.9" diff --git a/src/develop.rs b/src/develop.rs index 9483a56aa..f3096cfc1 100644 --- a/src/develop.rs +++ b/src/develop.rs @@ -17,6 +17,37 @@ use tempfile::TempDir; use tracing::instrument; use url::Url; +#[derive(PartialEq, Eq)] +enum InstallBackend { + Pip { path: Option }, + Uv, +} + +impl InstallBackend { + fn make_command(&self, python_path: &Path) -> Command { + match self { + InstallBackend::Pip { path } => match &path { + Some(path) => { + let mut cmd = Command::new(path); + cmd.arg("--python") + .arg(python_path) + .arg("--disable-pip-version-check"); + cmd + } + None => { + let mut cmd = Command::new(python_path); + cmd.arg("-m").arg("pip").arg("--disable-pip-version-check"); + cmd + } + }, + InstallBackend::Uv => { + let mut cmd = Command::new(python_path); + cmd.arg("-m").arg("uv").arg("pip"); + cmd + } + } + } +} /// Install the crate as module in the current virtualenv #[derive(Debug, clap::Parser)] pub struct DevelopOptions { @@ -58,23 +89,9 @@ pub struct DevelopOptions { /// `cargo rustc` options #[command(flatten)] pub cargo_options: CargoOptions, -} - -fn make_pip_command(python_path: &Path, pip_path: Option<&Path>) -> Command { - match pip_path { - Some(pip_path) => { - let mut cmd = Command::new(pip_path); - cmd.arg("--python") - .arg(python_path) - .arg("--disable-pip-version-check"); - cmd - } - None => { - let mut cmd = Command::new(python_path); - cmd.arg("-m").arg("pip").arg("--disable-pip-version-check"); - cmd - } - } + /// Use `uv` to install packages instead of `pip` + #[arg(long)] + pub uv: bool, } #[instrument(skip_all)] @@ -82,7 +99,7 @@ fn install_dependencies( build_context: &BuildContext, extras: &[String], interpreter: &PythonInterpreter, - pip_path: Option<&Path>, + install_backend: &InstallBackend, ) -> Result<()> { if !build_context.metadata23.requires_dist.is_empty() { let mut args = vec!["install".to_string()]; @@ -112,7 +129,8 @@ fn install_dependencies( } pkg.to_string() })); - let status = make_pip_command(&interpreter.executable, pip_path) + let status = install_backend + .make_command(&interpreter.executable) .args(&args) .status() .context("Failed to run pip install")?; @@ -128,37 +146,45 @@ fn pip_install_wheel( build_context: &BuildContext, python: &Path, venv_dir: &Path, - pip_path: Option<&Path>, wheel_filename: &Path, + pip_path: Option, + install_backend: &InstallBackend, ) -> Result<()> { - let mut pip_cmd = make_pip_command(python, pip_path); - let output = pip_cmd + let mut cmd = install_backend.make_command(python); + let output = cmd .args(["install", "--no-deps", "--force-reinstall"]) .arg(dunce::simplified(wheel_filename)) .output() .context(format!( "pip install failed (ran {:?} with {:?})", - pip_cmd.get_program(), - &pip_cmd.get_args().collect::>(), + cmd.get_program(), + &cmd.get_args().collect::>(), ))?; if !output.status.success() { bail!( "pip install in {} failed running {:?}: {}\n--- Stdout:\n{}\n--- Stderr:\n{}\n---\n", venv_dir.display(), - &pip_cmd.get_args().collect::>(), + &cmd.get_args().collect::>(), output.status, String::from_utf8_lossy(&output.stdout).trim(), String::from_utf8_lossy(&output.stderr).trim(), ); } - if !output.stderr.is_empty() { + // uv pip install sends logs to stderr thus only print this warning for pip + if !output.stderr.is_empty() && *install_backend != InstallBackend::Uv { eprintln!( "⚠️ Warning: pip raised a warning running {:?}:\n{}", - &pip_cmd.get_args().collect::>(), + &cmd.get_args().collect::>(), String::from_utf8_lossy(&output.stderr).trim(), ); } - fix_direct_url(build_context, python, pip_path)?; + // pip show --files is not supported by uv, thus + // default to using pip backend all the time + fix_direct_url( + build_context, + python, + &InstallBackend::Pip { path: pip_path }, + )?; Ok(()) } @@ -173,18 +199,18 @@ fn pip_install_wheel( fn fix_direct_url( build_context: &BuildContext, python: &Path, - pip_path: Option<&Path>, + install_backend: &InstallBackend, ) -> Result<()> { println!("✏️ Setting installed package as editable"); - let mut pip_cmd = make_pip_command(python, pip_path); - let output = pip_cmd + let mut cmd = install_backend.make_command(python); + let output = cmd .args(["show", "--files"]) .arg(&build_context.metadata23.name) .output() .context(format!( "pip show failed (ran {:?} with {:?})", - pip_cmd.get_program(), - &pip_cmd.get_args().collect::>(), + cmd.get_program(), + &cmd.get_args().collect::>(), ))?; if let Some(direct_url_path) = parse_direct_url_path(&String::from_utf8_lossy(&output.stdout))? { @@ -231,7 +257,15 @@ pub fn develop(develop_options: DevelopOptions, venv_dir: &Path) -> Result<()> { skip_install, pip_path, cargo_options, + uv, } = develop_options; + let install_backend = if uv { + InstallBackend::Uv + } else { + InstallBackend::Pip { + path: pip_path.clone(), + } + }; let mut target_triple = cargo_options.target.as_ref().map(|x| x.to_string()); let target = Target::from_target_triple(cargo_options.target)?; let python = target.get_venv_python(venv_dir); @@ -282,7 +316,7 @@ pub fn develop(develop_options: DevelopOptions, venv_dir: &Path) -> Result<()> { || anyhow!("Expected `python` to be a python interpreter inside a virtualenv ಠ_ಠ"), )?; - install_dependencies(&build_context, &extras, &interpreter, pip_path.as_deref())?; + install_dependencies(&build_context, &extras, &interpreter, &install_backend)?; let wheels = build_context.build_wheels()?; if !skip_install { @@ -291,8 +325,9 @@ pub fn develop(develop_options: DevelopOptions, venv_dir: &Path) -> Result<()> { &build_context, &python, venv_dir, - pip_path.as_deref(), filename, + pip_path.clone(), + &install_backend, )?; eprintln!( "🛠 Installed {}-{}", diff --git a/tests/cmd/develop.stdout b/tests/cmd/develop.stdout index 0940dc108..cb8ada219 100644 --- a/tests/cmd/develop.stdout +++ b/tests/cmd/develop.stdout @@ -52,6 +52,9 @@ Options: --future-incompat-report Outputs a future incompatibility report at the end of the build (unstable) + --uv + Use `uv` to install packages instead of `pip` + -h, --help Print help (see a summary with '-h') diff --git a/tests/common/develop.rs b/tests/common/develop.rs index 56aaae3c7..97dd16788 100644 --- a/tests/common/develop.rs +++ b/tests/common/develop.rs @@ -1,4 +1,6 @@ -use crate::common::{check_installed, create_conda_env, create_virtualenv, maybe_mock_cargo}; +use crate::common::{ + check_installed, create_conda_env, create_virtualenv, maybe_mock_cargo, TestInstallBackend, +}; use anyhow::Result; use maturin::{develop, CargoOptions, DevelopOptions}; use std::path::{Path, PathBuf}; @@ -12,6 +14,7 @@ pub fn test_develop( bindings: Option, unique_name: &str, conda: bool, + test_backend: TestInstallBackend, ) -> Result<()> { maybe_mock_cargo(); @@ -31,6 +34,7 @@ pub fn test_develop( "pip", "install", "--disable-pip-version-check", + "uv", "cffi", ]) .output()?; @@ -43,6 +47,7 @@ pub fn test_develop( ); } + let uv = matches!(test_backend, TestInstallBackend::Uv); let manifest_file = package.join("Cargo.toml"); let develop_options = DevelopOptions { bindings, @@ -57,6 +62,7 @@ pub fn test_develop( target_dir: Some(PathBuf::from(format!("test-crates/targets/{unique_name}"))), ..Default::default() }, + uv, }; develop(develop_options, &venv_dir)?; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f6ad9fdca..7c238f079 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -12,6 +12,11 @@ pub mod errors; pub mod integration; pub mod other; +pub enum TestInstallBackend { + Pip, + Uv, +} + /// Check that the package is either not installed or works correctly pub fn check_installed(package: &Path, python: &Path) -> Result<()> { let path = if cfg!(windows) { diff --git a/tests/run.rs b/tests/run.rs index 6bcac878d..af2c62c29 100644 --- a/tests/run.rs +++ b/tests/run.rs @@ -1,8 +1,12 @@ //! To speed up the tests, they are tests all collected in a single module -use common::{develop, errors, handle_result, integration, other, test_python_implementation}; +use common::{ + develop, errors, handle_result, integration, other, test_python_implementation, + TestInstallBackend, +}; use expect_test::expect; use maturin::pyproject_toml::SdistGenerator; +use rstest::rstest; use std::env; use std::path::Path; use time::macros::datetime; @@ -17,6 +21,7 @@ fn develop_pyo3_pure() { None, "develop-pyo3-pure", false, + TestInstallBackend::Pip, )); } @@ -30,6 +35,7 @@ fn develop_pyo3_pure_conda() { None, "develop-pyo3-pure-conda", true, + TestInstallBackend::Pip, )); } } @@ -41,6 +47,7 @@ fn develop_pyo3_mixed() { None, "develop-pyo3-mixed", false, + TestInstallBackend::Pip, )); } @@ -51,6 +58,7 @@ fn develop_pyo3_mixed_include_exclude() { None, "develop-pyo3-mixed-include-exclude", false, + TestInstallBackend::Pip, )); } @@ -61,6 +69,7 @@ fn develop_pyo3_mixed_submodule() { None, "develop-pyo3-mixed-submodule", false, + TestInstallBackend::Pip, )); } @@ -71,6 +80,7 @@ fn develop_pyo3_mixed_with_path_dep() { None, "develop-pyo3-mixed-with-path-dep", false, + TestInstallBackend::Pip, )); } @@ -81,6 +91,7 @@ fn develop_pyo3_mixed_implicit() { None, "develop-pyo3-mixed-implicit", false, + TestInstallBackend::Pip, )); } @@ -91,6 +102,7 @@ fn develop_pyo3_mixed_py_subdir() { None, "develop-pyo3-mixed-py-subdir", false, + TestInstallBackend::Pip, )); } @@ -101,6 +113,7 @@ fn develop_pyo3_mixed_src_layout() { None, "develop-pyo3-mixed-src", false, + TestInstallBackend::Pip, )); } @@ -116,6 +129,7 @@ fn develop_cffi_pure() { None, "develop-cffi-pure", false, + TestInstallBackend::Pip, )); } @@ -131,6 +145,7 @@ fn develop_cffi_mixed() { None, "develop-cffi-mixed", false, + TestInstallBackend::Pip, )); } @@ -142,6 +157,7 @@ fn develop_uniffi_pure() { None, "develop-uniffi-pure", false, + TestInstallBackend::Pip, )); } } @@ -153,6 +169,7 @@ fn develop_uniffi_pure_proc_macro() { None, "develop-uniffi-pure-proc-macro", false, + TestInstallBackend::Pip, )); } @@ -164,27 +181,36 @@ fn develop_uniffi_mixed() { None, "develop-uniffi-mixed", false, + TestInstallBackend::Pip, )); } } +#[rstest] +#[case(TestInstallBackend::Pip, "pip")] +#[case(TestInstallBackend::Uv, "uv")] #[test] -fn develop_hello_world() { +fn develop_hello_world(#[case] backend: TestInstallBackend, #[case] name: &str) { handle_result(develop::test_develop( "test-crates/hello-world", None, - "develop-hello-world", + format!("develop-hello-world-{}", name).as_str(), false, + backend, )); } +#[rstest] +#[case(TestInstallBackend::Pip, "pip")] +#[case(TestInstallBackend::Uv, "uv")] #[test] -fn develop_pyo3_ffi_pure() { +fn develop_pyo3_ffi_pure(#[case] backend: TestInstallBackend, #[case] name: &str) { handle_result(develop::test_develop( "test-crates/pyo3-ffi-pure", None, - "develop-pyo3-ffi-pure", + format!("develop-pyo3-ffi-pure-{}", name).as_str(), false, + backend, )); }