Zig can build for a different target than the machine running the compiler.
The target is chosen with -Dtarget when the build file uses the standard target option:
const target = b.standardTargetOptions(.{});An executable should receive that target through its root module:
const exe = b.addExecutable(.{
.name = "demo",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});Now the same project can be built for the host:
zig buildor for Linux on x86-64:
zig build -Dtarget=x86_64-linuxor for macOS on ARM64:
zig build -Dtarget=aarch64-macosor for Windows on x86-64:
zig build -Dtarget=x86_64-windowsA target names the machine the program will run on. It usually includes an architecture and an operating system:
x86_64-linux
aarch64-linux
aarch64-macos
x86_64-windows
wasm32-freestandingSome targets also specify ABI details:
x86_64-linux-gnu
x86_64-linux-muslThe target affects code generation, object format, linking rules, builtin values, and the available operating system APIs.
Source code can inspect the target at compile time:
const builtin = @import("builtin");
pub fn main() void {
if (builtin.os.tag == .windows) {
// Windows path
} else {
// POSIX path
}
}This is often better than having separate source files for every platform. Put small differences behind compile-time branches. Put large differences behind separate modules.
A build file can also choose platform-specific settings:
const builtin = @import("builtin");
if (builtin.os.tag == .windows) {
// This is the host running the build script, not necessarily the target.
}Be careful here. @import("builtin") inside build.zig describes the host running the build script. It does not describe the target passed to the executable.
Use the target value from the build options when configuring artifacts.
A common pattern is to make platform modules explicit:
const platform_mod = if (target.result.os.tag == .windows)
b.createModule(.{ .root_source_file = b.path("src/platform/windows.zig") })
else
b.createModule(.{ .root_source_file = b.path("src/platform/posix.zig") });
exe.root_module.addImport("platform", platform_mod);Then the source imports one stable name:
const platform = @import("platform");Cross builds are easiest when the program depends only on Zig and the target’s basic system interface.
C libraries make cross builds more complicated. A system library such as:
exe.linkSystemLibrary("sqlite3");must be available for the target, not merely for the host. Building on macOS for Linux requires a Linux version of that library.
Zig can provide libc for many targets, but it cannot magically provide every third-party C library. The build must know where headers and libraries for the target live.
For pure Zig dependencies, this problem is much smaller. The dependency is compiled for the same target as the main program.
Cross compilation should be tested early. A program that assumes local paths, native word size, host endianness, or host-only libraries may build on the developer machine and fail elsewhere.
Good cross-platform Zig code keeps these rules:
Use usize for sizes and indexes.
Use fixed-width integer types for file formats and protocols.
Do not assume path separators.
Do not assume endianness.
Do not assume C libraries exist on every target.
Keep operating-system code behind a small interface.
The build system makes cross compilation simple to request. The source code still has to be written with the target in mind.
Exercise 15-29. Build a program for x86_64-linux.
Exercise 15-30. Build the same program for x86_64-windows.
Exercise 15-31. Use @import("builtin") in source code to choose different behavior on Windows and POSIX systems.
Exercise 15-32. Move platform-specific code behind a module named platform.