Skip to content

feat(py_venv): Shell-less hermetic launcher for py_venv_binary#825

Merged
arrdem merged 5 commits intomainfrom
arrdem/feat-prebuilt-launcher
Mar 10, 2026
Merged

feat(py_venv): Shell-less hermetic launcher for py_venv_binary#825
arrdem merged 5 commits intomainfrom
arrdem/feat-prebuilt-launcher

Conversation

@arrdem
Copy link
Copy Markdown
Contributor

@arrdem arrdem commented Mar 6, 2026

Replaces the shell script launcher for py_venv_binary and py_venv_test with a statically-linked native binary from hermetic_launcher (~10-68KB). This enables running in shell-less environments like distroless containers (closes #581).

The existing venv_shim already handles full virtualenv activation (VIRTUAL_ENV, PATH, PYTHONHOME, PYTHONEXECUTABLE, PYTHONNOUSERSITE). BAZEL_TARGET/BAZEL_WORKSPACE/BAZEL_TARGET_NAME are provided via RunEnvironmentInfo for bazel run/bazel test, and also written to a .aspect_env file in the venv so that the venv_shim can apply them when running outside Bazel (e.g. in containers).

Unchanged: py_venv (plain venv rule) still uses a shell entrypoint. py_binary/py_test (non-venv rules) are also unchanged.

Changes are visible to end-users: yes

  • Searched for relevant documentation and updated as needed: no
  • Breaking change (forces users to change their own code or config): no
  • Suggested release notes appear below: yes

py_venv_binary and py_venv_test now produce statically-linked native binaries instead of shell scripts, enabling use in shell-less environments like distroless containers.

Test plan

  • Covered by existing test cases (py/tests/py-venv-*, py/tests/py-internal-venv, py/tests/py_venv_conflict)
  • New: py/tests/py-venv-env — verifies BAZEL_TARGET/BAZEL_WORKSPACE/BAZEL_TARGET_NAME and custom env dict entries via RunEnvironmentInfo
  • New: py/tests/py-venv-args — verifies CLI argument passthrough to the Python script
  • New: e2e/cases/oci/distroless — container_structure_test verifying execution in distroless (no-shell), checking VIRTUAL_ENV, BAZEL_TARGET, and no double-slash paths

🤖 Generated with Claude Code

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 6, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ arrdem
❌ claude
You have signed the CLA already but the status is still pending? Let us recheck it.

@aspect-workflows
Copy link
Copy Markdown

aspect-workflows Bot commented Mar 6, 2026

Bazel 8 (Test)

21 test targets passed

Targets
//examples/multi_version:py_version_default_test [k8-fastbuild]                         1s
//examples/multi_version:py_version_test [k8-fastbuild]                                 1s
//examples/pytest:absolute_main_test [k8-fastbuild]                                     446ms
//examples/pytest:main_with_colon_test [k8-fastbuild]                                   477ms
//examples/pytest:pytest_test [k8-fastbuild]                                            1s
//examples/pytest:sharded/test [k8-fastbuild]                                           3s
//examples/virtual_deps:pytest_test [k8-fastbuild]                                      1s
//py/private/py_venv:test_link [k8-fastbuild]                                           300ms
//py/tests/external-deps:test_can_import_runfiles_helper [k8-fastbuild]                 573ms
//py/tests/internal-deps:assert [k8-fastbuild]                                          454ms
//py/tests/py-internal-venv:test [k8-fastbuild]                                         95ms
//py/tests/py-test:test_env_vars [k8-fastbuild]                                         426ms
//py/tests/py-venv-args:test_args [k8-fastbuild]                                        80ms
//py/tests/py-venv-disable-systemsite:test [k8-fastbuild]                               90ms
//py/tests/py-venv-disable-usersite:test [k8-fastbuild]                                 115ms
//py/tests/py-venv-enable-site:test [k8-fastbuild]                                      105ms
//py/tests/py-venv-env:test_env_vars [k8-fastbuild]                                     88ms
//py/tests/py-venv-standalone-interpreter:test [k8-fastbuild]                           129ms
//py/tests/py_venv_conflict:validate_import_roots [k8-fastbuild]                        156ms
//py/tests/repo_relative_imports/test:test [k8-fastbuild]                               426ms
//uv/private/gazelle_manifest:test [k8-fastbuild]                                       1s

Total test execution time was 13s. 68 tests (76.4%) were fully cached saving 33s.


Bazel 9 (Test)

21 test targets passed

Targets
//examples/multi_version:py_version_default_test [k8-fastbuild]                         2s
//examples/multi_version:py_version_test [k8-fastbuild]                                 3s
//examples/pytest:absolute_main_test [k8-fastbuild]                                     480ms
//examples/pytest:main_with_colon_test [k8-fastbuild]                                   442ms
//examples/pytest:pytest_test [k8-fastbuild]                                            2s
//examples/pytest:sharded/test [k8-fastbuild]                                           3s
//examples/virtual_deps:pytest_test [k8-fastbuild]                                      1s
//py/private/py_venv:test_link [k8-fastbuild]                                           227ms
//py/tests/external-deps:test_can_import_runfiles_helper [k8-fastbuild]                 575ms
//py/tests/internal-deps:assert [k8-fastbuild]                                          533ms
//py/tests/py-internal-venv:test [k8-fastbuild]                                         83ms
//py/tests/py-test:test_env_vars [k8-fastbuild]                                         417ms
//py/tests/py-venv-args:test_args [k8-fastbuild]                                        80ms
//py/tests/py-venv-disable-systemsite:test [k8-fastbuild]                               77ms
//py/tests/py-venv-disable-usersite:test [k8-fastbuild]                                 118ms
//py/tests/py-venv-enable-site:test [k8-fastbuild]                                      91ms
//py/tests/py-venv-env:test_env_vars [k8-fastbuild]                                     78ms
//py/tests/py-venv-standalone-interpreter:test [k8-fastbuild]                           128ms
//py/tests/py_venv_conflict:validate_import_roots [k8-fastbuild]                        177ms
//py/tests/repo_relative_imports/test:test [k8-fastbuild]                               461ms
//uv/private/gazelle_manifest:test [k8-fastbuild]                                       1s

Total test execution time was 16s. 68 tests (76.4%) were fully cached saving 60s.


Bazel 8 (Test)

e2e

20 test targets passed

Targets
//cases/cross-repo-610:test [k8-fastbuild-ST-934aa07688c9]                              58ms
//cases/interpreter-version-541:test_3_10_classic [k8-fastbuild-ST-0d3860f234bd]        511ms
//cases/interpreter-version-541:test_3_10_venv [k8-fastbuild-ST-0d3860f234bd]           182ms
//cases/interpreter-version-541:test_3_11_classic [k8-fastbuild]                        292ms
//cases/interpreter-version-541:test_3_11_venv [k8-fastbuild]                           70ms
//cases/interpreter-version-541:test_3_9_classic [k8-fastbuild-ST-1311592336af]         459ms
//cases/interpreter-version-541:test_3_9_venv [k8-fastbuild-ST-1311592336af]            123ms
//cases/oci/distroless:amd64_exec_test [k8-fastbuild]                                   5s
//cases/repository-rule-deps-299:all_direct [k8-fastbuild]                              326ms
//cases/repository-rule-deps-299:test [k8-fastbuild]                                    310ms
//cases/uv-conflict-817:test_a [k8-fastbuild-ST-5afdf7c25843]                           363ms
//cases/uv-conflict-817:test_b [k8-fastbuild-ST-070c4b919806]                           1s
//cases/uv-conflict-gte:test_a [k8-fastbuild-ST-9bc7e04a90fe]                           159ms
//cases/uv-conflict-gte:test_b [k8-fastbuild-ST-b3876cbbba34]                           95ms
//cases/uv-deps-650/airflow:airflow [k8-fastbuild-ST-ba1312b31f78]                      287ms
//cases/uv-deps-650/extras:extras [k8-fastbuild-ST-9615f960e05a]                        653ms
//cases/uv-deps-650/say:say [k8-fastbuild-ST-934aa07688c9]                              126ms
//cases/uv-legacy-deps-750:googlemaps [k8-fastbuild-ST-aa01e8447a8d]                    688ms
//cases/uv-patching-829:test [k8-fastbuild-ST-15619629c831]                             353ms
//cases/uv-workspace-789:test [k8-fastbuild-ST-cae91f25f4bb]                            2s

Total test execution time was 13s. 1 test (4.8%) was fully cached saving 123ms.


Bazel 9 (Test)

e2e

20 test targets passed

Targets
//cases/cross-repo-610:test [k8-fastbuild-ST-934aa07688c9]                              60ms
//cases/interpreter-version-541:test_3_10_classic [k8-fastbuild-ST-0d3860f234bd]        736ms
//cases/interpreter-version-541:test_3_10_venv [k8-fastbuild-ST-0d3860f234bd]           86ms
//cases/interpreter-version-541:test_3_11_classic [k8-fastbuild]                        327ms
//cases/interpreter-version-541:test_3_11_venv [k8-fastbuild]                           43ms
//cases/interpreter-version-541:test_3_9_classic [k8-fastbuild-ST-1311592336af]         576ms
//cases/interpreter-version-541:test_3_9_venv [k8-fastbuild-ST-1311592336af]            108ms
//cases/oci/distroless:amd64_exec_test [k8-fastbuild]                                   4s
//cases/repository-rule-deps-299:all_direct [k8-fastbuild]                              438ms
//cases/repository-rule-deps-299:test [k8-fastbuild]                                    398ms
//cases/uv-conflict-817:test_a [k8-fastbuild-ST-5afdf7c25843]                           1s
//cases/uv-conflict-817:test_b [k8-fastbuild-ST-070c4b919806]                           363ms
//cases/uv-conflict-gte:test_a [k8-fastbuild-ST-9bc7e04a90fe]                           54ms
//cases/uv-conflict-gte:test_b [k8-fastbuild-ST-b3876cbbba34]                           80ms
//cases/uv-deps-650/airflow:airflow [k8-fastbuild-ST-ba1312b31f78]                      215ms
//cases/uv-deps-650/extras:extras [k8-fastbuild-ST-9615f960e05a]                        695ms
//cases/uv-deps-650/say:say [k8-fastbuild-ST-934aa07688c9]                              134ms
//cases/uv-legacy-deps-750:googlemaps [k8-fastbuild-ST-aa01e8447a8d]                    1s
//cases/uv-patching-829:test [k8-fastbuild-ST-15619629c831]                             151ms
//cases/uv-workspace-789:test [k8-fastbuild-ST-cae91f25f4bb]                            915ms

Total test execution time was 12s. 1 test (4.8%) was fully cached saving 81ms.


Bazel 8 (Test)

examples/uv_pip_compile

All tests were cache hits

1 test (100.0%) was fully cached saving 444ms.

claude and others added 3 commits March 7, 2026 00:16
Adopt hermetic_launcher to produce statically-linked native binaries
for py_venv_binary and py_venv_test instead of shell script wrappers.
This enables running in shell-less environments like distroless
containers.

Closes #581

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The starlark_doc_extract rule needs all transitive .bzl dependencies
declared via bzl_library. Add @hermetic_launcher//launcher:lib_bzl
to the py_venv bzl_library deps.
Add e2e tests that build a py_venv_binary into a distroless
(cc-debian12) container image and exec it, proving the hermetic
native launcher works without a shell.

Tests for both amd64 and arm64 (cross-arch tests skip as expected).
@arrdem arrdem force-pushed the arrdem/feat-prebuilt-launcher branch from 7771451 to 379e662 Compare March 7, 2026 07:20
arrdem and others added 2 commits March 7, 2026 00:55
- New py_venv_test for env vars (BAZEL_TARGET, BAZEL_WORKSPACE,
  BAZEL_TARGET_NAME, custom env dict) verifying RunEnvironmentInfo
  works correctly with the hermetic launcher
- New py_venv_test for argv passthrough
- Richer distroless container assertions: VIRTUAL_ENV set, no
  double-slash in paths, BAZEL_TARGET present

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ners

The hermetic launcher bypasses the shell `activate` script, so
BAZEL_TARGET and friends (set via RunEnvironmentInfo) are only available
under `bazel run`/`bazel test`.  When the binary runs directly—e.g. in a
distroless container—those vars were missing.

Write a `.aspect_env` key=value file into the venv at build time and
have the venv_shim apply it at exec time (only for vars not already set,
so explicit env and RunEnvironmentInfo still take precedence).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@arrdem arrdem merged commit b482f38 into main Mar 10, 2026
3 of 4 checks passed
@arrdem arrdem deleted the arrdem/feat-prebuilt-launcher branch March 10, 2026 15:20
arrdem added a commit that referenced this pull request Mar 11, 2026
Reverts b482f38 and the follow-up fix 4721efc.

The hermetic launcher (hermetic_launcher v0.0.4) has a manifest-based
runfiles resolution bug: it does exact-match lookups only, but the
runfiles spec requires prefix-matching to support TreeArtifact contents.
Since the venv is a TreeArtifact and the exec target is a path within it
(<tree_artifact>/bin/python), manifest resolution fails with ENOENT,
breaking `bazel run` for all py_venv_binary targets.

Upstream: hermeticbuild/hermetic-launcher#17
Fixes: #849

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
arrdem added a commit that referenced this pull request Mar 11, 2026
Reverts b482f38 and follow-up 4721efc.

The hermetic launcher's manifest-based runfiles resolution does
exact-match lookups, but the runfiles spec requires prefix-matching for
TreeArtifact contents. Since the venv is a TreeArtifact and the exec
target is `<tree_artifact>/bin/python`, manifest resolution fails with
ENOENT, breaking `bazel run` for all `py_venv_binary` targets.

Root cause analysis:
#849 (comment)
Upstream fix request:
hermeticbuild/hermetic-launcher#17

Fixes #849

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

- Breaking change (forces users to change their own code or config): no
- Suggested release notes appear below: yes

`py_venv_binary` and `py_venv_test` revert to shell script launchers.
The hermetic (shell-less) launcher from 1.9.0 is removed due to a
runfiles resolution bug that broke `bazel run`. Distroless/shell-less
container support will return once the upstream launcher supports
TreeArtifact resolution.

### Test plan

- Covered by existing test cases
- Manual: `bazel run //examples/py_binary:py_binary.venv` now succeeds

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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.

[FR]: Shell-less binary configuration

3 participants