tomato·bazeldocs v0 · latest
Docs/Reference/Modules/rules_uv

rules_uv

Bazel rules for uv (Astral's Python package manager)

Latest0.7.4
Versions12
CategoryBazel rules
MaintainersMatt Marshall
Registryhttps://registry.tbzl.dev/modules/rules_uv/
Sourcegithub.com/tomato-bazel/rules_uv
MODULE.bazelstarlark
bazel_dep(name = "rules_uv", version = "0.7.4")

View source & releases on GitHub ↗

Bazel rules for uv, Astral’s high-speed Python package + project manager. Two pieces:

  1. @uv//:binary — the uv CLI, built from source inside Bazel via rules_rust’s cargo_bootstrap_repository (so the binary is pinned to the same Rust toolchain + uv source revision across every machine in the org).

  2. @rules_uv//pip:pip.parse — a uv.lock@pip module extension. Same shape as rules_python’s pip_parse, but driven by uv’s resolver output: one Bazel-fetched repo per package, an aggregating hub repo with a requirement("<name>") macro, and transitive deps wired up by the lockfile.

Status: v0.7

What v0.7 adds on top of v0.6:

  • Editable workspace rootsuv.lock entries with source = { editable = "." } (uv’s standard pattern for a workspace’s own project) are now skipped rather than rejected. Their attached dev-dependencies table is mined for PEP 735 dependency groups.
  • PEP 735 dependency groups — new group(name) macro in the hub’s requirements.bzl returns labels for every package in a named group. Markers + extras per edge are honoured.
load("@pip//:requirements.bzl", "requirement", "group")

py_test(
    name = "tests",
    deps = [requirement("my_lib")] + group("dev"),
)

What ships:

  • uv binary, two interchangeable paths:
    • source = "build" (default) — built from astral-sh/uv source via rules_rust’s cargo_bootstrap_repository. ~12-min cold build; cached after. Highest hermeticity.
    • source = "prebuilt" — fetches the official release asset for the host platform (darwin_aarch64, darwin_x86_64, linux_aarch64, linux_x86_64). Seconds to fetch.
  • pip.parse reads uv.lock and materializes:
    • Pure-Python wheels (py3-none-any) — http_archive unpacks the wheel as a zip.
    • Native wheels (manylinux_*, macosx_*_arm64, …) — PEP 425 / PEP 600 tag scoring against the host triple + python_version. Best-matching wheel wins.
    • Sdists — shells to @uv//:uv (uv pip install --target=. --no-deps) at repo-rule time. Builds C extensions if the sdist has any. Choose between python = "host" (uses python3 on PATH) and python = "uv" (uses uv python install <version>).
    • Git sources (source = { git = "…", rev = "…" }) — fetched via new_git_repository with the BUILD wrapper.
    • Path sources (source = { path = "…" }) — symlinked into a Bazel repo via a thin new_local_repository-shaped rule.
    • Editable sources — explicitly rejected with a clear error.
  • Extras (requirement("pkg[extra]")) — per-extra Bazel sub-targets generated from each package’s [package.optional-dependencies] table. The extra target re-exports :pkg plus the extra’s deps.
  • Markers (marker = "python_full_version < '3.11'", etc.) — evaluated at extension time against the configured python_version
    • host platform. Edges whose markers fail are filtered out. PEP 508 subset: python_version, python_full_version, os_name, sys_platform, platform_system, platform_machine, extra; comparisons + and/or/not/in/not in/grouping.
  • Cross-platform wheels + pure-Python sdists (pip.parse(platforms = [...])) — when the consumer opts into multi-platform mode:
    • Packages with platform-divergent native wheels fan out into per-platform repos behind a selector that alias-select()s on @platforms//os + @platforms//cpu.
    • Pure-Python wheels stay single-repo.
    • Pure-Python sdists (v0.6) install once on the host with a native-extension check; pure-Python results become a single repo serving every target platform. Sdists that build native code still fail loudly (cross-arch reuse of a host-built .so/.dylib/.pyd is unsafe).
    • Git + path sources remain host-only and fail loudly under a cross-platform build.
  • End-to-end smoke tests:
    • examples/smoke/ — host-only build with all five lockfile-feature paths: pure wheel, native wheel, sdist install, certifi[bundle] extra, marker-gated dep filtered out.
    • examples/multiplatform/ — multi-platform lockfile + py_test that resolves a native wheel through the per-platform selector.

Deferred to v0.7+ (see docs/ROADMAP.md):

  • Migration to rules_python’s uv_toolchain once it leaves experimental.
  • Native-extension sdists in multi-platform mode (cross-compile).
  • musl + Windows platform tag tables in pip/private/platform.bzl.

Architecture

//uv                       uv binary + toolchain
  defs.bzl                 user-facing rule: uv_run
  toolchains.bzl           uv_toolchain rule
  extensions.bzl           module extension: fetch source + cargo_bootstrap
  private/known_versions.bzl  pinned uv versions + sha256s

//pip                      uv.lock → @pip
  extensions.bzl           module extension: pip.parse
  private/uvlock_to_json.py  TOML → JSON shim (uses py 3.11 stdlib)
  private/wheel_selection.bzl  pure-wheel-first artifact picker
  private/pip_package.BUILD.tpl  per-package BUILD template

//examples/smoke           end-to-end smoke test

Install

.bazelrc:

common --registry=https://registry.fastverk.com/
common --registry=https://bcr.bazel.build/

MODULE.bazel:

bazel_dep(name = "rules_uv", version = "0.5.0")
bazel_dep(name = "rules_python", version = "1.7.0")

uv = use_extension("@rules_uv//uv:extensions.bzl", "uv")
# Optional — omit the tag for the default `source = "build"` path.
uv.toolchain(source = "prebuilt")
use_repo(uv, "uv", "uv_source")
register_toolchains("@rules_uv//uv:uv_toolchain_def")

pip = use_extension("@rules_uv//pip:extensions.bzl", "pip")
pip.parse(
    hub_name = "pip",
    lock = "//:uv.lock",
    python_version = "3.12",   # used for wheel tag matching
    python = "host",           # "host" (python3 on PATH) | "uv"
)
use_repo(pip, "pip")

pip.parse

In a BUILD file:

load("@pip//:requirements.bzl", "requirement")
load("@rules_python//python:defs.bzl", "py_library")

py_library(
    name = "app",
    srcs = ["app.py"],
    deps = [
        requirement("idna"),
        requirement("certifi"),
    ],
)

requirement(name) is case-insensitive and accepts the same spellings PyPI does (_, -, . are folded together per PEP 503).

uv_run

bazel run-able wrapper around uv <subcommand> against the live workspace source (escapes the sandbox so uv lock, uv pip sync, etc. can write into the user’s tree):

load("@rules_uv//uv:defs.bzl", "uv_run")

uv_run(
    name = "lock",
    subcommand = "lock",
)

uv_run(
    name = "sync",
    subcommand = "pip",
    args = ["sync", "requirements.txt"],
)
bazel run //:lock
bazel run //:sync -- --refresh

Versioning

rules_uv versions track its own surface, not uv’s. The pinned uv version lives in uv/private/known_versions.bzl; override with uv.toolchain(version = "<new>") in your MODULE.bazel.

Usage#

Real usage, taken from the module’s examples/.

examples/multiplatform/BUILD.bazel

load("@multipip//:requirements.bzl", "requirement")
load("@rules_python//python:defs.bzl", "py_test")

py_test(
    name = "multiplatform_test",
    srcs = ["multiplatform_test.py"],
    deps = [
        requirement("idna"),         # pure wheel: single repo, no select
        requirement("markupsafe"),   # native wheel: per-platform select
        requirement("six"),          # pure-python sdist-only: single repo via host install (v0.6)
    ],
)

examples/smoke/BUILD.bazel

load("@pip//:requirements.bzl", "group", "requirement")
load("@rules_python//python:defs.bzl", "py_test")
load("@rules_uv//uv:defs.bzl", "uv_run")

py_test(
    name = "smoke_test",
    srcs = ["smoke_test.py"],
    deps = [
        # Base targets.
        requirement("idna"),
        requirement("markupsafe"),
        # Per-extra target: certifi's synthetic `bundle` extra
        # re-exports `:pkg` and adds a dep on idna. We pull this
        # instead of plain `requirement("certifi")` to exercise
        # the `pkg[extra]` resolution path.
        requirement("certifi[bundle]"),
    # PEP 735 dependency-group expansion (v0.7). The smoke lockfile's
    # editable root carries `[package.dev-dependencies] dev = [{name = "iniconfig"}]`
    # — `group("dev")` returns `[Label("@pip//:iniconfig")]`.
    ] + group("dev"),
)

# Smoke target for `uv_run` — `bazel run //examples/smoke:uv_version`
# prints the uv version (via the sandbox-escaping wrapper).
uv_run(
    name = "uv_version",
    subcommand = "--version",
)

Rules & providers#

Generated with Stardoc from the module's .bzl sources.

from docs/ROADMAP.md

rules_uv roadmap

v0.1

  • @uv//:binary built from source via cargo_bootstrap_repository.
  • uv_run macro: sandbox-escaping bazel run wrapper.
  • pip.parse module extension: uv.lock@pip hub + per-package repos.
  • Pure-Python wheel materialization (py3-none-any).
  • Sdist fallback (raw download; no build step yet).
  • End-to-end smoke test in examples/smoke.

v0.2 (this release)

  • Prebuilt-uv toolchain alternative. uv.toolchain(source = "prebuilt") fetches the official release asset for the host platform from astral-sh/uv releases. Supported hosts today: darwin_{aarch64,x86_64} and linux_{aarch64,x86_64}. musl + 32-bit + Windows triples are intentionally omitted until someone needs them — pinning shas we never test is security theater.
  • Unified target shape. Both build and prebuilt produce @uv//:binary as a File; uv_toolchain accepts the file directly (no more :install rust_binary indirection).

v0.3 (this release)

  • Native wheel selection. PEP 425 / PEP 600 tag scoring in pip/private/wheel_selection.bzl: parse wheel filenames, fan out compressed tag fields, score against a host-specific ordered tag list (pip/private/platform.bzl). MVP covers the 4 fastverk hosts (darwin_{aarch64,x86_64}, linux_{aarch64,x86_64}); rules_python’s whl_target_platforms is more thorough and will be the backing implementation once their internals stabilize.
  • Sdist installation via uv. sdist_install_repo (pip/private/sdist_install.bzl): downloads the sdist, shells to @uv//:uv (uv pip install --target=. --no-deps) at repo-rule time. Python interpreter via python = "host" (python3 on PATH) or python = "uv" (uv python install into a per-repo scratch dir).
  • python_version + python attrs on pip.parse. Wheel tag matching consults python_version; sdist install dispatches on python.

v0.4 (this release)

  • Extras: requirement("pkg[extra]") resolves to a per-extra Bazel target that re-exports :pkg plus the extra’s deps. Generated from each package’s [package.optional-dependencies] table.
  • Markers: PEP 508 subset evaluated at extension time against python_version + host platform. Edges whose markers fail are filtered out. Cross-platform select() is v0.5.
  • Git sources (source = { git = "…", rev = "…" }): new_git_repository with the BUILD wrapper.
  • Path sources (source = { path = "…" }): new_local_repository-style symlink rule.
  • Editable sources: explicit failure with a clear message (editable installs don’t translate to Bazel).
  • Hermetic uv invocation: --no-config on all uv pip and uv python install calls so the user’s ~/.config/uv/uv.toml (which on many machines points at a private index) doesn’t leak into sandbox builds.

v0.5 (this release)

  • Cross-platform wheels. pip.parse(platforms = [...]) opts the hub into multi-platform mode. Packages with platform-divergent native wheels fan out into per-platform repos (@<hub>__<pkg>__<platform>) behind a selector repo that emits alias(name = "pkg", actual = select(...)) over @platforms//os + @platforms//cpu constraint values. Non-host platform repos are declared but lazy-fetched — they only land on disk when Bazel’s configuration triggers that branch of the select().
  • Multi-platform smoke (examples/multiplatform/): pure-python wheel (idna) flows through the single-repo path; native wheel (markupsafe) flows through the per-platform select.

v0.6 (next)

Smoke fixtures for git + path sources

v0.4 wires git/path source materialization, but no smoke fixture exercises either. A fixture that lock-files a tiny pure-Python package from a pinned GitHub commit + a sibling local path package would catch regressions.

Sdist install in multi-platform mode

Today sdist install is host-only — if a multi-platform lockfile references an sdist-only package, the extension fails fast rather than silently producing a broken cross-platform target. v0.6 could support per-platform sdist installs by running uv pip install --target once per requested platform (each producing its own per-platform repo). Requires either cross-compilation toolchains on the host (rare) or Bazel platform-transition magic.

musl + Windows platform tag tables

pip/private/platform.bzl ships tag tables for the four fastverk hosts only. Adding musllinux + Windows entries (with @platforms//os:windows and a musl libc constraint) is mechanical once a consumer needs them.

Marker evaluator: spot tests

pip/private/markers.bzl is a hand-rolled PEP 508 subset parser. A skylib unittest suite covering operators, precedence, and the python_full_version vs python_version edge cases would lock the behavior down.

Beyond v0.6

  • uv_pip_compile: bazel run-able workflow to regenerate requirements.txt from a pyproject.toml (analogous to rules_uv upstream’s compile workflow).
  • Cross-platform wheels: support emitting select() deps when a package has multiple platform wheels but the consumer wants to target several configurations from one tree.
  • Stardoc-generated reference in /docs.

Delete uv/ when rules_python’s uv is stable

rules_python ships its own experimental uv toolchain primitive at @rules_python//python/uv:uv_toolchain.bzl and a binary-fetching module extension at @rules_python//python/uv:uv.bzl. Both are marked EXPERIMENTAL: This is experimental and may be removed without notice, so today rules_uv carries its own toolchain + fetch + build paths.

When rules_python promotes these out of experimental, rules_uv’s uv/ directory becomes pure duplication and should be removed:

  • Drop uv/extensions.bzl, uv/toolchains.bzl, uv/private/known_versions.bzl, uv/private/uv_source.BUILD.bazel.
  • Replace our uv_run macro with one that resolves through rules_python’s uv_toolchain_type.
  • The pip extension keeps using @uv//:binary at repo-rule time (just pointing at whichever target rules_python’s extension materializes by then).

This trims rules_uv down to its actual reason for existing: the uv.lock TOML → @pip materializer. Track upstream status at https://github.com/bazelbuild/rules_python/issues/ (search for “uv toolchain experimental”).

from docs/pip_extensions.md

pip_parse module extension — uv.lock → @ + per-pkg repos.

Counterpart to rules_python’s pip_parse, but driven by uv.lock instead of requirements.txt. For each package the lockfile resolves to, we create a Bazel-fetched repo containing the unpacked wheel (or installed sdist, or fetched git/path source). A hub repo aggregates these and exposes a requirement("<name>") macro plus pre-aliased @<hub>//<name>:pkg labels.

Consumer:

pip = use_extension("@rules_uv//pip:extensions.bzl", "pip")
pip.parse(
    hub_name = "pip",
    lock = "//:uv.lock",
    python_version = "3.12",
)
use_repo(pip, "pip")

Extras are exposed as additional sub-targets on the package repo:

load("@pip//:requirements.bzl", "requirement")
py_library(
    name = "app",
    deps = [
        requirement("requests"),              # base package
        requirement("requests[security]"),    # base + extra deps
    ],
)

Markers (e.g. marker = "python_version < '3.11'") are evaluated at extension time against the configured python_version + host platform. Edges whose markers fail are silently dropped from the generated BUILD — keeping the host-only view simple. Cross-platform select() is v0.5.

pip

pip = use_extension("@rules_uv//pip:extensions.bzl", "pip")
pip.parse(hub_name, lock, platforms, python, python_version, uv)

Materialize @ + per-pkg repos from a uv.lock.

TAG CLASSES

parse

Attributes

NameDescriptionTypeMandatoryDefault
hub_nameName of the hub repo (the @<hub_name>//… namespace).Stringoptional"pip"
lockLabel pointing at a uv.lock file.Labelrequired
platformsOptional list of <os>_<arch> platforms this lockfile should support. Default is host-only (the v0.4 behavior — select() is not introduced). Supported entries: darwin_aarch64, darwin_x86_64, linux_aarch64, linux_x86_64. Packages with platform-divergent native wheels fan out into per-platform repos behind a select() alias; sdist/git/path packages remain host-only and the build will fail loudly if a non-host platform tries to resolve them.List of stringsoptional[]
pythonHow to find a Python interpreter for sdist install. host uses python3 on PATH; uv runs uv python install <python_version> per package.Stringoptional"host"
python_versionPython major.minor used for wheel-tag matching and (when python = “uv”) the uv-managed interpreter.Stringoptional"3.12"
uvLabel of the uv binary used to install sdists.Labeloptional"@uv"

from docs/uv_defs.md

User-facing rules for rules_uv.

  • uv_run — sh_binary macro: bazel run //path:NAME invokes uv <subcommand> against the live workspace source. Intentionally non-hermetic (escapes the runfiles sandbox) for the dev loop (uv pip sync, uv lock, uv run …).

Lockfile-driven Python repo materialization lives in @rules_uv//pip:extensions.bzl (pip_parse), which is the rules_uv analogue of rules_python’s pip_parse but reads uv.lock rather than requirements.txt.

uv_run

load("@rules_uv//uv:defs.bzl", "uv_run")

uv_run(name, subcommand, args, **kwargs)

bazel run-able wrapper around uv <subcommand>.

Escapes the runfiles sandbox via BUILD_WORKSPACE_DIRECTORY so uv operates on the user’s source tree (uv lock, uv pip sync … both need to write into the workspace).

PARAMETERS

NameDescriptionDefault Value
nametarget name.none
subcommandfirst arg passed to uv (e.g. pip, lock, run).none
argsextra args appended after the subcommand.None
kwargsforwarded to the underlying sh_binary.none

from docs/uv_toolchains.md

Toolchain wrapper for the uv binary.

UvToolchainInfo.uv is a File for the uv executable. Consumers resolve it via ctx.toolchains["@rules_uv//uv:toolchain_type"].

The attr uses allow_single_file = True rather than executable = True because the bootstrapped binary at @uv//:binary is an alias to a source File (cargo_bootstrap_repository’s output) — Bazel rejects source files as executable attr inputs, so we accept the file and let the consuming rule mark it executable itself.

uv_toolchain

load("@rules_uv//uv:toolchains.bzl", "uv_toolchain")

uv_toolchain(name, uv)

Declares a uv toolchain.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
uvLabel of the uv binary (either built via cargo_bootstrap_repository or fetched as a prebuilt release asset).Labelrequired

UvToolchainInfo

load("@rules_uv//uv:toolchains.bzl", "UvToolchainInfo")

UvToolchainInfo(uv)

Information about a uv toolchain.

FIELDS

NameDescription
uvFile pointing at the uv executable.

Conformance#

2 findings across 1 invariant. 10 contested atoms. See how gating works or the full report.

D2 a non-dev register_toolchains propagates to every transitive consumer why this matters ↗
versiontoolchain
0.7.4//uv:uv_toolchain_def
0.7.4@rust_toolchains//:all

Contested atoms

Third-party modules where this module resolves a different version than others do. Not a violation of anything this module did — it is the actionable form of a registry-level convergence finding, and the sentence a maintainer can act on.

AtomResolved hereElsewhere
apple_support 1.24.2 2.2.0 ×1
bazel_skylib 1.8.2 1.9.0 ×2
gazelle 0.36.0 0.30.0 ×50.44.0 ×10.51.0 ×3
nlohmann_json 3.6.1 3.12.0.bcr.1 ×1
protobuf 33.4 34.0.bcr.1 ×2
rules_go 0.60.0 0.39.1 ×5
rules_jvm_external 6.7 6.8 ×4
rules_python 1.7.0 2.0.1 ×1
rules_swift 3.1.2 3.6.1 ×1
upb 0.0.0-20220923-a547704 0.0.0-20230516-61a97ef ×1

Dependencies#

rules_uv in the registry graph — what it depends on (left) and what depends on it (right).

Depends on

platforms1.0.0bazel_skylib1.8.2rules_rust0.70.0rules_python1.7.0rules_shell0.6.1devstardoc0.7.2devrules_jsonschema0.1.0dev

Used by (2 in the registry)

Versions#

12 published versions, newest first. Each resolves to an immutable, integrity-checked archive.

VersionIntegrity (sha256)Source archive
0.7.4 latest vlPl/TGBHaNF+cbw… tag archive ↗
0.7.3 3zI+FgC7T2Lts14X… tag archive ↗
0.7.2 BlIFBfKwGbxUpHp1… tag archive ↗
0.7.1 zEr82psXnaxkSUt4… tag archive ↗
0.7.0 LakkT3oy5Q8U0L10… tag archive ↗
0.6.0 lbx67u5jTIBi+qnr… tag archive ↗
0.5.1 Jilgp1SIwiUNjMqE… tag archive ↗
0.5.0 eG6oYlgcwSWU0Gdw… tag archive ↗
0.4.0 Q/pI4oqdtIcGC+iy… tag archive ↗
0.3.0 0hLHwQt6YEefJneQ… tag archive ↗
0.2.0 0nv8x5Jqf8R8tbon… tag archive ↗
0.1.0 eFUd4ga9sCLmgF34… tag archive ↗

Changelog#

All notable changes to rules_uv. The format is loosely Keep a Changelog — version headers mirror the published bazel-registry entries.

0.7.4 — extras wiring + nested extras + 3.11 shim

Three fixes to pip.parse’s extras handling, surfaced wiring deep pypi tools (e.g. awslabs.aws-api-mcp-server) hermetically:

  • Lock shim interpreter: prefer a versioned python3.13/3.12/3.11 for the uv.lock → JSON step. A bare python3 is frequently the system 3.9 on macOS, which lacks stdlib tomllib.
  • Extras-edge key: the shim projects requested extras under extras (a list), but the extension read extra — so pkg[extra] dependency edges were silently never wired to the dep’s per-extra target. A consumer of foo[bar] now depends on @hub__foo//:bar.
  • Nested extras: the shim preserves each extra member’s own extras, and per-extra targets wire them recursively through the target graph — e.g. fastmcp-slim[client,server]py-key-value-aio[filetree,keyring,memory]cachetools.

0.7.3 — uv resolution-markers

uv’s resolver can emit multiple [[package]] entries with the same package name, each gated by a resolution-markers array that scopes it to a subset of envs (most commonly per-Python- minor variants — selectsmart-dsi 2026.1.dev53787 for python_full_version == '3.12.*' alongside 2026.1.dev53830 for >= '3.13'). Pre-v0.7.3, we treated all entries as candidates and the extension crashed with “A repo named X is already generated by this module extension”.

v0.7.3: uvlock_to_json.py projects the resolution-markers array; _pip_extension_impl filters package entries up front, keeping only the env-matching variant. Empty lists (the unconditional common case) are unaffected. The reachability walk + materialize loop see a deduplicated package list.

0.7.2 — graceful skip for platform-incompatible packages

Real-world lockfiles often pin packages whose wheels only cover some platforms (Datadog’s ddtrace, the SAVVI private mirror’s build of ddtrace ships Linux + Windows wheels only; macOS arm64 local dev hits a wall). The reachability filter walks every edge without a marker, so these packages get visited and v0.7.1 then failed pip.parse for the entire lockfile.

v0.7.2:

  • select_artifact() returns None instead of fail()-ing when no host-compatible wheel exists and no sdist fallback is available.
  • _make_pkg_repo returns the sentinel "skipped:no-artifact" for these; the caller drops them from the hub’s requirement() set. Consumer-side requirement("ddtrace") then fails with “unknown package” + the known-packages list — the right surface for that error vs. a top-level pip.parse explosion.
  • Multi-platform mode (pip.parse(platforms = [...])) keeps the fail-loudly behavior; cross-platform builds genuinely need a resolvable artifact on every requested platform.

0.7.1 — real-world lockfile bug fixes

Surfaced when wiring selectsmart-engine (an 89-package uv workspace lockfile) through rules_uv end-to-end:

  • Starlark {!r} format strings: Starlark’s str.format() doesn’t support Python’s {!r} repr conversion. We had ~10 instances in error-path code (markers.bzl, wheel_selection.bzl, pip/extensions.bzl) that crashed with Error in format: Missing argument '!r' whenever they fired — masking the actual upstream errors. Replaced with explicit '{}' quoting.
  • PEP 508 environment variables: added implementation_name = "cpython" and platform_python_implementation = "CPython" to the marker env. selectsmart-engine’s lockfile carries marker = "implementation_name == 'pypy'" and similar on a few edges; without these vars the marker parser failed loudly. rules_uv is CPython-only today, so the values are constants.
  • Reachability filter: lockfiles list every package the resolver considered, including platform-gated ones like pywin32 (only reached via sys_platform == 'win32' edges). On hosts where those edges fail, the packages have no host-compatible artifact and we used to hard-fail trying to materialize them. v0.7.1 walks the dependency graph from each root (editable/virtual entries + dep-groups), filtering edges by host markers, and only materializes the reachable set. Extras are pulled in transitively.
  • PEP 425 abi3 stable-ABI relaxation: a wheel tagged cp38-abi3-<plat> (e.g. tornado, cryptography) is installable on any CPython ≥ 3.8 with the same platform. Our wheel selector required exact cpXY matches and rejected the wheel. Fixed by building an abi3_compatible_py_tags() helper that returns [cp{host_minor}, cp{host_minor-1}, …, cp30] and using it for py-tag scoring only when the wheel declares abi3 in its abi field.

No API changes; existing fixtures still pass.

0.7.0 — editable workspace roots + PEP 735 dependency groups

  • Editable workspace roots are now skipped, not rejected. uv writes source = { editable = "." } on the project’s own entry in uv.lock; v0.5 explicitly failed with “convert to a path source”. v0.7 skips the entry silently — the consumer’s source already lives in their own Bazel targets — but still mines the attached dev-dependencies table for dep-groups (see below). Closes the v0.6 compatibility gap that blocked uv workspaces.

  • PEP 735 dependency groups are now first-class. The lockfile parser (uvlock_to_json.py) hoists [package.dev-dependencies] (which despite the name carries every named group — dev, test, docs, …) to a top-level map. Per-edge markers and extras are honoured. The hub repo emits a new group(name) macro alongside requirement():

    load("@pip//:requirements.bzl", "requirement", "group")
    py_test(
        name = "tests",
        deps = [requirement("my_lib")] + group("dev"),
    )

    ALL_GROUPS constant lists every defined group name for introspection. Unknown group name fails the load with the known set.

  • Smoke fixture now exercises both: lockfile’s smoke entry is editable = "." with a dev = [{name = "iniconfig"}] group; examples/smoke:smoke_test pulls iniconfig via group("dev").

0.6.0 — pure-Python sdists in multi-platform mode

  • Pure-Python sdists are now allowed in pip.parse(platforms = ...) lockfiles. The extension installs the sdist once on the host with sdist_install_repo(forbid_native_extensions = True); the result is a single platform-agnostic repo shared by every target platform (same shape as a pure-Python wheel — no selector needed). v0.5 had this case fail loudly; the v0.6 path closes the gap for the common case (one-file utility packages that never bothered shipping wheels — six 1.4.1 is the smoke- test case in examples/multiplatform/).
  • Sdists that produce native extensions on the host install (.so / .pyd / .dylib / .dll files in the repo root) still fail loudly — cross-arch reuse of a host-built native extension would silently link wrong-arch binaries into other platforms. The error message points at the alternatives (pin a wheel, or drop the unsupported platform from platforms).
  • New sdist_install_repo(forbid_native_extensions) attr; default False so single-platform host-only installs work exactly like before.

0.5.1 — docs + CI infrastructure

  • Stardoc-generated reference docs in docs/ for uv_run, uv_toolchain + UvToolchainInfo, and the pip module extension. bazel run //docs:update regenerates; CI gates the committed copies via diff_test.
  • GitHub Actions CI: bazel test //... on ubuntu + macos, plus a buildifier lint job.
  • CHANGELOG.md (this file).

0.5.0 — cross-platform wheel select()

  • pip.parse(platforms = [<os>_<arch>, ...]) opts a hub into multi-platform mode. Packages with platform-divergent native wheels fan out into per-platform repos (@<hub>__<pkg>__<platform>) behind a selector repo that emits alias(actual = select({...})) over @platforms//os + @platforms//cpu. Non-host platform repos are declared but lazy-fetched.
  • Pure-Python wheels stay single-repo (platform-agnostic). Sdist + git + path sources stay host-only, and the extension fails loudly if a multi-platform lockfile points at an sdist-only package (sdist install is host-only — running uv pip install --target once per requested platform is on the v0.6 roadmap).
  • New examples/multiplatform/ end-to-end fixture.
  • Default platforms = [] keeps v0.4 host-only behavior intact — zero behavior change for existing consumers.

0.4.0 — extras, markers, git/path sources

  • Extras: requirement("pkg[extra]") resolves to a Bazel sub-target generated from each package’s [package.optional-dependencies]. The extra re-exports :pkg plus the extra’s filtered dep set.
  • Markers: PEP 508 subset evaluator (pip/private/markers.bzl) — recursive-descent parser covering ==/!=/</<=/>/>=/in/not in, and/or/not, grouping. Evaluated at extension time against python_version + host; edges whose markers fail are filtered out.
  • Git sources (source = { git = "...", rev = "..." }): fetched via new_git_repository.
  • Path sources (source = { path = "..." }): symlinked via a thin _path_repo rule.
  • Editable sources: explicit failure with a clear message.
  • Hermetic uv invocation: --no-config on every uv pip install and uv python install call so the developer’s ~/.config/uv/uv.toml can’t leak into sandbox builds.

0.3.0 — native wheel selection + sdist install via uv

  • Native wheel selection (pip/private/wheel_selection.bzl, pip/private/platform.bzl): PEP 425/600 tag scoring against a host-specific ordered tag list. Picks the best manylinux_*/macosx_*-tagged wheel for the host.
  • Sdist install (pip/private/sdist_install.bzl): sdist_install_repo repository_rule that downloads the sdist and shells to @uv//:uv (uv pip install --target=. --no-deps) at repo-rule time.
  • New pip.parse attrs: python_version (3.12 default, wheel tag matching) and python (host | uv, sdist install interpreter source).

0.2.0 — prebuilt-uv toolchain alternative

  • uv.toolchain(source = "prebuilt") fetches the official astral-sh/uv release asset for the host platform from GitHub Releases. Skips the ~12-min source-build cold path.
  • Supported hosts at pin: darwin_{aarch64,x86_64}, linux_{aarch64,x86_64}.
  • uv_toolchain.uv now takes a File label (allow_single_file = True) so both build and prebuilt modes satisfy it via @uv//:binary uniformly. The :install rust_binary indirection is gone.

0.1.0 — initial release

  • @uv//:binary built from astral-sh/uv source via rules_rust’s cargo_bootstrap_repository.
  • uv_run macro: sandbox-escaping bazel run wrapper.
  • pip.parse module extension: uv.lock@pip hub with per-package repos and a requirement("<name>") macro.
  • Pure-Python wheel materialization (py3-none-any) + raw-sdist fallback (no build step).
  • End-to-end smoke test in examples/smoke/.

← All modules