blob: c40e21f45b38865113965df0e8f51e9c44494f64 [file] [view]
:::{default-domain} bzl
:::
# Lock
:::{note}
`rules_python` supports `requirements.txt` and `uv.lock` formats. Generating
`uv.lock` with {obj}`lock` and consuming it to install packages via
{attr}`pip.parse.uv_lock` or {attr}`pip_parse.uv_lock` is supported under
both `bzlmod` and `WORKSPACE`.
#{gh-issue}`2787` tracks `pylock.toml` support.
:::
## requirements.txt
### pip compile
Generally, when working on a Python project, you'll have some dependencies that themselves have
other dependencies. You might also specify dependency bounds instead of specific versions.
So you'll need to generate a full list of all transitive dependencies and pinned versions
for every dependency.
Typically, you'd have your project dependencies specified in `pyproject.toml` or `requirements.in`
and generate the full pinned list of dependencies in `requirements_lock.txt`, which you can
manage with {obj}`compile_pip_requirements`:
```starlark
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
compile_pip_requirements(
name = "requirements",
src = "pyproject.toml",
requirements_txt = "requirements_lock.txt",
)
```
This rule generates two targets:
- `bazel run [name].update` will regenerate the `requirements_txt` file
- `bazel test [name]_test` will test that the `requirements_txt` file is up to date
Once you generate this fully specified list of requirements, you can install the requirements ([bzlmod](./download)/[WORKSPACE](./download-workspace)).
:::{warning}
If you're specifying dependencies in `pyproject.toml`, make sure to include the
`[build-system]` configuration, with pinned dependencies.
`compile_pip_requirements` will use the build system specified to read your
project's metadata, and you might see non-hermetic behavior if you don't pin the
build system.
Not specifying `[build-system]` at all will result in using a default
`[build-system]` configuration, which uses unpinned versions
([ref](https://peps.python.org/pep-0518/#build-system-table)).
:::
#### pip compile Dependency groups
pip-compile doesn't yet support pyproject.toml dependency groups. Follow
[pip-tools #2062](https://github.com/jazzband/pip-tools/issues/2062)
to see the status of their support.
In the meantime, support can be emulated by passing multiple files to `srcs`:
```starlark
compile_pip_requirements(
srcs = ["pyproject.toml", "requirements-dev.in"]
...
)
```
### uv pip compile
We also have a setup for the `uv pip compile` way of generating lock files.
#### Setup
A `uv` toolchain has to be registered, as well as a Python toolchain, which
{obj}`lock` uses to run `uv` with a hermetic interpreter.
##### Bzlmod
`rules_python` registers the `uv` toolchains for you; `MODULE.bazel` only has
to configure the version to download with the {obj}`uv` module extension:
```starlark
uv = use_extension(
"@rules_python//python/uv:uv.bzl",
"uv",
dev_dependency = True,
)
uv.configure(version = "0.13.5")
```
Without `version`, the default version specified by `rules_python` is used.
See {obj}`uv.configure` for using a mirror, explicit URLs or extra platforms.
To run the configured `uv` binary directly, run
`bazel run @rules_python//python/uv/bin:uv -- <args>` (or add
`use_repo(uv, "uv")` and run `bazel run @uv//:uv -- <args>`).
(uv-workspace-setup)=
##### WORKSPACE
The setup has two steps: {obj}`uv_toolchains_manifest` fetches the release
manifests of the `uv` versions you want, and the {obj}`uv_register_toolchains`
it exposes creates the toolchains for the platforms those releases have
archives for and registers them:
```starlark
load("@rules_python//python/uv:repositories.bzl", "uv_toolchains_manifest")
uv_toolchains_manifest(
name = "uv_manifest",
versions = ["0.13.5"],
)
load("@uv_manifest//:dist_manifest.bzl", "uv_register_toolchains")
uv_register_toolchains(name = "uv")
```
Any released `uv` version can be fetched this way; the download URLs and
checksums are read from the release manifests. The `configure` argument of
{obj}`uv_register_toolchains` takes the same entries as {obj}`uv.configure`.
Bazel prefers toolchains in registration order, so to replace one of the
registered toolchains with your own, register yours before calling
`uv_register_toolchains()` and remove ours; to add one without configuring it
here, register yours after:
```starlark
uv_toolchains_manifest(
name = "uv_manifest",
versions = ["0.13.5", "0.6.5"],
)
load("@uv_manifest//:dist_manifest.bzl", "uv_register_toolchains")
# Preferred over the toolchains registered below.
register_toolchains("//tools/uv:patched_linux_toolchain")
uv_register_toolchains(
name = "uv",
configure = [
# Replaced by the toolchain registered above.
{"platform": "x86_64-unknown-linux-gnu"},
# An additional platform for 0.13.5.
{
"platform": "aarch64-unknown-linux-musl",
"compatible_with": [
"@platforms//os:linux",
"@platforms//cpu:aarch64",
],
"target_settings": [Label("//tools/uv:is_musl")],
},
# The older version with the default platforms. Its toolchains
# come after those of 0.13.5 in `@uv//:all`, so they are only
# used where no earlier toolchain matches.
{"version": "0.6.5"},
],
)
# Used only when none of the toolchains above match.
register_toolchains("//tools/uv:freebsd_toolchain")
```
Using a manifest is optional. Without {obj}`uv_toolchains_manifest`, load
{obj}`uv_register_toolchains` directly from
`@rules_python//python/uv:repositories.bzl` and specify `urls` for every
platform:
```starlark
load("@rules_python//python/uv:repositories.bzl", "uv_register_toolchains")
uv_register_toolchains(
name = "uv",
version = "0.13.5",
configure = [
{
"platform": "x86_64-unknown-linux-gnu",
"urls": ["https://mirror.example.com/uv/uv-0.13.5-x86_64-unknown-linux-gnu.tar.gz"],
"sha256": "...",
},
],
)
```
#### Example usage
```starlark
load("@rules_python//python/uv:lock.bzl", "lock")
lock(
name = "requirements",
srcs = ["pyproject.toml", "requirements.in"],
out = "requirements_lock.txt",
)
```
#### `[tool.uv]` settings from pyproject.toml
When a `pyproject.toml` file is among the {arg}`lock.srcs`, the
{obj}`lock` rule auto-detects the project directory and passes
`--project <dir>` to `uv`. This causes `uv` to read
`[tool.uv]` settings from that `pyproject.toml`, such as
`no-build-isolation`, `exclude-dependencies`, and workspace members.
If multiple `pyproject.toml` files are in {arg}`lock.srcs`, the one
with the shortest directory path is selected (this heuristic works for
typical uv workspace layouts where the root configuration is at the
shortest path).
If the auto-detection picks the wrong project directory, use the
`project` parameter to override:
```starlark
lock(
name = "requirements",
srcs = ["pyproject.toml", "requirements.in"],
out = "requirements_lock.txt",
project = "subproject",
)
```
:::{warning}
**Known limitations of auto-detection**
1. **Workspace heuristic** — the shortest-path selection may incorrectly assume
the upper-most workspace `pyproject.toml` is the correct one. For monorepos
with multiple independent sub-projects, you must set `project` explicitly
for each {obj}`lock` target.
1. **No test target** — unlike {obj}`compile_pip_requirements`, no test target
is auto-created; see the {obj}`lock` docs for how to add one manually using
`diff_test` from `bazel_skylib`.
:::
## uv.lock
The {obj}`lock` rule also supports generating cross-platform `uv.lock` files
using `uv lock` when {arg}`lock.out` ends with `.lock` (such as `uv.lock`),
under both `bzlmod` and `WORKSPACE` (see the `uv` toolchain setup in the
`uv pip compile` section above). Because `uv.lock` already records distribution
URLs and hashes across platforms, {obj}`pip.parse` does not need to query the
PyPI Simple API or store distribution hashes in `MODULE.bazel.lock`.
### Example usage
In `BUILD.bazel`:
```starlark
load("@rules_python//python/uv:lock.bzl", "lock")
lock(
name = "uv_lock",
srcs = ["pyproject.toml"],
out = "uv.lock",
)
```
Run `bazel run //:uv_lock.update` to generate or update `uv.lock`, then pass it
to {obj}`pip.parse` in `MODULE.bazel` via {attr}`pip.parse.uv_lock`:
```starlark
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
pip.parse(
hub_name = "my_deps",
python_version = "3.13",
uv_lock = "//:uv.lock",
)
use_repo(pip, "my_deps")
```
Under `WORKSPACE`, pass it to {obj}`pip_parse` via {attr}`pip_parse.uv_lock`
instead:
```starlark
load("@rules_python//python:pip.bzl", "pip_parse")
pip_parse(
name = "my_deps",
python_interpreter_target = "@python_3_13_host//:python",
uv_lock = "//:uv.lock",
)
load("@my_deps//:requirements.bzl", "install_deps")
install_deps()
```
The distributions that are the same on all platforms are downloaded with the
Bazel downloader using the URLs and hashes from `uv.lock`, and the generated
`requirements.bzl` records them so that it can be vendored. For the
distributions that differ between platforms, `pip` selects the distribution for
the host platform from the ones locked in `uv.lock`. See the
`examples/pip_parse_vendored` example.
`[tool.uv]` settings auto-detection and the {arg}`lock.project` attribute work
the same way as described in the `uv pip compile` section above.
For more documentation see {obj}`lock`.