uvr build: Layered Builds Without Sources
uvr builds interdependent packages in topological layers, making earlier wheels available to later builds via --find-links. Here is a workspace where pkg-beta has a build-time dependency on pkg-alpha.
# packages/pkg-beta/pyproject.toml
[build-system]
requires = ["hatchling", "pkg-alpha"]
build-backend = "hatchling.build"You're releasing both simultaneously. pkg-alpha 0.1.5 doesn't exist on PyPI yet, and uv build uses build isolation where [tool.uv.sources] isn't available. uvr solves this by building in order.
# Layer 0 — no internal deps
uv build packages/pkg-alpha --out-dir dist/ --find-links dist/
# Layer 1 — depends on layer 0 (concurrent)
uv build packages/pkg-beta --out-dir dist/ --find-links dist/
uv build packages/pkg-delta --out-dir dist/ --find-links dist/
# Layer 2 — depends on layer 1
uv build packages/pkg-gamma --out-dir dist/ --find-links dist/--find-links dist/ tells uv's build isolation to resolve [build-system].requires from local wheels first.
Why --no-sources?
uvr passes --no-sources to uv build because source resolution during a release build is problematic.
- Correctness. Without it, an isolated build may pull an old version of the dependency from PyPI instead of the one you're releasing.
- Fidelity. Source builds (especially editable installs) can skip build logic that the real wheel includes.
- Efficiency. Every isolated
uv buildwould rebuild dependencies from source. Pre-built wheels avoid redundant work.
Topological sorting
Layer assignment via topo_layers() uses a modified Kahn's algorithm.
Input:
pkg-alpha deps: [] → layer 0
pkg-beta deps: [pkg-alpha] → layer 1
pkg-delta deps: [pkg-alpha] → layer 1
pkg-gamma deps: [pkg-beta] → layer 2Packages in the same layer have no interdependencies and build concurrently. Cycles raise RuntimeError.
Per-runner build matrix
Runners are configured per-package.
[tool.uvr.runners]
pkg-alpha = [["ubuntu-latest"], ["macos-latest"]]
pkg-beta = [["ubuntu-latest"]]Packages not listed default to [["ubuntu-latest"]]. Labels are lists for composite runner selection (e.g., ["self-hosted", "linux", "arm64"]).
The matrix is per-runner, not per-package. Each runner builds all its assigned packages in topological order, keeping wheels in a local dist/. This avoids coordinating artifact passing between separate CI jobs for build-time deps.
Build stages
For each runner, the planner generates a sequence of stages.
Setup. Create dist/ and fetch wheels for unchanged transitive deps.
mkdir -p dist
gh release download pkg-beta/v0.2.0 --pattern "*.whl" --dir dist/Build. One stage per topological layer, concurrent within each.
Layer 0:
pkg-alpha:
uv version 0.1.5 --directory packages/pkg-alpha
uv build packages/pkg-alpha --out-dir dist/ --find-links dist/
Layer 1: (concurrent)
pkg-beta:
uv version 0.2.0 --directory packages/pkg-beta
uv build packages/pkg-beta --out-dir dist/ --find-links dist/
pkg-delta:
uv version 0.3.0 --directory packages/pkg-delta
uv build packages/pkg-delta --out-dir dist/ --find-links dist/Cleanup. Remove wheels for packages built only as transitive deps (not assigned to this runner).
Parallel execution
- Stages run sequentially. Layer 1 waits for layer 0.
- Packages within a stage run concurrently via
ThreadPoolExecutor.
CI execution
Each runner gets its own CI job.
strategy:
fail-fast: false
matrix:
runner: ${{ fromJSON(inputs.plan).build_matrix }}
runs-on: ${{ matrix.runner }}uvr jobs build --plan "$UVR_PLAN" --runner '${{ toJSON(matrix.runner) }}'Each runner uploads wheels as wheels-<runner-labels>. The release job downloads all wheels-* artifacts and merges them.