Build system tricks

This community-driven doc is an attempt to expand on the official Zig Build System documentation as well as the past community contributions by xq, Sobeston and others.

Here you’ll find a list of tricks on how to use every part of the build system while making sure to conveniently name and lay out your build steps.

I. Basic Usage

1) Declare commonly used information upfront

// Resolve a query for a custom target
const my_target = b.resolveTargetQuery(.{
    .cpu_arch = .wasm32,
    .os_tag = .wasi,
});

// Resolve target triplet of "arch-os-libc" (native is default)
const target = b.standardTargetOptions(.{});

// Resolve optimization mode (debug is default)
const optimize = b.standardOptimizeOption(.{});

// Specify common lazy paths relative to the build script
const root_source_file = b.path("src/main.zig");

// Define single semantic version for your library, examples, etc.
const version = std.SemanticVersion{ .major = 0, .minor = 1, .patch = 0 };

2) Declare user options upfront

const is_enabled = b.option(bool, "is_enabled", "Enable some capability") orelse false;

const options = b.addOptions();
options.addOption(bool, "is_enabled", is_enabled);

3) Declare dependencies as well as their artifacts and modules upfront

// Declare package dependency called "johny" with your info
const johny_dep = b.dependency("johny", .{
    .target = target,
    .optimize = optimize,
});

// Declare dependency's artifact with the name from its build script
const johny_art = johny_dep.artifact("johny");

// Declare dependency's module with the name from its build script
const johny_mod = johny_dep.module("johny");

4) Declare your library’s module and expose it to your users

const lib_mod = b.addModule("my_mod", .{ .root_source_file = root_source_file });

5) Declare every major Compile or Run step upfront

const exe_step = b.step("exe", "Run executable");

const exe = b.addExecutable(.{
    .name = "my_exe",
    .target = target,
    .version = version,
    .optimize = optimize,
    .root_source_file = root_source_file,
});

// Add dependency module as the root module's import
exe.root_module.addImport("my_johny_import", johny_mod);

// Add user options as the root module's import
exe.root_module.addOptions("config", options);

6) Add build artifacts to depend on

// Add install artifact and depend the default step on it in one go
b.installArtifact(exe);

// Declare a separate run or install artifact step
const exe_run = b.addRunArtifact(exe);
const exe_install = b.addInstallArtifact(exe, .{});

// Pass CLI arguments to the run artifact
if (b.args) |args| {
    exe_run.addArgs(args);
}

// Optionally change the directory path to run the artifact in from current to custom
exe_run.setCwd(std.Build.LazyPath.relative("my-exe/"));

// Depend your pre-declared custom step (`zig build exe`) on the build artifact step
exe_step.dependOn(&exe_run.step);

// Depend the default step (`zig build`) on your custom step
b.default_step.dependOn(exe_step);

7) Add private modules for use only within your project

addModule provides the module for users of your project so they can import it in their projects. If you just want to use it privately in your own project, use createModule instead:

const hello_mod = b.createModule(.{
    .root_source_file = .{ .path = "src/hello.zig" },
});

// Add the private module as an anonymous import
exe.root_module.addAnonymousImport("hello", hello_mod);

II. Extra Steps

1) Emit docs into docs directory installed in prefix directory zig-out

const docs_step = b.step("docs", "Emit docs");

const docs_install = b.addInstallDirectory(.{
    .install_dir = .prefix,
    .install_subdir = "docs",
    .source_dir = lib.getEmittedDocs(),
});

docs_step.dependOn(&docs_install.step);
b.default_step.dependOn(docs_step);

2) Put static strings as global constants after the build function

const std = @import("std");

pub fn build(b: *std.Build) void {
    ...
}

const EXAMPLES_DIR = "examples/";

const EXAMPLE_NAMES = &.{
    "example1",
    "example2",
};

3) Run your library’s example suite

const examples_step = b.step("examples", "Run examples");

inline for (EXAMPLE_NAMES) |EXAMPLE_NAME| {
    const example = b.addExecutable(.{
        .name = EXAMPLE_NAME,
        .target = target,
        .version = version,
        .optimize = optimize,
        .root_source_file = std.Build.LazyPath.relative(EXAMPLES_DIR ++ EXAMPLE_NAME ++ "/main.zig"),
    });
    example.root_module.addImport("my_import", lib_mod);

    const example_run = b.addRunArtifact(example);
    examples_step.dependOn(&example_run.step);
}

b.default_step.dependOn(examples_step);

4) Run your test suite exposed with std.testing.refAllDecls

const tests_step = b.step("tests", "Run tests");

const tests = b.addTest(.{
    .target = target,
    .root_source_file = root_source_file,
});

const tests_run = b.addRunArtifact(tests);
tests_step.dependOn(&tests_run.step);
b.default_step.dependOn(tests_step);

5) Generate code coverage report with the kcov system dependency

const cov_step = b.step("cov", "Generate coverage");

const cov_run = b.addSystemCommand(&.{ "kcov", "--clean", "--include-pattern=src/", "kcov-output" });
cov_run.addArtifactArg(tests);

cov_step.dependOn(&cov_run.step);
b.default_step.dependOn(cov_step);

6) Run formatting checks

const lints_step = b.step("lints", "Run lints");

const lints = b.addFmt(.{
    .paths = &.{ "src", "build.zig" },
    .check = true,
});

lints_step.dependOn(&lints.step);
b.default_step.dependOn(lints_step);

7) Code generation tricks

// TODO
b.addWriteFiles()

8) Clean zig-out and zig-cache directories

const clean_step = b.step("clean", "Clean up");

clean_step.dependOn(&b.addRemoveDirTree(b.install_path).step);
if (@import("builtin").os.tag != .windows) {
    clean_step.dependOn(&b.addRemoveDirTree(b.pathFromRoot("zig-cache")).step);
}

III. C Dependencies

1) Add C-specific stuff to a Compile step

lib.addCSourceFiles(.{
    .root = C_ROOT_DIR,
    .files = &(C_CORE_FILES ++ C_LIB_FILES),
    .flags = C_FLAGS,
});
lib.addIncludePath(std.Build.LazyPath.relative(C_LIB_DIR));
lib.defineCMacro("MY_C_MACRO", null);
lib.linkSystemLibrary("readline");
lib.linkLibCpp();
lib.linkLibC();

Static string data goes after the build function to avoid cluttering the main script path.

const C_ROOT_DIR = "deps/my-c-lib/";

const C_CORE_FILES = .{
    "file1.c",
    "file2.c",
};

const C_LIB_FILES = .{
    "libfile1.c",
    "libfile2.c",
};

const C_FLAGS = &.{
    "-Wall",
    "-O2",
};

2) Link dynamic and static libraries

// If you are compiling multiple objects that have the same
// library dependencies, make a helper function to attach them in one go.
fn linkLibraries(obj: *std.Build.Step.Compile) void {
    // Link a static library (archive file)
    obj.addObjectFile(std.Build.LazyPath.relative("src/lib/mp_kernels.a"));

    // Enable searching for libraries prefixed with "lib",
    // such as "libcudart.so" or "libfoo.so"
    obj.addLibraryPath(std.Build.LazyPath.relative("deps/cuda/lib64"));

    // Perform a search for libraries in specified paths.
    // Note that the "lib" prefix has been omitted, such as in "cudart",
    // which actually has the name "libcudart.so"
    obj.linkSystemLibrary("cudart");
    obj.linkSystemLibrary("nvrtc");
    obj.linkSystemLibrary("cuda");

    // Link system libc
    obj.linkLibC();
}

3) Dependence on the Zig Compiler version

const builtin = @import("builtin");

comptime {
    const required_zig = "0.12.0-dev.3302";
    const current_zig = builtin.zig_version;
    const min_zig = std.SemanticVersion.parse(required_zig) catch unreachable;
    if (current_zig.order(min_zig) == .lt) {
        const error_message =
            \\Sorry, it looks like your version of zig is too old. :-(
            \\
            \\(Insert your program name here) requires development build {}
            \\
            \\Please download a development ("master") build from
            \\
            \\https://ziglang.org/download/
            \\
            \\
        ;
        @compileError(std.fmt.comptimePrint(error_message, .{min_zig}));
    }
}

4) Code generation tricks

// TODO
lib.installHeader()
20 Likes

Phew, first draft is out! This is pretty much all that I’ve used myself so far. Tried to structure everything as it tends to occur top-down in my build scripts, so pretty opinionated. I’m not too familiar with the codegen and the C stuff, so I left some todos to pick up from for later. Feel free to change anything.

3 Likes

Thank you for all these cases.

I added a cleaning step.

II.1) when emitting docs for lib, there is something unexpected. Because of lib.getEmittedDocs() you get documentation when you build lib, the problem with this is the delay, so I am redeclaring the lib for documentation only.

1 Like

Thanks, nice addition from you, too!

Yeah, the docs can only be emitted from some Compile thing, which isn’t too convenient, but kinda makes sense if the idea is to generate the docs only for publicly exposed things defined in files that are actually accessible from some Compile’s entry point.

Hey, great stuff @tensorush, thanks for your help! I added a blurb about dynamic and static library linking but anyone please feel free to edit it if you have a better suggestion.

Thanks again.

1 Like

Wouah thanks for this posts, it’s really helpful, Now I would like to ask in case anyone knows, basically I would like to start an open source tester for school projects in my school, which would require compiling C files, but since I can’t know the name for sure of those files, is there a way to specify a list of potential paths to look for c files to compile ? or something along those lines ? since I can know in advance the name of the folders (as they are required to be named like the project.)

Yes, I’ve seen that done. You can probe for “likely” install paths by checking to see if they exist.

I actually opted to not do this in Metaphor at the moment because CUDA can basically show up anywhere depending on how people have their system setup. I’m making it a requirement to symlink/copy it to a dependency folder that I’ll reference directly.

Now, are you talking about systems libraries as in shared objects? That’s much more doable but it’s kind of the same process. You probe for likely directories and then add them to the compile step. It will tell you if the search failed.

1 Like

I know that this is a wiki post that anyone can edit, but I don’t really like silently editing other people’s posts and think it’s more constructive to discuss improvements out in the open, so I’ll provide some suggestions and offer some constructive criticism instead.

First off, you should probably clarify which Zig version this is meant for because it’s currently mixing from different ones. b.resolveTargetQuery does not exist on 0.11. std.zig.system.NativeTargetInfo does not exist on master, and the ResolvedTarget returned by b.standardTargetOptions already has both the query and the resolved target.

Declare [things] upfront
Put static strings as global constants after the build function

I don’t think this is useful as prescriptive advice. I think the doc should focus on teaching the reader how to accomplish specific tasks, not how they should organize their code. Subjectively, I also think there is great value in a concise build script.

std.Build.LazyPath.relative("src/main.zig")

This might be a matter of taste but there is no real reason to use LazyPath.relative. If you peek at its implementation all it does is assert that the path is not absolute then returns .{ .path = "src/main.zig" } and I believe it is mostly a leftover from an earlier iteration of the build system. The union init is what zig init generates and in documentation I think that should be preferred over LazyPath.relative because the latter might falsely give the user the impression that it’s doing something special.

2) Declare user options upfront

“Options” is an unfortunately overloaded term in the current iteration of the build system, so it might be useful to explain the difference between project-specific build options that are exposed as -Doption=value on the command line, and Step.Options which generates code.

EXAMPLES_DIR ++ EXAMPLE_NAME ++ "/main.zig"

Use b.pathJoin to safely join path segments. In general it might be useful to mention that b exposes some string utility functions like b.fmt that let you accomplish common tasks without needing to manage memory.

8) Clean zig-out and zig-cache directories

The default zig build uninstall step should already delete the contents of zig-out. I also don’t know the exact implementation details but I would guess that deleting zig-cache while the build runner itself is running might not be a good idea and that it’s better/safer to manually delete zig-cache.

lib.addCSourceFiles(&(C_CORE_FILES ++ C_LIB_FILES), C_FLAGS)

addCSourceFiles uses a slightly different API on master, which is a bit friendlier with regard to common path segments:

exe.addCSourceFiles(.{
    .root = "src/c/", // this is optional, but useful
    .files = &.{ "alfa.c", "bravo.c" },
    .flags = &.{ "-Wall", "-O2" },
});
2 Likes

@castholm, this is exactly why we made this an open forum - thanks for your constructive feedback. I’ll tag it with the version but I’m sure we can work your suggestions in. I do think you should feel at liberty to edit these if you have more compact/elegant solutions.

1 Like

Unfortunately uninstall is a task that does nothing.

It works perfectly and I prefer it from manually deleting recursively the directory.

Also, just to be clear, the tags for adding versions can be appended when you either make the topic or if you edit the header of the topic. They’re not on the article itself. If you do it this way, you’ll see them show up when you search for tags like “12” (you’ll get zig version 12 tags in that case).

This might be different depending on OS, it doesn’t work on Windows and fails with the error

unable to recursively delete path ‘C:.…\zig-cache’: FileBusy

When you run zig build, zig compiles a build runner executable that it puts in zig-cache then runs, so it makes sense that the cache directory is off limits.

So basically the school I’m in is project based, all in C for the first year, there is a wide variety of subject but they start as pretty small projects, (like re implementing printf. all the way to building your own kernel, anyway, the school doesn’t allow you to use any external dependency, at most you are allowed a few functions from libc but that’s it, the rest has to be made from scratch. There are no official structure for testing and validating, students are correcting each other’s projects, by following some general broad testing guidelines.

A lot of projects have very popular automated testing suite made by members of the community, but they suffer a lot from a lack of precision, or portability (some wont’ compile or be usable on MacOS, or Windows) but there is a big lack of a true “ecosystem” and on my spare time I’d love to start using Zig, as way to build that ecosystem for my fellow comrade. Because Zig is the best language at using C in my opinion, and it can be used as the tool chain to provide cross compilation and also cross platform testing.

So as a rule of thumb projects are expected to be very much “self contained” with next to no external dependency except for libc. But as I’ve said while the projects are usually named the same the individual files not necessarily which is why I’d like to see if there is a way of achieving a very easy path that requires minimum efforts from the user to compile their projects and test them across platform ?, I’ve recently tried to play with the walking-directory thingy but couldn’t figure a way to use it to find .c files and compile them together. So any advice or guidance would be very much appreciated :slight_smile:

1 Like

Yes, you are right. It works everywhere but windows.

I remember the early days of Windows. Ιn order to uninstall a program, the uninstaller had to be deleted somehow. But the uninstaller was running and for some reason windows doesn’t allow deleting a running executable. The solution was to create a batch file, in a hidden console window, that tries to delete the uninstaller in a loop, when the uninstaller was deleted, the batch file deleted itself because it was a text file and not executable.
Maybe I can provide a similar solution for erasing zig-cache on windows :slight_smile:

This is awesome, thanks @tensorush for all the hard work!

3 Likes

Very good! :+1:

I have added a compiler dependency that is often used while Zig is still under heavy development.

2 Likes

I changed the deprecated std.Build.LazyPath.relative to std.Build.path in

const root_source_file = b.path("src/main.zig");
3 Likes

This is fucking gold. Absolute gold. Thank you thank you thank you.

2 Likes