Add templates for popular build backends (#7857)

Co-authored-by: konstin <konstin@mailbox.org>
This commit is contained in:
samypr100 2024-10-16 12:19:59 +00:00 committed by GitHub
parent ea0c32df8c
commit 319c0183c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1063 additions and 53 deletions

View file

@ -12,7 +12,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::{Index, IndexUrl, Origin, PipExtraIndex, PipFindLinks, PipIndex};
use uv_normalize::{ExtraName, PackageName};
@ -2525,6 +2525,10 @@ pub struct InitArgs {
#[arg(long, value_enum, conflicts_with = "script")]
pub vcs: Option<VersionControlSystem>,
/// Initialize a build-backend of choice for the project.
#[arg(long, value_enum, conflicts_with_all=["script", "no_package"])]
pub build_backend: Option<ProjectBuildBackend>,
/// Do not create a `README.md` file.
#[arg(long)]
pub no_readme: bool,

View file

@ -14,6 +14,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::*;
@ -36,6 +37,7 @@ mod name_specifiers;
mod overrides;
mod package_options;
mod preview;
mod project_build_backend;
mod sources;
mod target_triple;
mod trusted_host;

View file

@ -0,0 +1,20 @@
/// 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,
}

View file

@ -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<VersionControlSystem>,
build_backend: Option<ProjectBuildBackend>,
no_readme: bool,
author_from: Option<AuthorFrom>,
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<VersionControlSystem>,
build_backend: Option<ProjectBuildBackend>,
no_readme: bool,
author_from: Option<AuthorFrom>,
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<VersionControlSystem>,
build_backend: Option<ProjectBuildBackend>,
author_from: Option<AuthorFrom>,
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<VersionControlSystem>,
build_backend: Option<ProjectBuildBackend>,
author_from: Option<AuthorFrom>,
no_readme: bool,
package: bool,
@ -644,25 +652,13 @@ 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 {
// Create `src/{name}/__init__.py`, if it doesn't exist already.
let src_dir = path.join("src").join(&*name.as_dist_info_name());
let init_py = src_dir.join("__init__.py");
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}!")
"#},
)?;
}
// 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?
@ -710,6 +706,7 @@ impl InitProjectKind {
requires_python: &RequiresPython,
python_request: Option<&PythonRequest>,
vcs: Option<VersionControlSystem>,
build_backend: Option<ProjectBuildBackend>,
author_from: Option<AuthorFrom>,
no_readme: bool,
package: bool,
@ -726,31 +723,15 @@ 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");
if !init_py.try_exists()? {
fs_err::write(
init_py,
indoc::formatdoc! {r#"
def hello() -> str:
return "Hello from {name}!"
"#},
)?;
}
// 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 {
@ -807,18 +788,63 @@ 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(),
}
}
/// Generate the `[project.scripts]` section of a `pyproject.toml`.
@ -830,6 +856,198 @@ 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-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"] }}
"#},
)?;
}
}
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}})
"#},
)?;
}
}
_ => {}
}
Ok(())
}
/// Generate startup scripts for a package-based application or library.
fn generate_package_scripts(
package: &PackageName,
path: &Path,
build_backend: ProjectBuildBackend,
is_lib: bool,
) -> 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#"
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 native_src = src_dir.join("lib.rs");
if !native_src.try_exists()? {
fs_err::write(
native_src,
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 = pkg_dir.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 => {
// Generate main.cpp
let native_src = src_dir.join("main.cpp");
if !native_src.try_exists()? {
fs_err::write(
native_src,
indoc::formatdoc! {r#"
#include <pybind11/pybind11.h>
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 = pkg_dir.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,
};
// 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.
fn init_vcs(path: &Path, vcs: Option<VersionControlSystem>) -> Result<()> {
// Detect any existing version control system.

View file

@ -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,

View file

@ -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, Index, 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<VersionControlSystem>,
pub(crate) build_backend: Option<ProjectBuildBackend>,
pub(crate) no_readme: bool,
pub(crate) author_from: Option<AuthorFrom>,
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,

View file

@ -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,692 @@ 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).env_remove("VIRTUAL_ENV").arg("foo"), @r###"
success: true
exit_code: 0
----- stdout -----
Hello from foo!
----- stderr -----
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).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 -----
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-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"] }
"###
);
});
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 -----
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 <pybind11/pybind11.h>
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 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-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"] }
"###
);
});
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 -----
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 <pybind11/pybind11.h>
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(())
}

View file

@ -203,10 +203,41 @@ 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!
```
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
$ 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!
```
!!! tip
Changes to `lib.rs` or `main.cpp` will require running `--reinstall` when using binary build
backends such as `maturin` and `scikit-build-core`.
### Packaged applications
The `--package` flag can be passed to `uv init` to create a distributable application, e.g., if you
@ -257,7 +288,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 +298,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

View file

@ -467,6 +467,23 @@ uv init [OPTIONS] [PATH]
<li><code>none</code>: Do not infer the author information</li>
</ul>
</dd><dt><code>--build-backend</code> <i>build-backend</i></dt><dd><p>Initialize a build-backend of choice for the project</p>
<p>Possible values:</p>
<ul>
<li><code>hatch</code>: Use <a href='https://pypi.org/project/hatchling'>hatchling</a> as the project build backend</li>
<li><code>flit</code>: Use <a href='https://pypi.org/project/flit-core'>flit-core</a> as the project build backend</li>
<li><code>pdm</code>: Use <a href='https://pypi.org/project/pdm-backend'>pdm-backend</a> as the project build backend</li>
<li><code>setuptools</code>: Use <a href='https://pypi.org/project/setuptools'>setuptools</a> as the project build backend</li>
<li><code>maturin</code>: Use <a href='https://pypi.org/project/maturin'>maturin</a> as the project build backend</li>
<li><code>scikit</code>: Use <a href='https://pypi.org/project/scikit-build-core'>scikit-build-core</a> as the project build backend</li>
</ul>
</dd><dt><code>--cache-dir</code> <i>cache-dir</i></dt><dd><p>Path to the cache directory.</p>
<p>Defaults to <code>$HOME/Library/Caches/uv</code> on macOS, <code>$XDG_CACHE_HOME/uv</code> or <code>$HOME/.cache/uv</code> on Linux, and <code>%LOCALAPPDATA%\uv\cache</code> on Windows.</p>