From 5386701cc1796b956684a076f1537d948b917c1f Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 5 May 2025 15:52:31 +0200 Subject: [PATCH] Build backend: Make preview default and add configuration docs (#12804) Add configuration documentation for the build backend and make it the preview default. The build backend should generally work with default configuration unless you want specific features such as flat layout or module renaming, there is only a dedicated configuration, but no concept or guide page for the build backend. Once the build backend is stable, we can update the guide documentation to explain that uv defaults to its own build backend, but other build backends are also supported. The uv build backend becomes the default in preview, giving it more exposure from users and preparing it to make it the default proper. The current documentation retains warnings that the build backend is in preview. To see current uses of `uv_build` on GitHub: https://github.com/search?q=path%3A**%2Fpyproject.toml+uv_build%3E%3D0&type=code --------- Co-authored-by: Zanie Blue --- .../src/project_build_backend.rs | 3 +- crates/uv/src/commands/project/init.rs | 20 ++- crates/uv/tests/it/init.rs | 132 ++++++++++++++++++ docs/concepts/projects/init.md | 4 +- docs/configuration/build-backend.md | 92 ++++++++++++ docs/configuration/index.md | 1 + mkdocs.template.yml | 1 + pyproject.toml | 1 + 8 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 docs/configuration/build-backend.md 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]