Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 100 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,9 +496,106 @@ See [#83][pr-83] for more context.
### OSX: sysroot

For non-trivial programs (and for all darwin/arm64 cgo programs) MacOS SDK may
be necessary. Read [Jakub's comment][sysroot] about it. 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`.
be necessary. Read [Jakub's comment][sysroot] about it.

The `toolchains` module extension accepts an optional `sysroot` tag,
configuring an external sysroot (e.g. a macOS SDK) for the zig cc toolchains
targeting a given OS. It is entirely optional and backward-compatible: with no
`sysroot` tag, toolchains behave exactly as before. As discussed in [#10][pr-10],
the sysroot itself must come from an `http_archive` (or another repo rule
producing an equivalent filegroup) — `hermetic_cc_toolchain` does not fetch or
redistribute Apple's SDK itself.

```starlark
sysroot(
os, # "linux" | "windows" | "macos"
path = "", # exec-root-relative path to the sysroot
include_dirs = [], # extra cxx_builtin_include_directories (e.g. <path>/usr/include)
copts = [], # extra compiler flags, e.g. "-F <path>/System/Library/Frameworks"
linkopts = [], # extra linker flags
files = "", # label (as a string) of a filegroup staging the sysroot's files
)
```

`path`/`include_dirs` are registered as `builtin_sysroot`/
`cxx_builtin_include_directories` so Bazel's header-inclusion validation
accepts headers found there without forcing every SDK header onto the
compiler's `-I` search path ahead of libc++'s own headers (`include_dirs` are
searched with `-idirafter`, the lowest priority, after the toolchain's own
headers). `files` is required for the sysroot to work on actions that don't
otherwise declare it as a dependency (Bazel does not create the external
repo's symlink in the sandbox otherwise, and the `-I`/`-isysroot` paths would
dangle).

Two ways to get the SDK into a `files` filegroup:

**1. A prebuilt redistributable mirror**, e.g.
[`joseluisq/macosx-sdks`](https://github.com/joseluisq/macosx-sdks) (a widely
used mirror of the Xcode Command Line Tools SDK):

```starlark
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

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 = use_extension("@hermetic_cc_toolchain//toolchain:ext.bzl", "toolchains")
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 approach [openai/codex](https://github.com/openai/codex) takes for its
own, unrelated `llvm`-based toolchain — `http_archive`'s `type = "pkg"`
support extracts a `.pkg` payload the same way it extracts a `.tar.gz`):

```starlark
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",
)
```

Known limitation: pure zig-cc `-framework` linking of a cross target
(`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 (see [ziglang/zig#10299][sysroot]). Consumers with a raw
non-zig-cc linker in the loop (e.g. `rules_rust`'s `rust-lld`, or a
`rules_foreign_cc`/cmake build) aren't affected, since they never go through
zig cc's own linker driver for the final link.

In essence, OSX target support is not well tested with `hermetic_cc_toolchain`.
Also see [#10][pr-10].
Expand Down
2 changes: 2 additions & 0 deletions toolchain/BUILD.sdk.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ package(

declare_files(
os = {os},
sysroots_json = {sysroots_json},
)

declare_cc_toolchains(
os = {os},
sysroots_json = {sysroots_json},
zig_sdk_path = {zig_sdk_path},
)

Expand Down
43 changes: 39 additions & 4 deletions toolchain/defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,25 @@ def toolchains(
exec_platforms = {},
extra_exec_compatible_with = [],
extra_target_compatible_with = [],
extra_target_settings = []):
extra_target_settings = [],
sysroots = {}):
"""
Download zig toolchain and declare bazel toolchains.
The platforms are not registered automatically, that should be done by
the user with register_toolchains() in the WORKSPACE file. See README
for possible choices.

sysroots: optional dict keyed by target os ("macos"/"linux"/"windows")
with values {path, include_dirs, copts, linkopts, files}. When set, the
zig cc toolchains for that os use the external sysroot (e.g. a macOS
SDK). See the `sysroot` tag in toolchain/ext.bzl. Backward-compatible:
empty by default.
"""

# Repo rule attrs can't carry nested dicts, so serialize to JSON and
# decode inside the repository / the generated BUILD.
sysroots_json = json.encode(sysroots)

if not url_formats:
if "dev" in version:
mirror_format = URL_FORMAT_NIGHTLY.replace("https://ziglang.org/", "https://mirror.bazel.build/ziglang.org/")
Expand All @@ -98,6 +109,7 @@ def toolchains(
url_formats = url_formats,
host_platform_sha256 = host_platform_sha256,
host_platform_ext = host_platform_ext,
sysroots_json = sysroots_json,
)

private_repos = ["zig_config"]
Expand All @@ -112,6 +124,7 @@ def toolchains(
host_platform_ext = host_platform_ext,
exec_os = os,
exec_arch = arch,
sysroots_json = sysroots_json,
)
private_repos.append("zig_config-{}-{}".format(os, arch))

Expand Down Expand Up @@ -175,6 +188,7 @@ def _zig_repository_impl(repository_ctx):
"{os}": quote(exec_os),
"{exec_os}": exec_os,
"{exec_cpu}": exec_arch,
"{sysroots_json}": quote(repository_ctx.attr.sysroots_json),
},
)

Expand Down Expand Up @@ -289,6 +303,7 @@ zig_repository = repository_rule(
"host_platform_ext": attr.string_dict(),
"exec_os": attr.string(default = "HOST"),
"exec_arch": attr.string(default = "HOST"),
"sysroots_json": attr.string(default = "{}"),
},
environ = ["HERMETIC_CC_TOOLCHAIN_CACHE_PREFIX"],
implementation = _zig_repository_impl,
Expand All @@ -298,9 +313,27 @@ def filegroup(name, **kwargs):
native.filegroup(name = name, **kwargs)
return ":" + name

def declare_files(os):
def declare_files(os, sysroots_json = "{}"):
exe = ".exe" if os == "windows" else ""

# Per-target-OS sysroot file labels to STAGE into compile/link actions, so
# the sysroot's -I/-isysroot paths resolve in the exec root (see the
# `files` attr in toolchain/ext.bzl). Keyed by the "-<os>-" triple token.
sysroots = json.decode(sysroots_json) if sysroots_json else {}
_sysroot_token_files = {}
for _os_name, _cfg in sysroots.items():
_files = _cfg.get("files", "")
if _files:
_token = {"linux": "-linux-", "macos": "-macos-", "windows": "-windows-"}.get(_os_name)
if _token:
_sysroot_token_files[_token] = _files

def _sysroot_files_for(zigtarget):
for _token, _lbl in _sysroot_token_files.items():
if _token in zigtarget:
return [_lbl]
return []

native.exports_files(["zig{}".format(exe)], visibility = ["//visibility:public"])
if os == "windows":
native.alias(name = "zig", actual = ":zig.exe")
Expand All @@ -325,13 +358,15 @@ def declare_files(os):
srcs = _flatten(all_includes),
)

sysroot_files = _sysroot_files_for(target_config.zigtarget)

filegroup(
name = "{}_compiler_files".format(target_config.zigtarget),
srcs = [
":zig",
":{}_includes".format(target_config.zigtarget),
cxx_tool_label,
],
] + sysroot_files,
)

filegroup(
Expand All @@ -355,7 +390,7 @@ def declare_files(os):
"lib/c/**",
"lib/*.zig",
"lib/*.h",
]),
]) + sysroot_files,
)

filegroup(
Expand Down
54 changes: 54 additions & 0 deletions toolchain/ext.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,48 @@ _extra_exec_compatible_with = tag_class(
doc = "Extra constraints added to every toolchain's `exec_compatible_with`",
)

_sysroot = tag_class(
attrs = {
"os": attr.string(
values = ["linux", "windows", "macos"],
mandatory = True,
doc = "Target OS whose zig cc toolchains should use this sysroot.",
),
"path": attr.string(
doc = "Exec-root-relative path to the sysroot. Passed as " +
"`--sysroot <path>` (and `-isysroot <path>` on macos). " +
"May be empty if only include_dirs/copts/linkopts are needed.",
),
"include_dirs": attr.string_list(
doc = "Extra directories registered as the toolchain's " +
"cxx_builtin_include_directories (so Bazel's header " +
"validation accepts headers found there). Typically " +
"`<path>/usr/include` and the macOS framework roots.",
),
"copts": attr.string_list(
doc = "Extra compiler flags for this OS's toolchains, e.g. " +
"`-F <path>/System/Library/Frameworks`.",
),
"linkopts": attr.string_list(
doc = "Extra linker flags for this OS's toolchains, e.g. " +
"`-F <path>/System/Library/Frameworks` and `-L <path>/usr/lib`.",
),
"files": attr.string(
doc = "Optional label (as a string, e.g. \"@macos_sdk//:sysroot\") " +
"of a filegroup of the sysroot's files. It is added to the " +
"toolchains' compiler_files/linker_files so every compile/" +
"link action STAGES the sysroot into the exec root (Bazel " +
"does not create the external symlink otherwise, and the " +
"-I/-isysroot paths would dangle). Required for the sysroot " +
"to work on actions that don't otherwise depend on it (e.g. " +
"the @m4 BCR compile).",
),
},
doc = "Configure an external sysroot (e.g. a macOS SDK) for the zig cc " +
"toolchains targeting a given OS. Optional and backward-compatible: " +
"with no sysroot tag the toolchains behave exactly as before.",
)

def _toolchains_impl(mctx):
exec_platforms = {}
root_direct_deps = []
Expand All @@ -44,6 +86,7 @@ def _toolchains_impl(mctx):
extra_exec_compatible_with = []
extra_target_compatible_with = []
extra_target_settings = []
sysroots = {}

for mod in mctx.modules:
if mod.is_root:
Expand All @@ -62,11 +105,21 @@ def _toolchains_impl(mctx):
for tag in mod.tags.extra_target_compatible_with:
extra_target_compatible_with += tag.constraints

for tag in mod.tags.sysroot:
sysroots[tag.os] = {
"path": tag.path,
"include_dirs": tag.include_dirs,
"copts": tag.copts,
"linkopts": tag.linkopts,
"files": tag.files,
}

repos = zig_toolchains(
exec_platforms = exec_platforms,
extra_exec_compatible_with = extra_exec_compatible_with,
extra_target_compatible_with = extra_target_compatible_with,
extra_target_settings = extra_target_settings,
sysroots = sysroots,
)

root_direct_deps = list(repos.public) if is_non_dev_dependency else []
Expand All @@ -89,5 +142,6 @@ toolchains = module_extension(
"extra_exec_compatible_with": _extra_exec_compatible_with,
"extra_target_compatible_with": _extra_target_compatible_with,
"extra_target_settings": _extra_target_settings,
"sysroot": _sysroot,
},
)
Loading
Loading