Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions crates/uv-configuration/src/project_build_backend.rs
Original file line number Diff line number Diff line change
@@ -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))]
Expand All @@ -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.
Expand Down
20 changes: 18 additions & 2 deletions crates/uv/src/commands/project/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ pub(crate) async fn init(
no_config,
cache,
printer,
preview,
)
.await?;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -575,6 +577,7 @@ async fn init_project(
author_from,
no_readme,
package,
preview,
)?;

if let Some(workspace) = workspace {
Expand Down Expand Up @@ -702,6 +705,7 @@ impl InitProjectKind {
author_from: Option<AuthorFrom>,
no_readme: bool,
package: bool,
preview: PreviewMode,
) -> Result<()> {
match self {
InitProjectKind::Application => InitProjectKind::init_application(
Expand All @@ -716,6 +720,7 @@ impl InitProjectKind {
author_from,
no_readme,
package,
preview,
),
InitProjectKind::Library => InitProjectKind::init_library(
name,
Expand All @@ -729,6 +734,7 @@ impl InitProjectKind {
author_from,
no_readme,
package,
preview,
),
}
}
Expand All @@ -747,6 +753,7 @@ impl InitProjectKind {
author_from: Option<AuthorFrom>,
no_readme: bool,
package: bool,
preview: PreviewMode,
) -> Result<()> {
fs_err::create_dir_all(path)?;

Expand Down Expand Up @@ -779,7 +786,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)?;
Expand Down Expand Up @@ -829,6 +840,7 @@ impl InitProjectKind {
author_from: Option<AuthorFrom>,
no_readme: bool,
package: bool,
preview: PreviewMode,
) -> Result<()> {
if !package {
return Err(anyhow!("Library projects must be packaged"));
Expand All @@ -849,7 +861,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)?;
Expand Down
132 changes: 132 additions & 0 deletions crates/uv/tests/it/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
4 changes: 2 additions & 2 deletions docs/concepts/projects/init.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
92 changes: 92 additions & 0 deletions docs/configuration/build-backend.md
Original file line number Diff line number Diff line change
@@ -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/<module-name or project_name_with_underscores>/**`.
- `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/<module-name or project_name_with_underscores>/**`.
- `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
`<project root>/pyproject.toml`. For example, `assets/**/sample.csv` includes all `sample.csv` files
in `<project root>/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 `<project root>/dist`.

All fields accepting patterns use the reduced portable glob syntax from
[PEP 639](https://peps.python.org/pep-0639/#add-license-FILES-key).
1 change: 1 addition & 0 deletions docs/configuration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions mkdocs.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading