Skip to content

feat: add pyc compilation of first-party python code via actions - #1506

Open
jbedard wants to merge 5 commits into
mainfrom
py-compilation-aciton
Open

jbedard wants to merge 5 commits into
mainfrom
py-compilation-aciton

Conversation

@jbedard

@jbedard jbedard commented Aug 29, 2026

Copy link
Copy Markdown
Member

PYC compilation as an action in py_library for first-party py_library(srcs) as well as an aspect applied to py_library(deps) for legacy rules_python providers.

Changes are visible to end-users: yes

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

Add pyc = "source|pyc|pyc_only" flag alongside python_version,freethreaded flags in public APIs as well as a default via --config=@aspect_rules_py//py:pyc=source|pyc|pyc_only.

Test plan

  • Covered by existing test cases
  • New test cases added

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

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: 0076c8ac-b88d-40d2-b1eb-649781348bdb

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
📝 Walkthrough

Walkthrough

The change adds first-party Python bytecode modes, compilation providers, runtime packaging, OCI image support, rules_python interoperability, and broad integration coverage for source, pyc, and pyc_only.

Changes

First-party bytecode support

Layer / File(s) Summary
Compiler contracts and providers
py/BUILD.bazel, py/private/pyc.bzl, py/private/pyc_compile.py, py/private/interpreter/runtime.bzl
Adds the pyc build setting, bytecode providers, compiler actions, runtime compatibility checks, and optional compiler toolchain support.
Runtime and executable packaging
py/private/py_library.bzl, py/private/py_venv/*, py/private/py_pex_binary.bzl, py/private/py_pytest_test.bzl, py/private/unittest_main.py
Packages source or compiled files by mode, supports sourceless test loading, retains sources for pytest, and requires PEX binaries to use source mode.
OCI image integration
py/private/py_image_layer.bzl, e2e/cases/oci/*
Adds image-level mode selection, conflict validation, bytecode-aware layer grouping, source filtering, and snapshot coverage.
Validation and interoperability
py/tests/*, e2e/cases/pyc/*, e2e/rules-python-interop/*, e2e/rules-python-protobuf/*, examples/protobuf/*
Adds compiler, runtime, platform, rules_python, protobuf, coverage, and runfiles tests for all bytecode modes.
Documentation
README.md, docs/api/py.md, docs/interpreter.md, docs/migrating.md
Documents bytecode modes, flag inheritance, compilation scope, optimization restrictions, image behavior, and rules_python migration behavior.

Priority: ➖ Normal

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BazelTarget
  participant PycAspect
  participant PycCompiler
  participant RuntimeLauncher
  participant OCIImageLayer
  BazelTarget->>PycAspect: collect dependency sources
  PycAspect->>PycCompiler: compile compatible Python sources
  PycCompiler->>RuntimeLauncher: provide PycInfo artifacts
  RuntimeLauncher->>OCIImageLayer: expose selected source or bytecode files
  OCIImageLayer->>OCIImageLayer: validate mode and rewrite layers
Loading

Merge Risk: 🔵 Low · up to f6605

The bytecode feature is broadly mergeable, but the Starlark dependency metadata and protobuf pyc-only regression assertion should be corrected to keep release tooling and CI coverage accurate.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.82% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 49 functions across 50 files. (14 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: adding action-based PYC compilation for first-party Python code.
Description check ✅ Passed The description directly explains first-party compilation, rules_python dependency handling, public configuration, documentation updates, and test coverage.
Full details: Docstring Coverage

Explanation

Docstring coverage is 40.82% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 49 functions across 50 files. (14 skipped: 12 unsupported, 2 over the file limit.)

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch py-compilation-aciton

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.

layer: 0
files:
- -rwxr-xr-x 0 0 0 42 Jan 1 2023 ./app.runfiles/_main/oci/py_image_layer/branding/__init__.py
- -rwxr-xr-x 0 0 0 276 Jan 1 2023 ./app.runfiles/_main/oci/py_image_layer/branding/__pycache__/__init__.cpython-311.pyc

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

pyc = "pyc": source .py files having .pyc in __pycache__

---
layer: 0
files:
- -rwxr-xr-x 0 0 0 276 Jan 1 2023 ./app.runfiles/_main/oci/py_image_layer/branding/__init__.pyc

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

pyc = "pyc_only": no source .py files and only .pyc in its place

@jbedard
jbedard force-pushed the py-compilation-aciton branch from 8552c7c to b5dc5a4 Compare August 31, 2026 20:03
@aspect-workflows

aspect-workflows Bot commented Aug 31, 2026

Copy link
Copy Markdown

Aspect Workflows Tasks

📅 Mon Sep 21 21:27:27 UTC 2026

Task Results

  • buildifier · complete (clean) · 20.5s · GitHub Actions · Check
  • gazelle · complete (clean) · 21.8s · GitHub Actions · Check
  • test-e2e-bazel-8 [test] · complete (362/362 passed · 360 cached) · 49.7s · Aspect · GitHub Actions · Check
  • test-e2e-bazel-9 [test] · complete (351/351 passed · 349 cached) · 48.2s · Aspect · GitHub Actions · Check
  • test-e2e-crossbuild-bazel-8 [test] · complete (39/39 passed · 39 cached) · 37.8s · Aspect · GitHub Actions · Check
  • test-e2e-crossbuild-bazel-9 [test] · complete (39/39 passed · 39 cached) · 39.2s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-build-config-bazel-8 [test] · complete (1/1 passed · 1 cached) · 21.2s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-build-config-bazel-9 [test] · complete (1/1 passed · 1 cached) · 42.4s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-input-validation-bazel-8 [test] · complete (1/1 passed · 1 cached) · 21.7s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-input-validation-bazel-9 [test] · complete (1/1 passed · 1 cached) · 56.2s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-runtime-metadata-bazel-8 [test] · complete (2/2 passed · 2 cached) · 22.5s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-runtime-metadata-bazel-9 [test] · complete (2/2 passed · 2 cached) · 40.6s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-toolchain-settings-bazel-8 [test] · complete (1/1 passed · 1 cached) · 24s · Aspect · GitHub Actions · Check
  • test-e2e-interpreter-toolchain-settings-bazel-9 [test] · complete (1/1 passed · 1 cached) · 1m 6s · Aspect · GitHub Actions · Check
  • test-e2e-pyc-batch-bazel-8 [test] · complete (1/1 passed · 1 cached) · 21.6s · Aspect · GitHub Actions · Check
  • test-e2e-pyc-batch-bazel-9 [test] · complete (1/1 passed · 1 cached) · 54.4s · Aspect · GitHub Actions · Check
  • 🔄 test-e2e-rules-python-interop-bazel-8 [test] · running... (25 of 27 tests run) · 58m 31s · Aspect · GitHub Actions · Check
  • 🔄 test-e2e-rules-python-interop-bazel-9 [test] · running... (25 of 27 tests run) · 59m 39s · Aspect · GitHub Actions · Check
  • test-e2e-rules-python-protobuf-bazel-8 [test] · complete (6/6 passed · 6 cached) · 32.9s · Aspect · GitHub Actions · Check
  • test-e2e-rules-python-protobuf-bazel-9 [test] · complete (6/6 passed · 6 cached) · 36.4s · Aspect · GitHub Actions · Check
  • test-e2e-rules-python-provider-compat-bazel-8 [test] · complete (4/4 passed · 4 cached) · 29.3s · Aspect · GitHub Actions · Check
  • test-e2e-rules-python-provider-compat-bazel-9 [test] · complete (4/4 passed · 4 cached) · 1m 44s · Aspect · GitHub Actions · Check
  • test-examples-debugger-bazel-8 [test] · complete (1/1 passed · 1 cached) · 25.7s · Aspect · GitHub Actions · Check
  • test-examples-debugger-bazel-9 [test] · complete (1/1 passed · 1 cached) · 59s · Aspect · GitHub Actions · Check
  • test-examples-dev_deps-bazel-8 [test] · complete (1/1 passed · 1 cached) · 27s · Aspect · GitHub Actions · Check
  • test-examples-dev_deps-bazel-9 [test] · complete (1/1 passed · 1 cached) · 1m 6s · Aspect · GitHub Actions · Check
  • test-examples-django-bazel-8 [test] · complete (1/1 passed · 1 cached) · 27.3s · Aspect · GitHub Actions · Check
  • test-examples-django-bazel-9 [test] · complete (1/1 passed · 1 cached) · 1m 8s · Aspect · GitHub Actions · Check
  • test-examples-multi_version-bazel-8 [test] · complete (2/2 passed · 2 cached) · 25.5s · Aspect · GitHub Actions · Check
  • test-examples-multi_version-bazel-9 [test] · complete (2/2 passed · 2 cached) · 1m 4s · Aspect · GitHub Actions · Check
  • test-examples-protobuf-bazel-8 [test] · complete (1/1 passed · 1 cached) · 16.6s · Aspect · GitHub Actions · Check
  • test-examples-protobuf-bazel-9 [test] · complete (1/1 passed · 1 cached) · 19.8s · Aspect · GitHub Actions · Check
  • test-examples-py_binary-bazel-8 [test] · complete (1/1 passed · 1 cached) · 25.8s · Aspect · GitHub Actions · Check
  • test-examples-py_binary-bazel-9 [test] · complete (1/1 passed · 1 cached) · 48.7s · Aspect · GitHub Actions · Check
  • test-examples-py_pex_binary-bazel-8 [test] · complete (1/1 passed · 1 cached) · 26.6s · Aspect · GitHub Actions · Check
  • test-examples-py_pex_binary-bazel-9 [test] · complete (1/1 passed · 1 cached) · 54s · Aspect · GitHub Actions · Check
  • test-examples-py_venv-bazel-8 [test] · complete (3/3 passed · 3 cached) · 21.9s · Aspect · GitHub Actions · Check
  • test-examples-py_venv-bazel-9 [test] · complete (3/3 passed · 3 cached) · 42.2s · Aspect · GitHub Actions · Check
  • test-examples-pytest-bazel-8 [test] · complete (11/11 passed · 11 cached) · 40.8s · Aspect · GitHub Actions · Check
  • test-examples-pytest-bazel-9 [test] · complete (11/11 passed · 11 cached) · 50.2s · Aspect · GitHub Actions · Check
  • test-examples-uv_pip_compile-bazel-8 [test] · complete (1/1 passed · 1 cached) · 31.7s · Aspect · GitHub Actions · Check
  • test-examples-uv_pip_compile-bazel-9 [test] · complete (1/1 passed · 1 cached) · 51.7s · Aspect · GitHub Actions · Check
  • test-examples-virtual_deps-bazel-8 [test] · complete (1/1 passed · 1 cached) · 24.6s · Aspect · GitHub Actions · Check
  • test-examples-virtual_deps-bazel-9 [test] · complete (1/1 passed · 1 cached) · 50.1s · Aspect · GitHub Actions · Check
  • test-root-bazel-8 [test] · complete (437/437 passed · 437 cached) · 4m 47s · Aspect · GitHub Actions · Check
  • test-root-bazel-9 [test] · complete (436/436 passed · 436 cached) · 5m 41s · Aspect · GitHub Actions · Check

⏱ Last updated Mon Sep 21 22:27:01 UTC 2026 · 📊 GitHub API quota 806/7,700 (10% used, resets in 20m)
🚀 Powered by Aspect CLI (v2026.38.14)  |  Aspect Build · X · LinkedIn · YouTube

@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown

py_binary startup benchmark

Version Time (ms) vs BCR vs main Build (s)
BCR 1.11.7 133.6/133.3 ±1.6 1.59
main 52.6/52.5 ±0.6 -60.6% 1.63
PR 52.8/52.7 ±0.6 -60.5% +0.3% 1.35

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.5%). 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 1662/1515 ±364 404 20613 7380 (6675) 18.3 2.79
main 1601/1551 ±326 +2.3% 404 18363 7379 (6674) 18.3 2.49
PR 1714/1665 ±383 +9.9% +7.4% 404 18713 7394 (6689) 18.3 2.53

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 20.5%). 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.72/0.68 ±0.11 747
analysis main 0.70/0.67 ±0.14 668 -0.8%
analysis PR 0.70/0.69 ±0.12 731 ⚠️ +2.1% +2.8%
1p source BCR 2.0.0-alpha.6 0.73/0.70 ±0.10 4
1p source main 0.82/0.73 ±0.19 5 +5.1%
1p source PR 0.72/0.69 ±0.08 5 -1.5% -6.3%
3p wheel BCR 2.0.0-alpha.6 0.89/0.82 ±0.14 5
3p wheel main 0.88/0.85 ±0.07 6 +3.4%
3p wheel PR 0.90/0.87 ±0.08 6 +6.2% +2.6%

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.

@jbedard
jbedard force-pushed the py-compilation-aciton branch from b5dc5a4 to c572f70 Compare August 31, 2026 23:27
@jbedard

This comment was marked as resolved.

@chatgpt-codex-connector

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

jbedard added a commit that referenced this pull request Sep 2, 2026
…er a shared venv (#1515)

Prefactor for #1506

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

### Test plan

- Covered by existing test cases
- New test cases added
@jbedard
jbedard force-pushed the py-compilation-aciton branch 2 times, most recently from 72b09f0 to addd93c Compare September 2, 2026 23:56
@jbedard

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@jbedard
jbedard force-pushed the py-compilation-aciton branch 7 times, most recently from a939918 to acba6e7 Compare September 9, 2026 01:20
@jbedard

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@jbedard
jbedard force-pushed the py-compilation-aciton branch 2 times, most recently from 87748f3 to 4a31404 Compare September 9, 2026 06:11
@jbedard
jbedard changed the base branch from main to legacy2e2e September 9, 2026 06:16
@jbedard
jbedard force-pushed the py-compilation-aciton branch from 4a31404 to dbc5a45 Compare September 9, 2026 06:16

@zbarsky-openai zbarsky-openai 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.

Re-reviewed the rewritten ed8f20b. Comparing both immutable archive trees directly, only README.md, docs/interpreter.md, and the Ruff target in pyproject.toml changed; the bytecode implementation and tests are byte-identical.

The new ruleset-wide Python 3.10 minimum and interpreter-policy clarification address the ambiguity behind my earlier Python 3.8/3.9 finding. I no longer classify inability to run the compiler on those versions as a supported-version correctness blocker. An older PBS interpreter can still be downloaded/configured and the unchanged compiler still fails with a generic annotation TypeError rather than a clear unsupported-version error; I consider that a nonblocking diagnostics follow-up under the clarified policy.

The previously reported cross-ruleset source-sharing action conflict remains: its entire implementation is unchanged. The previous supported Bazel 8.6 controls passed with either owner individually and on the merge base, but failed even in default source mode when the two owners were combined. I’m not repeating that or the other unchanged findings inline. I found no new issue in this three-file rewrite.

-zbarskybot

@zbarsky-openai zbarsky-openai 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.

Re-reviewed the five-file e312809 update. I found one new issue in the OCI coverage regression: it can pass on query/archive errors and can miss a forbidden source even in a valid archive. I reproduced those cases with the exact shell block and GNU tar; a short forbidden-source archive failed as expected, and a .pyc-only control passed. The existing image test can replace the new shell inspection; details are inline.

The image transition now explicitly disables coverage instrumentation for the packaged image, and the compiler now postpones annotation evaluation. The previously reported cross-ruleset source-sharing action conflict is not changed by moving compiler inputs to tools: this increment leaves the output declarations and foreign-output reuse unchanged. I’m not duplicating that finding. I did not run the actual OCI coverage target locally.

-zbarskybot

Comment thread e2e/cases/oci/test.sh Outdated
Comment thread e2e/cases/oci/py_image_layer/BUILD.bazel Outdated
name = "my_app_layers_fp_pyc",
binary = ":my_app_bin",
layer_tier = ":my_app_tier",
pyc = "pyc",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

To me this attribute doesn't really tell me what happens when pyc = "pyc". perhaps precompile = "yes|no|auto|pyc_only"?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

WDYT about:

py_binary(bytecode = "none")   # sources only
py_binary(bytecode = "cache")  # sources plus __pycache__
py_binary(bytecode = "only")   # bytecode, no sources

?

PEP 3147 uses "sourceless distribution" / "legacy pyc files" - so maybe the "sourceless" or "legacy" is better for that one?

Also blank still means "use default", normally meaning fallback which is whatever is configured in your .bazelrc. I prefer blank/None (depending on macro vs internal where it must have a value) instead of "default"

Comment thread py/private/interpreter/runtime.bzl Outdated
)
embedded_args, transformed_args = launcher.append_runfile(
file = main,
file = pyc.entrypoint,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

shouldn't we fall back to main if pyc is disabled?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I believe it does that already

default = "",
values = [""] + PYC_MODES,
doc = """First-party bytecode packaging: `source` ships only `.py`
sources; `pyc` additionally ships PEP 3147 `__pycache__` bytecode;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

did we consider always using the bytecode specific extension described in https://peps.python.org/pep-3147?

This may allow better cache invalidation and action result retention.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The https://peps.python.org/pep-3147/#multiple-file-extensions section? That is under the "Alternatives" section of ideas that was rejected IIUC.

Is there another section in there I'm missing?

def _platform_cfg_impl(settings, attr):
result = {
"//command_line_option:platforms": [attr.platform] if attr.platform else settings["//command_line_option:platforms"],
"//command_line_option:collect_code_coverage": False,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is strange to me. usually bazel coverage does not lead to any artifact delivery except for cov files. why do we need to disable this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If we are running coverage then we need the original .py files to be shipped and not only .pyc files

pyc_only_sources = []
retained_source_paths = {}
retained_source_short_paths = {}
if effective_pyc == "pyc_only":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

shouldn't these be utility functions exported from pyc .bzl file?

def _py_python_pex_impl(ctx):
binary = ctx.attr.binary
binary = _single_target(ctx.attr.binary)
if PycModeInfo in binary and binary[PycModeInfo].mode != "source":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i don't why would every py_binary downstream rule needs to get a pyc attribute. if the a downstream does not want the pyc files, it can simply omit them when collecting .py files, that's what the providers are for.

pyc files are simply perf optimizations, so we shouldn't treat them as must be present or must not be, python interpreter will do the right thing when they are absent.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think this mess is because py_pex_binary is passed the py_binary which has converted all the PyInfo into plain DefaultInfo(executable, runfiles) and we've lost all our info. If that py_bianry did pyc_only then we lost all the sources.

That seems like either a bad py_pex_binary API, or maybe py_binary simply needs to export all its data in a PyInfo in case another rule wants to consume it instead of directly executing the the DefaultInfo(executable, runfiles)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

py_binary exporting the full PyInfo would also allow py_image_layer to consume that data easier maybe?

Comment thread py/private/pyc.bzl Outdated
Comment thread py/private/pyc.bzl Outdated
@jbedard
jbedard force-pushed the py-compilation-aciton branch from e312809 to 62475ea Compare September 18, 2026 18:07

@zbarsky-openai zbarsky-openai 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.

Re-reviewed 62475ea, including the compiler/toolchain changes, runtime interop, and removal of the image-level pyc override. Three new findings below; earlier findings are not repeated.

Validation: the exact compiler test file on CPython 3.12 and 3.13, isolated UTF-8 transport controls, the exact archive checker with a missing-source counterexample, and current-head CI logs. I did not run the complete Bazel/container suite locally.

-zbarskybot

Comment thread py/tests/pyc-compile/version_check_test.py
Comment thread e2e/cases/oci/py_image_layer/BUILD.bazel Outdated
Comment thread py/private/interpreter/repository.bzl

@zbarsky-openai zbarsky-openai 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.

Rechecked ba705e524b361725b77ce4adba59822cac482c89. The previous locale-fixture, retained-source assertion, and interpreter-snapshot findings are addressed; the 15 compiler tests pass on Python 3.12 and 3.13, and the archive controls distinguish missing source from bytecode. The remaining Bazel 8 container failure exposes one additional fixture mismatch below. I did not run a complete local Bazel/container suite.

-zbarskybot

Comment thread e2e/cases/oci/py_venv_image_layer/BUILD.bazel Outdated

@zbarsky-openai zbarsky-openai 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.

Rechecked fb79d328ab1968119c7661def67c2f3cfb788f8b as part of the requested review. The sourceless OCI command fixture is fixed, including an executed Bazel 8 CI pass. The updated pytest retention and unittest path handling introduce two collection/launch regressions, and the native bytecode reuse loop adds repeated transitive work; details below.

Validation includes paired real-pytest collection controls and the exact old/new unittest drivers through the native launcher. The analysis-cost finding follows from the provider traversal, not a measured timing regression. I did not run the complete local Bazel suite.

-zbarskybot

entrypoint = main_entry.pyc,
info = info,
mode = mode,
venv_files = depset(ctx.files.srcs + [main], transitive = [info.sourceless_files]) if ctx.attr.retain_srcs else info.sourceless_files,

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.

[P2] Preserve the package markers needed to collect retained tests. A valid target can put alpha/test_behavior.py and beta/test_behavior.py in srcs, with the two packages' __init__.py files owned by libraries in deps. This now retains the tests but leaves only __init__.pyc for their packages. Pytest's default package discovery checks for __init__.py, so it imports both tests as test_behavior and aborts with an import-file-mismatch error. A paired pytest 9.0.3 control passes both cases with the old source-retaining layout and fails collection with this layout. Retain the ancestor package markers required by collected tests without restoring all production dependency sources.

-zbarskybot

here = os.path.abspath(__file__)
for candidate in (driver_path, driver_path[:-len(".py")] + ".pyc"):
if candidate and here.endswith(candidate):
return here[:-len(candidate)]

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.

[P2] Resolve the test runfile instead of inferring a root from the driver's physical path. With manifest-only runfiles, the launcher resolves this generated driver under bazel-out, while a source test's manifest entry points into the source tree. Removing driver_path from __file__ therefore produces the generated-output prefix, and _import_test_modules searches there rather than at the mapped test file. A native-launcher control using the exact old/new drivers passes in source mode before this change and raises FileNotFoundError afterward; directory-runfiles controls pass. Manifest-only operation is already exercised by e2e/cases/venv-manifest-runfiles-1378/wrapper.sh. Resolve each source/bytecode candidate through the runfiles directory or manifest instead of this suffix heuristic.

-zbarskybot

Comment thread py/private/pyc.bzl Outdated
@jbedard
jbedard force-pushed the py-compilation-aciton branch from fb79d32 to 7eda5d8 Compare September 18, 2026 22:17

@zbarsky-openai zbarsky-openai 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.

Rechecked caedf11 against its current merge base as part of the requested review. One new cache-check failure is below. The native bytecode lookup no longer flattens the transitive bytecode set, but its replacement again trusts unadvertised .pyc action outputs (existing finding); that concern remains open. I have not duplicated the existing inline findings.

The compiler implementation is unchanged from the previous pass. Its 16 tests and focused one-shot/worker byte-identity and error-recovery controls pass on CPython 3.12. The CI failure below is reproduced in both Bazel 8 and 9 jobs for this revision; their synthetic merge contains the exact same test-script blob. No full local Bazel suite was run.

-zbarskybot

Comment thread e2e/cases/pyc/test.sh
@jbedard
jbedard force-pushed the py-compilation-aciton branch from caedf11 to f930148 Compare September 18, 2026 23:17

@zbarsky-openai zbarsky-openai 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.

Rechecked f930148 as part of the requested review. The batching change has one generated-source ownership issue and one optional test-layout simplification below. Existing findings are not duplicated; the execution-log-format conflict still fails the Bazel 8 cases job on this revision.

Validation: the new batching fixture executes successfully in Bazel 8 and 9 CI on the verified synthetic merge. Focused controls exercise the exact action-construction functions with the real compiler, including mixed reused outputs, batch sizes, worker/one-shot transports, and failure/retry behavior. The protobuf finding is supported by the upstream provider implementation and an exact-function action-registration trace; it is not a fresh native Bazel/protoc reproduction. No full local Bazel suite was run.

-zbarskybot

Comment thread py/private/pyc.bzl Outdated
- { path: "e2e/rules-python-interop", slug: "e2e-rules-python-interop", runner: "ubuntu-latest" }
- { path: "e2e/rules-python-provider-compat", slug: "e2e-rules-python-provider-compat", runner: "ubuntu-latest" }
- { path: "e2e/rules-python-protobuf", slug: "e2e-rules-python-protobuf", runner: "ubuntu-22.04-32core" }
- { path: "e2e/pyc-batch", slug: "e2e-pyc-batch", runner: "ubuntu-latest" }

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.

[P3] Fold the batching fixture into the existing cases workspace

Optional simplification: the new workspace adds 12 files, including a 728-line lockfile, plus two CI matrix jobs. The existing e2e/cases/pyc/test.sh already selects a unique-source closure with per-invocation batch-size overrides. Put the three-source batching library and 4/5/6 action-count checks there too, and pin its runtime tests to pyc_only. The deliberately conflicting batch_conflict_bin is outside that selected closure, so it need not force a separate module. A short .bazelrc example can preserve the global opt-in documentation without maintaining another module and CI pair.

-zbarskybot

@jbedard
jbedard force-pushed the py-compilation-aciton branch from f930148 to df7898c Compare September 18, 2026 23:28

@zbarsky-openai zbarsky-openai 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.

Re-reviewed df7898c9a92bd9a38d7613f95f890f95283179b9, including the two changed blobs against the previously reviewed cumulative change.

The execution-log correction now passes the cache-check section in both Bazel 8 and Bazel 9 CI. One new compatibility issue is below: custom native providers with partially precompiled sources lose the per-job output-layout distinction. The before/after proof uses exact aspect/action function bodies and the real compiler, not native Bazel analysis. Existing findings are not repeated.

-zbarskybot

Comment thread py/private/pyc.bzl Outdated
@jbedard
jbedard force-pushed the py-compilation-aciton branch from df7898c to 6683c4e Compare September 19, 2026 00:16

@zbarsky-openai zbarsky-openai 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.

One non-blocking correction to the automatic-sharding contract below. The existing aspect-output findings are unchanged and are not repeated here.

-zbarskybot

Comment thread README.md Outdated
Never shard sources the aspect infers for srcs-less rules: two forwarders
of the same generated file must register identical per-source actions.
Adds a srcs-less PyInfo forwarder fixture to rules-python-interop.
Decide --sourceless per compile group from each job's output count instead
of once per target, so a native PyInfo producer that precompiled only some
of its sources keeps both layouts correct.
Let srcs-less forwarders reuse the bytecode their forwarded rules_python
library advertises and see sources it omitted from runfiles, instead of
re-declaring the library's outputs. Extends the forwarder fixture to the
keep_source and omit_source precompile modes.

@zbarsky-openai zbarsky-openai 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.

Re-reviewed 8de28a914f35af7aff03ca69e9e8cb6eae13df5e as requested. The per-job output grouping and inferred-source fallback fix the earlier partial-precompilation and forwarder-only overlap reproductions. Two additional forwarding cases are below; unchanged findings are not repeated.

Validation: complete 108-file inventory against the unchanged merge base, reusing the previous review for 97 identical blobs. Paired exact-function action traces and real compiler/CPython loader controls confirm these cases. These are not native Bazel analysis or a full local Bazel test run.

-zbarskybot

Comment thread py/private/pyc.bzl
Comment on lines +411 to +416
for f in depset(transitive = [
getattr(info, "transitive_pyc_files", depset()),
getattr(info, "transitive_implicit_pyc_files", depset()),
]).to_list():
if _same_package(f, ctx.label):
existing[f.short_path] = f

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.

[P2] Preserve the producer’s optimization check across forwarding

Wrap a same-package rules_python_library(precompile = "enabled", precompile_optimize_level = 1) with the new forward_py_info(actual = ...). This fallback adopts its bytecode, but the check below reads the wrapper’s missing precompile_optimize_level as zero. The aspect follows only deps, so the producer’s conflict is not inherited through actual. Exact-function controls return complete = True with no conflicts; real SourceFileLoader and SourcelessFileLoader controls at runtime optimization zero then load the inherited level-1 code with __debug__ == False and assertions removed. Matching level-zero controls preserve assertions, and the direct producer is correctly rejected. Carry the producer’s compatibility/conflict information with reused bytecode, or reject reuse whose optimization cannot be established; an absent wrapper attribute is not proof of level zero. This is a new route around the previously fixed direct-producer check, not a native Bazel reproduction.

-zbarskybot

Comment thread py/private/pyc.bzl
Comment on lines +307 to +308
if not owned:
shards = 0

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.

[P2] Keep direct and forwarded plans identical for shared sources

A native rules_python_library(name = "lib", srcs = ["a.py", "b.py"], precompile = "disabled") can be reached both directly and through forward_py_info(actual = ":lib"). With consumer deps = [":lib", ":forward"] and pyc_shards = 1 or -1, the direct path batches both sources while this inferred path forces two per-file actions. Both claim the same natural .pyc and __pycache__ outputs with different input/output sets, so they cannot share generating actions. Paired traces of the exact old/current helpers show identical actions at 6683c4ec but conflicting actions here; the current default 0 remains identical. Forward the producer’s compiled mapping, or use a consistent per-source plan on every path that can rediscover these artifacts. Having a srcs attribute does not establish exclusive ownership. This is an action-registration proof, not a fresh native Bazel run.

-zbarskybot

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.

3 participants