Skip to content

toolchain: add configurable external sysroot support (zig cc) - #263

Open
DhashS wants to merge 2 commits into
uber:mainfrom
DhashS:sysroot-support
Open

DhashS wants to merge 2 commits into
uber:mainfrom
DhashS:sysroot-support

Conversation

@DhashS

@DhashS DhashS commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements configurable external sysroot support for the zig cc toolchains, as invited by the README's "OSX: sysroot" section:

Support for OSX sysroot is currently not implemented, but patches implementing it will be accepted, as long as the OSX sysroot must come through an http_archive.

Adds an optional sysroot tag to the toolchains module extension. Fully backward-compatible — with no sysroot tag, toolchains behave exactly as before.

zig_toolchains = use_extension("@hermetic_cc_toolchain//toolchain:ext.bzl", "toolchains")
zig_toolchains.sysroot(
    os = "macos",            # "linux" | "windows" | "macos"
    path = "...",            # exec-root-relative path to the sysroot
    include_dirs = [...],    # extra cxx_builtin_include_directories
    copts = [...],           # extra compiler flags
    linkopts = [...],        # extra linker flags
    files = "@macos_sdk//:sysroot",  # filegroup label staging the sysroot
)

Design notes

  • path/include_dirs are registered as builtin_sysroot/cxx_builtin_include_directories, so Bazel's header-inclusion validation accepts headers found under the sysroot without putting the whole SDK ahead of the toolchain's own libc++ headers on the -I search path. include_dirs are searched via -idirafter (lowest priority, after the toolchain's own headers) — a plain -I would shadow libc++'s own <cstdio> etc. with the SDK's C headers and break C++.
  • The implicit --sysroot=<path> Bazel normally auto-appends once builtin_sysroot is set gets suppressed (via an empty override of the built-in "sysroot" feature). Consumers with a raw, non-zig-cc linker in their link step (e.g. rules_rust's rust-lld, invoked directly to work around zig's own -oso_prefix gap) would otherwise choke on a --sysroot= flag their linker doesn't understand. Instead, copts/linkopts pass --sysroot/-isysroot/-F/-L explicitly, only to actions that actually go through zig cc.
  • files stages the sysroot into actions that need it but don't otherwise declare a dependency on it (e.g. a BCR module's own compile action) — Bazel won't create the external repo's sandbox symlink otherwise, and the -I/-isysroot paths would dangle.

Two usage examples

Both fetch the SDK via http_archive, per the README's constraint — they differ only in where the archive comes from.

1. A prebuilt redistributable mirror (joseluisq/macosx-sdks):

http_archive(
    name = "macos_sdk",
    build_file_content = """\
filegroup(
    name = "sysroot",
    srcs = glob(["usr/include/**", "usr/lib/**", "System/Library/Frameworks/**", "SDKSettings.*"]),
    visibility = ["//visibility:public"],
)
""",
    sha256 = "6e146275d19f027faa2e8354da5e0267513abf013b8f16ad65a231653a2b1c5d",
    strip_prefix = "MacOSX14.5.sdk",
    urls = ["https://github.com/joseluisq/macosx-sdks/releases/download/14.5/MacOSX14.5.sdk.tar.xz"],
)

zig_toolchains.sysroot(
    os = "macos",
    path = "external/+http_archive+macos_sdk",
    include_dirs = ["external/+http_archive+macos_sdk/usr/include"],
    copts = ["-iframework", "external/+http_archive+macos_sdk/System/Library/Frameworks"],
    files = "@@+http_archive+macos_sdk//:sysroot",
)

2. Apple's own CDN, downloading the Command Line Tools .pkg directly — the pattern openai/codex uses for its own (unrelated, LLVM-based) toolchain. http_archive's type = "pkg" extracts a .pkg payload the same way it extracts a .tar.gz:

http_archive(
    name = "macos_sdk",
    build_file_content = """\
filegroup(
    name = "sysroot",
    srcs = glob(["usr/include/**", "usr/lib/**", "System/Library/Frameworks/**"]),
    visibility = ["//visibility:public"],
)
""",
    sha256 = "5f044578cd78a3a9b9c965a42d56bad609ee5d252e1d4e6aa7c42fc3f35fee7b",
    strip_prefix = "Payload/Library/Developer/CommandLineTools/SDKs/MacOSX26.5.sdk",
    type = "pkg",
    urls = ["https://swcdn.apple.com/content/downloads/09/08/047-91568-A_Y1CFZWQCD4/4xekpyz43i26dbp4enxfro8eb1q7wiujh5/CLTools_macOSNMOS_SDK.pkg"],
)

zig_toolchains.sysroot(
    os = "macos",
    path = "external/+http_archive+macos_sdk",
    include_dirs = ["external/+http_archive+macos_sdk/usr/include"],
    files = "@@+http_archive+macos_sdk//:sysroot",
)

(Note: openai/codex itself doesn't use hermetic_cc_toolchain — it depends on a separate bzlmod module named llvm (hermeticbuild/hermetic-llvm) with its own osx.from_archive extension for the same "pull the SDK from Apple's CDN into a Bazel repo" idea. Cited here only as prior art for the download source, not as an existing hermetic_cc_toolchain consumer.)

Known limitation

Pure zig-cc -framework linking of a cross target (e.g. aarch64-macos-none from a non-macOS host) is still limited — zig resolves the top-level .tbd but its absolute /usr/lib reexports aren't re-rooted at the sysroot (ziglang/zig#10299). This doesn't affect consumers with a raw non-zig-cc linker in the loop (rust-lld, or a cmake-driven rules_foreign_cc build).

Testing

Verified end-to-end in a real downstream project: a cgo-enabled Go binary needing macOS's <resolv.h> (not in zig's own curated header subset) built alongside Rust binaries that link via rust-lld — both consuming the same registered toolchain, with and without the sysroot tag set, confirming the feature is a strict opt-in with no effect on existing (non-macOS, no-sysroot) targets.

DhashS added 2 commits August 12, 2026 18:13
Adds an optional `sysroot` tag to the `toolchains` module extension,
configuring an external sysroot (e.g. a macOS SDK) for the zig cc
toolchains targeting a given OS ("linux"/"windows"/"macos"). Entirely
optional and backward-compatible: with no `sysroot` tag, toolchains
behave exactly as before.

sysroot(os, path, include_dirs, copts, linkopts, files):
  - path/include_dirs are registered as builtin_sysroot/
    cxx_builtin_include_directories, so Bazel's header-inclusion
    validation accepts headers found under the sysroot without
    forcing them onto the compiler's -I search path ahead of the
    toolchain's own libc++ headers (include_dirs are searched with
    -idirafter, after the toolchain's own headers, not -I).
  - Bazel's implicit `--sysroot=<path>` (normally auto-appended once
    builtin_sysroot is set) is suppressed by overriding the built-in
    "sysroot" feature with an empty one; callers instead pass
    --sysroot/-isysroot to the *compiler* explicitly via copts, and
    SDK search paths (-F/-L) to the *linker* via linkopts. This keeps
    consumers that invoke a raw linker directly (rules_rust's
    rust-lld, which never goes through zig cc) unaffected by a
    --sysroot= flag their linker doesn't understand.
  - files is a label (string) of a filegroup that stages the
    sysroot's contents into every compile/link action that needs it
    but doesn't otherwise depend on it (e.g. a BCR module's own
    compile actions) — Bazel does not create the external repo's
    symlink in the sandbox otherwise, and the -I/-isysroot paths
    would dangle.

Per README's "OSX: sysroot" section, the sysroot itself must come
from an http_archive (or another repo rule producing an equivalent
filegroup) — this does not fetch or redistribute Apple's SDK.
Documents both a prebuilt-mirror-tarball source and an Apple-CDN
.pkg-download source in the README, since http_archive's `type =
"pkg"` handles the latter the same way it handles a .tar.gz.

Known limitation: pure zig-cc `-framework` linking of a cross target
(darwin from a non-macOS host) is still limited — zig resolves the
top-level .tbd but its absolute /usr/lib reexports aren't re-rooted
at the sysroot (ziglang/zig#10299). Consumers with a raw non-zig-cc
linker in the loop (rust-lld, or a cmake-driven rules_foreign_cc
build) aren't affected.

Verified end-to-end in a real downstream project building a
cgo-enabled Go binary that needs macOS's <resolv.h> (not in zig's own
curated header subset) alongside Rust binaries linked via rust-lld.
ci/lint runs buildifier -mode fix then asserts a clean git diff. The
sysroot changes had two formatting violations: unsorted kwargs in the
declare_cc_toolchains() call in BUILD.sdk.bazel, and a missing blank
line separating the isysroot block from the following comment in
cc_toolchains.bzl.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant