Random Musings

O for a muse of fire, that would ascend the brightest heaven of invention!


Exploring the zig build system

Friday, 5 Feb 2021 Tags: zig

Zig’s build system (aka Make for zig) is written in … zig. That may seem a bit odd for a compiled language (how does that even work) but it yields a few benefits:

  • there’s only 1 thing to learn
  • no macros or preprocessor required
  • as the dependency graph is directly known by the compiler, it should be able to do all sorts of clever optimisations, and be screamingly fast
  • you can do pretty much anything

Getting Started

We’ll start with a simple program that reads a file, and prints the contents to stdout, and call it kat - much of this is taken from the [ziglearn chapter 2] page.

zig provides generators via zig init-exe - let’s see what it spits out.

// src/main.zig
const std = @import("std");

pub fn main() anyerror!void {
    std.log.info("All your codebase are belong to us.", .{});
}

main.zig looks pretty similar to what we did above for Hello World, apart from the anyerror!void thing.

// build.zig
const Builder = @import("std").build.Builder;

pub fn build(b: *Builder) void {
    // Standard target options allows the person running `zig build` to choose
    // what target to build for. Here we do not override the defaults, which
    // means any target is allowed, and the default is native. Other options
    // for restricting supported target set are available.
    const target = b.standardTargetOptions(.{});

    // Standard release options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
    const mode = b.standardReleaseOptions();

    const exe = b.addExecutable("kat", "src/main.zig");
    exe.setTarget(target);
    exe.setBuildMode(mode);
    exe.install();

    const run_cmd = exe.run();
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

This looks like a bunch of boilerplate, adding an executable target, and a run step, with some dependencies between them. Maybe we shall just run this, aye?

$ zig run src/main.zig 

info: All your codebase are belong to us.

$ l
total 10
-rw-r--r--  1 dch  staff   976B Feb 12 14:32 build.zig
drwxr-xr-x  2 dch  staff     3B Feb 12 14:32 src/

$ zig build --verbose-cimport --verbose-cc
Code Generation [1965/2246] std.fmt.formatInt...
$ l
total 11
-rw-r--r--  1 dch  staff   976B Feb 12 14:32 build.zig
drwxr-xr-x  2 dch  staff     3B Feb 12 14:32 src/
drwxr-xr-x  5 dch  staff     5B Feb 12 14:42 zig-cache/
$ ./zig-cache/bin/kat
info: All your codebase are belong to us.

So far, so good. After the build process, we have a runnable target, defined in the build.zig file, and we can also build an executable directly, and run it, using the info defined in the build file too.


[zig]: https://ziglang.org/
[learn]: https://ziglang.org/learn
[samples]: https://ziglang.org/learn/samples
[stdlib]: https://ziglang.org/documentation/master/std
[ziglearn chapter 2]: https://ziglearn.org/chapter-2/
[video]: https://www.youtube.com/watch?v=vHWiDx_l4V0