Created
February 20, 2026 21:56
-
-
Save eamonburns/b1466035f8cca3a6a4def338e926d27a to your computer and use it in GitHub Desktop.
Zig "build runner" that simply prints the step dependency tree of the given top-level steps (tested with Zig 0.16.0-dev.2510+bcb5218a2)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const runner = @This(); | |
| const builtin = @import("builtin"); | |
| const std = @import("std"); | |
| const Io = std.Io; | |
| const assert = std.debug.assert; | |
| const fmt = std.fmt; | |
| const mem = std.mem; | |
| const process = std.process; | |
| const File = std.Io.File; | |
| const Step = std.Build.Step; | |
| const Watch = std.Build.Watch; | |
| const WebServer = std.Build.WebServer; | |
| const Allocator = std.mem.Allocator; | |
| const fatal = std.process.fatal; | |
| const Writer = std.Io.Writer; | |
| pub const root = @import("@build"); | |
| pub const dependencies = @import("@dependencies"); | |
| pub const std_options: std.Options = .{ | |
| .side_channels_mitigations = .none, | |
| .http_disable_tls = true, | |
| }; | |
| pub fn main(init: process.Init.Minimal) !void { | |
| // The build runner is often short-lived, but thanks to `--watch` and `--webui`, that's not | |
| // always the case. So, we do need a true gpa for some things. | |
| var debug_gpa_state: std.heap.DebugAllocator(.{}) = .init; | |
| defer _ = debug_gpa_state.deinit(); | |
| const gpa = debug_gpa_state.allocator(); | |
| var threaded: std.Io.Threaded = .init(gpa, .{ | |
| .environ = init.environ, | |
| .argv0 = .init(init.args), | |
| }); | |
| defer threaded.deinit(); | |
| const io = threaded.io(); | |
| // ...but we'll back our arena by `std.heap.page_allocator` for efficiency. | |
| var single_threaded_arena: std.heap.ArenaAllocator = .init(std.heap.page_allocator); | |
| defer single_threaded_arena.deinit(); | |
| var thread_safe_arena: std.heap.ThreadSafeAllocator = .{ | |
| .child_allocator = single_threaded_arena.allocator(), | |
| .io = io, | |
| }; | |
| const arena = thread_safe_arena.allocator(); | |
| const args = try init.args.toSlice(arena); | |
| // skip my own exe name | |
| var arg_idx: usize = 1; | |
| const zig_exe = nextArg(args, &arg_idx) orelse fatal("missing zig compiler path", .{}); | |
| const zig_lib_dir = nextArg(args, &arg_idx) orelse fatal("missing zig lib directory path", .{}); | |
| const build_root = nextArg(args, &arg_idx) orelse fatal("missing build root directory path", .{}); | |
| const cache_root = nextArg(args, &arg_idx) orelse fatal("missing cache root directory path", .{}); | |
| const global_cache_root = nextArg(args, &arg_idx) orelse fatal("missing global cache root directory path", .{}); | |
| const cwd: Io.Dir = .cwd(); | |
| const zig_lib_directory: std.Build.Cache.Directory = .{ | |
| .path = zig_lib_dir, | |
| .handle = try cwd.openDir(io, zig_lib_dir, .{}), | |
| }; | |
| const build_root_directory: std.Build.Cache.Directory = .{ | |
| .path = build_root, | |
| .handle = try cwd.openDir(io, build_root, .{}), | |
| }; | |
| const local_cache_directory: std.Build.Cache.Directory = .{ | |
| .path = cache_root, | |
| .handle = try cwd.createDirPathOpen(io, cache_root, .{}), | |
| }; | |
| const global_cache_directory: std.Build.Cache.Directory = .{ | |
| .path = global_cache_root, | |
| .handle = try cwd.createDirPathOpen(io, global_cache_root, .{}), | |
| }; | |
| var graph: std.Build.Graph = .{ | |
| .io = io, | |
| .arena = arena, | |
| .cache = .{ | |
| .io = io, | |
| .gpa = gpa, | |
| .manifest_dir = try local_cache_directory.handle.createDirPathOpen(io, "h", .{}), | |
| .cwd = try process.currentPathAlloc(io, single_threaded_arena.allocator()), | |
| }, | |
| .zig_exe = zig_exe, | |
| .environ_map = try init.environ.createMap(arena), | |
| .global_cache_root = global_cache_directory, | |
| .zig_lib_directory = zig_lib_directory, | |
| .host = .{ | |
| .query = .{}, | |
| .result = try std.zig.system.resolveTargetQuery(io, .{}), | |
| }, | |
| .time_report = false, | |
| }; | |
| graph.cache.addPrefix(.{ .path = null, .handle = cwd }); | |
| graph.cache.addPrefix(build_root_directory); | |
| graph.cache.addPrefix(local_cache_directory); | |
| graph.cache.addPrefix(global_cache_directory); | |
| graph.cache.hash.addBytes(builtin.zig_version_string); | |
| const builder = try std.Build.create( | |
| &graph, | |
| build_root_directory, | |
| local_cache_directory, | |
| dependencies.root_deps, | |
| ); | |
| // var targets = std.array_list.Managed([]const u8).init(arena); | |
| var debug_log_scopes = std.array_list.Managed([]const u8).init(arena); | |
| const install_prefix: ?[]const u8 = null; | |
| const dir_list = std.Build.DirList{}; | |
| var error_style: ErrorStyle = .verbose; | |
| var multiline_errors: MultilineErrors = .indent; | |
| const color: Color = .auto; | |
| const output_tmp_nonce: ?[16]u8 = null; | |
| const watch = false; | |
| const webui_listen: ?Io.net.IpAddress = null; | |
| if (std.zig.EnvVar.ZIG_BUILD_ERROR_STYLE.get(&graph.environ_map)) |str| { | |
| if (std.meta.stringToEnum(ErrorStyle, str)) |style| { | |
| error_style = style; | |
| } | |
| } | |
| if (std.zig.EnvVar.ZIG_BUILD_MULTILINE_ERRORS.get(&graph.environ_map)) |str| { | |
| if (std.meta.stringToEnum(MultilineErrors, str)) |style| { | |
| multiline_errors = style; | |
| } | |
| } | |
| const NO_COLOR = std.zig.EnvVar.NO_COLOR.isSet(&graph.environ_map); | |
| const CLICOLOR_FORCE = std.zig.EnvVar.CLICOLOR_FORCE.isSet(&graph.environ_map); | |
| graph.stderr_mode = switch (color) { | |
| .auto => try .detect(io, .stderr(), NO_COLOR, CLICOLOR_FORCE), | |
| .on => .escape_codes, | |
| .off => .no_color, | |
| }; | |
| if (webui_listen != null) { | |
| if (watch) fatal("using '--webui' and '--watch' together is not yet supported; consider omitting '--watch' in favour of the web UI \"Rebuild\" button", .{}); | |
| if (builtin.single_threaded) fatal("'--webui' is not yet supported on single-threaded hosts", .{}); | |
| } | |
| const main_progress_node = std.Progress.start(io, .{ | |
| .disable_printing = (color == .off), | |
| }); | |
| defer main_progress_node.end(); | |
| builder.debug_log_scopes = debug_log_scopes.items; | |
| builder.resolveInstallPrefix(install_prefix, dir_list); | |
| { | |
| var prog_node = main_progress_node.start("Configure", 0); | |
| defer prog_node.end(); | |
| try builder.runBuild(root); | |
| createModuleDependencies(builder) catch @panic("OOM"); | |
| } | |
| if (graph.needed_lazy_dependencies.entries.len != 0) { | |
| var buffer: std.ArrayList(u8) = .empty; | |
| for (graph.needed_lazy_dependencies.keys()) |k| { | |
| try buffer.appendSlice(arena, k); | |
| try buffer.append(arena, '\n'); | |
| } | |
| const s = std.fs.path.sep_str; | |
| const tmp_sub_path = "tmp" ++ s ++ (output_tmp_nonce orelse fatal("missing -Z arg", .{})); | |
| local_cache_directory.handle.writeFile(io, .{ | |
| .sub_path = tmp_sub_path, | |
| .data = buffer.items, | |
| .flags = .{ .exclusive = true }, | |
| }) catch |err| { | |
| fatal("unable to write configuration results to '{f}{s}': {s}", .{ | |
| local_cache_directory, tmp_sub_path, @errorName(err), | |
| }); | |
| }; | |
| process.exit(3); // Indicate configure phase failed with meaningful stdout. | |
| } | |
| if (builder.validateUserInputDidItFail()) { | |
| fatal(" access the help menu with 'zig build -h'", .{}); | |
| } | |
| validateSystemLibraryOptions(builder); | |
| var steps: std.ArrayList(*const Step) = .empty; | |
| while (nextArg(args, &arg_idx)) |arg| { | |
| const eql = struct { | |
| pub fn impl(a: []const u8, b: []const u8) bool { | |
| return mem.eql(u8, a, b); | |
| } | |
| }.impl; | |
| if (!mem.startsWith(u8, arg, "-")) { | |
| var tls = builder.top_level_steps.get(arg) orelse { | |
| fatal(" invalid top level step: {s}", .{arg}); | |
| }; | |
| try steps.append(arena, &tls.step); | |
| } else if ( | |
| // zig fmt: off | |
| eql(arg, "-p") or eql(arg, "--prefix") | |
| or eql(arg, "--prefix-lib-dir") | |
| or eql(arg, "--prefix-exe-dir") | |
| or eql(arg, "--prefix-include-dir") | |
| or eql(arg, "--sysroot") | |
| or eql(arg, "--maxrss") | |
| or eql(arg, "--test-timeout") | |
| or eql(arg, "--search-prefix") | |
| or eql(arg, "--libc") | |
| or eql(arg, "--debug-log") | |
| or eql(arg, "--libc-runtimes") or eql(arg, "--glibc-runtimes") | |
| or eql(arg, "--debounce") | |
| or eql(arg, "--seed") | |
| or eql(arg, "--summary") | |
| or eql(arg, "--multiline-errors") | |
| or eql(arg, "--error-style") | |
| // zig fmt: on | |
| ) arg_idx += 1; // HACK: to skip parameters to certain flags | |
| } | |
| if (steps.items.len == 0) try steps.append(arena, builder.default_step); | |
| // const steps: []const *const Step = if (step_names.len == 0) | |
| // &.{builder.default_step} | |
| // else blk: {}; | |
| var stderr_buf: [1024]u8 = undefined; | |
| const stderr = try io.lockStderr(&stderr_buf, graph.stderr_mode); | |
| defer io.unlockStderr(); | |
| const t = stderr.terminal(); | |
| printGraph(builder, steps.items, t) catch return stdout_writer_allocation.err.?; | |
| try stderr.file_writer.interface.flush(); | |
| } | |
| const Run = struct { | |
| gpa: Allocator, | |
| available_rss: usize, | |
| max_rss_is_default: bool, | |
| max_rss_mutex: Io.Mutex, | |
| skip_oom_steps: bool, | |
| unit_test_timeout_ns: ?u64, | |
| watch: bool, | |
| web_server: if (!builtin.single_threaded) ?WebServer else ?noreturn, | |
| /// Allocated into `gpa`. | |
| memory_blocked_steps: std.ArrayList(*Step), | |
| /// Allocated into `gpa`. | |
| step_stack: std.AutoArrayHashMapUnmanaged(*Step, void), | |
| error_style: ErrorStyle, | |
| multiline_errors: MultilineErrors, | |
| summary: Summary, | |
| }; | |
| const PrintNode = struct { | |
| parent: ?*PrintNode, | |
| last: bool = false, | |
| }; | |
| fn printPrefix(node: *PrintNode, stderr: Io.Terminal) !void { | |
| const parent = node.parent orelse return; | |
| const writer = stderr.writer; | |
| if (parent.parent == null) return; | |
| try printPrefix(parent, stderr); | |
| if (parent.last) { | |
| try writer.writeAll(" "); | |
| } else { | |
| try writer.writeAll(switch (stderr.mode) { | |
| .escape_codes => "\x1B\x28\x30\x78\x1B\x28\x42 ", // │ | |
| else => "| ", | |
| }); | |
| } | |
| } | |
| fn printChildNodePrefix(stderr: Io.Terminal) !void { | |
| try stderr.writer.writeAll(switch (stderr.mode) { | |
| .escape_codes => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", // └─ | |
| else => "+- ", | |
| }); | |
| } | |
| fn printGraph(builder: *std.Build, steps: []const *const Step, stderr: Io.Terminal) !void { | |
| var print_node: PrintNode = .{ .parent = null }; | |
| for (steps, 0..) |step, i| { | |
| print_node.last = i == builder.top_level_steps.values().len - 1; | |
| try printTreeStep(builder, step, stderr, &print_node); | |
| try stderr.writer.writeByte('\n'); | |
| } | |
| } | |
| fn printTreeStep( | |
| b: *std.Build, | |
| s: *const Step, | |
| stderr: Io.Terminal, | |
| parent_node: *PrintNode, | |
| ) !void { | |
| const writer = stderr.writer; | |
| try printPrefix(parent_node, stderr); | |
| if (parent_node.parent != null) { | |
| if (parent_node.last) { | |
| try printChildNodePrefix(stderr); | |
| } else { | |
| try writer.writeAll(switch (stderr.mode) { | |
| .escape_codes => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", // ├─ | |
| else => "+- ", | |
| }); | |
| } | |
| } | |
| // dep_prefix omitted here because it is redundant with the tree. | |
| try writer.writeAll(s.name); | |
| try writer.writeByte('\n'); | |
| const last_index = s.dependencies.items.len -| 1; | |
| for (s.dependencies.items, 0..) |dep, i| { | |
| var print_node: PrintNode = .{ | |
| .parent = parent_node, | |
| .last = i == last_index, | |
| }; | |
| try printTreeStep(b, dep, stderr, &print_node); | |
| } | |
| } | |
| fn nextArg(args: []const [:0]const u8, idx: *usize) ?[:0]const u8 { | |
| if (idx.* >= args.len) return null; | |
| defer idx.* += 1; | |
| return args[idx.*]; | |
| } | |
| const Color = std.zig.Color; | |
| const ErrorStyle = enum { | |
| verbose, | |
| minimal, | |
| verbose_clear, | |
| minimal_clear, | |
| fn verboseContext(s: ErrorStyle) bool { | |
| return switch (s) { | |
| .verbose, .verbose_clear => true, | |
| .minimal, .minimal_clear => false, | |
| }; | |
| } | |
| fn clearOnUpdate(s: ErrorStyle) bool { | |
| return switch (s) { | |
| .verbose, .minimal => false, | |
| .verbose_clear, .minimal_clear => true, | |
| }; | |
| } | |
| }; | |
| const MultilineErrors = enum { indent, newline, none }; | |
| const Summary = enum { all, new, failures, line, none }; | |
| fn validateSystemLibraryOptions(b: *std.Build) void { | |
| var bad = false; | |
| for (b.graph.system_library_options.keys(), b.graph.system_library_options.values()) |k, v| { | |
| switch (v) { | |
| .user_disabled, .user_enabled => { | |
| // The user tried to enable or disable a system library integration, but | |
| // the build script did not recognize that option. | |
| std.debug.print("system library name not recognized by build script: '{s}'\n", .{k}); | |
| bad = true; | |
| }, | |
| .declared_disabled, .declared_enabled => {}, | |
| } | |
| } | |
| if (bad) { | |
| std.debug.print(" access the help menu with 'zig build -h'\n", .{}); | |
| process.exit(1); | |
| } | |
| } | |
| /// Starting from all top-level steps in `b`, traverses the entire step graph | |
| /// and adds all step dependencies implied by module graphs. | |
| fn createModuleDependencies(b: *std.Build) Allocator.Error!void { | |
| const arena = b.graph.arena; | |
| var all_steps: std.AutoArrayHashMapUnmanaged(*Step, void) = .empty; | |
| var next_step_idx: usize = 0; | |
| try all_steps.ensureUnusedCapacity(arena, b.top_level_steps.count()); | |
| for (b.top_level_steps.values()) |tls| { | |
| all_steps.putAssumeCapacityNoClobber(&tls.step, {}); | |
| } | |
| while (next_step_idx < all_steps.count()) { | |
| const step = all_steps.keys()[next_step_idx]; | |
| next_step_idx += 1; | |
| // Set up any implied dependencies for this step. It's important that we do this first, so | |
| // that the loop below discovers steps implied by the module graph. | |
| try createModuleDependenciesForStep(step); | |
| try all_steps.ensureUnusedCapacity(arena, step.dependencies.items.len); | |
| for (step.dependencies.items) |other_step| { | |
| all_steps.putAssumeCapacity(other_step, {}); | |
| } | |
| } | |
| } | |
| /// If the given `Step` is a `Step.Compile`, adds any dependencies for that step which | |
| /// are implied by the module graph rooted at `step.cast(Step.Compile).?.root_module`. | |
| fn createModuleDependenciesForStep(step: *Step) Allocator.Error!void { | |
| const root_module = if (step.cast(Step.Compile)) |cs| root: { | |
| break :root cs.root_module; | |
| } else return; // not a compile step so no module dependencies | |
| // Starting from `root_module`, discover all modules in this graph. | |
| const modules = root_module.getGraph().modules; | |
| // For each of those modules, set up the implied step dependencies. | |
| for (modules) |mod| { | |
| if (mod.root_source_file) |lp| lp.addStepDependencies(step); | |
| for (mod.include_dirs.items) |include_dir| switch (include_dir) { | |
| .path, | |
| .path_system, | |
| .path_after, | |
| .framework_path, | |
| .framework_path_system, | |
| .embed_path, | |
| => |lp| lp.addStepDependencies(step), | |
| .other_step => |other| { | |
| other.getEmittedIncludeTree().addStepDependencies(step); | |
| step.dependOn(&other.step); | |
| }, | |
| .config_header_step => |other| step.dependOn(&other.step), | |
| }; | |
| for (mod.lib_paths.items) |lp| lp.addStepDependencies(step); | |
| for (mod.rpaths.items) |rpath| switch (rpath) { | |
| .lazy_path => |lp| lp.addStepDependencies(step), | |
| .special => {}, | |
| }; | |
| for (mod.link_objects.items) |link_object| switch (link_object) { | |
| .static_path, | |
| .assembly_file, | |
| => |lp| lp.addStepDependencies(step), | |
| .other_step => |other| step.dependOn(&other.step), | |
| .system_lib => {}, | |
| .c_source_file => |source| source.file.addStepDependencies(step), | |
| .c_source_files => |source_files| source_files.root.addStepDependencies(step), | |
| .win32_resource_file => |rc_source| { | |
| rc_source.file.addStepDependencies(step); | |
| for (rc_source.include_paths) |lp| lp.addStepDependencies(step); | |
| }, | |
| }; | |
| } | |
| } | |
| var stdio_buffer_allocation: [256]u8 = undefined; | |
| var stdout_writer_allocation: Io.File.Writer = undefined; | |
| fn initStdoutWriter(io: Io) *Writer { | |
| stdout_writer_allocation = Io.File.stdout().writerStreaming(io, &stdio_buffer_allocation); | |
| return &stdout_writer_allocation.interface; | |
| } |
Author
Author
Most notable lines are these functions:
https://gist.github.com/eamonburns/b1466035f8cca3a6a4def338e926d27a#file-build_runner-zig-L287-L328
And the lines where they are used:
https://gist.github.com/eamonburns/b1466035f8cca3a6a4def338e926d27a#file-build_runner-zig-L232-L237
Author
I tried to remove all unneeded lines, but there might be some unused code. Also, I recently realized that options that modify the builder (like -D) should actually be parsed rather than just ignored (because they affect the steps that are run).
Since the functions that I added are actually relatively small, it might not be a bad idea to PR this to the official build script as a --print-step-tree flag.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage
Download
build_runner.zigwget -O ~/build_runner.zig https://gist.githubusercontent.com/eamonburns/b1466035f8cca3a6a4def338e926d27a/raw/c7389f87e1d2c746491a34fac628d262302b3366/build_runner.zigUse when building a project
Output (for the default
zig init):Specify step(s):
zig build --build-runner ~/build_runner.zig run