Skip to lesson

learningzig.org / advanced / 13-build-system · lesson 13 of 25

TL;DR

Learn the Zig build system — configure build.zig for compilation, testing, and dependencies without Make or CMake. Free hands-on tutorial with runnable examples.

Key concepts

  • zig build system
  • build.zig tutorial
  • zig build steps
  • zig dependencies
  • zig compilation
  • zig cmake alternative

Build System

Zig's build system is written in Zig itself through build.zig. It handles compilation, testing, dependencies, and custom build steps — all without external tools like Make or CMake.

Build File Basics

Every Zig project has a build.zig that defines how to compile and test the project.

const std = @import("std");

// Simulating build.zig concepts
// In a real build.zig, you define build steps:

// pub fn build(b: *std.Build) void {
//     const target = b.standardTargetOptions(.{});
//     const optimize = b.standardOptimizeOption(.{});
//
//     const exe = b.addExecutable(.{
//         .name = "myapp",
//         .root_source_file = b.path("src/main.zig"),
//         .target = target,
//         .optimize = optimize,
//     });
//     b.installArtifact(exe);
// }

pub fn main() !void {
    // Build targets and their options
    const targets = [_][]const u8{ "native", "x86_64-linux", "aarch64-macos", "wasm32-wasi" };
    const modes = [_][]const u8{ "Debug", "ReleaseSafe", "ReleaseFast", "ReleaseSmall" };

    std.debug.print("Zig Build System Overview:\n\n", .{});

    std.debug.print("Available targets:\n", .{});
    for (targets) |target| {
        std.debug.print("  - {s}\n", .{target});
    }

    std.debug.print("\nOptimize modes:\n", .{});
    for (modes) |mode| {
        std.debug.print("  - {s}\n", .{mode});
    }

    std.debug.print("\nCommon commands:\n", .{});
    std.debug.print("  zig build          — Build the project\n", .{});
    std.debug.print("  zig build run      — Build and run\n", .{});
    std.debug.print("  zig build test     — Run tests\n", .{});
    std.debug.print("  zig build -Doptimize=ReleaseFast — Build optimized\n", .{});
}

Build Steps

Build steps define tasks like compile, test, and custom commands.

const std = @import("std");

// Simulating build step concepts
const BuildStep = struct {
    name: []const u8,
    description: []const u8,
    dependencies: []const []const u8,
};

pub fn main() !void {
    // A typical build.zig defines these steps
    const steps = [_]BuildStep{
        .{
            .name = "compile",
            .description = "Compile the executable",
            .dependencies = &.{},
        },
        .{
            .name = "test",
            .description = "Run unit tests",
            .dependencies = &.{},
        },
        .{
            .name = "run",
            .description = "Build and run the application",
            .dependencies = &.{"compile"},
        },
        .{
            .name = "install",
            .description = "Install to zig-out/bin",
            .dependencies = &.{"compile"},
        },
        .{
            .name = "docs",
            .description = "Generate documentation",
            .dependencies = &.{"compile"},
        },
    };

    std.debug.print("Build Steps:\n\n", .{});
    for (steps) |step| {
        std.debug.print("  {s}: {s}\n", .{ step.name, step.description });
        if (step.dependencies.len > 0) {
            std.debug.print("    depends on:", .{});
            for (step.dependencies) |dep| {
                std.debug.print(" {s}", .{dep});
            }
            std.debug.print("\n", .{});
        }
    }

    // Example build.zig with steps:
    // pub fn build(b: *std.Build) void {
    //     const exe = b.addExecutable(.{ ... });
    //     b.installArtifact(exe);
    //
    //     const run_cmd = b.addRunArtifact(exe);
    //     const run_step = b.step("run", "Run the app");
    //     run_step.dependOn(&run_cmd.step);
    //
    //     const tests = b.addTest(.{ .root_source_file = b.path("src/main.zig") });
    //     const test_step = b.step("test", "Run unit tests");
    //     test_step.dependOn(&tests.step);
    // }

    std.debug.print("\nUsage:\n", .{});
    std.debug.print("  zig build run   — executes 'compile' then 'run'\n", .{});
    std.debug.print("  zig build test  — runs unit tests\n", .{});
}

Optimize Modes

Zig offers four optimize modes that affect safety checks, performance, and binary size.

const std = @import("std");

const OptimizeMode = struct {
    name: []const u8,
    safety: bool,
    speed: []const u8,
    size: []const u8,
    use_case: []const u8,
};

pub fn main() !void {
    const modes = [_]OptimizeMode{
        .{
            .name = "Debug",
            .safety = true,
            .speed = "Slow",
            .size = "Large",
            .use_case = "Development — full safety checks, debug info",
        },
        .{
            .name = "ReleaseSafe",
            .safety = true,
            .speed = "Fast",
            .size = "Medium",
            .use_case = "Production — optimized with safety checks kept",
        },
        .{
            .name = "ReleaseFast",
            .safety = false,
            .speed = "Fastest",
            .size = "Medium",
            .use_case = "Performance-critical — all optimizations, no safety",
        },
        .{
            .name = "ReleaseSmall",
            .safety = false,
            .speed = "Fast",
            .size = "Smallest",
            .use_case = "Embedded/WASM — minimize binary size",
        },
    };

    std.debug.print("Zig Optimize Modes:\n\n", .{});
    std.debug.print("{s:<16} {s:<8} {s:<10} {s:<10} {s}\n", .{ "Mode", "Safety", "Speed", "Size", "Use Case" });
    std.debug.print("{s:-<16} {s:-<8} {s:-<10} {s:-<10} {s:-<30}\n", .{ "", "", "", "", "" });

    for (modes) |mode| {
        const safety_str: []const u8 = if (mode.safety) "Yes" else "No";
        std.debug.print("{s:<16} {s:<8} {s:<10} {s:<10} {s}\n", .{
            mode.name,
            safety_str,
            mode.speed,
            mode.size,
            mode.use_case,
        });
    }

    // Build command examples:
    std.debug.print("\nBuild commands:\n", .{});
    std.debug.print("  zig build                            — Debug (default)\n", .{});
    std.debug.print("  zig build -Doptimize=ReleaseSafe     — Safe release\n", .{});
    std.debug.print("  zig build -Doptimize=ReleaseFast     — Max performance\n", .{});
    std.debug.print("  zig build -Doptimize=ReleaseSmall    — Min size\n", .{});
}

Cross-Compilation

Zig supports cross-compilation out of the box — no additional toolchains needed.

const std = @import("std");

const Target = struct {
    triple: []const u8,
    os: []const u8,
    arch: []const u8,
    note: []const u8,
};

pub fn main() !void {
    const targets = [_]Target{
        .{ .triple = "x86_64-linux-gnu", .os = "Linux", .arch = "x86_64", .note = "Most servers" },
        .{ .triple = "aarch64-linux-gnu", .os = "Linux", .arch = "ARM64", .note = "AWS Graviton, RPi" },
        .{ .triple = "x86_64-macos", .os = "macOS", .arch = "x86_64", .note = "Intel Mac" },
        .{ .triple = "aarch64-macos", .os = "macOS", .arch = "ARM64", .note = "Apple Silicon" },
        .{ .triple = "x86_64-windows-gnu", .os = "Windows", .arch = "x86_64", .note = "Windows desktop" },
        .{ .triple = "wasm32-wasi", .os = "WASI", .arch = "WASM32", .note = "WebAssembly" },
    };

    std.debug.print("Cross-Compilation Targets:\n\n", .{});
    for (targets) |target| {
        std.debug.print("  {s}\n", .{target.triple});
        std.debug.print("    OS: {s}, Arch: {s}\n", .{ target.os, target.arch });
        std.debug.print("    Note: {s}\n\n", .{target.note});
    }

    // In build.zig, cross-compilation is just:
    // const target = b.standardTargetOptions(.{});
    // Then build with:
    // zig build -Dtarget=aarch64-linux-gnu

    std.debug.print("Commands:\n", .{});
    std.debug.print("  zig build -Dtarget=x86_64-linux-gnu    — Cross-compile for Linux\n", .{});
    std.debug.print("  zig build -Dtarget=wasm32-wasi         — Build for WASM\n", .{});
    std.debug.print("  zig build -Dtarget=aarch64-macos       — Build for Apple Silicon\n", .{});
}

Try It Yourself

const std = @import("std");

// Simulate a build configuration system
const Config = struct {
    name: []const u8,
    version: []const u8,
    optimize: []const u8,
    target: []const u8,
    features: []const []const u8,

    fn display(self: Config) void {
        std.debug.print("Project: {s} v{s}\n", .{ self.name, self.version });
        std.debug.print("Target:  {s}\n", .{self.target});
        std.debug.print("Mode:    {s}\n", .{self.optimize});
        std.debug.print("Features:\n", .{});
        for (self.features) |feature| {
            std.debug.print("  + {s}\n", .{feature});
        }
    }
};

pub fn main() !void {
    // Different build configurations
    const configs = [_]Config{
        .{
            .name = "myapp",
            .version = "1.0.0",
            .optimize = "Debug",
            .target = "native",
            .features = &.{ "logging", "hot-reload", "debug-ui" },
        },
        .{
            .name = "myapp",
            .version = "1.0.0",
            .optimize = "ReleaseSafe",
            .target = "x86_64-linux-gnu",
            .features = &.{ "logging", "metrics" },
        },
        .{
            .name = "myapp",
            .version = "1.0.0",
            .optimize = "ReleaseSmall",
            .target = "wasm32-wasi",
            .features = &.{"core-only"},
        },
    };

    for (configs, 0..) |config, i| {
        if (i > 0) std.debug.print("\n---\n\n", .{});
        config.display();
    }

    // In real build.zig, features are build options:
    // const enable_logging = b.option(bool, "logging", "Enable logging") orelse true;
    // if (enable_logging) {
    //     exe.root_module.addCMacro("ENABLE_LOGGING", "1");
    // }
}

Key Takeaways

  • build.zig is the project's build configuration — written in Zig, not a DSL
  • Build steps define tasks like compile, test, run, and custom commands
  • Four optimize modes: Debug, ReleaseSafe, ReleaseFast, ReleaseSmall
  • Cross-compilation works out of the box — just pass -Dtarget=...
  • zig build run compiles and runs; zig build test runs all test blocks
  • Build options (b.option) let users configure features at build time

Pro Tip: Use ReleaseSafe for production builds unless you have strong reasons not to. It keeps runtime safety checks (bounds checking, integer overflow detection) while still applying optimizations. The performance cost of safety checks is typically under 5%, but they catch bugs that would otherwise be silent memory corruption.

Next Steps

Zig's build system can do more than compile Zig code -- it can also link C libraries directly. Next, you'll learn how to call C functions from Zig and integrate existing C codebases into your projects.

Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.