Skip to content

Commit b6de8cb

Browse files
feat(uv): Add a clean-sheet dependency implementation (#650)
[Design doc](https://docs.google.com/document/d/1UFessu8_BwmfMJXDvhyhaASrNMo7_3-gO1lTVASbJbs) An implementation of pip based on consuming PEP-751 [1] like lockfiles. Specifically uv lockfiles, which contain internal dependency graph information that the PEP-751 specification labels optional. Follows in the footsteps of rules_js's pnpm support by consuming a lockfile which contains enough information to produce materialize dependencies without performing _any_ repository time operations which could be platform dependent. ## Features - Supports cross-platform builds of wheels - Supports hermetic source builds of wheels - Automatically handles dependency cycles - Creates unified pip hubs which span virtualenv/dependency solution boundaries - Pip library targets from deactivated venvs are _incompatible_ - Platform constraints on pip libraries do not prevent the creation of a library target - Allows for `--editable`-like workflows - Supports `console_scripts` entrypoints* ## Example ``` # MODULE.bazel content uv = use_extensioin("@aspect_rules_py//uv:extesion.bzl", "uv") uv.declare_hub(hub_name = "pip") uv.declare_venv(hub_name = "pip", venv_name = "a") uv.lockfile(hub_name = "pip", venv_name = "a", src = "third_party/py/venvs/uv-a.lock") uv.declare_venv(hub_name = "pip", venv_name = "b") uv.lockfile(hub_name = "pip", venv_name = "b", src = "third_party/py/venvs/uv-b.toml") use_repo(pip, "pip") ``` ``` # BUILD.bazel content py_venv_binary( name = "foo", srcs = [ "foo.py", ], main = "foo.py", deps = [ "@pip//cowsay", # Pull cowsay from the configured venv ], venv = "a", # Configure the default venv to be "a"; may be overriden at the CLI ) ``` The active venv state can be overriden at the cli by specifying `--'@pip//venv=b'` here for instance, or by using transitions to(re) set that same flag. ## Appendix [1] https://peps.python.org/pep-0751/ [2] https://peps.python.org/pep-0751/#locking-build-requirements-for-sdists ### Changes are visible to end-users: yes - Searched for relevant documentation and updated as needed: no - Breaking change (forces users to change their own code or config): no - Suggested release notes appear below: yes `aspect_rules_py//uv` is now available as an alternative to `pip.parse` ### To do list - ~[x] Audit the comments/FIXMEs for accuracy~ fast-follow - ~[x] Go over zbarsky's nits~ mostly rejected for pythonstyle - ~[x] Create a quick and dirty `uv_lock` rule~ fast-follow - [x] Document the extension - [x] Document that we consider the extension is unstable - [x] Replace `pip.parse` internally entirely - [x] Add support for annotating _replacement_ of pip deps with internal builds (`--editable` / vendoring) - [x] Implement an "all deps for all venvs" data source comparable to the `rules_python` equivalent - [x] Go back over the interpreter compatibility machinery and align it with `rules_python`'s config settings for now - [x] Go back over the interpreter feature flags and align it with `rules_python`'s config settings for now bazel-contrib/rules_python#3314 - ~Investigate implementing a `dist_info` target comparable to that which `rules_python` generates~ There isn't a great way to do this because dist-info is in our world part of the installed package, so any such target is just ignoring the PyInfo details on the fileset. - [x] Provide a pattern for implementing/activating venv entrypoints - [x] Implement conditional activation of deps - [x] Implement an "all active deps" target comparable to the `rules_python` all list - [x] Look into platform conditional deps & how they get represented - [x] Flatten the git log - [x] Ensure `python_version` transition consistency with `rules_python` - [x] Consider a feature flag to turn on `rules_python`'s [package name normalization](https://github.com/bazel-contrib/rules_python/blob/main/python/private/normalize_name.bzl) so that migration is easier. - [x] Implement extra activation - [x] Match the `rules_python` `@hub//package[:package]` syntax? - [X] Get a `toml.bzl` working - [X] Toolchainize the `uv` dependency ### Test plan - [X] Manually test flipping the venv command line flag - [X] Manually test flipping the venv transition attr - [x] Create `py_venv_test`s covering that different versions of the same package can be concurrently configured via different venvs - [x] Create a `py_venv_test` covering that Airflow or another package with dependency cycles can be provisioned - [x] Create a `py_venv_binary` embedded in and transitioned for a Linux OCI container across arch boundaries - [x] Create a `py_venv_test` covering overriding a pip dep with a 1stparty target --------- Co-authored-by: aspect-marvin[bot] <[email protected]>
1 parent 2dadcf0 commit b6de8cb

File tree

119 files changed

+16899
-1304
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

119 files changed

+16899
-1304
lines changed

.bazelrc

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ import %workspace%/bazel/defaults.bazelrc
22

33
common --enable_bzlmod
44

5-
# Ignore slow manual and release targets
6-
# Prevents materializing crossbuild
7-
common --build_tag_filters=-manual,-release
8-
95
common --test_output=errors
106

117
# Define value used by tests
128
common --define=SOME_VAR=SOME_VALUE
139

10+
# Set the default virtualenv to 'default'
11+
common --@pypi//venv=default
12+
1413
common --incompatible_enable_cc_toolchain_resolution
1514

1615
# TODO(bzlmod): Don't break proto

.bcr/patches/remove_dev_deps.patch

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
--- a/MODULE.bazel 2025-11-04 23:45:40.547616185 +0000
2-
+++ b/MODULE.bazel 2025-11-04 23:45:40.547616185 +0000
3-
@@ -24,554 +24,3 @@
4-
"@aspect_rules_py//py/private/toolchain/shim/...",
5-
)
1+
--- a/MODULE.bazel 2025-11-06 12:48:12
2+
+++ b/MODULE.bazel 2025-11-06 12:48:12
3+
@@ -40,558 +40,5 @@
4+
# HACK: In prod the includer's patch inserts the use_repo for multitool. This
5+
# solves the problem of needing a use_repo here in prod and below in dev.
66

77
-################################################################################
88
-# Dev deps
@@ -29,7 +29,7 @@
2929
-# include("//bazel/include:release.MODULE.bazel")
3030
-# include("//bazel/include:multitool.MODULE.bazel")
3131
-# include("//bazel/include:tools.MODULE.bazel")
32-
-
32+
3333
-################################################################################
3434
-# Begin included content
3535
-
@@ -169,15 +169,7 @@
169169
-# from bazel/include/rust.MODULE.bazel
170170
-# Rust configuration
171171
-
172-
-bazel_dep(name = "rules_rust", version = "0.66.0")
173-
-single_version_override(
174-
- module_name = "rules_rust",
175-
- patch_strip = 1,
176-
- patches = [
177-
- "//bazel/patches:rules_rust.patch",
178-
- ],
179-
-)
180-
-
172+
-bazel_dep(name = "rules_rust", version = "0.67.0")
181173
-bazel_dep(name = "openssl", version = "3.3.1.bcr.6")
182174
-
183175
-RUST_EDITION = "2024"
@@ -493,23 +485,35 @@
493485
- python_version = "3.12",
494486
-)
495487
-
488+
-# We still use pip for testing the virtual deps machinery
496489
-pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip", dev_dependency = True)
497490
-pip.parse(
498491
- hub_name = "django",
499492
- python_version = "3.9",
500493
- requirements_lock = "//py/tests/virtual/django:requirements.txt",
501494
-)
502-
-pip.parse(
495+
-use_repo(pip, "django")
496+
-
497+
-# For everything else, we use our own uv machinery
498+
-uv = use_extension("//uv/unstable:extension.bzl", "uv", dev_dependency = True)
499+
-uv.declare_hub(
503500
- hub_name = "pypi",
504-
- python_version = "3.9",
505-
- requirements_lock = "//:requirements.txt",
506501
-)
507-
-pip.parse(
502+
-uv.declare_venv(
508503
- hub_name = "pypi",
509-
- python_version = "3.12",
510-
- requirements_lock = "//:requirements.txt",
504+
- venv_name = "default",
505+
-)
506+
-uv.lockfile(
507+
- src = "//:uv.lock",
508+
- hub_name = "pypi",
509+
- venv_name = "default",
510+
-)
511+
-uv.unstable_annotate_requirements(
512+
- src = "//:annotations.toml",
513+
- hub_name = "pypi",
514+
- venv_name = "default",
511515
-)
512-
-use_repo(pip, "django", "pypi")
516+
-use_repo(uv, "pypi")
513517
-
514518
-http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
515519
-
@@ -536,18 +540,18 @@
536540
-
537541
-########################################
538542
-# from bazel/include/release.MODULE.bazel
539-
-bazel_dep(name = "with_cfg.bzl", version = "0.11.0")
540543
-bazel_dep(name = "rules_pkg", version = "1.1.0")
541544
-
542545
-########################################
543546
-# from bazel/include/multitool.MODULE.bazel
544547
-# Multitool configuration
545548
-
549+
-# Multitool is a prod dep (for now) so we don't need this
546550
-bazel_dep(name = "rules_multitool", version = "1.9.0")
547551
-
548552
-multitool = use_extension("@rules_multitool//multitool:extension.bzl", "multitool")
549553
-multitool.hub(lockfile = "//tools:tools.lock.json")
550-
-use_repo(multitool, "multitool")
554+
use_repo(multitool, "multitool")
551555
-
552556
-########################################
553557
-# from bazel/include/tools.MODULE.bazel

BUILD.bazel

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
load("@gazelle//:def.bzl", "gazelle")
2-
load("@pypi//:requirements.bzl", "all_whl_requirements")
32
load("@rules_python//python/pip_install:requirements.bzl", "compile_pip_requirements")
4-
load("@rules_python_gazelle_plugin//manifest:defs.bzl", "gazelle_python_manifest")
5-
load("@rules_python_gazelle_plugin//modules_mapping:def.bzl", "modules_mapping")
3+
load("//uv/private/manifest:defs.bzl", "gazelle_python_manifest")
64

75
# gazelle:exclude internal_python_deps.bzl
86
# gazelle:exclude internal_deps.bzl
@@ -12,9 +10,10 @@ load("@rules_python_gazelle_plugin//modules_mapping:def.bzl", "modules_mapping")
1210

1311
gazelle_python_manifest(
1412
name = "gazelle_python_manifest",
15-
modules_mapping = ":modules_map",
16-
pip_repository_name = "pypi",
17-
requirements = "requirements.txt",
13+
hub = "pypi",
14+
venvs = [
15+
"default",
16+
],
1817
)
1918

2019
gazelle(
@@ -28,8 +27,3 @@ compile_pip_requirements(
2827
requirements_in = "requirements.in",
2928
requirements_txt = "requirements.txt",
3029
)
31-
32-
modules_mapping(
33-
name = "modules_map",
34-
wheels = all_whl_requirements,
35-
)

MODULE.bazel

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ bazel_dep(name = "bazel_lib", version = "3.0.0")
1111
bazel_dep(name = "bazel_skylib", version = "1.4.2")
1212
bazel_dep(name = "platforms", version = "1.0.0")
1313
bazel_dep(name = "rules_python", version = "1.0.0")
14+
bazel_dep(name = "with_cfg.bzl", version = "0.11.0")
1415

1516
tools = use_extension("//py:extensions.bzl", "py_tools")
1617
tools.rules_py_tools()
@@ -24,6 +25,21 @@ register_toolchains(
2425
"@aspect_rules_py//py/private/toolchain/shim/...",
2526
)
2627

28+
toml = use_extension("//uv/private/tomltool:extension.bzl", "tomltool")
29+
use_repo(
30+
toml,
31+
"toml2json_aarch64_linux_gnu",
32+
"toml2json_aarch64_osx_libsystem",
33+
"toml2json_x86_64_linux_gnu",
34+
"toml2json_x86_64_osx_libsystem",
35+
)
36+
37+
host = use_extension("//uv/private/host:extension.bzl", "host_platform")
38+
use_repo(host, "aspect_rules_py_uv_host")
39+
40+
# HACK: In prod the includer's patch inserts the use_repo for multitool. This
41+
# solves the problem of needing a use_repo here in prod and below in dev.
42+
2743
################################################################################
2844
# Dev deps
2945
#
@@ -189,15 +205,7 @@ platforms = [
189205
# from bazel/include/rust.MODULE.bazel
190206
# Rust configuration
191207

192-
bazel_dep(name = "rules_rust", version = "0.66.0")
193-
single_version_override(
194-
module_name = "rules_rust",
195-
patch_strip = 1,
196-
patches = [
197-
"//bazel/patches:rules_rust.patch",
198-
],
199-
)
200-
208+
bazel_dep(name = "rules_rust", version = "0.67.0")
201209
bazel_dep(name = "openssl", version = "3.3.1.bcr.6")
202210

203211
RUST_EDITION = "2024"
@@ -513,23 +521,35 @@ python.toolchain(
513521
python_version = "3.12",
514522
)
515523

524+
# We still use pip for testing the virtual deps machinery
516525
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip", dev_dependency = True)
517526
pip.parse(
518527
hub_name = "django",
519528
python_version = "3.9",
520529
requirements_lock = "//py/tests/virtual/django:requirements.txt",
521530
)
522-
pip.parse(
531+
use_repo(pip, "django")
532+
533+
# For everything else, we use our own uv machinery
534+
uv = use_extension("//uv/unstable:extension.bzl", "uv", dev_dependency = True)
535+
uv.declare_hub(
523536
hub_name = "pypi",
524-
python_version = "3.9",
525-
requirements_lock = "//:requirements.txt",
526537
)
527-
pip.parse(
538+
uv.declare_venv(
528539
hub_name = "pypi",
529-
python_version = "3.12",
530-
requirements_lock = "//:requirements.txt",
540+
venv_name = "default",
531541
)
532-
use_repo(pip, "django", "pypi")
542+
uv.lockfile(
543+
src = "//:uv.lock",
544+
hub_name = "pypi",
545+
venv_name = "default",
546+
)
547+
uv.unstable_annotate_requirements(
548+
src = "//:annotations.toml",
549+
hub_name = "pypi",
550+
venv_name = "default",
551+
)
552+
use_repo(uv, "pypi")
533553

534554
http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
535555

@@ -556,13 +576,13 @@ http_file(
556576

557577
########################################
558578
# from bazel/include/release.MODULE.bazel
559-
bazel_dep(name = "with_cfg.bzl", version = "0.11.0")
560579
bazel_dep(name = "rules_pkg", version = "1.1.0")
561580

562581
########################################
563582
# from bazel/include/multitool.MODULE.bazel
564583
# Multitool configuration
565584

585+
# Multitool is a prod dep (for now) so we don't need this
566586
bazel_dep(name = "rules_multitool", version = "1.9.0")
567587

568588
multitool = use_extension("@rules_multitool//multitool:extension.bzl", "multitool")

README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
# Aspect's Bazel rules for Python
22

3-
`aspect_rules_py` is a layer on top of `rules_python`, the standard Python ruleset hosted at
4-
https://github.com/bazelbuild/rules_python.
5-
The lower layer of `rules_python` is currently reused, dealing with the toolchain and dependencies.
3+
`aspect_rules_py` is a layer on top of [rules_python](https://github.com/bazel-contrib/rules_python), the reference Python ruleset.
64

7-
However, this ruleset introduces a new implementation of `py_library`, `py_binary`, and `py_test`.
5+
The lower layer of `rules_python` is currently reused, dealing with interpreter toolchains and other details.
6+
7+
However, this ruleset introduces new implementations of `py_library`, `py_binary`, `py_test` and now `uv`.
88
Our philosophy is to behave more like idiomatic python ecosystem tools, where rules_python is closely
99
tied to the way Google does Python development in their internal monorepo, google3.
1010
However we try to maintain compatibility with rules_python's rules for most use cases.
1111

12-
| Layer | Legacy | Recommended |
13-
| ------------------------------------------- | ------------ | -------------------- |
14-
| toolchain: fetch hermetic interpreter | rules_python | rules_python |
15-
| pip.parse: fetch and install deps from pypi | rules_python | rules_python |
16-
| gazelle: generate BUILD files | rules_python | [`aspect configure`] |
17-
| rules: user-facing implementations | rules_python | **rules_py** |
12+
| Layer | Legacy | Recommended |
13+
| ------------------------------------- | ------------ | ----------------------- |
14+
| toolchain: fetch hermetic interpreter | rules_python | rules_python |
15+
| deps: fetch and install from pypi | rules_python | **aspect_rules_py//uv** |
16+
| rules: user-facing implementations | rules_python | **aspect_rules_py//py** |
17+
| gazelle: generate BUILD files | rules_python | [`aspect configure`] |
1818

1919
[`aspect configure`]: https://docs.aspect.build/cli/commands/aspect_configure
2020

annotations.toml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
## Annotations.toml
2+
#
3+
# This is an aspect_rules_py//uv specific configuration file which allows
4+
# packages to be annotated with their build time dependencies. This fills a gap in
5+
# both the pylock and uv lockfile formats, neither of which allow for build
6+
# requirements to be specified.
7+
#
8+
# For instance if a package requires cython be available, this is how you can
9+
# configure it to be delivered.
10+
#
11+
# Takes the place of giant MODULE.bazel annotation tables.
12+
#
13+
# In future this machinery may allow for annotating requirements with
14+
# Bazel-managed dependencies (C libraries, etc.) by label. The exact semantics
15+
# there are TBD.
16+
17+
# We version lockfiles and support semver semantics here
18+
version = "0.0.0"
19+
20+
# Bravado doesn't have bdists, need to build it. Mark explicitly that we need
21+
# wheel setuptools and build in order to do so; build being the standard build
22+
# tool driver.
23+
[[package]]
24+
name = "bravado-core"
25+
build-dependencies = [
26+
{ name = "build" },
27+
{ name = "setuptools" },
28+
]
29+
30+
# We can also annotate packages as providing console scripts we care about.
31+
# Declared console scripts will have Bazel targets generated for them.
32+
#
33+
# Unlike `rules_python` which sees post-install package contents,
34+
# `aspect_rules_py//uv` doesn't and so can't discover entrypoints to generate
35+
# Bazel targets ahead of time. They have to be predeclared if you want to
36+
# reference them as targets.
37+
[[package]]
38+
name = "pytest"
39+
40+
[package.entry-points.console-scripts]
41+
pytest = "pytest:console_main"
42+
"py.test" = "pytest:console_main"

bazel/include/multitool.MODULE.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Multitool configuration
22

3+
# Multitool is a prod dep (for now) so we don't need this
34
bazel_dep(name = "rules_multitool", version = "1.9.0")
45

56
multitool = use_extension("@rules_multitool//multitool:extension.bzl", "multitool")

bazel/include/python.MODULE.bazel

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,35 @@ python.toolchain(
1212
python_version = "3.12",
1313
)
1414

15+
# We still use pip for testing the virtual deps machinery
1516
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip", dev_dependency = True)
1617
pip.parse(
1718
hub_name = "django",
1819
python_version = "3.9",
1920
requirements_lock = "//py/tests/virtual/django:requirements.txt",
2021
)
21-
pip.parse(
22+
use_repo(pip, "django")
23+
24+
# For everything else, we use our own uv machinery
25+
uv = use_extension("//uv/unstable:extension.bzl", "uv", dev_dependency = True)
26+
uv.declare_hub(
2227
hub_name = "pypi",
23-
python_version = "3.9",
24-
requirements_lock = "//:requirements.txt",
2528
)
26-
pip.parse(
29+
uv.declare_venv(
2730
hub_name = "pypi",
28-
python_version = "3.12",
29-
requirements_lock = "//:requirements.txt",
31+
venv_name = "default",
32+
)
33+
uv.lockfile(
34+
src = "//:uv.lock",
35+
hub_name = "pypi",
36+
venv_name = "default",
37+
)
38+
uv.unstable_annotate_requirements(
39+
src = "//:annotations.toml",
40+
hub_name = "pypi",
41+
venv_name = "default",
3042
)
31-
use_repo(pip, "django", "pypi")
43+
use_repo(uv, "pypi")
3244

3345
http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
3446

bazel/include/release.MODULE.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
bazel_dep(name = "with_cfg.bzl", version = "0.11.0")
21
bazel_dep(name = "rules_pkg", version = "1.1.0")

0 commit comments

Comments
 (0)