Skip to content

feat: Add cross-compilation support for sdist packages with native extensions - #1363

Draft
xangcastle wants to merge 57 commits into
mainfrom
xangcastle/cross-sdist
Draft

xangcastle wants to merge 57 commits into
mainfrom
xangcastle/cross-sdist

Conversation

@xangcastle

@xangcastle xangcastle commented Jul 23, 2026

Copy link
Copy Markdown
Member

Enable pep517_native_whl to cross-compile Python sdists containing C or C++ extensions when the target platform differs from the exec host.
The native_build_toolchain sentinel is now optional, so a missing resolution signals cross-compilation mode and falls through to a user-registered cross CC toolchain (such as toolchains_llvm) instead of hard-failing.
A new cc_layer.bzl module extracts compiler paths, CFLAGS, and LDFLAGS from the resolved cross CC toolchain at analysis time, and build_helper.py generates compiler wrappers that re-inject -target/--sysroot identity flags, filter incompatible linker flags from the exec host, and override LDSHARED to use the cross CC toolchain's link flags. The target interpreter's _sysconfigdata is loaded via _PYTHON_SYSCONFIGDATA_NAME so setuptools produces correct EXT_SUFFIX and SOABI tags, and a post-build check validates the wheel's platform tag against the target before accepting the output.


Changes are visible to end-users: yes/no

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

Test plan

  • Covered by existing test cases
  • New test cases added
  • Manual testing; please provide instructions so we can reproduce:

@aspect-workflows

aspect-workflows Bot commented Jul 23, 2026

Copy link
Copy Markdown

✨ Aspect Workflows Tasks

📅 Fri Aug 14 08:43:19 UTC 2026

✅ 44 successful tasks

  • ✅ buildifier · ⏱ 19.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ gazelle · ⏱ 20.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ test-e2e-bazel-8 [test] · ⏱ 5m 31s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (265/265 passed)
  • ✅ test-e2e-bazel-9 [test] · ⏱ 4m 50s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (261/261 passed)
  • ✅ test-e2e-crossbuild-bazel-8 [test] · ⏱ 7m 56s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (51/51 passed)
  • ✅ test-e2e-crossbuild-bazel-9 [test] · ⏱ 7m 50s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (51/51 passed)
  • ✅ test-e2e-interpreter-build-config-bazel-8 [test] · ⏱ 19s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-build-config-bazel-9 [test] · ⏱ 58.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-input-validation-bazel-8 [test] · ⏱ 22.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-input-validation-bazel-9 [test] · ⏱ 39.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-runtime-metadata-bazel-8 [test] · ⏱ 26.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed · 2 cached)
  • ✅ test-e2e-interpreter-runtime-metadata-bazel-9 [test] · ⏱ 34.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-e2e-interpreter-toolchain-settings-bazel-8 [test] · ⏱ 21.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-interpreter-toolchain-settings-bazel-9 [test] · ⏱ 40.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-rules-proto-grpc-python-bazel-8 [test] · ⏱ 35.4s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-rules-proto-grpc-python-bazel-9 [test] · ⏱ 1m 9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-e2e-rules-python-interop-bazel-8 [test] · ⏱ 1m 2s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (15/15 passed)
  • ✅ test-e2e-rules-python-interop-bazel-9 [test] · ⏱ 1m 25s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (15/15 passed)
  • ✅ test-e2e-rules-python-provider-compat-bazel-8 [test] · ⏱ 24.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-e2e-rules-python-provider-compat-bazel-9 [test] · ⏱ 37.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-examples-debugger-bazel-8 [test] · ⏱ 26.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-debugger-bazel-9 [test] · ⏱ 38.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-dev_deps-bazel-8 [test] · ⏱ 28.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-dev_deps-bazel-9 [test] · ⏱ 47.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-django-bazel-8 [test] · ⏱ 24.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-django-bazel-9 [test] · ⏱ 58.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-multi_version-bazel-8 [test] · ⏱ 33.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-examples-multi_version-bazel-9 [test] · ⏱ 44.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (2/2 passed)
  • ✅ test-examples-protobuf-bazel-8 [test] · ⏱ 1m 17s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-protobuf-bazel-9 [test] · ⏱ 1m 52s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-py_binary-bazel-8 [test] · ⏱ 20.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-py_binary-bazel-9 [test] · ⏱ 37.5s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_pex_binary-bazel-8 [test] · ⏱ 21.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-py_pex_binary-bazel-9 [test] · ⏱ 50.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-examples-py_venv-bazel-8 [test] · ⏱ 23s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-examples-py_venv-bazel-9 [test] · ⏱ 56.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (3/3 passed)
  • ✅ test-examples-pytest-bazel-8 [test] · ⏱ 40.6s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (11/11 passed)
  • ✅ test-examples-pytest-bazel-9 [test] · ⏱ 1m 9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (11/11 passed)
  • ✅ test-examples-uv_pip_compile-bazel-8 [test] · ⏱ 22.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-uv_pip_compile-bazel-9 [test] · ⏱ 36.4s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-virtual_deps-bazel-8 [test] · ⏱ 24.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-examples-virtual_deps-bazel-9 [test] · ⏱ 49.3s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)
  • ✅ test-root-bazel-8 [test] · ⏱ 2m 37s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (299/299 passed)
  • ✅ test-root-bazel-9 [test] · ⏱ 3m 15s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (298/298 passed)

⏱ Last updated Fri Aug 14 08:54:55 UTC 2026 · 📊 GitHub API quota 1,677/15,000 (11% used, resets in 45m)
🚀 Powered by Aspect CLI (v2026.28.2)  |  Aspect Build · X · LinkedIn · YouTube

@github-actions

github-actions Bot commented Jul 23, 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) 192.352 191.013 ±5.609 46.90
HEAD main 61.220 61.020 ±0.956 -68.2% 10.73
This PR 60.268 60.153 ±0.739 -68.7% -1.6% 7.18

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 Targets Actions
BCR 2.0.0-alpha.5 (baseline) 11086.165 11090.357 ±201.523 301 13672
HEAD main 9719.055 9752.386 ±111.763 -12.3% 301 13540
This PR 9793.936 9790.988 ±155.722 -11.7% +0.8% 301 13540

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.

@xangcastle
xangcastle requested a review from jbedard July 23, 2026 01:39
@xangcastle
xangcastle force-pushed the xangcastle/cross-sdist branch 2 times, most recently from 5e9c4f1 to cd6dfa7 Compare July 23, 2026 05:33
@xangcastle xangcastle changed the title feat: cross sdist build feat: Add cross-compilation support for sdist packages with native extensions Jul 23, 2026

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

Reviewing this draft at the author's explicit request.

rules_pycross already provides the native CC layer, separate execution and target Python interpreters, PEP 517 cross-build environment, and target-aware sysconfig that this change reimplements. Its v2 alpha also exposes those primitives through a public backend integration API. [0] [1] [2] [3]

The proposed implementation instead infers cross-compilation from an absent optional native toolchain, omits target native dependency and runtime closures, hard-codes macOS deployment tags, and rejects valid Windows wheels. The Linux-only fixture checks the ELF architecture and filename without proving that a cross-built native extension actually loads.

Please first evaluate integration with rules_pycross, including the v2 alpha's transitive-dependency compatibility. If that integration is not viable, document the actual blocker and propose the smallest reusable upstream interface before introducing a parallel cross-compilation implementation. [4]

[0] https://github.com/jvolkman/rules_pycross/blob/v2.0.0-alpha.2/pycross/backend.bzl#L63-L83
[1] https://github.com/jvolkman/rules_pycross/blob/74ee87c7d8eab76a673c07df3ec9e55d1e629e49/pycross/toolchain.bzl#L33-L55
[2] https://github.com/jvolkman/rules_pycross/blob/74ee87c7d8eab76a673c07df3ec9e55d1e629e49/pycross/private/build/actions/cc_layer.bzl#L126-L182
[3] https://github.com/jvolkman/rules_pycross/blob/74ee87c7d8eab76a673c07df3ec9e55d1e629e49/pycross/private/build/tools/utils/sysconfig_utils.py#L97-L169
[4] https://github.com/bazelbuild/bazel-central-registry/blob/main/modules/rules_pycross/2.0.0-alpha.2/MODULE.bazel

— tamirdex

Comment thread uv/private/pep517_whl/rule.bzl Outdated

target_os, target_cpu = get_target_platform(ctx)

return struct(

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 returns compiler tools and flags, but not the CcInfo headers, dependent static/shared libraries, or target C++ runtime that a real source-built extension requires. The wheel action cannot materialize that dependency closure; inspecting the toy geohash ELF cannot establish that the resulting extension links or loads. The existing pycross layer explicitly gathers all three. Please compose with that layer or explain and test the real alternative. [0]

[0] https://github.com/jvolkman/rules_pycross/blob/74ee87c7d8eab76a673c07df3ec9e55d1e629e49/pycross/private/build/actions/cc_layer.bzl#L126-L182

— tamirdex

sysconfig_file = _find_sysconfigdata(runtime)
if sysconfig_file:
extra_inputs.append(depset([sysconfig_file]))
env["RULES_PY_TARGET_SYSCONFIGDATA"] = sysconfig_file.path

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.

Copying one target _sysconfigdata file into a build still executed by the host interpreter does not create a target Python environment. Build backends continue to observe host interpreter identity, packaging tags, and platform behavior; the later validation checks only the final platform segment, so a host cp313 ABI can pass for a cp312 target with the same OS and CPU. pycross carries both interpreters and builds the appropriate cross environment instead. [0]

[0] https://github.com/jvolkman/rules_pycross/blob/74ee87c7d8eab76a673c07df3ec9e55d1e629e49/pycross/private/build/actions/pep517_action.bzl#L81-L103

— tamirdex

return "linux-" + _PYTHON_CPU_MAP.get(target_cpu, target_cpu)
if target_os == "darwin":
cpu = "arm64" if target_cpu == "aarch64" else target_cpu
return "macosx-11.0-" + cpu

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 macOS deployment version is a property of the target interpreter/SDK, not always 11.0. Hard-coding it produces an incorrect compatibility tag for targets requiring another deployment version. Derive the platform and deployment target from target sysconfig, as pycross already does, and add an actual Darwin cross-build regression. [0]

[0] https://github.com/jvolkman/rules_pycross/blob/74ee87c7d8eab76a673c07df3ec9e55d1e629e49/pycross/private/build/tools/utils/sysconfig_utils.py#L97-L169

— tamirdex

)
exit(1)

if expected_cpu not in platform_tag:

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.

A valid Windows x86-64 wheel has platform tag win_amd64, but _expected_cpu_in_tag returns x86_64 here. Consequently every such wheel fails this validation despite being correct; Windows x86 has the analogous win32/i686 mismatch. Conversely unrecognized target OS/CPU values silently skip validation above. Derive supported wheel tags from actual target metadata and fail closed for unsupported targets.

— tamirdex

Comment thread e2e/cases/uv-deps-650/crossbuild/BUILD.bazel Outdated
@xangcastle
xangcastle force-pushed the xangcastle/cross-sdist branch 2 times, most recently from c1e9648 to 4a95d04 Compare July 23, 2026 17:36
@xangcastle
xangcastle marked this pull request as ready for review July 23, 2026 18:16
@jbedard
jbedard requested a review from tamird July 23, 2026 22:20

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

Re-reviewing because Jason explicitly requested review of 9f3b372.

The new execution transition still selects host_platform and repository-time host libc, rather than the wheel action’s actual execution platform. The associated frontend test removes its execution-platform assertion. This cannot build correctly under heterogeneous remote execution; the new documentation itself confirms that the design assumes exec == host.

The five existing review threads remain current and unanswered. The replacement CC layer does not stage transitive CcInfo headers, native libraries or runtime dependencies; the PEP 517 frontend still runs the host interpreter with only a target sysconfig file; macOS wheel tags still hard-code deployment target 11.0; and wheel validation still rejects valid win_amd64 tags. The Linux fixture checks one patched extension’s ELF and filename but never executes the cross-built artifact or verifies a native dependency, target Python ABI, Darwin or remote worker.

rules_pycross already publicly exports target-aware CC extraction, PEP 517 actions, wheel repair and execution-platform transitions. Please evaluate composing those existing primitives before maintaining a second incomplete cross builder. If a dependency or compatibility issue prevents reuse, identify it and add real target-runtime and remote-execution coverage before calling this general cross-compilation.

https://github.com/jvolkman/rules_pycross/blob/v2.0.0-alpha.2/pycross/backend.bzl#L28-L39
https://github.com/jvolkman/rules_pycross/blob/v2.0.0-alpha.2/pycross/backend.bzl#L63-L83

— tamirdex

@jbedard
jbedard marked this pull request as draft July 24, 2026 19:14
@xangcastle
xangcastle force-pushed the xangcastle/cross-sdist branch 3 times, most recently from 01fdffa to 9061079 Compare August 6, 2026 17:59
xangcastle added a commit that referenced this pull request Aug 11, 2026
Ports four cases from rules_pycross's e2e suite, exercising the
**existing** sdist→wheel pipeline (host builds only — no
cross-compilation involved). Split out of #1363, where these cases don't
depend on the cross feature; every test passes against current `main`
unchanged.

## Cases

- **pycross-setuptools** (`build_setuptools`): three setuptools
C-extension sdists — PyYAML (Cython-generated extension, with an
observable `PYYAML_FORCE_LIBYAML=0` env override), setproctitle (plain
C, `pre_build_patches`), zstandard (vendored libzstd, `resource_set`).
- **pycross-patches** (`patches_and_hooks`): pre-build and post-install
patch phases stacked on setproctitle — the post-install hunk carries the
pre-build patch's output as context, so an ordering regression fails the
build itself.
- **pycross-pure-python** (`build_pure_python`): hatchling and flit-core
backends, runtime imports, site-packages placement hygiene, plus a
`collect_wheels` matrix asserting anyarch wheels stay `-none-any` under
non-host platform transitions (exec-platform resolution of the build
tooling, no native toolchain needed).
- **pycross-distutils-probe**: build-action env hygiene — a fresh child
interpreter spawned by the backend must resolve `distutils` on Python
3.12+, guarding `rule.bzl`'s `_INHERITED_PYTHON_ENV` filter. Fails only
in the child, so it's invisible to a plain "does it build" check.

Shared `tools/`: `collect_wheels` macro (adds the wheel-tag assertion
the rules_pycross original lacks) and `check_wheel_tags.py`.

## Not included

`pycross-setuptools`' cross matrix (`native_wheels*`,
`check_wheel_native.py`): building a C extension for a non-host platform
needs `native_build_toolchain_type` resolution for that platform, which
lands with #1363. The second commit documents that scope cut.

---

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


### Test plan

- New test cases added

---------

Co-authored-by: Jason Bedard <jason+github@jbedard.ca>
@xangcastle
xangcastle force-pushed the xangcastle/cross-sdist branch from 324b8e6 to 2273078 Compare August 11, 2026 04:44
@xangcastle
xangcastle force-pushed the xangcastle/cross-sdist branch from e29dda8 to 9762970 Compare August 13, 2026 07:49
@coderabbitai

coderabbitai Bot commented Aug 13, 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: Pro Plus

Run ID: cc36652f-768d-4f04-8cad-74ee9ba5df9d

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

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

xangcastle added a commit that referenced this pull request Aug 13, 2026
…rkspace (#1431)

Groundwork for the upcoming cross-compilation support, split out to slim
down #1363.

The rules_pycross ports (`pycross-distutils-probe`, `pycross-patches`,
`pycross-pure-python`, `pycross-setuptools`) landed in `e2e/cases`, but
they are
sdist-crossbuild suites: their hubs carry package-specific overrides
(`default_build_dependencies`, pre/post-install patches, `resource_set`)
that
shouldn't share a module with unrelated cases. This PR moves them into
their own
`e2e/crossbuild` workspace — a minimal `MODULE.bazel` (rules_py via
`local_path_override`, `bazel_lib`, the LLVM toolchain, PBS interpreters
3.12/3.13, uv) — which is where the cross-build test matrix from #1363
will land
next.

Everything moves as pure renames; the wheel-collection tooling
(`collect_wheels`, `check_wheel_tags`) comes along since
`pycross-pure-python`
was its last consumer under `e2e/cases`. CI gets the new workspace in
the test
matrix and the macOS smoke job, which previously covered these suites
through
`e2e/cases`. No rule code changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
The four rules_pycross ports moved from e2e/cases into the crossbuild
workspace, where the cross toolchains they exercise actually live:

- restore the pycross-setuptools native_wheels matrix deferred out of
  #1413 (zstandard rebuilt for amd64/arm64 with ELF and wheel-tag
  assertions) — under this workspace's registered cross CC toolchain it
  runs where it couldn't on main
- carry the reviewed refinements from #1413 over the original port
- pin setproctitle to -std=gnu17: gcc_toolchain's GCC defaults to C23,
  where `bool` is a keyword and 1.3.2's `typedef char bool` breaks —
  coverage the e2e/cases host toolchain never gave us
- drop the cases, their MODULE includes and the collect_wheels tooling
  from e2e/cases
uv-deps-650/crossbuild's test_crossbuild_pyc.sh guarded whl_install's
compileall picking the exec-platform interpreter under a cross
transition; its removal left that regression uncovered. The restored
guard is stronger than the original "it builds" check:

- the platform pins //uv/private/pyc:precompile=True, so the coverage
  survives a future default flip instead of silently draining
- the sh_test asserts .pyc files actually exist in the cross-built
  install tree, catching exec_matches_target silently skipping
  compileall — verified to fail when precompile is off

Asserted over the binary's runfiles rather than an image layer:
py_image_layer deliberately drops __pycache__ from layer mtrees.
The committed lock carried lockFileVersion 28 (a bazel 9 artifact), which
--lockfile_mode=error rejects under the workspace's .bazelversion.
Behavior-neutral cleanup, verified by a full from-scratch rebuild of the
e2e/crossbuild suite (every action key changed with the tool):

- _get_wrapper_flags no longer reimplements the sysroot absolutization
  that _absolutize_sysroot_flags already does; it absolutizes first and
  only filters. The execroot rationale now lives in one place.
- A NoReturn _die() replaces nine print-to-stderr-then-exit blocks. The
  build-command-detection branch keeps an explicit raise because ty does
  not narrow NoReturn in module-level flow.
- _expected_cpu_in_tag drops the identity entries from its maps.
- Comments and docstrings compress to their load-bearing rationale; the
  module docstring now describes what the script actually is. Prose that
  restated the code, duplicated an in-template comment, or carried
  reference URLs is gone.
@xangcastle
xangcastle force-pushed the xangcastle/cross-sdist branch from 9762970 to 1e4e576 Compare August 14, 2026 03:03
Split rule.bzl following the one-rule-per-file convention py/private
already uses: pep517_whl.bzl (anyarch), pep517_native_whl.bzl (native +
cross), common.bzl (shared helpers and base attrs), with defs.bzl as the
package's single public surface. cc_layer.bzl and exec_transition.bzl
were already factored by concern and stay put.

The runtime tools move to tools/: build_helper.py (the main of every
generated build_tool py_binary) and memory_monitor.py, each still
following the package's constraints (single-file helper, conditional
py_library).

Mechanical fallout: the sdist_build BUILD template loads defs.bzl and
points at the tools/ labels (generated-repo snapshots regenerated in the
root and e2e/cases workspaces), test data labels and the
dirname/TEST_SRCDIR-walking path assumptions follow the move, and the CI
typecheck file lists pick up the new paths.
Two builder jobs produce docker-loadable OCI tarballs (new oci_load +
tarball targets in pep517_cross_case, tagged manual) and native runner
jobs docker-load and run them — no QEMU in the execution loop, so
emulation can neither mask nor introduce failures:

- crossbuild-images (linux/amd64 builder) uploads the arm64 tarballs;
  crossbuild-run-arm64 executes them on ubuntu-24.04-arm. Native amd64
  execution already happens in the e2e-crossbuild test job.
- crossbuild-images-darwin (gated like smoke, 10x-billed) cross-builds
  psutil/contourpy/rpds_py for both arches; crossbuild-run-darwin-built
  executes them on native amd64 + arm64. The macOS smoke job filters
  -requires-docker, so this is the first execution coverage the
  darwin-to-linux path gets.

The reverse direction (arm64 builder -> amd64 execution) is documented
as blocked in the workflow comment: gcc_toolchain registers exec=x86_64
only and //tools:rust_host_sysroot pins the x86_64 rust toolchain.
The native-runner jobs remove QEMU from the execution loop, but the
builder still needs it during the build itself: meson's compiler sanity
check and cc.run() probes execute target-arch binaries through the
exe_wrapper, which relies on binfmt_misc (contourpy failed with
"Executables created by cpp compiler ... are not runnable").
The smoke job and the darwin image builder invoke bazel with --config=ci
in every workspace; this one lost the definition when the branch's
.bazelrc superseded main's in the merge, failing with "Config value 'ci'
is not defined in any .rc file".
…licitly

The BCR llvm toolchain supplies its C++/unwind runtime (libc++.a,
libc++abi.a, libunwind.a) as toolchain inputs via static_runtime_lib —
never as link-action flags — and ships empty stub libraries on the
search path, so the wrapper's name-based "-lc++ -lc++abi" (and rustc's
"-lgcc_s" fold to "-lunwind") resolved against stubs. The resulting
.so's linked cleanly (undefined symbols are legal in shared links) and
only exploded at dlopen on the target: contourpy with 45 undefined
std::__1 symbols, rpds_py with undefined _Unwind_*. First caught by the
darwin-built native-execution CI jobs; gcc_toolchain never hits this
because its self-contained driver resolves its own runtime.

cc_layer now extracts static_runtime_lib behind the -nostdlib++ marker
(so the gcc path is untouched), the rule forwards the archives as action
inputs plus RULES_PY_CXX_STATIC_RUNTIME, and the cross wrapper links
them as absolute paths in dependency order on every ELF link, dropping
-lgcc_s. Archive semantics keep this inert for pure-C links.
Temporary instrumentation: the darwin builder's contourpy build fails in
meson's sysconfig python lookup with a bare UnicodeDecodeError one-liner;
MESON_FORCE_BACKTRACE names the file and line doing the read.
Both wrappers classified anything without "-c" as a link. meson probes
the compiler through the wrapper with "-E -v -", "-print-search-dirs",
and "--version"; the link-only additions were mostly inert there until
the static runtime archives became positional inputs — "-E" preprocesses
positional inputs, so libc++.a turned into megabytes of binary on stdout
that meson strictly utf-8 decodes (its sysconfig python lookup died with
UnicodeDecodeError on darwin hosts, where the absence of pkg-config is
what routes meson into that code path).

is_link now excludes -c/-E/-S/-fsyntax-only and the introspection
queries in both the native and cross wrappers.
An ubuntu-24.04-arm builder cross-compiles the per-backend subset to
amd64 and a native amd64 runner executes the artifacts — the reverse of
the existing direction. gcc_toolchain has no arm64-exec variant, so this
is also the first Linux-hosted exercise of the llvm toolchain fallback
(the same path darwin hosts take). rust and the CC layer needed no
changes: rust_host_sysroot re-resolves via exec_transition on any host,
and rules_rust already registers both linux target triples.
The passthrough exe_wrapper assumed Linux hosts can always run
target-arch ELFs via binfmt, but a dynamically-linked probe also needs
QEMU_LD_PREFIX to resolve the target's ld.so — and only gcc_toolchain's
layout provides that sysroot. Under the llvm fallback (empty sysroot, as
exercised by the arm64 builder) meson's sanity check died with "not
runnable". Gate the wrapper on RULES_PY_TARGET_GCC_SYSROOT: elsewhere
meson gets needs_exe_wrapper=true with no wrapper, the same honest
skip-or-explicit-error semantics darwin hosts already had.
…wnloads

--config=ci turns on --remote_download_outputs=minimal, so on cache hits
the tarball this job uploads never lands in bazel-bin and the cp fails;
toplevel keeps the byte savings for everything else.
The builder fleet is now three-way (amd64, arm64, darwin); the
unqualified name predates the other two.
Six flat jobs rendered as dependency spaghetti in the Actions graph.
Each builder direction is now one workflow_call node
(crossbuild-{amd64,arm64,darwin}) that expands to its own images->run
chain, parameterized over the four real differences: builder runner,
setup flavor, case list, and native-executor matrix. The build command
is now uniform too — every builder gets --config=ci with toplevel
output materialization and the meson traceback instrumentation.
…skip

The previous needs list referenced the pre-refactor job names (twice),
which is a workflow startup error — the whole CI run failed to launch.
test-all now needs the three reusable-workflow nodes and requires
success from everything unconditional; crossbuild-darwin may be skipped
(it only runs on main and '*macos*' branches) but must pass when it ran.
Drop every container_structure_test from e2e/crossbuild: they compiled
and executed on the same host through a docker+QEMU sandwich, and the
crossbuild-verify pipelines have superseded them — each case's OCI
tarball is uploaded and `docker run` on native amd64 and arm64 runners,
which is where execution verification now lives exclusively. geohash and
zstandard gain the same {amd64,arm64}_load/_tarball targets the macro
declares, so the builder's tarball query picks them up automatically;
the amd64 pipeline now ships both arch sets (in-suite amd64 command
tests were the only native amd64 execution before).

`bazel test //...` in the workspace is structural-only (ELF arch, ABI
tags, byte-diffs) and no longer needs docker at all. The
container_structure_test dep is gone; uv_bin pin restored to 0.11.21,
the only version pinned in versions.bzl.
test-all's needs pointed at the reusable-workflow job names that the
flat-jobs rework removed — a workflow startup error. It now gates on the
three crossbuild-run jobs, tolerating the skip of the gated darwin one.

With the in-suite container tests gone, the amd64 builder was uploading
arm64 tarballs only, leaving the macro cases without native amd64
execution — it now ships both arch sets (with toplevel output
materialization) and its run job fans out to native amd64 and arm64
runners, mirroring the darwin pipeline's shape.
crossbuild-images-amd64 builds only the amd64 tarballs (native builds:
no cross machinery, no binfmt, no QEMU step) and a single
crossbuild-run-amd64-built job executes the artifact on a separate
native amd64 machine. arm64-target coverage from this host moves
entirely to the other pipeline directions.
The point of this direction is empirical cross-compilation validation:
build every case's arm64 image on the amd64 host (binfmt present only
for meson's build-time probes) and execute the artifact on a native
arm64 runner — one builder job, one runner job.
Same build-without-the-bytes failure the arm64 builder hit: with
--config=ci, disk-cache hits leave the tarball unmaterialized and the
cp fails. toplevel downloads fix it, as on the other builders.
@jbedard
jbedard force-pushed the main branch 2 times, most recently from b25e5ec to 25d3b44 Compare August 25, 2026 18:24
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