Skip to content
Merged
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
12 changes: 6 additions & 6 deletions MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 32 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

Expand Down Expand Up @@ -55,10 +56,35 @@ register_toolchains("@zig_toolchains//:all")

Replace `<commit_sha>` 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
Expand Down
7 changes: 7 additions & 0 deletions zig/extensions.bzl
Original file line number Diff line number Diff line change
@@ -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")

Expand Down Expand Up @@ -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 = []
Expand Down
35 changes: 35 additions & 0 deletions zig/private/versions.bzl
Original file line number Diff line number Diff line change
@@ -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())
39 changes: 12 additions & 27 deletions zig/private/zig_repository.bzl
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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.",
)
94 changes: 40 additions & 54 deletions zig/rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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,
Expand All @@ -167,16 +151,18 @@ 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,
out = out.path,
)

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",
Expand Down
Loading