Skip to content

feat(uv): build Rust sdists with the project's rules_rust or rules_rs toolchain - #1526

Open
xangcastle wants to merge 19 commits into
mainfrom
xangcastle/pep517-rust-toolchain
Open

xangcastle wants to merge 19 commits into
mainfrom
xangcastle/pep517-rust-toolchain

Conversation

@xangcastle

@xangcastle xangcastle commented Sep 3, 2026

Copy link
Copy Markdown
Member

Rust sdists (maturin, setuptools-rust) build with the Rust toolchain the project registers. rules_py depends on neither rules_rust nor rules_rs and works with both.

API

  • uv.rust_toolchain(toolchain = "@rules_rust//rust/toolchain:current_rust_toolchain", lock = ...): module-wide or per lock, the lock wins. sdist_build wires it, plus an exec-configured rust_host_sysroot layer, into every sdist whose backend is maturin, or setuptools with setuptools-rust declared in its build requirements. Such an sdist with no toolchain fails at repository generation naming the declaration to add. Stray .rs files (zstandard's optional extension) neither wire nor demand one.
  • uv.override_package(cargo_lock = ...): a Cargo.lock for an sdist that ships none. Crates are vendored from the lock at repository generation and cargo builds offline; git and path sources are rejected.
  • bazel run @sdist_build__<project>__<pkg>__<ver>//:cargo_lock writes that lock with the project's toolchain.

Build

  • Cross: the target toolchain's sysroot merged with the exec platform's rust-std, so build scripts and proc-macros compile against the exec std. cc-rs finds the wired C toolchain under CC_<triple>, CXX_<triple>, AR_<triple> and RANLIB_<triple>.
  • Reproducible wheels: --remap-path-prefix for sandbox and execroot, -C codegen-units=1 and a host-independent -C metadata= for target crates, maturin's SBOM off. MATURIN_NO_INSTALL_RUST=1 on every build.

e2e/crossbuild: pycross-rust (rules_rust), pycross-rust-rs (rules_rs, lock-scoped), pycross-tiktoken (setuptools-rust, cargo_lock); their wheels run on native amd64 and arm64 runners. test.sh aggregates per-case scripts; pycross-tiktoken's runs :cargo_lock. The BUILD every sdist_build repository generates is snapshotted under snapshots/, so which sdists get toolchains, the Rust layer or vendored crates is a reviewable diff.


Changes are visible to end-users: yes

New uv.rust_toolchain(), uv.override_package(cargo_lock = ...) and :cargo_lock targets. Rust sdists no longer need a per-package uv.override_package(toolchains = [...]).

Test plan

  • Analysis, Starlark and helper unit tests for the wiring, the vendoring, the rustc wrapper and the lock generator.
  • cd e2e/crossbuild && bazel test //... && bash test.sh passes locally with the sandbox network blocked, cross-building pydantic-core (rules_rust and rules_rs) and tiktoken for linux/amd64 and linux/arm64 from a macOS host.
  • The same commit checked out at two paths builds byte-identical cross pydantic-core wheels.
  • docs/uv.md updated.

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: d93f339e-c22e-49ea-acfc-d2824a68e32d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

py_binary startup benchmark

Version Time (ms) vs BCR vs main Build (s)
BCR 1.11.7 157.7/157.6 ±1.0 1.87
main 55.5/55.4 ±0.6 -64.9% 1.68
PR 55.3/55.2 ±0.5 -65.0% -0.3% 1.51

Time = mean/median ±stddev.
Measured with hyperfine --warmup 5 --runs 50 --shell=none on Linux
Gate: PR vs HEAD main median (threshold: 10%, and must exceed the 2×SE noise floor, here 0.4%). BCR is shown only as a historical baseline.
Build time: cold bazel build //:bench with isolated output base, no disk cache; external repos prefetched so network is excluded.

sys.path quality

Version entries sp roots dupes
BCR 1.11.7 6 1 0
main 7 2 0
PR 7 2 0

sys.path quality measured by bench_syspath inside the assembled venv: sys.path entries, distinct site-packages roots, duplicate realpaths. Duplicates indicate symlink redundancy; many roots suggest an inefficient venv layout.

Bazel analysis benchmark

Version Time (ms) vs BCR vs main Targets Actions Configured (ext) Cfg/target Actions/cfg
BCR 2.0.0-alpha.6 948/902 ±134 404 20613 7380 (6675) 18.3 2.79
main 965/952 ±158 +5.5% 404 18363 7379 (6674) 18.3 2.49
PR 974/967 ±160 +7.2% +1.6% 404 18363 7379 (6674) 18.3 2.49

Measured with hyperfine --warmup 1 --runs 10 on Linux
Time = mean/median ±stddev. Cfg = configured targets; (ext) = the count in external repos (the @pypi hub machinery).
Gate: PR vs HEAD main median (threshold: 10%, and must exceed the 2×SE noise floor, here 15.0%). BCR is shown only as a historical baseline.
Command: warm-server bazel build --nobuild //workspace/..., analysis cache discarded each run via a fresh --action_env value; no disk cache.
Workload: 100 py_test targets — 2 test files/package in file generation mode (one venv per test file); a single dep_group.

py_image_layer benchmark

Scenario Version Time (s) Actions vs BCR vs main
analysis BCR 2.0.0-alpha.6 0.70/0.65 ±0.14 747
analysis main 0.66/0.59 ±0.14 668 -9.1%
analysis PR 0.68/0.62 ±0.14 668 -4.8% +4.7%
1p source BCR 2.0.0-alpha.6 0.68/0.67 ±0.04 4
1p source main 0.69/0.68 ±0.05 5 +0.6%
1p source PR 0.69/0.67 ±0.05 5 -0.7% -1.3%
3p wheel BCR 2.0.0-alpha.6 0.82/0.82 ±0.02 5
3p wheel main 0.86/0.85 ±0.03 6 +4.6%
3p wheel PR 0.86/0.86 ±0.04 6 +5.5% +0.8%

Time = mean/median ±stddev.
Measured with hyperfine on Linux, building //workspace:image_layers (10 binaries, ~30-wheel dep pool, grouped first-party/pip/interpreter tier) with isolated output base, no disk cache.
Scenarios: analysis = warm-server bazel build --nobuild, re-analyzed each run via a fresh --action_env value; incrementals run against a built state with warm analysis: source = append to the last package's lib.py, wheel = rewrite click post_install_patches content.
Actions: for Analysis, the total action count behind the image target from aquery deps(...); for incrementals, actions re-executed for the mutation, from a single instrumented run's BEP build metrics (deterministic; per-mnemonic breakdown in the *-actions.json artifacts). Informational only, not gated.
Gate: PR vs HEAD main median per scenario (threshold: 10%, and must exceed the 2×SE noise floor). BCR is shown only as a historical baseline.

Base automatically changed from xangcastle/pep517-derived-env to main September 8, 2026 12:00
@xangcastle
xangcastle force-pushed the xangcastle/pep517-rust-toolchain branch from 227775d to c303a1b Compare September 8, 2026 12:07
@aspect-workflows

aspect-workflows Bot commented Sep 8, 2026

Copy link
Copy Markdown

Aspect Workflows Tasks

📅 Fri Sep 18 02:03:11 UTC 2026

Task Results

  • buildifier · complete (clean) · 19.6s · GitHub Actions · Check
  • gazelle · complete (clean) · 18s · GitHub Actions · Check
  • test-e2e-bazel-8 [test] · complete (324/324 passed · 322 cached) · 53s · Aspect · GitHub Actions · Check
  • test-e2e-bazel-9 [test] · complete (314/314 passed · 312 cached) · 46s · Aspect · GitHub Actions · Check
  • test-e2e-crossbuild-bazel-8 [test] · complete (61/61 passed · 61 cached) · 46.9s · Aspect · GitHub Actions · Check
  • test-e2e-crossbuild-bazel-9 [test] · complete (61/61 passed · 61 cached) · 47.3s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-build-config-bazel-8 [test] · complete (1/1 passed · 1 cached) · 23.3s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-build-config-bazel-9 [test] · complete (1/1 passed · 1 cached) · 49.1s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-input-validation-bazel-8 [test] · complete (1/1 passed · 1 cached) · 18.8s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-input-validation-bazel-9 [test] · complete (1/1 passed · 1 cached) · 39s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-runtime-metadata-bazel-8 [test] · complete (2/2 passed · 2 cached) · 23.1s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-runtime-metadata-bazel-9 [test] · complete (2/2 passed · 2 cached) · 26.8s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-toolchain-settings-bazel-8 [test] · complete (1/1 passed · 1 cached) · 19.9s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-toolchain-settings-bazel-9 [test] · complete (1/1 passed · 1 cached) · 45s · Aspect · GitHub Actions · Check
  • test-e2e-rules-python-interop-bazel-8 [test] · complete (16/16 passed · 16 cached) · 50.5s · Aspect · GitHub Actions · Check
  • test-e2e-rules-python-interop-bazel-9 [test] · complete (16/16 passed · 16 cached) · 1m 55s · Aspect · GitHub Actions · Check
  • test-e2e-rules-python-protobuf-bazel-8 [test] · complete (1/1 passed · 1 cached) · 34.7s · Aspect · GitHub Actions · Check
  • test-e2e-rules-python-protobuf-bazel-9 [test] · complete (1/1 passed · 1 cached) · 36.8s · Aspect · GitHub Actions · Check
  • test-e2e-rules-python-provider-compat-bazel-8 [test] · complete (4/4 passed · 4 cached) · 21.8s · Aspect · GitHub Actions · Check
  • test-e2e-rules-python-provider-compat-bazel-9 [test] · complete (4/4 passed · 4 cached) · 1m 13s · Aspect · GitHub Actions · Check
  • test-examples-debugger-bazel-8 [test] · complete (1/1 passed · 1 cached) · 23.5s · Aspect · GitHub Actions · Check
  • test-examples-debugger-bazel-9 [test] · complete (1/1 passed · 1 cached) · 43.4s · Aspect · GitHub Actions · Check
  • test-examples-dev_deps-bazel-8 [test] · complete (1/1 passed · 1 cached) · 28.4s · Aspect · GitHub Actions · Check
  • test-examples-dev_deps-bazel-9 [test] · complete (1/1 passed · 1 cached) · 49.7s · Aspect · GitHub Actions · Check
  • test-examples-django-bazel-8 [test] · complete (1/1 passed · 1 cached) · 27.9s · Aspect · GitHub Actions · Check
  • test-examples-django-bazel-9 [test] · complete (1/1 passed · 1 cached) · 57s · Aspect · GitHub Actions · Check
  • test-examples-multi_version-bazel-8 [test] · complete (2/2 passed · 2 cached) · 27.1s · Aspect · GitHub Actions · Check
  • test-examples-multi_version-bazel-9 [test] · complete (2/2 passed · 2 cached) · 26.7s · Aspect · GitHub Actions · Check
  • test-examples-protobuf-bazel-8 [test] · complete (1/1 passed · 1 cached) · 17.4s · Aspect · GitHub Actions · Check
  • test-examples-protobuf-bazel-9 [test] · complete (1/1 passed · 1 cached) · 20.1s · Aspect · GitHub Actions · Check
  • test-examples-py_binary-bazel-8 [test] · complete (1/1 passed · 1 cached) · 21.4s · Aspect · GitHub Actions · Check
  • test-examples-py_binary-bazel-9 [test] · complete (1/1 passed · 1 cached) · 1m 1s · Aspect · GitHub Actions · Check
  • test-examples-py_pex_binary-bazel-8 [test] · complete (1/1 passed · 1 cached) · 23.4s · Aspect · GitHub Actions · Check
  • test-examples-py_pex_binary-bazel-9 [test] · complete (1/1 passed · 1 cached) · 48.8s · Aspect · GitHub Actions · Check
  • test-examples-py_venv-bazel-8 [test] · complete (3/3 passed · 3 cached) · 29.1s · Aspect · GitHub Actions · Check
  • test-examples-py_venv-bazel-9 [test] · complete (3/3 passed · 3 cached) · 48.1s · Aspect · GitHub Actions · Check
  • test-examples-pytest-bazel-8 [test] · complete (11/11 passed · 11 cached) · 26.1s · Aspect · GitHub Actions · Check
  • test-examples-pytest-bazel-9 [test] · complete (11/11 passed · 11 cached) · 50.1s · Aspect · GitHub Actions · Check
  • test-examples-uv_pip_compile-bazel-8 [test] · complete (1/1 passed · 1 cached) · 22.6s · Aspect · GitHub Actions · Check
  • test-examples-uv_pip_compile-bazel-9 [test] · complete (1/1 passed · 1 cached) · 24.8s · Aspect · GitHub Actions · Check
  • test-examples-virtual_deps-bazel-8 [test] · complete (1/1 passed · 1 cached) · 19.9s · Aspect · GitHub Actions · Check
  • test-examples-virtual_deps-bazel-9 [test] · complete (1/1 passed · 1 cached) · 37.5s · Aspect · GitHub Actions · Check
  • test-root-bazel-8 [test] · complete (386/386 passed · 386 cached) · 3m 4s · Aspect · GitHub Actions · Check
  • test-root-bazel-9 [test] · complete (385/385 passed · 385 cached) · 5m 4s · Aspect · GitHub Actions · Check

⏱ Last updated Fri Sep 18 02:08:25 UTC 2026 · 📊 GitHub API quota 469/7,700 (6% used, resets in 54m)
🚀 Powered by Aspect CLI (v2026.38.14)  |  Aspect Build · X · LinkedIn · YouTube

@xangcastle
xangcastle force-pushed the xangcastle/pep517-rust-toolchain branch from c303a1b to 7ad6e75 Compare September 8, 2026 12:36
@xangcastle
xangcastle marked this pull request as ready for review September 8, 2026 13:01
@xangcastle

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ad6e75644

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread uv/private/sdist_build/repository.bzl
@xangcastle
xangcastle requested a review from jbedard September 8, 2026 13:26
Comment thread uv/private/sdist_build/repository.bzl Outdated
Comment thread uv/private/extension/defs.bzl Outdated
Comment thread docs/uv.md Outdated
@xangcastle
xangcastle force-pushed the xangcastle/pep517-rust-toolchain branch 2 times, most recently from 32680ac to 8954140 Compare September 8, 2026 21:57
@xangcastle
xangcastle requested a review from jbedard September 8, 2026 22:18
@xangcastle
xangcastle force-pushed the xangcastle/pep517-rust-toolchain branch 2 times, most recently from 454282e to b2164a9 Compare September 9, 2026 13:19
@xangcastle xangcastle changed the title feat(uv): rust_toolchain on uv.project wires Rust sdist builds automatically feat(uv): build Rust sdists with rules_py's own Rust toolchain Sep 9, 2026
jbedard pushed a commit that referenced this pull request Sep 9, 2026
CI and e2e only: no change under `uv/`; the public API is unchanged.

**Native-arch verification** (`.github/workflows/ci-workflows.yaml`):
each crossbuild case exports `<case>_{amd64,arm64}_bundle` tarballs
(wheel plus its standalone `test_<case>.py`,
`e2e/crossbuild/tools/wheel_export.bzl`). Three pipelines cross-build on
one runner, upload the bundles, and `pip install` + run them on a native
runner of the target architecture, with no containers and no emulation
in the verdict:

- darwin → linux/amd64 + linux/arm64 (gated to `main` and `*macos*`
branches),
- ubuntu/amd64 → ubuntu/arm64,
- ubuntu/arm64 → ubuntu/amd64.

`test-all` gates on the two Linux pipelines and accepts `skipped` for
the macOS one outside the gated branches. The bundle-building steps pass
`--remote_download_outputs=toplevel`: `--config=ci` builds without the
bytes, and with a warm cache the tarballs this job uploads were not
materialized (the first run of this branch passed only because its cache
was cold).

**New cases**, all built for linux/amd64 and linux/arm64 with
`collect_wheels` (Tag metadata, ELF arch of every bundled `.so`) plus a
native import test:

- `pycross-geohash`: C++ extension with no upstream wheel. On a macOS
host, `test.sh` also cross-builds it for macOS amd64 (a manual
`collect_wheels` with `check_elf = False`; the platform transition
always resolves to os:macos, so `target_compatible_with` cannot tell
hosts apart).
- `pycross-msgpack`: Cython-generated C extension via setuptools.
- `pycross-numpy`: meson-python with `config_settings = {"setup-args":
["-Dblas=none", "-Dlapack=none"]}` (#1524); the hermetic sandbox has no
BLAS.
- `pycross-psutil`: plain setuptools C extension.
- `pycross-compile_pyc`: regression guard for cross-arch `whl_install`
pyc precompilation.

The Rust cases stay out of this PR on purpose: `pycross-rust` keeps
building as on `main` and gets no export, so the pipelines never depend
on the Rust toolchain work in #1526. Once that lands, the Rust cases
join the native runs with a one-line `pycross_wheel_export` each.

The macOS `smoke` job is removed: the root and `e2e/cases` suites
already run on Linux for every PR, and the crossbuild cases are
exercised by the darwin cross-build pipeline above, so it tripled macOS
minutes without covering anything new.

`collect_wheels` gains `tags` and `check_elf` for the macOS collection;
`pycross-setuptools` shares its setproctitle wheel label between
`collect_wheels` and the export instead of repeating it.

---

### Changes are visible to end-users: no

### Test plan

- New test cases added
- `cd e2e/crossbuild && bazel test //... && bash test.sh` passes
locally.
- CI: the three build/run pipelines plus `test-all`.
@xangcastle xangcastle closed this Sep 9, 2026
@xangcastle xangcastle reopened this Sep 9, 2026
@xangcastle
xangcastle force-pushed the xangcastle/pep517-rust-toolchain branch from 0ea8797 to 1b7bec1 Compare September 10, 2026 12:07
@xangcastle xangcastle changed the title feat(uv): build Rust sdists with rules_py's own Rust toolchain feat(uv): build Rust sdists with the project's rules_rust or rules_rs toolchain Sep 10, 2026
Comment thread e2e/crossbuild/MODULE.bazel
…tically

`uv.project(rust_toolchain = "@rules_rust//rust/toolchain:current_rust_toolchain")`
declares the project's Rust toolchain once. sdist_build already knows each
sdist's build backend and declared build requirements from the configure
tool; when the backend is maturin, or setuptools-rust is among the
requirements, the generated pep517_native_whl gets the toolchain plus a
local rust_host_sysroot layer in `toolchains`. pep517_native_whl derives
CARGO/RUSTC/RULES_PY_RUST_HOST_SYSROOT from their make-variables, so no
per-package uv.override_package is needed for Rust.

rust_host_sysroot moves from e2e/crossbuild/tools into rules_py: it
re-resolves the toolchain in the exec configuration, because under a cross
transition current_rust_toolchain returns the target-targeting toolchain,
whose sysroot lacks the exec-platform std that build scripts and
proc-macros need. It reads platform_common.ToolchainInfo only, so rules_py
takes no dependency on rules_rust.

Requirement names are normalized from their PEP 508 spelling (extras,
specifiers and markers dropped): the configure tool reports build
requirements as declared, e.g. `setuptools-rust>=1.7`.

Inferred setuptools-rust counts too: the configure tool adds it for sdists
that ship .rs files without declaring it, and injects it into the build
venv, so the build needs the toolchain just the same. The inference is
limited to setuptools backends (or a bare setup.py); numpy vendors meson's
test suite, .rs files included, and stays a meson build.

The helper no longer re-derives "is this a Rust build" from pyproject.toml,
which would miss inferred builds: CARGO wired by the rule is the signal for
the cargo cross environment. rust_toolchain travels as a label, not a
string, so a relative or apparent-repo spelling resolves where the user
wrote it.

Tests: unittests for the detection and for the BUILD wiring it emits; an
analysis test with a fake toolchain proves rust_host_sysroot exports the
exec-configured sysroot; pycross-rust is the end-to-end consumer, now with
no override at all.
…ot explicitly

New e2e/crossbuild case pycross-rust-rs builds pydantic-core with rules_rs
toolchains: a native import and linux/amd64 + linux/arm64 cross builds
through uv.project(rust_toolchain = ...), mirroring pycross-rust. rules_rs
declares its toolchains with rules_rust's rust_toolchain rule inside the
patched rules_rust repository it provisions, so the same
current_rust_toolchain label and providers apply, and both rulesets coexist
in one workspace because each carries its own toolchain type. The case pins
that the wiring is a provider contract, not a dependency on one ruleset.

It also exposed a layout assumption: the helper ran rustc bare, which infers
its sysroot from its own location, and rules_rs fetches rustc and rust-std
into separate repositories (E0463: can't find crate for `std`). The rule
now derives RUST_SYSROOT, the sysroot rules_rust's toolchain assembles, and
the helper passes it: natively through a rustc wrapper (--sysroot), under
cross as the target side of the merged sysroot instead of dirname(rustc).

Tests: unit tests for the native cargo env and for the merge with a
rules_rs-style layout; an analysis test with a fake toolchain exporting
CARGO/RUSTC/RUST_SYSROOT asserts the derived env keys and that the
make-variable names do not leak.
…line

Cargo fetched its dependencies inside the build action, with whatever
network the sandbox allowed: not hermetic, not reproducible beyond the lock
the sdist happened to ship, and dead under remote execution. The C-family
backends never had this problem: their build deps come from the uv lock and
their toolchains are action inputs.

sdist_configure now reads the sdist's Cargo.lock (anywhere in the tree,
shallowest first: setuptools-rust keeps its crate under src/) and reports the
crates it pins. For a Rust build, sdist_build downloads each crates.io crate
through Bazel's downloader with the lock's checksum into a cargo vendor
directory, and the generated pep517_native_whl passes it to the helper, which
writes the source replacement into the sandbox CARGO_HOME and sets
CARGO_NET_OFFLINE. Locks pinning git or path sources are rejected outright.

Sdists without a Cargo.lock build online with a warning, or take one from
uv.override_package(cargo_lock = ...): vendored the same way and placed next
to the crate's Cargo.toml before the build. pycross-tiktoken (setuptools-rust,
no shipped lock) is that consumer.

e2e/crossbuild now runs every sdist build with sandbox network blocked; the
whole suite passes offline, Rust cases included.
Without a toolchain, cargo or maturin fail deep inside the build action
with no hint of the cause. sdist_build now fails while the repository is
generated, naming the attribute to set, when the sdist declares a Rust
build: a maturin backend, or setuptools-rust among its declared build
requirements. Explicit uv.override_package(toolchains = [...]) entries are
trusted as the by-hand escape hatch.

Inferred setuptools-rust (stray .rs files) is only a guess and never
demands a toolchain: zstandard ships an optional rust-ext it does not
build by default. A guess still wires the project's toolchain when there
is one; without one the build proceeds unwired, as before, and fails only
if the backend really needs rustc.
pycross-rust, pycross-rust-rs and pycross-tiktoken get the pycross_wheel_export
from #1507, so CI installs and runs their cross-built wheels on native amd64
and arm64 runners like every other crossbuild case. The rules_rs case's test
file takes the bundle's name so the runner finds it. The crossbuild README
lists the Rust cases alongside the other backends.
… toolchain

State the design plainly: fetching rustc and cargo is the Rust rulesets' job
and rules_py depends on neither, so the project points uv.project at the
toolchain it registers. Give the rules_rust label and the rules_rs wiring the
e2e uses, and note that only declared Rust builds demand a toolchain.
@xangcastle
xangcastle force-pushed the xangcastle/pep517-rust-toolchain branch from 1b7bec1 to 1f4c6cd Compare September 10, 2026 19:30
…n uv.project()

`uv.project()` goes back to hub, lock and pyproject. The toolchains the
module's sdists build with move to their own tag on the same extension,
resolved in _parse_projects next to override_package and annotate_packages
and producing the same sdist_build repositories:

    uv.package_toolchains(rust_toolchain = "@rules_rust//rust/toolchain:current_rust_toolchain")

Without `lock` the declaration covers every uv.project() of the module, one
line per MODULE.bazel instead of one per lock. With `lock` it applies to that
project only and wins over the module-wide one, which is how a workspace
builds one project on rules_rust and another on rules_rs. Two declarations
with the same scope, a lock without a uv.project(), and a tag naming no
toolchain fail with their own messages.

The attribute stays `rust_toolchain` rather than a generic list because
sdist_build has to know which toolchain is the Rust one to wire it only into
Rust builds with the exec-sysroot layer; other project-level toolchains get
their own attribute when needed. `cargo_lock` remains per package on
override_package: it is data for the offline crate vendoring, not a
toolchain.

e2e/crossbuild exercises the three scopes: pycross-rust declares the
module-wide rules_rust toolchain, pycross-rust-rs overrides it for its lock
with the rules_rs one, pycross-tiktoken inherits.
maturin 1.8+ fetches rustc through puccinialin when it finds no cargo, so a
Rust sdist with no toolchain wired, or one the setuptools-rust inference
left unwired, would build with a toolchain downloaded inside the action.
Every sdist build now carries MATURIN_NO_INSTALL_RUST=1; a package env entry
still wins.
Comment thread e2e/crossbuild/pycross-rust-rs/setup.MODULE.bazel Outdated
Comment thread uv/private/extension/defs.bzl
…dists

The rustc wrapper only injected --sysroot, so a Rust wheel carried the
sandbox and execroot paths of the host that built it (debug info, panic
locations) and LLVM's per-host module ids in symbol names: the wheel action
itself hits the remote cache, but its bytes differ per host, so every
downstream action (whl_install, venvs) misses. The wrapper now remaps the
sandbox root and the execroot away and compiles target crates as a single
codegen unit; build scripts and proc-macros, which never reach the wheel,
keep cargo's codegen. Native builds treat every crate as target.

Crates with C or C++ sources (ring, zstd-sys) build them through cc-rs,
which looks up CC_<triple>, CXX_<triple>, AR_<triple> and RANLIB_<triple>
before falling back to the PATH; only CARGO_TARGET_<TRIPLE>_LINKER was set,
so those objects came from whatever compiler the runner had. Cross builds
now export the wired C toolchain under both spellings, with a ranlib
wrapper over `ar s` for toolchains that ship none. The linker stays the C
driver: linking with c++ would add an implicit libstdc++ dependency to
every Rust extension, not only the ones with C++ sources.

Tests run the generated wrapper around an argv-echoing rustc and check the
cc-rs variables end to end through the cross env.
The CycloneDX SBOM maturin writes into the wheel records every crate as
path+file:///<sandbox>/..., and the sandbox id differs per action, so two
builds of one sdist never produced the same wheel even with the rustc
wrapper remapping paths. maturin 1.15 has no flag or variable for it, only
the [tool.maturin.sbom] table, appended to the extracted pyproject.toml
unless the sdist configures it itself.
…toolchains

Two review asks on uv.package_toolchains(). A declaration can now name a
package, like uv.override_package(): `name` alone covers that package in
every project of the module, `name` with `lock` covers it in one project.
And the tag carries `toolchains`, the generic list uv.override_package()
forwards per package, so a JDK or Ant reach every source build of a module
or project with one declaration.

Each attribute resolves from the most specific declaration that sets it:
package in lock, package everywhere, lock, module. `rust_toolchain` stays
its own attribute because sdist_build wires it only into maturin and
setuptools-rust builds with the exec-sysroot layer, while `toolchains` is
forwarded as-is. A `name` that matches no source build fails, like an
uv.override_package() that matches nothing; two declarations with the same
scope fail.

pycross-jdk moves its JDK and Ant from the per-package override to a
lock-scoped declaration.
@xangcastle
xangcastle requested a review from jbedard September 11, 2026 04:00
Comment thread docs/uv.md
Comment thread e2e/crossbuild/pycross-tiktoken/setup.MODULE.bazel
@jbedard

jbedard commented Sep 14, 2026

Copy link
Copy Markdown
Member

@tamird please review the primary rust sdists API here. Also review the API proposals made in PR comments as well as any other sdists and crossbuild related issues you find

Cargo's `-C metadata=` hash, which rustc mangles into every symbol and
uses as the crate's stable id, mixes in `rustc -vV` (host triple included)
and the paths of host-compiled build scripts, so a wheel's `.so` differed
by build host even with paths remapped. The rustc wrapper now gives target
crates a hash derived from host-independent identity: the toolchain's
`rustc --version` string, `CARGO_PKG_NAME` and `CARGO_PKG_VERSION` from the
environment cargo hands rustc, the crate name, its types, cfgs, target and
the codegen options its profile sets. Two versions of one package hash
apart, and so do the two compilations a native build makes of one crate
under differing `build-override` and normal profiles. Path-carrying
options (`linker`, `link-arg`) are excluded; `-C extra-filename` stays
cargo's so artifact names never collide. Host crates keep cargo's hash:
they never reach the wheel.

The wrapper is exercised around an argv-echoing fake rustc: same hash from
two sandboxes, different across versions, features and toolchains, host
crates untouched, both `-C metadata=` spellings.
crossbuild/test.sh becomes the same aggregator cases/test.sh is: it runs
every `<case>/test.sh`, so a case that needs a real `bazel run`, the host's
identity or network the action sandbox denies owns its own script instead
of growing the workspace-level one. The macOS amd64 cross build of
pycross-geohash moves to pycross-geohash/test.sh.
Every sdist_build repository of a Rust sdist now carries a
`cargo_lock_generator` target. `bazel run` extracts the sdist, runs
`cargo generate-lockfile` on its shallowest manifest with the project's
own `rust_toolchain` (cargo and rustc from the toolchain's runfiles) and
copies the lock into the workspace: `<name>-<version>.Cargo.lock` at the
root, the first argument, or in place when `uv.override_package(cargo_lock
= ...)` already declares one. It prints the override to add. The warning
for an sdist that ships no Cargo.lock names the target.

The generated script is checked in an analysis test around a fake
toolchain exposing cargo and rustc as Files; the BUILD rendering and the
lock path derivation have unit tests; pycross-tiktoken/test.sh runs the
real target against crates.io and checks the result is a lock for that
crate. Regenerating tiktoken's declared lock reproduces the checked-in
file byte for byte.

docs: the generator, the symbol-hash normalization, and why rules_py
writes no PYO3_CONFIG_FILE (maturin writes its own in a cross build;
setuptools-rust derives the target from PYO3_CROSS_PYTHON_VERSION).
The Rust toolchain gets its own tag, `uv.rust_toolchain(toolchain = ...,
lock = ...)`, scoped to the module or to one project's lock; a lock-scoped
declaration wins for its project. Detection makes the wide scope precise:
sdist_build wires the toolchain only into builds whose backend is maturin
or setuptools-rust, so nothing else sees it.

Toolchains a single package needs beyond Rust (a JDK, Ant) stay where they
were before bc44642, on `uv.override_package(toolchains = ...)`. Their
files become action inputs of that package's build alone, instead of every
sdist built from source in the scope. The `name`-scoped variant and the
"matches no source build" check go away with the generic list; pycross-jdk
returns to its per-package override.

`cargo_lock` remains an override_package attribute: a lock pins one sdist
version's crates, a build input like pre_build_patches, not a toolchain.
@xangcastle
xangcastle requested a review from jbedard September 15, 2026 20:27
xangcastle added a commit that referenced this pull request Sep 15, 2026
…-sdist-macos

Brings #1526's final state onto the spike: uv.rust_toolchain(toolchain, lock)
replaces the spike's uv.project(rust_toolchain = ...), generic toolchains stay
per package on uv.override_package, offline crate vendoring with the
:cargo_lock generator, the reproducible rustc wrapper (remapped paths, one
codegen unit, host-independent -C metadata), cc-rs environment, maturin SBOM
off, and the per-case test.sh aggregator.

The spike's own helper work is replayed on top: pkg-config's python3 pointed
at the interpreter the wheel is built for, meson-python's build dir kept so
meson-log.txt survives a failed build, and static libstdc++ for GNU C++
links. pycross-bcrypt and pycross-rpds_py move to the module-wide
uv.rust_toolchain() declared in pycross-rust.
Comment thread docs/uv.md Outdated
The configure tool infers setuptools-rust from stray .rs files so the
package lands in the build venv. That guess also drove the toolchain
wiring: with a module-wide uv.rust_toolchain(), zstandard (an optional Rust
extension its setup.py never builds by default) got the toolchain and the
exec sysroot layer as action inputs, a :cargo_lock target, and 47 crates
vendored at repository generation. Wiring now requires a declared Rust
build, the same predicate the fail-early check already used: a maturin
backend, or setuptools-rust in [build-system].requires, setup.cfg or
setup.py setup_requires. Inference keeps injecting setuptools-rust into
the venv and nothing else.
crossbuild pins the generated BUILD.bazel of its 16 sdist_build
repositories under snapshots/, one write_source_files diff test each, so
the toolchains, Rust sysroot layer, vendored crates and build deps every
sdist receives are a reviewable diff. A toolchain reaching a package whose
backend does not need it now fails the suite instead of silently growing
its action inputs. `bazel run //:sdist_build_snapshots` updates them.
xangcastle added a commit that referenced this pull request Sep 18, 2026
…-sdist-macos

Syncs the spike with #1526's Rust wiring fix (declared Rust builds only,
so zstandard's optional extension no longer pulls the toolchain or vendors
crates) and the sdist_build BUILD snapshots. The snapshot list covers the
spike's 18 repositories, bcrypt and rpds_py included, and setproctitle at
the 1.3.7 the spike pins.
@xangcastle
xangcastle requested a review from jbedard September 18, 2026 02:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants