Skip to content

feat(py): ship the venv's bin/activate in py_image_layer - #1536

Draft
xangcastle wants to merge 1 commit into
mainfrom
venv-pack-e2e
Draft

xangcastle wants to merge 1 commit into
mainfrom
venv-pack-e2e

Conversation

@xangcastle

Copy link
Copy Markdown
Member

py_venv writes bin/activate but keeps it in venv_only, which only reaches the target's own DefaultInfo.runfiles. py_image_layer builds the source layer from VirtualenvInfo.runtime_files, so an extracted layer had bin/python, pyvenv.cfg and site-packages but could not be sourced.

Change

  • VirtualenvInfo gains an activate field: the File on the executable py_venv, None on the internal lib variant.
  • The py_image_layer aspect adds it to the source layer when present. Binaries with expose_venv = False are unchanged.
  • The two oci/py_venv_image_layer listing snapshots gain the one bin/activate row.

New e2e: e2e/cases/venv-pack

Layers a py_venv, extracts the tars under $TEST_TMPDIR, checks no symlink is absolute or dangling, then under env -i sources bin/activate and asserts sys.prefix, sys.base_prefix, sys.executable, cowsay.__file__ and every sys.path entry resolve inside the relocated tree, and that deactivate restores the shell. This is the conda-pack replacement path: tar the runfiles tree, untar on a host with no Python, activate.

Why not tar.bzl

tar.bzl 0.10.1 mutate(preserve_symlinks = True) cannot produce this archive: its awk only classifies readlink results containing ../ as links, so sibling-relative bin/python3 -> python stays type=file and bsdtar fails on the dangling chain. Documented in the case's BUILD.bazel.

`py_venv` writes `bin/activate` but keeps it in `venv_only`, which only
reaches the target's own `DefaultInfo.runfiles`. `py_image_layer` builds
the source layer from `VirtualenvInfo.runtime_files`, so an extracted
layer had `bin/python`, `pyvenv.cfg` and site-packages but could not be
`source`d.

`VirtualenvInfo` gains an `activate` field (`File` on the executable
`py_venv`, `None` on the internal lib variant) and the layer aspect adds
it to the source layer when present. Binaries with `expose_venv = False`
are unchanged. The `oci/py_venv_image_layer` listings gain the one row.

New e2e `cases/venv-pack`: layers a `py_venv`, extracts them under
`$TEST_TMPDIR`, checks no symlink is absolute or dangling, then under
`env -i` sources `bin/activate` and asserts `sys.prefix`,
`sys.base_prefix`, `sys.executable`, `cowsay.__file__` and every
`sys.path` entry resolve inside the relocated tree, and that
`deactivate` restores the shell. This is the conda-pack replacement
path: tar the runfiles tree, untar on a host with no Python, activate.

tar.bzl 0.10.1 `mutate(preserve_symlinks = True)` cannot produce this
archive: its awk only classifies readlink results containing `../` as
links, so sibling-relative `bin/python3 -> python` stays `type=file` and
bsdtar fails on the dangling chain.
@coderabbitai

coderabbitai Bot commented Sep 10, 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: d92a8518-cae5-41ae-9f50-2dc000ed5a9e

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.

@aspect-workflows

aspect-workflows Bot commented Sep 10, 2026

Copy link
Copy Markdown

✨ Aspect Workflows Tasks

📅 Thu Sep 10 00:41:42 UTC 2026

✅ 44 successful tasks

  • ✅ buildifier · ⏱ 21.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ gazelle · ⏱ 19.4s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ test-e2e-bazel-8 [test] · ⏱ 6m 26s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (325/325 passed)
  • ✅ test-e2e-bazel-9 [test] · ⏱ 6m 24s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (315/315 passed)
  • ✅ test-e2e-crossbuild-bazel-8 [test] · ⏱ 6m 54s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (39/39 passed)
  • ✅ test-e2e-crossbuild-bazel-9 [test] · ⏱ 7m 2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (39/39 passed)
  • ✅ test-e2e-interpreter-build-config-bazel-8 [test] · ⏱ 21s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-build-config-bazel-9 [test] · ⏱ 1m 3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-input-validation-bazel-8 [test] · ⏱ 20.4s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-input-validation-bazel-9 [test] · ⏱ 53.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-runtime-metadata-bazel-8 [test] · ⏱ 29.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-e2e-interpreter-runtime-metadata-bazel-9 [test] · ⏱ 1m 32s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-e2e-interpreter-toolchain-settings-bazel-8 [test] · ⏱ 17.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-toolchain-settings-bazel-9 [test] · ⏱ 55.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-rules-python-interop-bazel-8 [test] · ⏱ 48.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (16/16 passed)
  • ✅ test-e2e-rules-python-interop-bazel-9 [test] · ⏱ 1m 15s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (16/16 passed)
  • ✅ test-e2e-rules-python-protobuf-bazel-8 [test] · ⏱ 1m 50s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-rules-python-protobuf-bazel-9 [test] · ⏱ 1m 42s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-rules-python-provider-compat-bazel-8 [test] · ⏱ 24.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (4/4 passed)
  • ✅ test-e2e-rules-python-provider-compat-bazel-9 [test] · ⏱ 52.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (4/4 passed)
  • ✅ test-examples-debugger-bazel-8 [test] · ⏱ 26.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-debugger-bazel-9 [test] · ⏱ 55.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-dev_deps-bazel-8 [test] · ⏱ 26.6s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-dev_deps-bazel-9 [test] · ⏱ 46.6s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-django-bazel-8 [test] · ⏱ 24.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-django-bazel-9 [test] · ⏱ 54.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-multi_version-bazel-8 [test] · ⏱ 30.6s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-examples-multi_version-bazel-9 [test] · ⏱ 1m 3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-examples-protobuf-bazel-8 [test] · ⏱ 1m 42s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-protobuf-bazel-9 [test] · ⏱ 2m 20s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-py_binary-bazel-8 [test] · ⏱ 28.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_binary-bazel-9 [test] · ⏱ 38.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_pex_binary-bazel-8 [test] · ⏱ 23s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_pex_binary-bazel-9 [test] · ⏱ 57.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_venv-bazel-8 [test] · ⏱ 27.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-examples-py_venv-bazel-9 [test] · ⏱ 53.6s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-examples-pytest-bazel-8 [test] · ⏱ 38s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (11/11 passed)
  • ✅ test-examples-pytest-bazel-9 [test] · ⏱ 58.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (11/11 passed)
  • ✅ test-examples-uv_pip_compile-bazel-8 [test] · ⏱ 29.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-uv_pip_compile-bazel-9 [test] · ⏱ 44.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-virtual_deps-bazel-8 [test] · ⏱ 26.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-virtual_deps-bazel-9 [test] · ⏱ 49s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-root-bazel-8 [test] · ⏱ 12m 36s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (373/373 passed)
  • ✅ test-root-bazel-9 [test] · ⏱ 12m 33s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (372/372 passed)

⏱ Last updated Thu Sep 10 00:54:11 UTC 2026 · 📊 GitHub API quota 1,452/15,000 (10% used, resets in 43m)
🚀 Powered by Aspect CLI (v2026.28.2)  |  Aspect Build · X · LinkedIn · YouTube

@github-actions

Copy link
Copy Markdown

py_binary startup benchmark

Version Time (ms) vs BCR vs main Build (s)
BCR 1.11.7 162.0/141.8 ±36.3 1.37
main 52.8/52.2 ±1.9 -63.2% 1.54
PR 51.4/51.2 ±0.6 -63.9% -1.9% 1.65

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 1.1%). 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 1578/1529 ±264 404 20613 7380 (6675) 18.3 2.79
main 1566/1484 ±306 -3.0% 404 18363 7379 (6674) 18.3 2.49
PR 1565/1433 ±302 -6.3% -3.4% 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 18.3%). 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.68/0.64 ±0.14 747
analysis main 0.70/0.65 ±0.15 668 +2.6%
analysis PR 0.68/0.62 ±0.15 668 -2.1% -4.6%
1p source BCR 2.0.0-alpha.6 0.66/0.65 ±0.04 4
1p source main 0.68/0.67 ±0.04 5 +3.3%
1p source PR 0.68/0.68 ±0.04 5 +4.4% +1.0%
3p wheel BCR 2.0.0-alpha.6 0.81/0.81 ±0.02 5
3p wheel main 0.86/0.86 ±0.03 6 +6.2%
3p wheel PR 0.86/0.86 ±0.02 6 +6.2% -0.0%

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.

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.

1 participant