Skip to content

feat: support aarch64 hosts via a host_arch attribute - #236

Open
janickm wants to merge 1 commit into
f0rmiga:mainfrom
janickm:feat/aarch64-host-toolchains
Open

feat: support aarch64 hosts via a host_arch attribute#236
janickm wants to merge 1 commit into
f0rmiga:mainfrom
janickm:feat/aarch64-host-toolchains

Conversation

@janickm

@janickm janickm commented Aug 6, 2026

Copy link
Copy Markdown

Problem

Every toolchain this module generates hardcodes exec_compatible_with = [linux, x86_64] --
it is a string literal in the hub's BUILD template, while target_compatible_with right
beside it is a format field:

toolchain(
    name = "cc_toolchain{suffix}",
    exec_compatible_with = [
        "@platforms//os:linux",
        "@platforms//cpu:x86_64",   # literal
    ],
    target_compatible_with = {target_compatible_with},   # templated

So on an aarch64 host Bazel matches no cc toolchain at all -- every C/C++/Fortran action
fails with No matching toolchains found for @rules_cc//cc:toolchain_type, not just
cross-compiles.

The binaries to fix it already exist. Since
08072026, gcc-builds
publishes a -host-aarch64 archive for every target it builds, so all four GCC versions
currently pinned to 08072026 or later (14.3.0, 15.2.0, 16.1.0, 16.2.0) have aarch64-hosted
builds for aarch64, armv7 and x86_64 targets. Only the Starlark wiring was missing.

Change

A host_arch attribute names the architecture the toolchain binaries run on. It
defaults to rctx.os.arch, so a native build on either architecture needs no configuration,
and it drives:

  1. Which archive is fetched -- AVAILABLE_GCC_VERSIONS gains a host dimension:
    {version: {target_arch: {host_arch: {url, sha256}}}}.
  2. exec_compatible_with -- now a templated hub attribute defaulting to
    ["@platforms//os:linux", "@platforms//cpu:{host_arch}"], mirroring
    target_compatible_with.
  3. The builtin include layout -- see below.

The include layout was a latent bug

A native build lays libstdc++ out flat (include/c++/<version>) while a cross build
nests it under the target triple (<triple>/include/c++/<version>). The existing code keyed
that on target_arch == ARCHS.x86_64, which was correct only because every published build
was x86_64-hosted. It is now keyed on host_arch == target_arch, which is what the archives
actually do -- verified by listing all five tarballs:

host target layout
x86_64 x86_64 flat
x86_64 aarch64 nested
aarch64 aarch64 flat
aarch64 x86_64 nested
aarch64 armv7 nested

Getting this wrong is quiet: a nonexistent -isystem directory is ignored, so it surfaces
much later as a missing <string> naming no path at all.

Note this needs no binary_prefix handling: _detect_binary_prefix (added in #238) probes
the extracted archive, and every aarch64-hosted archive ships a prefixed bin/<triple>-as,
so detection already gets it right. Confirmed for all five combinations.

Backwards compatibility

Existing entries keep their exact URLs and digests -- asserted mechanically: all 18
(url, sha256) pairs on main appear verbatim, and the 12 added ones are all
-host-aarch64 archives.

A hand-written gcc_versions in the old {version: {target: {url, sha256}}} shape still
works and is read as x86_64-hosted. Combining it with a non-x86_64 host fails with a message
saying what to write instead:

gcc_versions for 16.2.0 target x86_64 lists a single (x86_64-hosted) build, but this
toolchain is aarch64-hosted. Give it a {host_arch: {url, sha256}} mapping.

and asking for a host that has no published build:

gcc 12.5.0 for target aarch64 has no aarch64-hosted build. Available: x86_64.

Verification

Rebased onto main (ed8c2ab), so this now sits on top of the hub/per-version split from
#239 and the prefix detection from #238.

Everything the CI workflow runs passes:

  • bazel test //... under both --config bzlmod and --config workspace (14 tests).
  • Every version in the gcc_versions matrix: 12.5.0 13.4.0 14.3.0 15.2.0 16.1.0 16.2.0.
  • All four sanitizers, --config lld, and bazel coverage.
  • USE_BAZEL_VERSION=9.1.0 bazel test -- //... -//docs/....
  • Cross-compilation to aarch64_linux, armv7_linux and x86_64_linux.

All host/target combinations were checked to fetch, resolve and declare the right
constraints -- that the compiler binary's ELF machine type matches its host_arch, that
the binary prefix is detected correctly, and that every declared builtin include
directory actually exists on disk:

declared exec_compatible_with target_compatible_with gcc ELF prefix includes
x86_64 / x86_64 x86_64 x86_64 x86-64 x86_64-linux- flat, all exist
x86_64 / aarch64 x86_64 aarch64 x86-64 aarch64-linux- nested, all exist
aarch64 / aarch64 aarch64 aarch64 AArch64 aarch64-linux- flat, all exist
aarch64 / x86_64 aarch64 x86_64 AArch64 x86_64-linux- nested, all exist
aarch64 / armv7 aarch64 armv7 AArch64 arm-linux-gnueabihf- nested, all exist

The two x86_64-hosted rows are unchanged from before this PR.

//tests/host_arch asserts the aarch64-hosted compiler is an ARM ELF by reading e_machine
at offset 18, so it needs no ARM hardware and runs on any host. Confirmed it fails when
pointed at the x86_64-hosted cross toolchain (e_machine 0x3e00, want 0xb7), so it is not a
test that cannot fail. The toolchain is declared but deliberately not registered, since
its binaries cannot execute on x86_64 CI. It is marked incompatible for 12.5.0 and 13.4.0,
whose releases publish no aarch64-hosted build, the same way //tests/lld handles the
versions built without lld -- being unanalyzed, the archive that does not exist is never
fetched.

Notes

  • Docs regenerated with bazel run //docs:update; //docs:update_test passes.
  • docs/updating-gcc-builds.md said the -host-aarch64 variants "are ignored. Filter them
    out" -- no longer true, so it now documents the host dimension, and the native-vs-cross
    include layout under "Handling tarball layout changes".
  • docs/README.md gains a short "Choosing the host architecture" section.
  • host_arch is values = ["", "aarch64", "x86_64"]; armv7 is a target only.
  • Downstream context: this unblocks running Bazel natively on aarch64 CI runners (DGX Spark
    / GB10). I validated it against that repo by pointing it at this branch: with host_arch
    the local workaround -- a use_repo_rule wrapper re-declaring toolchain() with the
    right exec constraint, plus hand-written gcc_versions, binary_prefix and includes --
    deletes entirely and becomes one gcc.toolchain(host_arch = "aarch64", ...) call. Its
    full test suite passes, --config=arm64 still resolves the x86_64-hosted cross toolchain,
    and with an aarch64 execution platform registered resolution picks the ARM-hosted one.

@f0rmiga

f0rmiga commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Thanks for the contribution, can you rebase onto main, please?

Every toolchain this module generates hardcodes
`exec_compatible_with = [linux, x86_64]` -- it is a string literal in the hub's
BUILD template, while target_compatible_with beside it is a format field. So on
an aarch64 host Bazel matches no cc toolchain at all and every C/C++ action fails
resolution, not just cross-compiles.

The binaries to fix that already exist: gcc-builds has published `-host-aarch64`
archives since 08072026, covering aarch64, armv7 and x86_64 targets for every GCC
version pinned to 08072026 or later (14.3.0, 15.2.0, 16.1.0 and 16.2.0). Only the
Starlark wiring was missing.

`host_arch` names the architecture the toolchain binaries RUN on. It defaults to
`rctx.os.arch`, so a native build on either architecture needs no configuration,
and it drives three things:

- which archive is fetched, via a host dimension on AVAILABLE_GCC_VERSIONS;
- `exec_compatible_with`, now a templated hub attribute defaulting to
  `[linux, {host_arch}]`, mirroring target_compatible_with;
- the builtin include layout.

That last one was a latent bug. A NATIVE build lays libstdc++ out flat
(`include/c++/<version>`) while a CROSS build nests it under the target triple,
and the existing code keyed that on `target_arch == x86_64` -- correct only
because every published build was x86_64-hosted. It is now keyed on
`host_arch == target_arch`, which is what the archives actually do: the
aarch64-hosted aarch64 build is flat, and the aarch64-hosted x86_64 one is
nested. Getting this wrong is quiet, because a nonexistent -isystem directory is
ignored and only surfaces later as a missing <string>.

Backwards compatible. Existing entries keep their exact URLs and digests, and the
binary prefix needs no host handling because _detect_binary_prefix already probes
the extracted archive. A hand-written `gcc_versions` in the old
`{version: {target: {url, sha256}}}` shape still works and is read as
x86_64-hosted; combining it with a non-x86_64 host fails with a message saying
what to write instead.

Verified all four host/target combinations (plus aarch64-hosted armv7) resolve,
fetch the archive whose ELF machine matches their host_arch, and declare only
include directories that exist -- and that the x86_64-hosted paths are unchanged.
//tests/host_arch asserts the aarch64-hosted compiler is an ARM ELF by reading
e_machine, so it runs anywhere; it fails if pointed at the x86_64-hosted cross
toolchain. It is skipped for 12.5.0 and 13.4.0, whose releases publish no
aarch64-hosted build, as //tests/lld is for the versions built without lld.
@janickm
janickm force-pushed the feat/aarch64-host-toolchains branch from d3ee3f7 to 3176328 Compare August 12, 2026 09:18
@janickm

janickm commented Aug 12, 2026

Copy link
Copy Markdown
Author

Rebased onto main (ed8c2ab) -- thanks for the pointer, #238 and #239 changed enough that this
is a genuine re-application rather than a replay:

  • exec_compatible_with now lives in the hub repo from feat: select the GCC version through a build flag #239, so it became a
    _TOOLCHAIN_DECLARATION_ATTRS entry alongside target_compatible_with, rendered once per
    declared version for both the cc and Fortran toolchain()s.
  • The host-dependent binary_prefix default I had is gone: _detect_binary_prefix from feat: update gcc builds to 10/08/2026 #238
    already probes bin/<triple>-as, and every aarch64-hosted archive ships one. Verified for all
    five host/target combinations, so that part of the patch was simply obsoleted by your change.
  • The version table now pins 14.3.0/15.2.0/16.1.0 to 08072026 and 16.2.0 to 10082026 -- which
    happen to be exactly the releases that publish -host-aarch64 assets. So instead of the earlier
    "existing pins stay on 03062026, new host builds come from 08072026" split, every one of the four
    versions gets an aarch64-hosted entry from the release it is already pinned to. The 18 existing
    (url, sha256) pairs are unchanged, asserted mechanically.
  • //tests/host_arch is now target_compatible_with-gated for 12.5.0 and 13.4.0, the same way
    //tests/lld gates the versions built without lld, since the per-version hub would otherwise try
    to fetch an aarch64-hosted 12.5.0 that does not exist under the gcc_versions CI matrix.
  • docs/updating-gcc-builds.md from feat: update gcc builds to 10/08/2026 #238 explicitly said the -host-aarch64 variants "are
    ignored. Filter them out when collecting assets" -- that is no longer true, so I updated it, and
    documented the native-vs-cross include layout in its layout-changes section.

Re-verified everything the workflow runs on the rebase: //... under both bzlmod and workspace,
all six versions in the gcc_versions matrix, all four sanitizers, --config lld, bazel coverage,
Bazel 9.1.0, and cross-compiles to all three platforms. Full details and the per-combination table
(ELF machine, detected prefix, include-dir existence) are in the updated description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants