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.zigis 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 runcompiles and runs;zig build testruns all test blocks- Build options (
b.option) let users configure features at build time
Pro Tip: Use
ReleaseSafefor 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.
Next lesson
Zig C Interop — Call C from Zig Guide
Call C from Zig with zero overhead — use @cImport to include C headers, link C libraries, and pass data between Zig and C seamlessly. Free tutorial with examples.
28 min