Zig changes quickly before 1.0. That includes the build system.
This matters because old examples may stop compiling. A build.zig file written for Zig 0.11, 0.12, or 0.13 may need edits before it works on Zig 0.16.
The official current release is Zig 0.16.0, not Zig 1.16. The examples in this chapter use the Zig 0.16 build style. Zig 0.16.0 also deprecates @cImport; C translation is moving toward the build system instead of being a language builtin.
The Newer Module-Centered Style
Modern Zig build files are centered around modules.
You will often see this shape:
const exe = b.addExecutable(.{
.name = "hello",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});Older Zig examples may use fields directly on the executable, such as:
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,In Zig 0.16-style build files, the root source file, target, optimization mode, and imports usually belong to the module.
That is why this book has used:
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),The important idea is simple: an executable or library is an artifact, and the code it compiles is described by a module.
b.path Instead of Raw Paths
Old examples may contain path expressions like this:
.{ .path = "src/main.zig" }Newer examples usually use:
b.path("src/main.zig")This is clearer because the path is resolved relative to the package.
A normal executable uses it like this:
const exe = b.addExecutable(.{
.name = "app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});For beginners, use b.path(...) in new code.
Imports Belong to Modules
In older examples, you may see dependencies attached in ways that no longer match current style.
In Zig 0.16-style code, imports are usually added to a module:
exe.root_module.addImport("parser", parser.module("parser"));That means the source code can write:
const parser = @import("parser");Each root module has its own imports.
If your executable needs a dependency, add it to the executable root module.
If your tests need the same dependency, add it to the test root module too.
exe.root_module.addImport("parser", parser.module("parser"));
unit_tests.root_module.addImport("parser", parser.module("parser"));Do not assume imports automatically carry from one artifact to another.
Options Are Added as Modules
Build options also follow the module pattern.
In build.zig:
const options = b.addOptions();
options.addOption(bool, "enable_logging", enable_logging);
options.addOption([]const u8, "version", version);
exe.root_module.addOptions("build_options", options);In source code:
const build_options = @import("build_options");This is a clean replacement for preprocessor-style configuration.
The build file creates a small generated module. The program imports it like normal Zig code.
C Translation Is Moving Into the Build System
One of the important Zig 0.16 changes is around C import and translation.
Historically, Zig code could write:
const c = @cImport({
@cInclude("stdio.h");
});In Zig 0.16.0, @cImport is deprecated. The release notes say C translation will be handled through the build system rather than the @cImport language builtin.
The practical lesson is this: avoid designing new projects around heavy use of @cImport.
For now, you may still encounter it in examples and existing code. But when writing new Zig 0.16-era projects, expect C interop to become more build-system-driven over time.
Build Scripts Are Still Zig Code
Even though the APIs change, the basic idea stays stable.
A build file is still Zig code:
const std = @import("std");
pub fn build(b: *std.Build) void {
// describe the build here
}You still create artifacts:
const exe = b.addExecutable(.{ ... });You still install artifacts:
b.installArtifact(exe);You still create named steps:
const run_step = b.step("run", "Run the program");You still connect dependencies:
run_step.dependOn(&run_cmd.step);The surface API changes, but the mental model remains: build.zig describes a graph of build steps.
Reading Older Build Examples
When you find an old Zig build example, check for these signs:
.root_source_file directly inside addExecutable
.{ .path = "..." }
old dependency APIs
old package manager examples
heavy @cImport usage
missing root_moduleThese are clues that the example may need updating.
A modernized executable usually looks like this:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "hello",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(exe);
}This is the baseline pattern to remember.
zig build --help
The build system exposes project-specific help.
Run:
zig build --helpThe Zig documentation notes that this shows command-line usage and includes options declared by the project’s build.zig script.
That means custom build options should have clear descriptions:
const enable_tls = b.option(
bool,
"tls",
"Enable TLS support",
) orelse false;Then a user can discover it from the command line.
A good build file teaches the user how to build the project.
Incremental Build Workflows
Zig 0.16 also continues the push toward faster edit-build-test cycles.
The Zig project has described incremental compilation support for zig build workflows using options such as:
zig build -fincremental --watchThis is useful during development because it can reduce the time spent getting compile errors after edits.
You do not need this for every beginner project. But it is worth knowing that zig build is not only a release command. It is also becoming a development workflow command.
A Practical Zig 0.16 Build Template
For many beginner projects, this is a good starting point:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);
}This gives the project three useful commands:
zig build
zig build run
zig build testIt also supports:
zig build -Dtarget=x86_64-linux
zig build -Doptimize=ReleaseFast
zig build run -- arg1 arg2That is enough structure for many early projects.
Common Migration Problems
A common problem is copying an old build.zig from a blog post and seeing errors about unknown fields or missing methods.
That usually means the build API changed.
Another problem is adding an import to the executable but forgetting the test artifact.
Another problem is using @cImport heavily in new code. It may still appear in existing code, but Zig 0.16 marks it as deprecated and points toward build-system-based C translation.
Another problem is assuming every dependency exposes a module with the same name as the package. The package decides which modules it exposes.
The Important Idea
Zig’s build system is part of the language ecosystem, and before Zig 1.0 it can change between releases.
For Zig 0.16-style projects, remember these patterns:
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
})exe.root_module.addImport("name", dep.module("name"));exe.root_module.addOptions("build_options", options);Use current release notes and current documentation when upgrading build files. Old Zig examples are useful, but they are not always directly copyable.