diff --git a/crates/uv-configuration/src/project_build_backend.rs b/crates/uv-configuration/src/project_build_backend.rs index 86410e793..c95da6bf8 100644 --- a/crates/uv-configuration/src/project_build_backend.rs +++ b/crates/uv-configuration/src/project_build_backend.rs @@ -1,5 +1,5 @@ /// Available project build backends for use in `pyproject.toml`. -#[derive(Clone, Copy, Debug, PartialEq, Default, serde::Deserialize)] +#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] @@ -11,7 +11,6 @@ pub enum ProjectBuildBackend { #[cfg_attr(feature = "schemars", schemars(skip))] /// Use uv as the project build backend. Uv, - #[default] #[serde(alias = "hatchling")] #[cfg_attr(feature = "clap", value(alias = "hatchling"))] /// Use [hatchling](https://pypi.org/project/hatchling) as the project build backend. diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index b75fb2f2a..bd16549f4 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -149,6 +149,7 @@ pub(crate) async fn init( no_config, cache, printer, + preview, ) .await?; @@ -289,6 +290,7 @@ async fn init_project( no_config: bool, cache: &Cache, printer: Printer, + preview: PreviewMode, ) -> Result<()> { // Discover the current workspace, if it exists. let workspace_cache = WorkspaceCache::default(); @@ -579,6 +581,7 @@ async fn init_project( author_from, no_readme, package, + preview, )?; if let Some(workspace) = workspace { @@ -706,6 +709,7 @@ impl InitProjectKind { author_from: Option, no_readme: bool, package: bool, + preview: PreviewMode, ) -> Result<()> { match self { InitProjectKind::Application => InitProjectKind::init_application( @@ -720,6 +724,7 @@ impl InitProjectKind { author_from, no_readme, package, + preview, ), InitProjectKind::Library => InitProjectKind::init_library( name, @@ -733,6 +738,7 @@ impl InitProjectKind { author_from, no_readme, package, + preview, ), } } @@ -751,6 +757,7 @@ impl InitProjectKind { author_from: Option, no_readme: bool, package: bool, + preview: PreviewMode, ) -> Result<()> { fs_err::create_dir_all(path)?; @@ -783,7 +790,11 @@ impl InitProjectKind { } // Add a build system - let build_backend = build_backend.unwrap_or_default(); + let build_backend = match build_backend { + Some(build_backend) => build_backend, + None if preview.is_enabled() => ProjectBuildBackend::Uv, + None => ProjectBuildBackend::Hatch, + }; pyproject.push('\n'); pyproject.push_str(&pyproject_build_system(name, build_backend)); pyproject_build_backend_prerequisites(name, path, build_backend)?; @@ -833,6 +844,7 @@ impl InitProjectKind { author_from: Option, no_readme: bool, package: bool, + preview: PreviewMode, ) -> Result<()> { if !package { return Err(anyhow!("Library projects must be packaged")); @@ -853,7 +865,11 @@ impl InitProjectKind { ); // Always include a build system if the project is packaged. - let build_backend = build_backend.unwrap_or_default(); + let build_backend = match build_backend { + Some(build_backend) => build_backend, + None if preview.is_enabled() => ProjectBuildBackend::Uv, + None => ProjectBuildBackend::Hatch, + }; pyproject.push('\n'); pyproject.push_str(&pyproject_build_system(name, build_backend)); pyproject_build_backend_prerequisites(name, path, build_backend)?; diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index 48a06ddd4..9fff06a91 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -446,6 +446,138 @@ fn init_library() -> Result<()> { Ok(()) } +/// Test the uv build backend with using `uv init --lib --preview`. To be merged with the regular +/// init lib test once the uv build backend becomes the stable default. +#[test] +fn init_library_preview() -> 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("--preview"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(&pyproject_toml)?; + let mut filters = context.filters(); + filters.push((r#"\["uv_build>=.*,<.*"\]"#, r#"["uv_build[SPECIFIERS]"]"#)); + insta::with_settings!({ + filters => 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 = ["uv_build[SPECIFIERS]"] + build-backend = "uv_build" + "# + ); + }); + + 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().arg("--preview").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; use `--active` to target the active environment instead + 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(()) +} + +/// Test the uv build backend with using `uv init --package --preview`. To be merged with the regular +/// init lib test once the uv build backend becomes the stable default. +#[test] +fn init_package_preview() -> Result<()> { + let context = TestContext::new("3.12"); + + let child = context.temp_dir.child("foo"); + child.create_dir_all()?; + + uv_snapshot!(context.filters(), context.init().current_dir(&child).arg("--package").arg("--preview"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Initialized project `foo` + "###); + + let pyproject = fs_err::read_to_string(child.join("pyproject.toml"))?; + let mut filters = context.filters(); + filters.push((r#"\["uv_build>=.*,<.*"\]"#, r#"["uv_build[SPECIFIERS]"]"#)); + insta::with_settings!({ + filters => 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 = ["uv_build[SPECIFIERS]"] + build-backend = "uv_build" + "# + ); + }); + + Ok(()) +} + #[test] fn init_bare_lib() { let context = TestContext::new("3.12"); diff --git a/docs/concepts/projects/init.md b/docs/concepts/projects/init.md index 75fe1adbb..3a8dd244e 100644 --- a/docs/concepts/projects/init.md +++ b/docs/concepts/projects/init.md @@ -202,8 +202,8 @@ build-backend = "hatchling.build" !!! tip You can select a different build backend template by using `--build-backend` with `hatchling`, - `flit-core`, `pdm-backend`, `setuptools`, `maturin`, or `scikit-build-core`. An alternative - backend is required if you want to create a [library with extension modules](#projects-with-extension-modules). + `uv_build`, `flit-core`, `pdm-backend`, `setuptools`, `maturin`, or `scikit-build-core`. An + alternative backend is required if you want to create a [library with extension modules](#projects-with-extension-modules). The created module defines a simple API function: diff --git a/docs/configuration/build-backend.md b/docs/configuration/build-backend.md new file mode 100644 index 000000000..6825919f0 --- /dev/null +++ b/docs/configuration/build-backend.md @@ -0,0 +1,92 @@ +# The uv build backend + +!!! note + + The uv build backend is currently in preview and may change without warning. + + When preview mode is not enabled, uv uses [hatchling](https://pypi.org/project/hatchling/) as the default build backend. + +A build backend transforms a source tree (i.e., a directory) into a source distribution or a wheel. +While uv supports all build backends (as specified by PEP 517), it includes a `uv_build` backend +that integrates tightly with uv to improve performance and user experience. + +The uv build backend currently only supports Python code. An alternative backend is required if you +want to create a +[library with extension modules](../concepts/projects/init.md#projects-with-extension-modules). + +To use the uv build backend as [build system](../concepts/projects/config.md#build-systems) in an +existing project, add it to the `[build-system]` section in your `pyproject.toml`: + +```toml +[build-system] +requires = ["uv_build>=0.6.13,<0.7"] +build-backend = "uv_build" +``` + +!!! important + + The uv build backend follows the same [versioning policy](../reference/policies/versioning.md), + setting an upper bound on the `uv_build` version ensures that the package continues to build in + the future. + +You can also create a new project that uses the uv build backend with `uv init`: + +```shell +uv init --build-backend uv +``` + +`uv_build` is a separate package from uv, optimized for portability and small binary size. The `uv` +command includes a copy of the build backend, so when running `uv build`, the same version will be +used for the build backend as for the uv process. Other build frontends, such as `python -m build`, +will choose the latest compatible `uv_build` version. + +## Include and exclude configuration + +To select which files to include in the source distribution, uv first adds the included files and +directories, then removes the excluded files and directories. This means that exclusions always take +precedence over inclusions. + +When building the source distribution, the following files and directories are included: + +- `pyproject.toml` +- The module under `tool.uv.build-backend.module-root`, by default + `src//**`. +- `project.license-files` and `project.readme`. +- All directories under `tool.uv.build-backend.data`. +- All patterns from `tool.uv.build-backend.source-include`. + +From these, `tool.uv.build-backend.source-exclude` and the default excludes are removed. + +When building the wheel, the following files and directories are included: + +- The module under `tool.uv.build-backend.module-root`, by default + `src//**`. +- `project.license-files` and `project.readme`, as part of the project metadata. +- Each directory under `tool.uv.build-backend.data`, as data directories. + +From these, `tool.uv.build-backend.source-exclude`, `tool.uv.build-backend.wheel-exclude` and the +default excludes are removed. The source dist excludes are applied to avoid source tree to wheel +source builds including more files than source tree to source distribution to wheel build. + +There are no specific wheel includes. There must only be one top level module, and all data files +must either be under the module root or in the appropriate +[data directory](../reference/settings.md#build-backend_data). Most packages store small data in the +module root alongside the source code. + +## Include and exclude syntax + +Includes are anchored, which means that `pyproject.toml` includes only +`/pyproject.toml`. For example, `assets/**/sample.csv` includes all `sample.csv` files +in `/assets` or any child directory. To recursively include all files under a +directory, use a `/**` suffix, e.g. `src/**`. + +!!! note + + For performance and reproducibility, avoid patterns without an anchor such as `**/sample.csv`. + +Excludes are not anchored, which means that `__pycache__` excludes all directories named +`__pycache__` and its children anywhere. To anchor a directory, use a `/` prefix, e.g., `/dist` will +exclude only `/dist`. + +All fields accepting patterns use the reduced portable glob syntax from +[PEP 639](https://peps.python.org/pep-0639/#add-license-FILES-key). diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 38c68875f..813519629 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -6,6 +6,7 @@ Read about the various ways to configure uv: - [Using environment variables](./environment.md) - [Configuring authentication](./authentication.md) - [Configuring package indexes](./indexes.md) +- [The uv build backend](build-backend.md) Or, jump to the [settings reference](../reference/settings.md) which enumerates the available configuration options. diff --git a/mkdocs.template.yml b/mkdocs.template.yml index ac4f32663..8f00d782b 100644 --- a/mkdocs.template.yml +++ b/mkdocs.template.yml @@ -140,6 +140,7 @@ nav: - Authentication: configuration/authentication.md - Package indexes: configuration/indexes.md - Installer: configuration/installer.md + - Build backend: configuration/build-backend.md - The pip interface: - pip/index.md - Using environments: pip/environments.md diff --git a/pyproject.toml b/pyproject.toml index ffa0c9a08..73b59cee1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,7 @@ version_files = [ "docs/guides/integration/pre-commit.md", "docs/guides/integration/github.md", "docs/guides/integration/aws-lambda.md", + "docs/configuration/build-backend.md", ] [tool.mypy]