Skip to content

perf(uv): extract wheel metadata per-wheel to avoid fetching sibling platform wheels#1356

Open
jbedard wants to merge 1 commit into
mainfrom
platform-wheels
Open

perf(uv): extract wheel metadata per-wheel to avoid fetching sibling platform wheels#1356
jbedard wants to merge 1 commit into
mainfrom
platform-wheels

Conversation

@jbedard

@jbedard jbedard commented Jul 20, 2026

Copy link
Copy Markdown
Member

Each public whl_install repo (@…__pkg__ver) ran _extract_wheel_metadata over every selectable platform wheel at repo-rule time, peeking RECORD and entry_points.txt to derive top-level names, namespace/native layout, and console scripts. Reading a wheel forces fetching it, and the metadata was declared via a whl_files label_list, so resolving one package pulled down its Linux, macOS, Windows, and musl wheels even though only the host's is ever used.

Move the peek into a new per-wheel whl_dist repo that downloads one wheel and carries its RECORD-derived layout as a PyWheelMetadataInfo provider. whl_install now reads the layout off whichever wheel the select chain resolves to, instead of a basename-keyed dict covering every platform. This restores Bazel's lazy select fetching: only the wheel a configuration actually selects is downloaded and inspected.

Building numpy (27 platform wheels) on macOS/arm64 now fetches exactly one wheel instead of all 27.

  • add whl_install/metadata.bzl: RECORD/entry_points parsing + a composed extract_install_metadata() (moved out of repository.bzl)
  • add whl_install/dist_repository.bzl: the whl_dist repo rule
  • add whl_dist build rule + PyWheelMetadataInfo; fold SourceBuiltWheelInfo into it; whl_install reads the provider and drops its 8 string_list_dict attrs
  • strip extraction, the whl_files/metadata_directory attrs, and the label-mapping helper from whl_install's repo rule
  • wire the extension/lockfile to whl_dist (@whl__…//:whl)
  • regenerate uv_hub + e2e snapshots

Changes are visible to end-users: no

Test plan

  • Covered by existing test cases
  • New test cases added

@aspect-workflows

aspect-workflows Bot commented Jul 20, 2026

Copy link
Copy Markdown

✨ Aspect Workflows Tasks

📅 Mon Jul 20 21:49:55 UTC 2026

✅ 40 successful tasks

  • ✅ buildifier · ⏱ 16.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ gazelle · ⏱ 18.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ test-e2e-bazel-8 [test] · ⏱ 2m 31s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (205/205 passed)
  • ✅ test-e2e-bazel-9 [test] · ⏱ 2m 21s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (199/199 passed)
  • ✅ test-e2e-interpreter-build-config-bazel-8 [test] · ⏱ 33.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-build-config-bazel-9 [test] · ⏱ 35.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-input-validation-bazel-8 [test] · ⏱ 14.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-input-validation-bazel-9 [test] · ⏱ 33.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-runtime-metadata-bazel-8 [test] · ⏱ 20.6s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-e2e-interpreter-runtime-metadata-bazel-9 [test] · ⏱ 56.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-e2e-interpreter-toolchain-settings-bazel-8 [test] · ⏱ 17.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-toolchain-settings-bazel-9 [test] · ⏱ 33.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-rules-proto-grpc-python-bazel-8 [test] · ⏱ 1m 46s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-rules-proto-grpc-python-bazel-9 [test] · ⏱ 1m 31s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-rules-python-interop-bazel-8 [test] · ⏱ 30.6s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (7/7 passed)
  • ✅ test-e2e-rules-python-interop-bazel-9 [test] · ⏱ 42.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (7/7 passed)
  • ✅ test-examples-debugger-bazel-8 [test] · ⏱ 28.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-debugger-bazel-9 [test] · ⏱ 37.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-dev_deps-bazel-8 [test] · ⏱ 27.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-dev_deps-bazel-9 [test] · ⏱ 1m 3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-django-bazel-8 [test] · ⏱ 21.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-django-bazel-9 [test] · ⏱ 43.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-multi_version-bazel-8 [test] · ⏱ 23.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-examples-multi_version-bazel-9 [test] · ⏱ 57.6s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-examples-protobuf-bazel-8 [test] · ⏱ 1m 39s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-protobuf-bazel-9 [test] · ⏱ 2m 39s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-py_binary-bazel-8 [test] · ⏱ 23.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_binary-bazel-9 [test] · ⏱ 42s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-py_pex_binary-bazel-8 [test] · ⏱ 25.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_pex_binary-bazel-9 [test] · ⏱ 47.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-py_venv-bazel-8 [test] · ⏱ 21.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-examples-py_venv-bazel-9 [test] · ⏱ 41.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-examples-pytest-bazel-8 [test] · ⏱ 41s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (9/9 passed)
  • ✅ test-examples-pytest-bazel-9 [test] · ⏱ 1m 19s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (9/9 passed)
  • ✅ test-examples-uv_pip_compile-bazel-8 [test] · ⏱ 26.4s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-uv_pip_compile-bazel-9 [test] · ⏱ 46.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-virtual_deps-bazel-8 [test] · ⏱ 22.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-virtual_deps-bazel-9 [test] · ⏱ 47s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-root-bazel-8 [test] · ⏱ 3m 8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (256/256 passed)
  • ✅ test-root-bazel-9 [test] · ⏱ 3m 23s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (255/255 passed)

⏱ Last updated Mon Jul 20 21:54:07 UTC 2026 · 📊 GitHub API quota 959/15,000 (6% used, resets in 13m)
🚀 Powered by Aspect CLI (v2026.28.2)  |  Aspect Build · X · LinkedIn · YouTube

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown

py_binary startup benchmark

Version Mean (ms) Median (ms) ± stddev vs BCR vs main Build (s)
BCR 1.11.7 (baseline) 172.051 171.983 ±1.003 36.22
HEAD main 56.658 56.062 ±2.700 -67.1% 10.16
This PR 55.385 55.190 ±0.527 -67.8% -2.2% 7.32

Measured with hyperfine --warmup 5 --runs 50 on Linux
Gate: PR vs HEAD main (threshold: 10%). BCR is shown only as a historical baseline.
Build time: cold bazel build //:bench with isolated output base, no disk cache.

sys.path quality

Version sys.path entries distinct site-packages roots duplicate realpaths
BCR 1.11.7 (baseline) 6 1 0
HEAD main 7 2 0
This PR 7 2 0

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

Bazel analysis benchmark

Version Mean (ms) Median (ms) ± stddev vs BCR vs main Packages Targets
BCR 2.0.0-alpha.4 (baseline) 10730.047 10687.278 ±148.959 101 301
HEAD main 10331.825 10278.327 ±264.476 -3.7% 101 301
This PR 9822.941 9810.457 ±152.145 -8.5% -4.9% 101 301

Measured with hyperfine --warmup 1 --runs 10 on Linux
Gate: PR vs HEAD main (threshold: 10%). BCR is shown only as a historical baseline.
Command: cold bazel build --nobuild //workspace/... with isolated output base, no disk cache.

Auxiliary metrics

Version Loaded packages Configured targets
BCR 2.0.0-alpha.4 (baseline) 101 301
HEAD main 101 301
This PR 101 301

@tamird tamird left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The per-bdist provider is the cleaner lazy boundary: the existing selector can choose one wheel repo, so universal wheels avoid #1348's extra install repo/selector. The benchmark agrees (101 loaded packages / 301 configured targets unchanged, with analysis about 5.6% faster).

I found three correctness regressions and one important coverage gap:

  • both e2e jobs currently fail on the new provider requirement before the patch-failure action runs;
  • replacing http_file drops its authenticated-download and default canonical-ID behavior;
  • supported hashless wheels are now reported reproducible without recording the discovered integrity;
  • the tests no longer exercise the repository rule or prove that an inactive wheel stays unfetched.

This supersedes the lazy-metadata half of #1348, but it does not deduplicate actual_install actions across lock universes; that remains separate work. The existing downstream lazy/grouping fixtures and their //file labels cannot be applied unchanged on top of this design.

# active configuration — its own repo's RECORD-derived metadata (or empty,
# for a source-built wheel). A sibling platform wheel's surface can't leak
# in: it lives in a different repo that is never fetched or consulted here.
meta = ctx.attr.src[PyWheelMetadataInfo]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This unconditionally requires PyWheelMetadataInfo, but the unchanged e2e/cases/patch-failure/BUILD.bazel:35-51 passes the raw :fake_wheel genrule to both installs. Both Bazel 8 and 9 jobs now stop here with:

<target //patch-failure:fake_wheel> (rule 'genrule') doesn't contain declared provider 'PyWheelMetadataInfo'
FAIL: expected the patch-failure diagnostic on stderr

So patch behavior is no longer tested. Wrap the generated wheel once in source_built_wheel (its layout is intentionally unknown) and point both installs at that wrapper, or retain the raw-wheel fallback if direct callers are still supported.

url = rctx.attr.url,
output = basename,
sha256 = rctx.attr.sha256,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Replacing http_file with a bare rctx.download drops two existing downloader guarantees. Bazel's http_file passes auth = get_auth(ctx, source_urls), so wheel URLs requiring $NETRC / ~/.netrc now fail with 401/403. It also passes the URL-derived default canonical_id; without that, a changed URL with a stale hash can reuse the old repository-cache entry locally and only fail on a clean machine.

Please preserve both behaviors (including the default-canonical-ID environment contract) when moving the download into whl_dist, and add authenticated-index coverage.

console_scripts = _attr("console_scripts", meta.console_scripts),
))

return rctx.repo_metadata(reproducible = True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is not always reproducible. Hashless wheels are explicitly supported (uv/private/extension/lockfile.bzl:33-41 and the uv-dep-hashes fixture), and defs.bzl:665 passes an empty sha256 here. The replaced http_file captures download_info.integrity and returns attrs_for_reproducibility when no hash was supplied; this rule discards the download result and always claims reproducibility.

Capture the returned integrity and report it through repo_metadata(attrs_for_reproducibility = ...) for the hashless case (or reject unsupported unpinned input). Otherwise a mutable find-links URL can silently change or leave stale bytes/layout cached without a discovered checksum.

sha256 = rctx.attr.sha256,
)

meta = extract_install_metadata(rctx, rctx.path(basename), _metadata_directory(basename))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new repository rule and the central invariant here are not exercised by the tests. The updated analysis tests construct whl_dist build targets from local empty files, while the snapshot updates remove all RECORD-derived topology payload for cffi, cryptography, azure, grpcio, and numpy without replacing it with per-wheel snapshots. The performance workload uses a universal wheel or fetches everything before timing, so it also cannot show lazy sibling fetching.

Please add an e2e case with two platform wheels where the inactive URL is deliberately unavailable and the active target still builds, and retain a representative extracted-metadata/provider snapshot (including namespace/native topology). That proves both “only the selected repo is fetched” and that the moved RECORD extraction still produces the advertised layout.

…platform wheels

Each public whl_install repo (@…__pkg__ver) ran _extract_wheel_metadata
over every selectable platform wheel at repo-rule time, peeking RECORD and
entry_points.txt to derive top-level names, namespace/native layout, and
console scripts. Reading a wheel forces fetching it, and the metadata was
declared via a whl_files label_list, so resolving one package pulled down
its Linux, macOS, Windows, and musl wheels even though only the host's is
ever used.

Move the peek into a new per-wheel `whl_dist` repo that downloads one wheel
and carries its RECORD-derived layout as a PyWheelMetadataInfo provider.
whl_install now reads the layout off whichever wheel the select chain
resolves to, instead of a basename-keyed dict covering every platform. This
restores Bazel's lazy select fetching: only the wheel a configuration
actually selects is downloaded and inspected.

Building numpy (27 platform wheels) on macOS/arm64 now fetches exactly one
wheel instead of all 27.

- add whl_install/metadata.bzl: RECORD/entry_points parsing + a composed
  extract_install_metadata() (moved out of repository.bzl)
- add whl_install/dist_repository.bzl: the whl_dist repo rule
- add whl_dist build rule + PyWheelMetadataInfo; fold SourceBuiltWheelInfo
  into it; whl_install reads the provider and drops its 8 string_list_dict
  attrs
- strip extraction, the whl_files/metadata_directory attrs, and the
  label-mapping helper from whl_install's repo rule
- wire the extension/lockfile to whl_dist (@whl__…//:whl)
- regenerate uv_hub + e2e snapshots
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