From b15a0f8ca10d5d160a977904f6f6dc182f8dd2ea Mon Sep 17 00:00:00 2001 From: Abhishek Rai Date: Sun, 19 Apr 2026 13:19:36 -0700 Subject: [PATCH] feat: productionize Zig toolchain setup --- MODULE.bazel.lock | 12 ++--- README.md | 38 +++++++++++--- zig/extensions.bzl | 7 +++ zig/private/versions.bzl | 35 +++++++++++++ zig/private/zig_repository.bzl | 39 +++++--------- zig/rules.bzl | 94 +++++++++++++++------------------- 6 files changed, 132 insertions(+), 93 deletions(-) create mode 100644 zig/private/versions.bzl diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index e4bbe88..fb5b2b8 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -189,14 +189,14 @@ "moduleExtensions": { "//zig:extensions.bzl%zig": { "general": { - "bzlTransitiveDigest": "VC7m0+AazLUhKgJl0i71MBRzAsgfidDInr5S8NPJMOg=", - "usagesDigest": "iEp0QLPEFK94gZxh67yAIWjjvAnxBEn2cCMoE+RRHY0=", + "bzlTransitiveDigest": "FUodQNJVgCSqxwkc4v/mbiiCeZFZOAk1f3buP+yS+tE=", + "usagesDigest": "1zsLy7Z6DquC93RpnXpFVDS+CNStZmTrLh3/REgEtcQ=", "recordedInputs": [], "generatedRepoSpecs": { "zig_macos_aarch64": { "repoRuleId": "@@//zig/private:zig_repository.bzl%zig_repository", "attributes": { - "zig_version": "0.15.2", + "zig_version": "0.13.0", "platform": "macos-aarch64", "index_key": "aarch64-macos" } @@ -204,7 +204,7 @@ "zig_macos_x86_64": { "repoRuleId": "@@//zig/private:zig_repository.bzl%zig_repository", "attributes": { - "zig_version": "0.15.2", + "zig_version": "0.13.0", "platform": "macos-x86_64", "index_key": "x86_64-macos" } @@ -212,7 +212,7 @@ "zig_linux_x86_64": { "repoRuleId": "@@//zig/private:zig_repository.bzl%zig_repository", "attributes": { - "zig_version": "0.15.2", + "zig_version": "0.13.0", "platform": "linux-x86_64", "index_key": "x86_64-linux" } @@ -220,7 +220,7 @@ "zig_linux_aarch64": { "repoRuleId": "@@//zig/private:zig_repository.bzl%zig_repository", "attributes": { - "zig_version": "0.15.2", + "zig_version": "0.13.0", "platform": "linux-aarch64", "index_key": "aarch64-linux" } diff --git a/README.md b/README.md index 8997454..8ac13d3 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,12 @@ Bazel rules for building [Zig](https://ziglang.org/) projects. ## Features -- `zig_binary` — compile Zig source files into an executable -- `zig_library` — compile Zig source files into a static library (`.a`) -- `zig_test` — compile and run Zig tests with `bazel test` +- `zig_binary` compiles Zig source files into an executable +- `zig_library` compiles Zig source files into a static library (`.a`) +- `zig_test` compiles and runs Zig tests with `bazel test` - Bazel toolchain integration with platform-aware compiler resolution +- Hermetic Zig action caches that live under Bazel action outputs, not shared `/tmp` state +- Pinned Zig SDK metadata checked into this repository for reproducible toolchain resolution ## Requirements @@ -21,12 +23,11 @@ The repository currently runs CI on: - Bazel 9.0.1 on Linux and macOS - Bazel 9.0.2 on Linux and macOS -This gives us coverage for the minimum supported Bazel 9 line and a newer Bazel 9 patch release that is currently available on GitHub-hosted runners. +This gives coverage for the minimum supported Bazel 9 line and a newer Bazel 9 patch release that is currently available on GitHub-hosted runners. CI currently runs: -- `bazel build //...` -- `bazel test ...` when Bazel test targets are present +- `bazel test //...` As the ruleset grows, this matrix can expand to include more Bazel versions, additional examples, and stricter validation. @@ -55,10 +56,35 @@ register_toolchains("@zig_toolchains//:all") Replace `` with the commit you want to pin to. +### Supported Zig SDK versions + +Zig SDK download metadata is pinned in `zig/private/versions.bzl`. + +Today that means: + +- `zig.toolchain(zig_version = "0.13.0")` works +- unpinned versions fail during module resolution with a clear error + +That keeps toolchain URLs stable and reviewable in-repo instead of depending on whatever `https://ziglang.org/download/index.json` returns later. + ### WORKSPACE WORKSPACE is not supported. Bazel 9+ requires Bzlmod. +## Exec platform vs target platform + +These rules currently resolve the Zig SDK as a normal Bazel toolchain, so the SDK is selected for the action's exec platform. + +In practice: + +- local macOS builds use a macOS Zig SDK +- local Linux builds use a Linux Zig SDK +- remote execution would pick the Zig SDK that matches the remote exec platform + +`zig_binary`, `zig_library`, and `zig_test` do not yet model Bazel target-platform driven cross-compilation. They invoke Zig without a `-target` flag, so outputs are currently host-native for the chosen exec-platform SDK. + +So for now, treat these rules as host-native builds with exec-platform-aware toolchain selection, not full Bazel cross-compilation rules yet. + ## Rules ### `zig_binary` example diff --git a/zig/extensions.bzl b/zig/extensions.bzl index 2b42362..51422dd 100644 --- a/zig/extensions.bzl +++ b/zig/extensions.bzl @@ -1,5 +1,6 @@ """Module extension for setting up the Zig toolchain.""" +load("//zig/private:versions.bzl", "pinned_zig_versions") load("//zig/private:zig_repository.bzl", "zig_repository") load("//zig/private:zig_toolchains_repo.bzl", "zig_toolchains_repo") @@ -37,6 +38,12 @@ def _zig_impl(ctx): fail("No zig.toolchain() tag was specified. " + "Add zig.toolchain(zig_version = \"0.13.0\") to your MODULE.bazel.") + if zig_version not in pinned_zig_versions(): + fail("Unsupported zig_version '{}'. Pinned versions: {}.".format( + zig_version, + ", ".join(pinned_zig_versions()), + )) + toolchain_names = [] toolchain_repo_names = [] os_constraints = [] diff --git a/zig/private/versions.bzl b/zig/private/versions.bzl new file mode 100644 index 0000000..03a515c --- /dev/null +++ b/zig/private/versions.bzl @@ -0,0 +1,35 @@ +"""Pinned Zig SDK metadata used by rules_zig.""" + +_ZIG_VERSIONS = { + "0.13.0": { + "aarch64-linux": struct( + tarball = "https://ziglang.org/download/0.13.0/zig-linux-aarch64-0.13.0.tar.xz", + sha256 = "041ac42323837eb5624068acd8b00cd5777dac4cf91179e8dad7a7e90dd0c556", + strip_prefix = "zig-linux-aarch64-0.13.0", + ), + "aarch64-macos": struct( + tarball = "https://ziglang.org/download/0.13.0/zig-macos-aarch64-0.13.0.tar.xz", + sha256 = "46fae219656545dfaf4dce12fb4e8685cec5b51d721beee9389ab4194d43394c", + strip_prefix = "zig-macos-aarch64-0.13.0", + ), + "x86_64-linux": struct( + tarball = "https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz", + sha256 = "d45312e61ebcc48032b77bc4cf7fd6915c11fa16e4aad116b66c9468211230ea", + strip_prefix = "zig-linux-x86_64-0.13.0", + ), + "x86_64-macos": struct( + tarball = "https://ziglang.org/download/0.13.0/zig-macos-x86_64-0.13.0.tar.xz", + sha256 = "8b06ed1091b2269b700b3b07f8e3be3b833000841bae5aa6a09b1a8b4773effd", + strip_prefix = "zig-macos-x86_64-0.13.0", + ), + }, +} + +def get_zig_sdk(version, index_key): + version_entry = _ZIG_VERSIONS.get(version) + if not version_entry: + return None + return version_entry.get(index_key) + +def pinned_zig_versions(): + return sorted(_ZIG_VERSIONS.keys()) diff --git a/zig/private/zig_repository.bzl b/zig/private/zig_repository.bzl index 0570d6f..d90de2b 100644 --- a/zig/private/zig_repository.bzl +++ b/zig/private/zig_repository.bzl @@ -1,36 +1,21 @@ -"""Repository rule for downloading a Zig SDK.""" +"""Repository rule for downloading a pinned Zig SDK.""" -_ZIG_INDEX_URL = "https://ziglang.org/download/index.json" +load("//zig/private:versions.bzl", "get_zig_sdk", "pinned_zig_versions") def _zig_repository_impl(ctx): - ctx.report_progress("Fetching Zig download index") - index_path = ctx.path("index.json") - ctx.download( - url = [_ZIG_INDEX_URL], - output = index_path, - ) - index = json.decode(ctx.read(index_path)) - ctx.delete(index_path) - - version_entry = index.get(ctx.attr.zig_version) - if not version_entry: - fail("Zig version '{}' not found in the download index.".format(ctx.attr.zig_version)) - - platform_entry = version_entry.get(ctx.attr.index_key) - if not platform_entry: - fail("Platform '{}' not found for Zig version '{}'.".format( - ctx.attr.index_key, + sdk = get_zig_sdk(ctx.attr.zig_version, ctx.attr.index_key) + if not sdk: + fail("Unsupported Zig SDK '{} / {}'. Pinned versions: {}.".format( ctx.attr.zig_version, + ctx.attr.index_key, + ", ".join(pinned_zig_versions()), )) - tarball_url = platform_entry["tarball"] - shasum = platform_entry["shasum"] - ctx.report_progress("Downloading Zig SDK") ctx.download_and_extract( - url = [tarball_url], - sha256 = shasum, - stripPrefix = tarball_url.split("/")[-1].removesuffix(".tar.xz").removesuffix(".zip"), + url = [sdk.tarball], + sha256 = sdk.sha256, + stripPrefix = sdk.strip_prefix, ) zig_exe = "zig.exe" if "windows" in ctx.attr.platform else "zig" @@ -67,9 +52,9 @@ zig_repository = repository_rule( mandatory = True, ), "index_key": attr.string( - doc = "The arch-os key used in the Zig download index (e.g., 'aarch64-macos').", + doc = "The arch-os key used in pinned Zig SDK metadata (e.g., 'aarch64-macos').", mandatory = True, ), }, - doc = "Downloads a Zig SDK for a specific platform.", + doc = "Downloads a pinned Zig SDK for a specific platform.", ) diff --git a/zig/rules.bzl b/zig/rules.bzl index 2044d6d..dd8688f 100644 --- a/zig/rules.bzl +++ b/zig/rules.bzl @@ -8,40 +8,49 @@ ZigLibraryInfo = provider( }, ) +def _select_main_src(srcs, main, rule_name): + if len(srcs) == 0: + fail("{} requires at least one source file in srcs.".format(rule_name)) + + if main: + for src in srcs: + if src.basename == main or src.short_path.endswith(main): + return src + fail("main file '{}' not found in srcs.".format(main)) + + return srcs[0] + +def _declare_cache_dirs(ctx): + return ( + ctx.actions.declare_directory(ctx.label.name + "_cache"), + ctx.actions.declare_directory(ctx.label.name + "_global_cache"), + ) + +def _add_common_args(args, main_src, out, cache_dir, global_cache_dir): + args.add(main_src) + args.add("-femit-bin=" + out.path) + args.add("--cache-dir") + args.add(cache_dir.path) + args.add("--global-cache-dir") + args.add(global_cache_dir.path) + def _zig_binary_impl(ctx): zig_toolchain = ctx.toolchains["//zig:toolchain_type"] zig_info = zig_toolchain.zig_info zig_exe = zig_info.zig_exe out = ctx.actions.declare_file(ctx.label.name) + cache_dir, global_cache_dir = _declare_cache_dirs(ctx) srcs = ctx.files.srcs - if len(srcs) == 0: - fail("zig_binary requires at least one source file in srcs.") - - # Determine the main source file. - main_src = None - if ctx.attr.main: - for src in srcs: - if src.short_path.endswith(ctx.attr.main): - main_src = src - break - if not main_src: - fail("main file '{}' not found in srcs.".format(ctx.attr.main)) - else: - main_src = srcs[0] + main_src = _select_main_src(srcs, ctx.attr.main, "zig_binary") args = ctx.actions.args() args.add("build-exe") - args.add(main_src) - args.add("-femit-bin=" + out.path) - args.add("--cache-dir") - args.add("/tmp/zig-cache") - args.add("--global-cache-dir") - args.add("/tmp/zig-global-cache") + _add_common_args(args, main_src, out, cache_dir, global_cache_dir) ctx.actions.run( - outputs = [out], + outputs = [out, cache_dir, global_cache_dir], inputs = srcs, executable = zig_exe, arguments = [args], @@ -78,33 +87,17 @@ def _zig_library_impl(ctx): zig_exe = zig_info.zig_exe out = ctx.actions.declare_file("lib" + ctx.label.name + ".a") + cache_dir, global_cache_dir = _declare_cache_dirs(ctx) srcs = ctx.files.srcs - if len(srcs) == 0: - fail("zig_library requires at least one source file in srcs.") - - main_src = None - if ctx.attr.main: - for src in srcs: - if src.short_path.endswith(ctx.attr.main): - main_src = src - break - if not main_src: - fail("main file '{}' not found in srcs.".format(ctx.attr.main)) - else: - main_src = srcs[0] + main_src = _select_main_src(srcs, ctx.attr.main, "zig_library") args = ctx.actions.args() args.add("build-lib") - args.add(main_src) - args.add("-femit-bin=" + out.path) - args.add("--cache-dir") - args.add("/tmp/zig-cache") - args.add("--global-cache-dir") - args.add("/tmp/zig-global-cache") + _add_common_args(args, main_src, out, cache_dir, global_cache_dir) ctx.actions.run( - outputs = [out], + outputs = [out, cache_dir, global_cache_dir], inputs = srcs, executable = zig_exe, arguments = [args], @@ -141,21 +134,12 @@ def _zig_test_impl(ctx): zig_exe = zig_info.zig_exe srcs = ctx.files.srcs - if len(srcs) == 0: - fail("zig_test requires at least one source file in srcs.") - - main_filename = ctx.attr.main if ctx.attr.main else srcs[0].basename - main_src = None - for src in srcs: - if src.basename == main_filename or src.short_path.endswith(main_filename): - main_src = src - break - if not main_src: - fail("main file '{}' not found in srcs.".format(main_filename)) + main_src = _select_main_src(srcs, ctx.attr.main, "zig_test") out = ctx.actions.declare_file(ctx.label.name) src_tree = ctx.actions.declare_directory(ctx.label.name + "_srcs") zig_link = ctx.actions.declare_file(ctx.label.name + "_zig") + cache_dir, global_cache_dir = _declare_cache_dirs(ctx) ctx.actions.symlink( output = zig_link, @@ -167,8 +151,10 @@ def _zig_test_impl(ctx): for src in srcs: copy_commands.append("cp '{}' '{}/{}'".format(src.path, src_tree.path, src.basename)) - command = "set -euo pipefail\nROOT=\"$PWD\"\nmkdir -p '{src_tree}'\n{copies}\ncd '{src_tree}'\n\"$ROOT/{zig}\" test '{main}' -femit-bin=\"$ROOT/{out}\" --cache-dir /tmp/zig-cache --global-cache-dir /tmp/zig-global-cache".format( + command = "set -euo pipefail\nROOT=\"$PWD\"\nmkdir -p '{src_tree}' '{cache_dir}' '{global_cache_dir}'\n{copies}\ncd '{src_tree}'\n\"$ROOT/{zig}\" test '{main}' -femit-bin=\"$ROOT/{out}\" --cache-dir \"$ROOT/{cache_dir}\" --global-cache-dir \"$ROOT/{global_cache_dir}\"".format( src_tree = src_tree.path, + cache_dir = cache_dir.path, + global_cache_dir = global_cache_dir.path, copies = "\n".join(copy_commands), zig = zig_link.path, main = main_src.basename, @@ -176,7 +162,7 @@ def _zig_test_impl(ctx): ) ctx.actions.run_shell( - outputs = [out, src_tree], + outputs = [out, src_tree, cache_dir, global_cache_dir], inputs = srcs + [zig_link], command = command, mnemonic = "ZigCompileTest",