From 10deb7f337494dca5fa3dc9ef696996554d30760 Mon Sep 17 00:00:00 2001 From: Abhishek Rai Date: Sun, 19 Apr 2026 17:58:52 -0700 Subject: [PATCH] Add zig_library and zig_binary deps support --- README.md | 30 +++++++++++ examples/deps/BUILD.bazel | 18 +++++++ examples/deps/greeter.zig | 4 ++ examples/deps/main.zig | 9 ++++ examples/deps/message.zig | 6 +++ zig/rules.bzl | 104 +++++++++++++++++++++++++++++++++++--- 6 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 examples/deps/BUILD.bazel create mode 100644 examples/deps/greeter.zig create mode 100644 examples/deps/main.zig create mode 100644 examples/deps/message.zig diff --git a/README.md b/README.md index 8ac13d3..96855dc 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ zig_binary( |-----------|-------------|----------| | `srcs` | List of `.zig` source files | Yes | | `main` | Root source file to compile. Defaults to the first file in `srcs`. | No | +| `deps` | `zig_library` dependencies exposed as Zig imports and link inputs | No | ### `zig_library` example @@ -118,10 +119,39 @@ zig_library( ) ``` +`zig_library` dependencies are imported by module name. By default that is the target name, and you can override it with `module_name`. + | Attribute | Description | Required | |-----------|-------------|----------| | `srcs` | List of `.zig` source files | Yes | | `main` | Root source file for the library. Defaults to the first file in `srcs`. | No | +| `module_name` | Import name exposed to dependents. Defaults to the target name. | No | +| `deps` | Other `zig_library` targets this library imports or links | No | + +### Dependency composition example + +```starlark +load("@rules_zig//zig:defs.bzl", "zig_binary", "zig_library") + +zig_library( + name = "greeter", + srcs = ["greeter.zig"], +) + +zig_library( + name = "message", + srcs = ["message.zig"], + deps = [":greeter"], +) + +zig_binary( + name = "hello_deps", + srcs = ["main.zig"], + deps = [":message"], +) +``` + +`message.zig` can then use `const greeter = @import("greeter");`, and `main.zig` can use `const message = @import("message");`. ### `zig_test` example diff --git a/examples/deps/BUILD.bazel b/examples/deps/BUILD.bazel new file mode 100644 index 0000000..3a8c936 --- /dev/null +++ b/examples/deps/BUILD.bazel @@ -0,0 +1,18 @@ +load("//zig:defs.bzl", "zig_binary", "zig_library") + +zig_library( + name = "greeter", + srcs = ["greeter.zig"], +) + +zig_library( + name = "message", + srcs = ["message.zig"], + deps = [":greeter"], +) + +zig_binary( + name = "hello_deps", + srcs = ["main.zig"], + deps = [":message"], +) diff --git a/examples/deps/greeter.zig b/examples/deps/greeter.zig new file mode 100644 index 0000000..06211a2 --- /dev/null +++ b/examples/deps/greeter.zig @@ -0,0 +1,4 @@ +pub fn greet(name: []const u8) []const u8 { + _ = name; + return "Hello"; +} diff --git a/examples/deps/main.zig b/examples/deps/main.zig new file mode 100644 index 0000000..ec4f25c --- /dev/null +++ b/examples/deps/main.zig @@ -0,0 +1,9 @@ +const std = @import("std"); +const message = @import("message"); + +pub fn main() !void { + const allocator = std.heap.page_allocator; + const text = try message.render(allocator, "Bazel"); + defer allocator.free(text); + std.debug.print("{s}\n", .{text}); +} diff --git a/examples/deps/message.zig b/examples/deps/message.zig new file mode 100644 index 0000000..625a754 --- /dev/null +++ b/examples/deps/message.zig @@ -0,0 +1,6 @@ +const std = @import("std"); +const greeter = @import("greeter"); + +pub fn render(allocator: std.mem.Allocator, name: []const u8) ![]u8 { + return std.fmt.allocPrint(allocator, "{s}, {s}!", .{ greeter.greet(name), name }); +} diff --git a/zig/rules.bzl b/zig/rules.bzl index dd8688f..4519902 100644 --- a/zig/rules.bzl +++ b/zig/rules.bzl @@ -4,7 +4,12 @@ ZigLibraryInfo = provider( doc = "Information about a compiled Zig library.", fields = { "archive": "File: The compiled static library archive (.a).", - "srcs": "depset of File: The original Zig source files.", + "archives": "depset of File: This library archive plus transitive dependency archives.", + "main_src": "File: The root Zig source file for this module.", + "module_dep_graph": "dict(string, list[string]): Transitive module to direct-import dependencies.", + "module_name": "string: Import name exposed for this library.", + "modules": "dict(string, File): Transitive import-name to root-source mapping.", + "srcs": "depset of File: The original Zig source files, including transitive dependency sources.", }, ) @@ -26,8 +31,65 @@ def _declare_cache_dirs(ctx): 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) +def _collect_dep_info(deps): + modules = {} + module_dep_graph = {} + transitive_srcs = [] + transitive_archives = [] + + for dep in deps: + dep_info = dep[ZigLibraryInfo] + transitive_srcs.append(dep_info.srcs) + transitive_archives.append(dep_info.archives) + for module_name, module_src in dep_info.modules.items(): + existing = modules.get(module_name) + if existing and existing.path != module_src.path: + fail("duplicate Zig module name '{}' provided by dependencies".format(module_name)) + modules[module_name] = module_src + for module_name, module_deps in dep_info.module_dep_graph.items(): + existing = module_dep_graph.get(module_name) + if existing and existing != module_deps: + fail("conflicting dependency graph for Zig module '{}'".format(module_name)) + module_dep_graph[module_name] = module_deps + + return modules, module_dep_graph, transitive_srcs, transitive_archives + +def _add_module_args(args, dep_modules, module_dep_graph): + emitted = {} + + for _ in dep_modules.keys(): + progressed = False + for module_name in sorted(dep_modules.keys()): + if module_name in emitted: + continue + + ready = True + for dep in module_dep_graph.get(module_name, []): + if dep not in emitted: + ready = False + break + + if ready: + for dep in module_dep_graph.get(module_name, []): + args.add("--dep") + args.add(dep) + args.add("-M{}={}".format(module_name, dep_modules[module_name].path)) + emitted[module_name] = True + progressed = True + + if len(emitted) == len(dep_modules): + return + if not progressed: + fail("cyclic Zig module dependencies detected in deps") + +def _add_common_args(args, main_src, out, cache_dir, global_cache_dir, root_deps, dep_modules, module_dep_graph, dep_archives): + for module_name in root_deps: + args.add("--dep") + args.add(module_name) + args.add("-Mroot=" + main_src.path) + _add_module_args(args, dep_modules, module_dep_graph) + for archive in dep_archives: + args.add(archive.path) args.add("-femit-bin=" + out.path) args.add("--cache-dir") args.add(cache_dir.path) @@ -44,14 +106,17 @@ def _zig_binary_impl(ctx): srcs = ctx.files.srcs main_src = _select_main_src(srcs, ctx.attr.main, "zig_binary") + dep_modules, module_dep_graph, transitive_srcs, transitive_archives = _collect_dep_info(ctx.attr.deps) + root_deps = [dep[ZigLibraryInfo].module_name for dep in ctx.attr.deps] + dep_archives = depset(transitive = transitive_archives).to_list() args = ctx.actions.args() args.add("build-exe") - _add_common_args(args, main_src, out, cache_dir, global_cache_dir) + _add_common_args(args, main_src, out, cache_dir, global_cache_dir, root_deps, dep_modules, module_dep_graph, dep_archives) ctx.actions.run( outputs = [out, cache_dir, global_cache_dir], - inputs = srcs, + inputs = depset(srcs, transitive = transitive_srcs + transitive_archives), executable = zig_exe, arguments = [args], mnemonic = "ZigCompile", @@ -76,6 +141,10 @@ zig_binary = rule( "main": attr.string( doc = "The main source file to compile. Defaults to the first file in srcs.", ), + "deps": attr.label_list( + doc = "Zig libraries to make available as imports and link inputs.", + providers = [ZigLibraryInfo], + ), }, executable = True, toolchains = ["//zig:toolchain_type"], @@ -91,14 +160,21 @@ def _zig_library_impl(ctx): srcs = ctx.files.srcs main_src = _select_main_src(srcs, ctx.attr.main, "zig_library") + dep_modules, module_dep_graph, transitive_srcs, transitive_archives = _collect_dep_info(ctx.attr.deps) + root_deps = [dep[ZigLibraryInfo].module_name for dep in ctx.attr.deps] + + module_name = ctx.attr.module_name or ctx.label.name + existing = dep_modules.get(module_name) + if existing and existing.path != main_src.path: + fail("module name '{}' conflicts with a dependency".format(module_name)) args = ctx.actions.args() args.add("build-lib") - _add_common_args(args, main_src, out, cache_dir, global_cache_dir) + _add_common_args(args, main_src, out, cache_dir, global_cache_dir, root_deps, dep_modules, module_dep_graph, depset(transitive = transitive_archives).to_list()) ctx.actions.run( outputs = [out, cache_dir, global_cache_dir], - inputs = srcs, + inputs = depset(srcs, transitive = transitive_srcs + transitive_archives), executable = zig_exe, arguments = [args], mnemonic = "ZigCompileLib", @@ -109,7 +185,12 @@ def _zig_library_impl(ctx): DefaultInfo(files = depset([out])), ZigLibraryInfo( archive = out, - srcs = depset(srcs), + archives = depset([out], transitive = transitive_archives), + main_src = main_src, + module_dep_graph = dict(module_dep_graph, **{module_name: root_deps}), + module_name = module_name, + modules = dict(dep_modules, **{module_name: main_src}), + srcs = depset(srcs, transitive = transitive_srcs), ), ] @@ -124,6 +205,13 @@ zig_library = rule( "main": attr.string( doc = "The root source file for the library. Defaults to the first file in srcs.", ), + "module_name": attr.string( + doc = "Import name exposed to dependents. Defaults to the target name.", + ), + "deps": attr.label_list( + doc = "Other zig_library targets this library imports or links.", + providers = [ZigLibraryInfo], + ), }, toolchains = ["//zig:toolchain_type"], )