From 462cf44bbcae4f458c9bbec59e4df277db95616b Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:34:41 -0400 Subject: [PATCH 1/5] feat(init): support different init build backends --- crates/uv-cli/src/lib.rs | 9 +- crates/uv-configuration/src/lib.rs | 2 + .../src/project_build_backend.rs | 22 + crates/uv/src/commands/project/init.rs | 314 +++++- crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 7 +- crates/uv/tests/it/init.rs | 960 +++++++++++++++++- docs/concepts/projects.md | 78 +- docs/reference/cli.md | 21 + 9 files changed, 1381 insertions(+), 33 deletions(-) create mode 100644 crates/uv-configuration/src/project_build_backend.rs diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 8cdbde832203..d41e52f67e38 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -11,7 +11,7 @@ use url::Url; use uv_cache::CacheArgs; use uv_configuration::{ ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier, - TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem, + ProjectBuildBackend, TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem, }; use uv_distribution_types::{FlatIndexLocation, IndexUrl}; use uv_normalize::{ExtraName, PackageName}; @@ -2461,6 +2461,13 @@ pub struct InitArgs { #[arg(long, value_enum, conflicts_with = "script")] pub vcs: Option, + /// Initialize a build-backend of choice for the project. + /// + /// By default, uv will use (`hatchling`). Use `--build-backend` to specify an + /// alternative build backend. + #[arg(long, value_enum, conflicts_with_all=["script", "no_package"])] + pub build_backend: Option, + /// Do not create a `README.md` file. #[arg(long)] pub no_readme: bool, diff --git a/crates/uv-configuration/src/lib.rs b/crates/uv-configuration/src/lib.rs index fbd40a32beb8..90cdfb0c6ae5 100644 --- a/crates/uv-configuration/src/lib.rs +++ b/crates/uv-configuration/src/lib.rs @@ -13,6 +13,7 @@ pub use name_specifiers::*; pub use overrides::*; pub use package_options::*; pub use preview::*; +pub use project_build_backend::*; pub use sources::*; pub use target_triple::*; pub use trusted_host::*; @@ -34,6 +35,7 @@ mod name_specifiers; mod overrides; mod package_options; mod preview; +mod project_build_backend; mod sources; mod target_triple; mod trusted_host; diff --git a/crates/uv-configuration/src/project_build_backend.rs b/crates/uv-configuration/src/project_build_backend.rs new file mode 100644 index 000000000000..4dbbe2c9795a --- /dev/null +++ b/crates/uv-configuration/src/project_build_backend.rs @@ -0,0 +1,22 @@ +/// Available project build backends for use in `pyproject.toml`. +#[derive(Clone, Copy, Debug, PartialEq, Default, serde::Deserialize)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub enum ProjectBuildBackend { + #[default] + /// Use [hatchling](https://pypi.org/project/hatchling) as the project build backend. + Hatch, + /// Use [flit-core](https://pypi.org/project/flit-core) as the project build backend. + Flit, + /// Use [pdm-backend](https://pypi.org/project/pdm-backend) as the project build backend. + PDM, + /// Use [setuptools](https://pypi.org/project/setuptools) as the project build backend. + Setuptools, + /// Use [maturin](https://pypi.org/project/maturin) as the project build backend. + Maturin, + /// Use [scikit-build-core](https://pypi.org/project/scikit-build-core) as the project build backend. + Scikit, + /// Use [meson-python](https://pypi.org/project/meson-python) as the project build backend. + Meson, +} diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 4e2b8fc8159e..b3f4400a1b78 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -9,7 +9,7 @@ use tracing::{debug, warn}; use uv_cache::Cache; use uv_cli::AuthorFrom; use uv_client::{BaseClientBuilder, Connectivity}; -use uv_configuration::{VersionControlError, VersionControlSystem}; +use uv_configuration::{ProjectBuildBackend, VersionControlError, VersionControlSystem}; use uv_fs::{Simplified, CWD}; use uv_git::GIT; use uv_pep440::Version; @@ -38,6 +38,7 @@ pub(crate) async fn init( package: bool, init_kind: InitKind, vcs: Option, + build_backend: Option, no_readme: bool, author_from: Option, no_pin_python: bool, @@ -115,6 +116,7 @@ pub(crate) async fn init( package, project_kind, vcs, + build_backend, no_readme, author_from, no_pin_python, @@ -246,6 +248,7 @@ async fn init_project( package: bool, project_kind: InitProjectKind, vcs: Option, + build_backend: Option, no_readme: bool, author_from: Option, no_pin_python: bool, @@ -486,6 +489,7 @@ async fn init_project( &requires_python, python_request.as_ref(), vcs, + build_backend, author_from, no_readme, package, @@ -576,6 +580,7 @@ impl InitProjectKind { requires_python: &RequiresPython, python_request: Option<&PythonRequest>, vcs: Option, + build_backend: Option, author_from: Option, no_readme: bool, package: bool, @@ -588,6 +593,7 @@ impl InitProjectKind { requires_python, python_request, vcs, + build_backend, author_from, no_readme, package, @@ -601,6 +607,7 @@ impl InitProjectKind { requires_python, python_request, vcs, + build_backend, author_from, no_readme, package, @@ -618,6 +625,7 @@ impl InitProjectKind { requires_python: &RequiresPython, python_request: Option<&PythonRequest>, vcs: Option, + build_backend: Option, author_from: Option, no_readme: bool, package: bool, @@ -644,24 +652,24 @@ impl InitProjectKind { pyproject.push_str(&pyproject_project_scripts(name, name.as_str(), "main")); // Add a build system + let build_backend = build_backend.unwrap_or_default(); pyproject.push('\n'); - pyproject.push_str(pyproject_build_system()); + pyproject.push_str(&pyproject_build_system(name, build_backend)); + pyproject_build_backend_prerequisites(name, path, build_backend)?; } // Create the source structure. if package { + // Retrieve build backend + let build_backend = build_backend.unwrap_or_default(); + // Create `src/{name}/__init__.py`, if it doesn't exist already. let src_dir = path.join("src").join(&*name.as_dist_info_name()); + fs_err::create_dir_all(&src_dir)?; let init_py = src_dir.join("__init__.py"); + let packaged_script = generate_package_script(name, path, build_backend, false)?; if !init_py.try_exists()? { - fs_err::create_dir_all(&src_dir)?; - fs_err::write( - init_py, - indoc::formatdoc! {r#" - def main() -> None: - print("Hello from {name}!") - "#}, - )?; + fs_err::write(init_py, packaged_script)?; } } else { // Create `hello.py` if it doesn't exist @@ -710,6 +718,7 @@ impl InitProjectKind { requires_python: &RequiresPython, python_request: Option<&PythonRequest>, vcs: Option, + build_backend: Option, author_from: Option, no_readme: bool, package: bool, @@ -726,24 +735,20 @@ impl InitProjectKind { let mut pyproject = pyproject_project(name, requires_python, author.as_ref(), no_readme); // Always include a build system if the project is packaged. + let build_backend = build_backend.unwrap_or_default(); pyproject.push('\n'); - pyproject.push_str(pyproject_build_system()); + pyproject.push_str(&pyproject_build_system(name, build_backend)); + pyproject_build_backend_prerequisites(name, path, build_backend)?; fs_err::write(path.join("pyproject.toml"), pyproject)?; // Create `src/{name}/__init__.py`, if it doesn't exist already. let src_dir = path.join("src").join(&*name.as_dist_info_name()); fs_err::create_dir_all(&src_dir)?; - let init_py = src_dir.join("__init__.py"); + let packaged_script = generate_package_script(name, path, build_backend, true)?; if !init_py.try_exists()? { - fs_err::write( - init_py, - indoc::formatdoc! {r#" - def hello() -> str: - return "Hello from {name}!" - "#}, - )?; + fs_err::write(init_py, packaged_script)?; } // Create a `py.typed` file @@ -807,18 +812,69 @@ fn pyproject_project( dependencies = [] "#, readme = if no_readme { "" } else { "\nreadme = \"README.md\"" }, - authors = author.map_or_else(String::new, |author| format!("\nauthors = [\n {} \n]", author.to_toml_string())), + authors = author.map_or_else(String::new, |author| format!("\nauthors = [\n {}\n]", author.to_toml_string())), requires_python = requires_python.specifiers(), } } /// Generate the `[build-system]` section of a `pyproject.toml`. -fn pyproject_build_system() -> &'static str { - indoc::indoc! {r#" - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" - "#} +/// Generate the `[tool.]` section of a `pyproject.toml` where applicable. +fn pyproject_build_system(package: &PackageName, build_backend: ProjectBuildBackend) -> String { + let module_name = package.as_dist_info_name(); + match build_backend { + // Pure-python backends + ProjectBuildBackend::Hatch => indoc::indoc! {r#" + [build-system] + requires = ["hatchling"] + build-backend = "hatchling.build" + "#} + .to_string(), + ProjectBuildBackend::Flit => indoc::indoc! {r#" + [build-system] + requires = ["flit_core>=3.2,<4"] + build-backend = "flit_core.buildapi" + "#} + .to_string(), + ProjectBuildBackend::PDM => indoc::indoc! {r#" + [build-system] + requires = ["pdm-backend"] + build-backend = "pdm.backend" + "#} + .to_string(), + ProjectBuildBackend::Setuptools => indoc::indoc! {r#" + [build-system] + requires = ["setuptools>=61"] + build-backend = "setuptools.build_meta" + "#} + .to_string(), + // Binary build backends + ProjectBuildBackend::Maturin => indoc::formatdoc! {r#" + [tool.maturin] + module-name = "{module_name}._core" + python-packages = ["{module_name}"] + python-source = "src" + + [build-system] + requires = ["maturin>=1.0,<2.0"] + build-backend = "maturin" + "#}, + ProjectBuildBackend::Scikit => indoc::indoc! {r#" + [tool.scikit-build] + minimum-version = "build-system.requires" + build-dir = "build/{wheel_tag}" + + [build-system] + requires = ["scikit-build-core>=0.10", "pybind11"] + build-backend = "scikit_build_core.build" + "#} + .to_string(), + ProjectBuildBackend::Meson => indoc::indoc! {r#" + [build-system] + requires = ["meson-python", "pybind11"] + build-backend = "mesonpy" + "#} + .to_string(), + } } /// Generate the `[project.scripts]` section of a `pyproject.toml`. @@ -830,6 +886,212 @@ fn pyproject_project_scripts(package: &PackageName, executable_name: &str, targe "#} } +/// Generate additional files as needed for specific build backends. +fn pyproject_build_backend_prerequisites( + package: &PackageName, + path: &Path, + build_backend: ProjectBuildBackend, +) -> Result<()> { + let module_name = package.as_dist_info_name(); + match build_backend { + ProjectBuildBackend::Maturin => { + // Generate Cargo.toml + let build_file = path.join("Cargo.toml"); + if !build_file.try_exists()? { + fs_err::write( + build_file, + indoc::formatdoc! {r#" + [package] + name = "{module_name}" + version = "0.1.0" + edition = "2021" + + [lib] + name = "_core" + # "cdylib" is necessary to produce a shared library for Python to import from. + crate-type = ["cdylib"] + + [dependencies] + # "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so) + # "abi3-py38" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.8 + pyo3 = {{ version = "0.22.3", features = ["extension-module", "abi3-py38"] }} + "#}, + )?; + } + } + ProjectBuildBackend::Scikit => { + // Generate CMakeLists.txt + let build_file = path.join("CMakeLists.txt"); + if !build_file.try_exists()? { + fs_err::write( + build_file, + indoc::formatdoc! {r#" + cmake_minimum_required(VERSION 3.15) + project(${{SKBUILD_PROJECT_NAME}} LANGUAGES CXX) + + set(PYBIND11_FINDPYTHON ON) + find_package(pybind11 CONFIG REQUIRED) + + pybind11_add_module(_core MODULE src/main.cpp) + install(TARGETS _core DESTINATION ${{SKBUILD_PROJECT_NAME}}) + "#}, + )?; + } + } + ProjectBuildBackend::Meson => { + // Generate meson.build + let build_file = path.join("meson.build"); + if !build_file.try_exists()? { + fs_err::write( + build_file, + indoc::formatdoc! {r#" + project( + '{module_name}', + 'cpp', + version: '0.1.0', + meson_version: '>= 1.2.3', + default_options: [ + 'cpp_std=c++11', + ], + ) + + py = import('python').find_installation(pure: false) + pybind11_dep = dependency('pybind11') + + py.extension_module('_core', + 'src/main.cpp', + subdir: '{module_name}', + install: true, + dependencies : [pybind11_dep], + ) + + install_subdir('src/{module_name}', install_dir: py.get_install_dir() / '{module_name}', strip_directory: true) + "#}, + )?; + } + } + _ => {} + } + Ok(()) +} + +/// Generate startup scripts for a package-based application or library. +fn generate_package_script( + package: &PackageName, + path: &Path, + build_backend: ProjectBuildBackend, + is_lib: bool, +) -> Result { + let module_name = package.as_dist_info_name(); + + // Python script for pure-python packaged apps or libs + let pure_python_script = if is_lib { + indoc::formatdoc! {r#" + def hello() -> str: + return "Hello from {package}!" + "#} + } else { + indoc::formatdoc! {r#" + def main() -> None: + print("Hello from {package}!") + "#} + }; + + // Python script for binary-based packaged apps or libs + let binary_call_script = if is_lib { + indoc::formatdoc! {r#" + from {module_name}._core import hello_from_bin + + def hello() -> str: + return hello_from_bin() + "#} + } else { + indoc::formatdoc! {r#" + from {module_name}._core import hello_from_bin + + def main() -> None: + print(hello_from_bin()) + "#} + }; + + // .pyi file for binary script + let pyi_contents = indoc::indoc! {r" + from __future__ import annotations + + def hello_from_bin() -> str: ... + "}; + + let package_script = match build_backend { + ProjectBuildBackend::Maturin => { + // Generate lib.rs + let lib_rs = path.join("src").join("lib.rs"); + if !lib_rs.try_exists()? { + fs_err::write( + lib_rs, + indoc::formatdoc! {r#" + use pyo3::prelude::*; + + #[pyfunction] + fn hello_from_bin() -> String {{ + return "Hello from {package}!".to_string(); + }} + + /// A Python module implemented in Rust. The name of this function must match + /// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to + /// import the module. + #[pymodule] + fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {{ + m.add_function(wrap_pyfunction!(hello_from_bin, m)?)?; + Ok(()) + }} + "#}, + )?; + } + // Generate .pyi file + let pyi_file = path.join("src").join(&*module_name).join("_core.pyi"); + if !pyi_file.try_exists()? { + fs_err::write(pyi_file, pyi_contents)?; + }; + // Return python script calling binary + binary_call_script + } + ProjectBuildBackend::Scikit | ProjectBuildBackend::Meson => { + // Generate main.cpp + let lib_rs = path.join("src").join("main.cpp"); + if !lib_rs.try_exists()? { + fs_err::write( + lib_rs, + indoc::formatdoc! {r#" + #include + + std::string hello_from_bin() {{ return "Hello from {package}!"; }} + + namespace py = pybind11; + + PYBIND11_MODULE(_core, m) {{ + m.doc() = "pybind11 hello module"; + + m.def("hello_from_bin", &hello_from_bin, R"pbdoc( + A function that returns a Hello string. + )pbdoc"); + }} + "#}, + )?; + } + // Generate .pyi file + let pyi_file = path.join("src").join(&*module_name).join("_core.pyi"); + if !pyi_file.try_exists()? { + fs_err::write(pyi_file, pyi_contents)?; + }; + // Return python script calling binary + binary_call_script + } + _ => pure_python_script, + }; + + Ok(package_script) +} + /// Initialize the version control system at the given path. fn init_vcs(path: &Path, vcs: Option) -> Result<()> { // Detect any existing version control system. diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index e6e444cccd0f..682f3e57a1a5 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1217,6 +1217,7 @@ async fn run_project( args.package, args.kind, args.vcs, + args.build_backend, args.no_readme, args.author_from, args.no_pin_python, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 80f274b7d0b1..0c3de8b93766 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -21,8 +21,8 @@ use uv_client::Connectivity; use uv_configuration::{ BuildOptions, Concurrency, ConfigSettings, DevMode, EditableMode, ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, KeyringProviderType, - NoBinary, NoBuild, PreviewMode, Reinstall, SourceStrategy, TargetTriple, TrustedHost, - TrustedPublishing, Upgrade, VersionControlSystem, + NoBinary, NoBuild, PreviewMode, ProjectBuildBackend, Reinstall, SourceStrategy, TargetTriple, + TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, }; use uv_distribution_types::{DependencyMetadata, IndexLocations}; use uv_install_wheel::linker::LinkMode; @@ -164,6 +164,7 @@ pub(crate) struct InitSettings { pub(crate) package: bool, pub(crate) kind: InitKind, pub(crate) vcs: Option, + pub(crate) build_backend: Option, pub(crate) no_readme: bool, pub(crate) author_from: Option, pub(crate) no_pin_python: bool, @@ -185,6 +186,7 @@ impl InitSettings { lib, script, vcs, + build_backend, no_readme, author_from, no_pin_python, @@ -208,6 +210,7 @@ impl InitSettings { package, kind, vcs, + build_backend, no_readme, author_from, no_pin_python, diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index 6cbba5124157..d6299c728349 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -2316,7 +2316,7 @@ fn init_with_author() { description = "Add your description here" readme = "README.md" authors = [ - { name = "Alice", email = "alice@example.com" } + { name = "Alice", email = "alice@example.com" } ] requires-python = ">=3.12" dependencies = [] @@ -2338,7 +2338,7 @@ fn init_with_author() { description = "Add your description here" readme = "README.md" authors = [ - { name = "Alice", email = "alice@example.com" } + { name = "Alice", email = "alice@example.com" } ] requires-python = ">=3.12" dependencies = [] @@ -2380,3 +2380,959 @@ fn init_with_author() { ); }); } + +/// Run `uv init --app --package --build-backend flit` to create a packaged application project +#[test] +fn init_application_package_flit() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + let pyproject_toml = child.join("pyproject.toml"); + let init_py = child.join("src").join("foo").join("__init__.py"); + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--app").arg("--package").arg("--build-backend").arg("flit"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + foo = "foo:main" + + [build-system] + requires = ["flit_core>=3.2,<4"] + build-backend = "flit_core.buildapi" + "### + ); + }); + + let init = fs_err::read_to_string(init_py)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + init, @r###" + def main() -> None: + print("Hello from foo!") + "### + ); + }); + + uv_snapshot!(context.filters(), context.run().current_dir(&child).arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Hello from foo! + + ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + foo==0.1.0 (from file://[TEMP_DIR]/foo) + "###); + + Ok(()) +} + +/// Run `uv init --lib --build-backend flit` to create an library project +#[test] +fn init_library_flit() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + let pyproject_toml = child.join("pyproject.toml"); + let init_py = child.join("src").join("foo").join("__init__.py"); + let py_typed = child.join("src").join("foo").join("py.typed"); + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--lib").arg("--build-backend").arg("flit"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["flit_core>=3.2,<4"] + build-backend = "flit_core.buildapi" + "### + ); + }); + + let init = fs_err::read_to_string(init_py)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + init, @r###" + def hello() -> str: + return "Hello from foo!" + "### + ); + }); + + let py_typed = fs_err::read_to_string(py_typed)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + py_typed, @"" + ); + }); + + uv_snapshot!(context.filters(), context.run().current_dir(&child).arg("python").arg("-c").arg("import foo; print(foo.hello())"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Hello from foo! + + ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + foo==0.1.0 (from file://[TEMP_DIR]/foo) + "###); + + Ok(()) +} + +/// Run `uv init --app --package --build-backend maturin` to create a packaged application project +#[test] +fn init_app_build_backend_maturin() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + let pyproject_toml = child.join("pyproject.toml"); + let init_py = child.join("src").join("foo").join("__init__.py"); + let pyi_file = child.join("src").join("foo").join("_core.pyi"); + let lib_core = child.join("src").join("lib.rs"); + let build_file = child.join("Cargo.toml"); + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--app").arg("--package").arg("--build-backend").arg("maturin"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + foo = "foo:main" + + [tool.maturin] + module-name = "foo._core" + python-packages = ["foo"] + python-source = "src" + + [build-system] + requires = ["maturin>=1.0,<2.0"] + build-backend = "maturin" + "### + ); + }); + + let init = fs_err::read_to_string(init_py)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + init, @r###" + from foo._core import hello_from_bin + + def main() -> None: + print(hello_from_bin()) + "### + ); + }); + + let pyi_contents = fs_err::read_to_string(pyi_file)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyi_contents, @r###" + from __future__ import annotations + + def hello_from_bin() -> str: ... + "### + ); + }); + + let lib_core_contents = fs_err::read_to_string(lib_core)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lib_core_contents, @r###" + use pyo3::prelude::*; + + #[pyfunction] + fn hello_from_bin() -> String { + return "Hello from foo!".to_string(); + } + + /// A Python module implemented in Rust. The name of this function must match + /// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to + /// import the module. + #[pymodule] + fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(hello_from_bin, m)?)?; + Ok(()) + } + "### + ); + }); + + let build_file_contents = fs_err::read_to_string(build_file)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + build_file_contents, @r###" + [package] + name = "foo" + version = "0.1.0" + edition = "2021" + + [lib] + name = "_core" + # "cdylib" is necessary to produce a shared library for Python to import from. + crate-type = ["cdylib"] + + [dependencies] + # "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so) + # "abi3-py38" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.8 + pyo3 = { version = "0.22.3", features = ["extension-module", "abi3-py38"] } + "### + ); + }); + + uv_snapshot!(context.filters(), context.run().current_dir(&child).arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Hello from foo! + + ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + foo==0.1.0 (from file://[TEMP_DIR]/foo) + "###); + + Ok(()) +} + +/// Run `uv init --app --package --build-backend scikit` to create a packaged application project +#[test] +fn init_app_build_backend_scikit() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + let pyproject_toml = child.join("pyproject.toml"); + let init_py = child.join("src").join("foo").join("__init__.py"); + let pyi_file = child.join("src").join("foo").join("_core.pyi"); + let lib_core = child.join("src").join("main.cpp"); + let build_file = child.join("CMakeLists.txt"); + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--app").arg("--package").arg("--build-backend").arg("scikit"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + foo = "foo:main" + + [tool.scikit-build] + minimum-version = "build-system.requires" + build-dir = "build/{wheel_tag}" + + [build-system] + requires = ["scikit-build-core>=0.10", "pybind11"] + build-backend = "scikit_build_core.build" + "### + ); + }); + + let init = fs_err::read_to_string(init_py)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + init, @r###" + from foo._core import hello_from_bin + + def main() -> None: + print(hello_from_bin()) + "### + ); + }); + + let pyi_contents = fs_err::read_to_string(pyi_file)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyi_contents, @r###" + from __future__ import annotations + + def hello_from_bin() -> str: ... + "### + ); + }); + + let lib_core_contents = fs_err::read_to_string(lib_core)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lib_core_contents, @r###" + #include + + std::string hello_from_bin() { return "Hello from foo!"; } + + namespace py = pybind11; + + PYBIND11_MODULE(_core, m) { + m.doc() = "pybind11 hello module"; + + m.def("hello_from_bin", &hello_from_bin, R"pbdoc( + A function that returns a Hello string. + )pbdoc"); + } + "### + ); + }); + + let build_file_contents = fs_err::read_to_string(build_file)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + build_file_contents, @r###" + cmake_minimum_required(VERSION 3.15) + project(${SKBUILD_PROJECT_NAME} LANGUAGES CXX) + + set(PYBIND11_FINDPYTHON ON) + find_package(pybind11 CONFIG REQUIRED) + + pybind11_add_module(_core MODULE src/main.cpp) + install(TARGETS _core DESTINATION ${SKBUILD_PROJECT_NAME}) + "### + ); + }); + + // We do not test with uv run since it would otherwise require specific CXX build tooling + + Ok(()) +} + +/// Run `uv init --app --package --build-backend meson` to create a packaged application project +#[test] +fn init_app_build_backend_meson() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + let pyproject_toml = child.join("pyproject.toml"); + let init_py = child.join("src").join("foo").join("__init__.py"); + let pyi_file = child.join("src").join("foo").join("_core.pyi"); + let lib_core = child.join("src").join("main.cpp"); + let build_file = child.join("meson.build"); + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--app").arg("--package").arg("--build-backend").arg("meson"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [project.scripts] + foo = "foo:main" + + [build-system] + requires = ["meson-python", "pybind11"] + build-backend = "mesonpy" + "### + ); + }); + + let init = fs_err::read_to_string(init_py)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + init, @r###" + from foo._core import hello_from_bin + + def main() -> None: + print(hello_from_bin()) + "### + ); + }); + + let pyi_contents = fs_err::read_to_string(pyi_file)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyi_contents, @r###" + from __future__ import annotations + + def hello_from_bin() -> str: ... + "### + ); + }); + + let lib_core_contents = fs_err::read_to_string(lib_core)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lib_core_contents, @r###" + #include + + std::string hello_from_bin() { return "Hello from foo!"; } + + namespace py = pybind11; + + PYBIND11_MODULE(_core, m) { + m.doc() = "pybind11 hello module"; + + m.def("hello_from_bin", &hello_from_bin, R"pbdoc( + A function that returns a Hello string. + )pbdoc"); + } + "### + ); + }); + + let build_file_contents = fs_err::read_to_string(build_file)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + build_file_contents, @r###" + project( + 'foo', + 'cpp', + version: '0.1.0', + meson_version: '>= 1.2.3', + default_options: [ + 'cpp_std=c++11', + ], + ) + + py = import('python').find_installation(pure: false) + pybind11_dep = dependency('pybind11') + + py.extension_module('_core', + 'src/main.cpp', + subdir: 'foo', + install: true, + dependencies : [pybind11_dep], + ) + + install_subdir('src/foo', install_dir: py.get_install_dir() / 'foo', strip_directory: true) + "### + ); + }); + + // We do not test with uv run since it would otherwise require specific CXX build tooling + + Ok(()) +} + +/// Run `uv init --lib --build-backend maturin` to create a packaged application project +#[test] +fn init_lib_build_backend_maturin() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + let pyproject_toml = child.join("pyproject.toml"); + let init_py = child.join("src").join("foo").join("__init__.py"); + let pyi_file = child.join("src").join("foo").join("_core.pyi"); + let lib_core = child.join("src").join("lib.rs"); + let build_file = child.join("Cargo.toml"); + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--lib").arg("--build-backend").arg("maturin"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [tool.maturin] + module-name = "foo._core" + python-packages = ["foo"] + python-source = "src" + + [build-system] + requires = ["maturin>=1.0,<2.0"] + build-backend = "maturin" + "### + ); + }); + + let init = fs_err::read_to_string(init_py)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + init, @r###" + from foo._core import hello_from_bin + + def hello() -> str: + return hello_from_bin() + "### + ); + }); + + let pyi_contents = fs_err::read_to_string(pyi_file)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyi_contents, @r###" + from __future__ import annotations + + def hello_from_bin() -> str: ... + "### + ); + }); + + let lib_core_contents = fs_err::read_to_string(lib_core)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lib_core_contents, @r###" + use pyo3::prelude::*; + + #[pyfunction] + fn hello_from_bin() -> String { + return "Hello from foo!".to_string(); + } + + /// A Python module implemented in Rust. The name of this function must match + /// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to + /// import the module. + #[pymodule] + fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(hello_from_bin, m)?)?; + Ok(()) + } + "### + ); + }); + + let build_file_contents = fs_err::read_to_string(build_file)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + build_file_contents, @r###" + [package] + name = "foo" + version = "0.1.0" + edition = "2021" + + [lib] + name = "_core" + # "cdylib" is necessary to produce a shared library for Python to import from. + crate-type = ["cdylib"] + + [dependencies] + # "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so) + # "abi3-py38" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.8 + pyo3 = { version = "0.22.3", features = ["extension-module", "abi3-py38"] } + "### + ); + }); + + uv_snapshot!(context.filters(), context.run().current_dir(&child).arg("python").arg("-c").arg("import foo; print(foo.hello())"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + Hello from foo! + + ----- stderr ----- + warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + foo==0.1.0 (from file://[TEMP_DIR]/foo) + "###); + + Ok(()) +} + +/// Run `uv init --lib --build-backend scikit` to create a packaged application project +#[test] +fn init_lib_build_backend_scikit() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + let pyproject_toml = child.join("pyproject.toml"); + let init_py = child.join("src").join("foo").join("__init__.py"); + let pyi_file = child.join("src").join("foo").join("_core.pyi"); + let lib_core = child.join("src").join("main.cpp"); + let build_file = child.join("CMakeLists.txt"); + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--lib").arg("--build-backend").arg("scikit"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [tool.scikit-build] + minimum-version = "build-system.requires" + build-dir = "build/{wheel_tag}" + + [build-system] + requires = ["scikit-build-core>=0.10", "pybind11"] + build-backend = "scikit_build_core.build" + "### + ); + }); + + let init = fs_err::read_to_string(init_py)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + init, @r###" + from foo._core import hello_from_bin + + def hello() -> str: + return hello_from_bin() + "### + ); + }); + + let pyi_contents = fs_err::read_to_string(pyi_file)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyi_contents, @r###" + from __future__ import annotations + + def hello_from_bin() -> str: ... + "### + ); + }); + + let lib_core_contents = fs_err::read_to_string(lib_core)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lib_core_contents, @r###" + #include + + std::string hello_from_bin() { return "Hello from foo!"; } + + namespace py = pybind11; + + PYBIND11_MODULE(_core, m) { + m.doc() = "pybind11 hello module"; + + m.def("hello_from_bin", &hello_from_bin, R"pbdoc( + A function that returns a Hello string. + )pbdoc"); + } + "### + ); + }); + + let build_file_contents = fs_err::read_to_string(build_file)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + build_file_contents, @r###" + cmake_minimum_required(VERSION 3.15) + project(${SKBUILD_PROJECT_NAME} LANGUAGES CXX) + + set(PYBIND11_FINDPYTHON ON) + find_package(pybind11 CONFIG REQUIRED) + + pybind11_add_module(_core MODULE src/main.cpp) + install(TARGETS _core DESTINATION ${SKBUILD_PROJECT_NAME}) + "### + ); + }); + + // We do not test with uv run since it would otherwise require specific CXX build tooling + + Ok(()) +} + +/// Run `uv init --lib --build-backend meson` to create a packaged application project +#[test] +fn init_lib_build_backend_meson() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + let pyproject_toml = child.join("pyproject.toml"); + let init_py = child.join("src").join("foo").join("__init__.py"); + let pyi_file = child.join("src").join("foo").join("_core.pyi"); + let lib_core = child.join("src").join("main.cpp"); + let build_file = child.join("meson.build"); + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--lib").arg("--build-backend").arg("meson"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "foo" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["meson-python", "pybind11"] + build-backend = "mesonpy" + "### + ); + }); + + let init = fs_err::read_to_string(init_py)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + init, @r###" + from foo._core import hello_from_bin + + def hello() -> str: + return hello_from_bin() + "### + ); + }); + + let pyi_contents = fs_err::read_to_string(pyi_file)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyi_contents, @r###" + from __future__ import annotations + + def hello_from_bin() -> str: ... + "### + ); + }); + + let lib_core_contents = fs_err::read_to_string(lib_core)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lib_core_contents, @r###" + #include + + std::string hello_from_bin() { return "Hello from foo!"; } + + namespace py = pybind11; + + PYBIND11_MODULE(_core, m) { + m.doc() = "pybind11 hello module"; + + m.def("hello_from_bin", &hello_from_bin, R"pbdoc( + A function that returns a Hello string. + )pbdoc"); + } + "### + ); + }); + + let build_file_contents = fs_err::read_to_string(build_file)?; + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + build_file_contents, @r###" + project( + 'foo', + 'cpp', + version: '0.1.0', + meson_version: '>= 1.2.3', + default_options: [ + 'cpp_std=c++11', + ], + ) + + py = import('python').find_installation(pure: false) + pybind11_dep = dependency('pybind11') + + py.extension_module('_core', + 'src/main.cpp', + subdir: 'foo', + install: true, + dependencies : [pybind11_dep], + ) + + install_subdir('src/foo', install_dir: py.get_install_dir() / 'foo', strip_directory: true) + "### + ); + }); + + // We do not test with uv run since it would otherwise require specific CXX build tooling + + Ok(()) +} diff --git a/docs/concepts/projects.md b/docs/concepts/projects.md index 6dc5cbba7fc1..83602a6f8f15 100644 --- a/docs/concepts/projects.md +++ b/docs/concepts/projects.md @@ -203,10 +203,59 @@ def hello() -> str: And you can import and execute it using `uv run`: ```console -$ uv run python -c "import example_lib; print(example_lib.hello())" +$ uv run --directory example-lib python -c "import example_lib; print(example_lib.hello())" Hello from example-lib! ``` +In addition, you can further customize the build backend of a packaged application by specifying +`--build-backend` including binary build backends such as `maturin`. + +```console +$ uv init --lib --build-backend maturin example-lib +$ tree example-lib +example-lib +├── .python-version +├── Cargo.toml +├── README.md +├── pyproject.toml +└── src + ├── lib.rs + └── example_lib + ├── py.typed + ├── __init__.py + └── _core.pyi +``` + +And you can import and execute it using `uv run`: + +```console +$ uv run --directory example-lib python -c "import example_lib; print(example_lib.hello())" +Hello from example-lib! +``` + +Other build backends supported by `uv init` include `hatchling`, `flit-core`, `pdm-backend`, +`setuptools`, `maturin`, `scikit-build-core`, and `meson-python`. + +!!! tip + +Changes to `lib.rs` or `main.cpp` will require running `--reinstall` when using binary build +backends such as `maturin`, `scikit-build-core`, or `meson-python`. + +!!! note + +[meson-editable]: + https://mesonbuild.com/meson-python/how-to-guides/editable-installs.html#build-dependencies + +When `meson-python` is used with editable installations, it may be beneficial to leverage +`--no-build-isolation` such that changes to `main.cpp` can be automatically rebuilt on import. See +more on this topic in [Meson Editable Installs][meson-editable]. You may also further customize +build isolation behavior using `dependency-metadata` as described in the dedicated +[build isolation](#build-isolation) section. + +As of time of writing, `meson-python` cannot detect a moving build environment. As a result, +commands such as `uv sync` or `uv run` will not behave as expected. In such cases, it is recommended +to leverage `--no-editable` instead to avoid errors. + ### Packaged applications The `--package` flag can be passed to `uv init` to create a distributable application, e.g., if you @@ -257,7 +306,7 @@ build-backend = "hatchling.build" Which can be executed with `uv run`: ```console -$ uv run example-packaged-app +$ uv run --directory example-packaged-app example-packaged-app Hello from example-packaged-app! ``` @@ -267,6 +316,31 @@ Hello from example-packaged-app! However, this may require changes to the project directory structure, depending on the build backend. +In addition, you can further customize the build backend of a packaged application by specifying +`--build-backend` including binary build backends such as `maturin`. + +```console +$ uv init --app --package --build-backend maturin example-packaged-app +$ tree example-packaged-app +example-packaged-app +├── .python-version +├── Cargo.toml +├── README.md +├── pyproject.toml +└── src + ├── lib.rs + └── example_packaged_app + ├── __init__.py + └── _core.pyi +``` + +Which can also be executed with `uv run`: + +```console +$ uv run --directory example-packaged-app example-packaged-app +Hello from example-packaged-app! +``` + ## Project environments When working on a project with uv, uv will create a virtual environment as needed. While some uv diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 5add2f5ddb46..6aa86a7c8fd4 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -453,6 +453,27 @@ uv init [OPTIONS] [PATH]
  • none: Do not infer the author information
  • +
    --build-backend build-backend

    Initialize a build-backend of choice for the project.

    + +

    By default, uv will use (hatchling). Use --build-backend to specify an alternative build backend.

    + +

    Possible values:

    + +
      +
    • hatch: Use hatchling as the project build backend
    • + +
    • flit: Use flit-core as the project build backend
    • + +
    • pdm: Use pdm-backend as the project build backend
    • + +
    • setuptools: Use setuptools as the project build backend
    • + +
    • maturin: Use maturin as the project build backend
    • + +
    • scikit: Use scikit-build-core as the project build backend
    • + +
    • meson: Use meson-python as the project build backend
    • +
    --cache-dir cache-dir

    Path to the cache directory.

    Defaults to $HOME/Library/Caches/uv on macOS, $XDG_CACHE_HOME/uv or $HOME/.cache/uv on Linux, and %LOCALAPPDATA%\uv\cache on Windows.

    From 8a513ef7f14957c91af4500fa6408942977182bf Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Sat, 12 Oct 2024 13:08:22 -0400 Subject: [PATCH 2/5] fixup! feedback --- crates/uv-cli/src/lib.rs | 3 - crates/uv/src/commands/project/init.rs | 76 ++++++++++++-------------- crates/uv/tests/it/init.rs | 8 +-- docs/reference/cli.md | 4 +- 4 files changed, 40 insertions(+), 51 deletions(-) diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index d41e52f67e38..286bc1428f40 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2462,9 +2462,6 @@ pub struct InitArgs { pub vcs: Option, /// Initialize a build-backend of choice for the project. - /// - /// By default, uv will use (`hatchling`). Use `--build-backend` to specify an - /// alternative build backend. #[arg(long, value_enum, conflicts_with_all=["script", "no_package"])] pub build_backend: Option, diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index b3f4400a1b78..ddb48213f1be 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -656,21 +656,9 @@ impl InitProjectKind { pyproject.push('\n'); pyproject.push_str(&pyproject_build_system(name, build_backend)); pyproject_build_backend_prerequisites(name, path, build_backend)?; - } - // Create the source structure. - if package { - // Retrieve build backend - let build_backend = build_backend.unwrap_or_default(); - - // Create `src/{name}/__init__.py`, if it doesn't exist already. - let src_dir = path.join("src").join(&*name.as_dist_info_name()); - fs_err::create_dir_all(&src_dir)?; - let init_py = src_dir.join("__init__.py"); - let packaged_script = generate_package_script(name, path, build_backend, false)?; - if !init_py.try_exists()? { - fs_err::write(init_py, packaged_script)?; - } + // Generate `src` files + generate_package_scripts(name, path, build_backend, false)?; } else { // Create `hello.py` if it doesn't exist // TODO(zanieb): Only create `hello.py` if there are no other Python files? @@ -742,20 +730,8 @@ impl InitProjectKind { fs_err::write(path.join("pyproject.toml"), pyproject)?; - // Create `src/{name}/__init__.py`, if it doesn't exist already. - let src_dir = path.join("src").join(&*name.as_dist_info_name()); - fs_err::create_dir_all(&src_dir)?; - let init_py = src_dir.join("__init__.py"); - let packaged_script = generate_package_script(name, path, build_backend, true)?; - if !init_py.try_exists()? { - fs_err::write(init_py, packaged_script)?; - } - - // Create a `py.typed` file - let py_typed = src_dir.join("py.typed"); - if !py_typed.try_exists()? { - fs_err::write(py_typed, "")?; - } + // Generate `src` files + generate_package_scripts(name, path, build_backend, true)?; // Write .python-version if it doesn't exist. if let Some(python_request) = python_request { @@ -913,8 +889,8 @@ fn pyproject_build_backend_prerequisites( [dependencies] # "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so) - # "abi3-py38" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.8 - pyo3 = {{ version = "0.22.3", features = ["extension-module", "abi3-py38"] }} + # "abi3-py39" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.9 + pyo3 = {{ version = "0.22.4", features = ["extension-module", "abi3-py39"] }} "#}, )?; } @@ -976,14 +952,18 @@ fn pyproject_build_backend_prerequisites( } /// Generate startup scripts for a package-based application or library. -fn generate_package_script( +fn generate_package_scripts( package: &PackageName, path: &Path, build_backend: ProjectBuildBackend, is_lib: bool, -) -> Result { +) -> Result<()> { let module_name = package.as_dist_info_name(); + let src_dir = path.join("src"); + let pkg_dir = src_dir.join(&*module_name); + fs_err::create_dir_all(&pkg_dir)?; + // Python script for pure-python packaged apps or libs let pure_python_script = if is_lib { indoc::formatdoc! {r#" @@ -1024,10 +1004,10 @@ fn generate_package_script( let package_script = match build_backend { ProjectBuildBackend::Maturin => { // Generate lib.rs - let lib_rs = path.join("src").join("lib.rs"); - if !lib_rs.try_exists()? { + let native_src = src_dir.join("lib.rs"); + if !native_src.try_exists()? { fs_err::write( - lib_rs, + native_src, indoc::formatdoc! {r#" use pyo3::prelude::*; @@ -1048,7 +1028,7 @@ fn generate_package_script( )?; } // Generate .pyi file - let pyi_file = path.join("src").join(&*module_name).join("_core.pyi"); + let pyi_file = pkg_dir.join("_core.pyi"); if !pyi_file.try_exists()? { fs_err::write(pyi_file, pyi_contents)?; }; @@ -1057,10 +1037,10 @@ fn generate_package_script( } ProjectBuildBackend::Scikit | ProjectBuildBackend::Meson => { // Generate main.cpp - let lib_rs = path.join("src").join("main.cpp"); - if !lib_rs.try_exists()? { + let native_src = src_dir.join("main.cpp"); + if !native_src.try_exists()? { fs_err::write( - lib_rs, + native_src, indoc::formatdoc! {r#" #include @@ -1079,7 +1059,7 @@ fn generate_package_script( )?; } // Generate .pyi file - let pyi_file = path.join("src").join(&*module_name).join("_core.pyi"); + let pyi_file = pkg_dir.join("_core.pyi"); if !pyi_file.try_exists()? { fs_err::write(pyi_file, pyi_contents)?; }; @@ -1089,7 +1069,21 @@ fn generate_package_script( _ => pure_python_script, }; - Ok(package_script) + // Create `src/{name}/__init__.py`, if it doesn't exist already. + let init_py = pkg_dir.join("__init__.py"); + if !init_py.try_exists()? { + fs_err::write(init_py, package_script)?; + } + + // Create `src/{name}/py.typed`, if it doesn't exist already. + if is_lib { + let py_typed = pkg_dir.join("py.typed"); + if !py_typed.try_exists()? { + fs_err::write(py_typed, "")?; + } + } + + Ok(()) } /// Initialize the version control system at the given path. diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index d6299c728349..93db5674e199 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -2660,8 +2660,8 @@ fn init_app_build_backend_maturin() -> Result<()> { [dependencies] # "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so) - # "abi3-py38" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.8 - pyo3 = { version = "0.22.3", features = ["extension-module", "abi3-py38"] } + # "abi3-py39" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.9 + pyo3 = { version = "0.22.4", features = ["extension-module", "abi3-py39"] } "### ); }); @@ -3061,8 +3061,8 @@ fn init_lib_build_backend_maturin() -> Result<()> { [dependencies] # "extension-module" tells pyo3 we want to build an extension module (skips linking against libpython.so) - # "abi3-py38" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.8 - pyo3 = { version = "0.22.3", features = ["extension-module", "abi3-py38"] } + # "abi3-py39" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.9 + pyo3 = { version = "0.22.4", features = ["extension-module", "abi3-py39"] } "### ); }); diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 6aa86a7c8fd4..1baa9149183b 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -453,9 +453,7 @@ uv init [OPTIONS] [PATH]
  • none: Do not infer the author information
  • -
    --build-backend build-backend

    Initialize a build-backend of choice for the project.

    - -

    By default, uv will use (hatchling). Use --build-backend to specify an alternative build backend.

    +
    --build-backend build-backend

    Initialize a build-backend of choice for the project

    Possible values:

    From 616dcd393c5a5f7ffd340802fbf947da0252cc5f Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Tue, 15 Oct 2024 17:42:36 -0400 Subject: [PATCH 3/5] fixup! meson-removal --- .../src/project_build_backend.rs | 2 - crates/uv/src/commands/project/init.rs | 40 +-- crates/uv/tests/it/init.rs | 263 ------------------ docs/concepts/projects.md | 19 +- docs/reference/cli.md | 2 - 5 files changed, 3 insertions(+), 323 deletions(-) diff --git a/crates/uv-configuration/src/project_build_backend.rs b/crates/uv-configuration/src/project_build_backend.rs index 4dbbe2c9795a..de5623077279 100644 --- a/crates/uv-configuration/src/project_build_backend.rs +++ b/crates/uv-configuration/src/project_build_backend.rs @@ -17,6 +17,4 @@ pub enum ProjectBuildBackend { Maturin, /// Use [scikit-build-core](https://pypi.org/project/scikit-build-core) as the project build backend. Scikit, - /// Use [meson-python](https://pypi.org/project/meson-python) as the project build backend. - Meson, } diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index ddb48213f1be..4814609a9a9f 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -844,12 +844,6 @@ fn pyproject_build_system(package: &PackageName, build_backend: ProjectBuildBack build-backend = "scikit_build_core.build" "#} .to_string(), - ProjectBuildBackend::Meson => indoc::indoc! {r#" - [build-system] - requires = ["meson-python", "pybind11"] - build-backend = "mesonpy" - "#} - .to_string(), } } @@ -914,38 +908,6 @@ fn pyproject_build_backend_prerequisites( )?; } } - ProjectBuildBackend::Meson => { - // Generate meson.build - let build_file = path.join("meson.build"); - if !build_file.try_exists()? { - fs_err::write( - build_file, - indoc::formatdoc! {r#" - project( - '{module_name}', - 'cpp', - version: '0.1.0', - meson_version: '>= 1.2.3', - default_options: [ - 'cpp_std=c++11', - ], - ) - - py = import('python').find_installation(pure: false) - pybind11_dep = dependency('pybind11') - - py.extension_module('_core', - 'src/main.cpp', - subdir: '{module_name}', - install: true, - dependencies : [pybind11_dep], - ) - - install_subdir('src/{module_name}', install_dir: py.get_install_dir() / '{module_name}', strip_directory: true) - "#}, - )?; - } - } _ => {} } Ok(()) @@ -1035,7 +997,7 @@ fn generate_package_scripts( // Return python script calling binary binary_call_script } - ProjectBuildBackend::Scikit | ProjectBuildBackend::Meson => { + ProjectBuildBackend::Scikit => { // Generate main.cpp let native_src = src_dir.join("main.cpp"); if !native_src.try_exists()? { diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index 93db5674e199..2ef396c62d6c 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -2809,139 +2809,6 @@ fn init_app_build_backend_scikit() -> Result<()> { Ok(()) } -/// Run `uv init --app --package --build-backend meson` to create a packaged application project -#[test] -fn init_app_build_backend_meson() -> Result<()> { - let context = TestContext::new("3.12"); - - let child = context.temp_dir.child("foo"); - child.create_dir_all()?; - - let pyproject_toml = child.join("pyproject.toml"); - let init_py = child.join("src").join("foo").join("__init__.py"); - let pyi_file = child.join("src").join("foo").join("_core.pyi"); - let lib_core = child.join("src").join("main.cpp"); - let build_file = child.join("meson.build"); - - uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--app").arg("--package").arg("--build-backend").arg("meson"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Initialized project `foo` - "###); - - let pyproject = fs_err::read_to_string(&pyproject_toml)?; - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - pyproject, @r###" - [project] - name = "foo" - version = "0.1.0" - description = "Add your description here" - readme = "README.md" - requires-python = ">=3.12" - dependencies = [] - - [project.scripts] - foo = "foo:main" - - [build-system] - requires = ["meson-python", "pybind11"] - build-backend = "mesonpy" - "### - ); - }); - - let init = fs_err::read_to_string(init_py)?; - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - init, @r###" - from foo._core import hello_from_bin - - def main() -> None: - print(hello_from_bin()) - "### - ); - }); - - let pyi_contents = fs_err::read_to_string(pyi_file)?; - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - pyi_contents, @r###" - from __future__ import annotations - - def hello_from_bin() -> str: ... - "### - ); - }); - - let lib_core_contents = fs_err::read_to_string(lib_core)?; - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - lib_core_contents, @r###" - #include - - std::string hello_from_bin() { return "Hello from foo!"; } - - namespace py = pybind11; - - PYBIND11_MODULE(_core, m) { - m.doc() = "pybind11 hello module"; - - m.def("hello_from_bin", &hello_from_bin, R"pbdoc( - A function that returns a Hello string. - )pbdoc"); - } - "### - ); - }); - - let build_file_contents = fs_err::read_to_string(build_file)?; - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - build_file_contents, @r###" - project( - 'foo', - 'cpp', - version: '0.1.0', - meson_version: '>= 1.2.3', - default_options: [ - 'cpp_std=c++11', - ], - ) - - py = import('python').find_installation(pure: false) - pybind11_dep = dependency('pybind11') - - py.extension_module('_core', - 'src/main.cpp', - subdir: 'foo', - install: true, - dependencies : [pybind11_dep], - ) - - install_subdir('src/foo', install_dir: py.get_install_dir() / 'foo', strip_directory: true) - "### - ); - }); - - // We do not test with uv run since it would otherwise require specific CXX build tooling - - Ok(()) -} - /// Run `uv init --lib --build-backend maturin` to create a packaged application project #[test] fn init_lib_build_backend_maturin() -> Result<()> { @@ -3206,133 +3073,3 @@ fn init_lib_build_backend_scikit() -> Result<()> { Ok(()) } - -/// Run `uv init --lib --build-backend meson` to create a packaged application project -#[test] -fn init_lib_build_backend_meson() -> Result<()> { - let context = TestContext::new("3.12"); - - let child = context.temp_dir.child("foo"); - child.create_dir_all()?; - - let pyproject_toml = child.join("pyproject.toml"); - let init_py = child.join("src").join("foo").join("__init__.py"); - let pyi_file = child.join("src").join("foo").join("_core.pyi"); - let lib_core = child.join("src").join("main.cpp"); - let build_file = child.join("meson.build"); - - uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--lib").arg("--build-backend").arg("meson"), @r###" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Initialized project `foo` - "###); - - let pyproject = fs_err::read_to_string(&pyproject_toml)?; - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - pyproject, @r###" - [project] - name = "foo" - version = "0.1.0" - description = "Add your description here" - readme = "README.md" - requires-python = ">=3.12" - dependencies = [] - - [build-system] - requires = ["meson-python", "pybind11"] - build-backend = "mesonpy" - "### - ); - }); - - let init = fs_err::read_to_string(init_py)?; - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - init, @r###" - from foo._core import hello_from_bin - - def hello() -> str: - return hello_from_bin() - "### - ); - }); - - let pyi_contents = fs_err::read_to_string(pyi_file)?; - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - pyi_contents, @r###" - from __future__ import annotations - - def hello_from_bin() -> str: ... - "### - ); - }); - - let lib_core_contents = fs_err::read_to_string(lib_core)?; - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - lib_core_contents, @r###" - #include - - std::string hello_from_bin() { return "Hello from foo!"; } - - namespace py = pybind11; - - PYBIND11_MODULE(_core, m) { - m.doc() = "pybind11 hello module"; - - m.def("hello_from_bin", &hello_from_bin, R"pbdoc( - A function that returns a Hello string. - )pbdoc"); - } - "### - ); - }); - - let build_file_contents = fs_err::read_to_string(build_file)?; - insta::with_settings!({ - filters => context.filters(), - }, { - assert_snapshot!( - build_file_contents, @r###" - project( - 'foo', - 'cpp', - version: '0.1.0', - meson_version: '>= 1.2.3', - default_options: [ - 'cpp_std=c++11', - ], - ) - - py = import('python').find_installation(pure: false) - pybind11_dep = dependency('pybind11') - - py.extension_module('_core', - 'src/main.cpp', - subdir: 'foo', - install: true, - dependencies : [pybind11_dep], - ) - - install_subdir('src/foo', install_dir: py.get_install_dir() / 'foo', strip_directory: true) - "### - ); - }); - - // We do not test with uv run since it would otherwise require specific CXX build tooling - - Ok(()) -} diff --git a/docs/concepts/projects.md b/docs/concepts/projects.md index 83602a6f8f15..66eaa032a9b7 100644 --- a/docs/concepts/projects.md +++ b/docs/concepts/projects.md @@ -234,27 +234,12 @@ Hello from example-lib! ``` Other build backends supported by `uv init` include `hatchling`, `flit-core`, `pdm-backend`, -`setuptools`, `maturin`, `scikit-build-core`, and `meson-python`. +`setuptools`, `maturin`, and `scikit-build-core`. !!! tip Changes to `lib.rs` or `main.cpp` will require running `--reinstall` when using binary build -backends such as `maturin`, `scikit-build-core`, or `meson-python`. - -!!! note - -[meson-editable]: - https://mesonbuild.com/meson-python/how-to-guides/editable-installs.html#build-dependencies - -When `meson-python` is used with editable installations, it may be beneficial to leverage -`--no-build-isolation` such that changes to `main.cpp` can be automatically rebuilt on import. See -more on this topic in [Meson Editable Installs][meson-editable]. You may also further customize -build isolation behavior using `dependency-metadata` as described in the dedicated -[build isolation](#build-isolation) section. - -As of time of writing, `meson-python` cannot detect a moving build environment. As a result, -commands such as `uv sync` or `uv run` will not behave as expected. In such cases, it is recommended -to leverage `--no-editable` instead to avoid errors. +backends such as `maturin`, and `scikit-build-core`. ### Packaged applications diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 1baa9149183b..f88e1d132f17 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -469,8 +469,6 @@ uv init [OPTIONS] [PATH]
  • maturin: Use maturin as the project build backend
  • scikit: Use scikit-build-core as the project build backend
  • - -
  • meson: Use meson-python as the project build backend
  • --cache-dir cache-dir

    Path to the cache directory.

    From 576f6d078375c372ef07b45cee49f2c96a56a1e7 Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 16 Oct 2024 14:05:43 +0200 Subject: [PATCH 4/5] `env_remove("VIRTUAL_ENV")` in tests --- crates/uv/tests/it/init.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index 2ef396c62d6c..d039dc8f471b 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -2437,14 +2437,13 @@ fn init_application_package_flit() -> Result<()> { ); }); - uv_snapshot!(context.filters(), context.run().current_dir(&child).arg("foo"), @r###" + uv_snapshot!(context.filters(), context.run().current_dir(&child).env_remove("VIRTUAL_ENV").arg("foo"), @r###" success: true exit_code: 0 ----- stdout ----- Hello from foo! ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 1 package in [TIME] @@ -2519,14 +2518,13 @@ fn init_library_flit() -> Result<()> { ); }); - uv_snapshot!(context.filters(), context.run().current_dir(&child).arg("python").arg("-c").arg("import foo; print(foo.hello())"), @r###" + uv_snapshot!(context.filters(), context.run().current_dir(&child).env_remove("VIRTUAL_ENV").arg("python").arg("-c").arg("import foo; print(foo.hello())"), @r###" success: true exit_code: 0 ----- stdout ----- Hello from foo! ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 1 package in [TIME] @@ -2666,14 +2664,13 @@ fn init_app_build_backend_maturin() -> Result<()> { ); }); - uv_snapshot!(context.filters(), context.run().current_dir(&child).arg("foo"), @r###" + uv_snapshot!(context.filters(), context.run().current_dir(&child).env_remove("VIRTUAL_ENV").arg("foo"), @r###" success: true exit_code: 0 ----- stdout ----- Hello from foo! ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 1 package in [TIME] @@ -2934,14 +2931,13 @@ fn init_lib_build_backend_maturin() -> Result<()> { ); }); - uv_snapshot!(context.filters(), context.run().current_dir(&child).arg("python").arg("-c").arg("import foo; print(foo.hello())"), @r###" + uv_snapshot!(context.filters(), context.run().current_dir(&child).env_remove("VIRTUAL_ENV").arg("python").arg("-c").arg("import foo; print(foo.hello())"), @r###" success: true exit_code: 0 ----- stdout ----- Hello from foo! ----- stderr ----- - warning: `VIRTUAL_ENV=[VENV]/` does not match the project environment path `.venv` and will be ignored Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Creating virtual environment at: .venv Resolved 1 package in [TIME] From 1c2d67e2e46096bb4d42f0650fa0068b46f0087f Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 16 Oct 2024 14:11:04 +0200 Subject: [PATCH 5/5] Edit documentation --- docs/concepts/projects.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/concepts/projects.md b/docs/concepts/projects.md index 66eaa032a9b7..d4aa0766f805 100644 --- a/docs/concepts/projects.md +++ b/docs/concepts/projects.md @@ -207,8 +207,8 @@ $ uv run --directory example-lib python -c "import example_lib; print(example_li Hello from example-lib! ``` -In addition, you can further customize the build backend of a packaged application by specifying -`--build-backend` including binary build backends such as `maturin`. +You can select a different build backend template by using `--build-backend` with `hatchling`, +`flit-core`, `pdm-backend`, `setuptools`, `maturin`, or `scikit-build-core`. ```console $ uv init --lib --build-backend maturin example-lib @@ -233,13 +233,10 @@ $ uv run --directory example-lib python -c "import example_lib; print(example_li Hello from example-lib! ``` -Other build backends supported by `uv init` include `hatchling`, `flit-core`, `pdm-backend`, -`setuptools`, `maturin`, and `scikit-build-core`. - !!! tip Changes to `lib.rs` or `main.cpp` will require running `--reinstall` when using binary build -backends such as `maturin`, and `scikit-build-core`. +backends such as `maturin` and `scikit-build-core`. ### Packaged applications