Zig Build#
Basics#
Zig build scripts (usually named build.zig
) are ordinary Zig programs with a special exported function (pub fn build(b: *std.build.Builder) void
) utilizing std.build.Builder
The build runner is invoked by zig build
which in turn invokes said build.zig:build()
- create DAG of
std.build.Step
nodes where eachStep
- executes a part of our build process
- has a set of dependencies that need to be made before the step itself is made
- user can invoke named steps by calling
zig build step-name
or predefined steps (e.g.install
) - create with
Builder.step
:
Zigpub fn build(b: *std.build.Builder) void { const named_step = b.step("step-name", "This is what is shown in help"); }
Compiling Executable#
Source Compilation#
Builder
exposes Builder.addExecutable
which will create us a new LibExeObjStep
-
a convenient wrapper around
zig build-exe
,zig build-lib
,zig build-obj
orzig test
depending on how it is initialized -
example:
pub fn build(b: *std.build.Builder) void {
const exe = b.addExecutable("fresh", "src/main.zig");
const target = b.standardTargetOptions(.{});
exe.setTarget(target);
const mode = b.standardReleaseOptions();
exe.setBuildMode(mode);
const compile_step = b.step("compile", "Compiles src/main.zig");
compile_step.dependOn(&exe.step);
}
-
create with
Builder.addExecutable
that will compile main.zig into fresh/fresh.exe -
add dependency graph with
compile_step.dependOn(&exe.step);
. This is how we build our dependency graph and declare that whencompile_step
is made,exe
also needs to be made.
Cross Compilation#
- cross compilation is enabled by setting the target and build mode of our program:
exe.setBuildMode(.ReleaseSafe);
will pass-O ReleaseSafe
to the build invocation.exe.setTarget(...);
will set what-target ...
will see.Builder.standardReleaseOptions
/Builder.standardTargetOptions
: convenience functions to make both the build mode and the target available as a command line option-
invoke
zig build --help
to see command line options added bystandardTargetOptions
(first two) andstandardReleaseOptions
(rest)
ZigProject-Specific Options: -Dtarget=[string] The CPU architecture, OS, and ABI to build for -Dcpu=[string] Target CPU features to add or subtract -Drelease-safe=[bool] Optimizations on and safety on -Drelease-fast=[bool] Optimizations on and safety off -Drelease-small=[bool] Size optimizations on and safety off
-
example command line:
Zigzig build -Dtarget=x86_64-windows-gnu -Dcpu=athlon_fx zig build -Drelease-safe=true zig build -Drelease-small
Installing Artifacts#
Installation involves making a step on the install
step of the Builder
-
install
step always created and accessed viaBuilder.getInstallStep()
-
InstallArtifactStep
is build step responsible for copying exe artifact to install directory
pub fn build(b: *std.build.Builder) void {
const exe = b.addExecutable("fresh", "src/main.zig");
const install_exe = b.addInstallArtifact(exe);
b.getInstallStep().dependOn(&install_exe.step);
}
This will now do several things:
b.addInstallArtifact
creates a newInstallArtifactStep
that copies the compilation result ofexe
to$prefix/bin
(usuallyzig-out
)InstallArtifactStep
(implicitly) depends onexe
so will buildexe
as well- invoke by
zig build install
(or justzig build
for short) - uninstall the artifact by invoking
zig build uninstall
- the
InstallArtifactStep
registers the output file forexe
in a list that allows uninstalling it again - NOTE: deletes all files created by
zig build install
, but not directories! - Other helper functions
-
b.installArtifact(exe)/exe.install()
: convenience functions to wrap above steps. Ex:
Zigpub fn build(b: *std.build.Builder) void { const exe = b.addExecutable("fresh", "src/main.zig"); b.installArtifact(exe); // Helper 1 exe.install(); // OR Helper 2 }
-
Builder.installFile/installDirectory/etc
: install other types of artifacts
Running Applications#
Can run programs from build script for convenience
- usually exposed via a
run
step that can be invoked viazig build run
pub fn build(b: *std.build.Builder) void {
const exe = b.addExecutable("fresh", "src/main.zig");
const run_step = std.build.RunStep.create(exe.builder, "run fresh");
run_step.addArtifactArg(exe);
const step = b.step("run", "Runs the executable");
step.dependOn(&run_step.step);
}
-
std.build.RunStep
runs any executable on the system -
RunStep.addArg
will add a single string argument to argv. -
RunStep.addArgs
will add several strings at the same time -
RunStep.addArtifactArg
will add the result file of aLibExeObjStep
to argv -
RunStep.addFileSourceArg
will add any file generated by other steps to the argv -
NOTE: first argument must be the path to the executable we want to run. In this case, we want to run the compiled output of
exe
-
NOTE:
RunStep
runs executable in the compile cache directory, not install directory (e.g../zig-cache/o/b0f56fa4ce81bb82c61d98fb6f77b809/fresh
vszig-out/bin/fresh
) -
exe.run()
: helper convenience function for aboveZigpub fn build(b: *std.build.Builder) void { const exe = b.addExecutable("fresh", "src/main.zig"); const run_step = exe.run(); const step = b.step("run", "Runs the executable"); step.dependOn(&run_step.step); }
-
Builder.args
contains command line args that can be passed to process. Ex:Zigpub fn build(b: *std.build.Builder) void { const exe = b.addExecutable("fresh", "src/main.zig"); const run_step = exe.run(); if (b.args) |args| { run_step.addArgs(args); } const step = b.step("run", "Runs the executable"); step.dependOn(&run_step.step); }
Bashzig build run -- -o foo.bin foo.asm
Recipes#
Link zig library#
- Use
LibExeObjStep.addPackage/addPackagePath
with aPkg{ .name = "library", .path = "/path/to/the/library"}
- use
const library = @import("library");
in your root source file - Set output directory:
foo_lib.setOutputDir(output_path);
- note: this should be done before
foo_lib.setTarget(..)
as that will recompute the full output path - Get lib output lib path source:
exe.linkSystemLibrary(foo_lib.getOutputLibSource());
Use a native (C) library#
- Use
LibExeObjStep.linkSystemLibrary()
with your library's name @cInclude()
in your source code
Use a native (C++) library#
- use
LibExeObjStep.linkSystemLibrary("c++");
Use build-time custom command line flags (-Dsomething
)#
- Use
LibExeObjStep.addBuildOption()
to add a value to thebuild_options
package - To get this value from the building user, use
Builder.option()
- Supported types for
option
are Strings and Enums (-Dname=value
style), Booleans (-Dname
,-Dname=true
,-Dname=false
style) and list of strings (-Dname=value -Dname=value2
style) - Use from your source code like
const should_do_thing = @import("build_options").do_thing;
build_options#
provide compile-time configuration to your code
- the build system can create a package called
build_options
to communicate values frombuild.zig
to your project's source code - how to use:
-
create
OptionStep
inbuild.zig
with declarations to populate:
Zigconst build_options = b.addOptions(); build_options.addOption(bool, "enable_tracy", false); build_options.addOption(bool, "enable_tracy_callstack", false); build_options.addOption(bool, "enable_tracy_allocation", false);
-
add the options package to exe artifact:
exe.addOptions("build_options", build_options);
-
NOTE: if a package requires
build options
, must manually add it to its dependencies
Zigconst fooPkg = Pkg{ .name = "foo", .path = FileSource{ .path = "foo.zig" }, .dependencies = &[_]Pkg{ build_options.getPackage("build_options"), }};
-
can provide user input for these in form of
-Dname=value
flags. - can get the value a user provided (or
null
if they didn't, so useorelse
on anything you get from this) usingBuilder.option(type, name, description)
Run commands as build steps#
- Use
Builder.addSystemCommand()
to get a step that runs your command - create a top level step using
b.step()
- make the top level step depend on your run step using
top.dependOn(&run.step)
Get actual compile/link flags#
zig build --verbose
: emits the actual command passed tozig build-exe/lib/obj
with the compilation flagszig build --verbose-link
: will emit the linker flags passed to llvm
Generate documentation#
- Use
Builder.addTest()
to get a step that will test your program that we will calltest_doc
- make it emit documentation using
test_doc.emit_docs = true;
- make it stop emitting binary files using
test_doc.emit_bin = false
- finally set the output directory to some folder using for example
test_doc.output_dir = "docs"
- create a top level step using
b.step()
- make that newly created step depends on documentation step using
doc_step.dependOn(&test_doc.step)
Translate-C#
Incrementally Porting C App Series#
- Incrementally Porting C App: Part1
- Incrementally Porting C App: Part2
- Incrementally Porting C App: Part3
- Incrementally Porting C App: Part4
Internals#
Overview#
std.build.Builder
: representing a pending build and a DAG of all of its associated steps and their respective settingsbuild.zig:pub fn build(b: *Builder) void
is responsible for adding the custom build logic for module to said Builder- invoking
zig build
does under the hood is building and running lib/std/special/build_runner.zig, - just a normal Zig application with
pub fn main()
and all the things you might already know from your actual project build_runner
imports your project'sbuild.zig
(it does this with a magic@import("@build")
)- somewhere in its belly invokes your
pub fn build(b: *Builder)
on aBuilder
it created earlier - The very last thing it does is hand over to this
Builder
you got to modify usingmake()
- the main workhorse is
LibExeObjStep.make
which spawns the actual zig compiler (e.g.zig build-exe/zig build-lib/zig cc
) with the builder/step settings converted as command line args - code at src/main.zig
Compiler Stages#
Zig uses multiple compiler stages for bootstrapping the compiler:
- zig0: is just the c++ compiler as a static library
- only implements the backend for build-exe/obj etc
- stage1: is the current compiler, written in C++, compiled with Clang
- uses zig0 library to build pieces of stage2 in (subcommands like translate-c etc)
- stage2: is the current project, written in Zig, compiled with stage1
- stage3: is the fully self-hosted, stage2 code compiled with stage2
- stage1 doesn't implement full optimizations so stage2 binary is not optimized
- stage3 binary is optimized b/c stage2 implements optimizations/much better codegen
std.build.Builder#
The core build graph coordinator. Main purpose:
- Coordinate and execute
Step
s that describe different stages of a build - Provide default target and release mode for
Step
s - Provide
build_options
std.build.Step#
the base node in the build DAG
- two noteworthy properties:
makeFn
: does the actual work which implementing this step entailsdependencies
: anArrayList
of differentStep
s that must be executed before this one (though that isn't handled byStep
itself)- you'll mostly use structs that wrap a bare
Step
BuildExeObjStep
: this is the big one that actually does all of the compiling workLogStep
: very simple step that writes something to stderrRunStep
: which runs a system command- these are usually constructed with one of many convenience methods on
Builder
likebuilder.addTranslateC(std.build.FileSource)
- to get a quick overview of them, grep for
pub fn add
while in the source file
std.build.LibExeObjStep#
main step capable of invoking the zig compiler on your sources and turning them into executables or shared objects/DLLs
- usually constructed with one of
Builder
saddX
methods and then its myriad settings modified - finally call
install()
to create a build artifact in./zig-cache/bin
(this path is also adjustable usingsetOutputDir
) - can also use a
LibExeObjStep
to run your tests as done in the default build.zig for libraries
std.zig.CrossTarget#
defines project Build Targets
build.zig
template exposes the full power of Zig's cross-compiling to the building user- use
LibExeObjStep.setTarget(std.CrossTarget)
to set targets - easiest way is calling it with
std.CrossTarget
.parse(std.CrossTarget.ParseOptions)
to get a interface reminiscent of the-target
CLI option - The
ParseOptions
struct is fairly well documented in the source. Builder.standardTargetOptions()
is convenience wrapper aroundstd.CrossTarget.parse()
Compiler Internals#
- Linking: Coff.zig:linkWithLLD
- Zig Stage2 Compiler Internals by Mitchell Hashimoto
- Zig Tokenizer
- Zig Parser
- Zig AstGen: AST => ZIR
- Zig Sema: ZIR => AIR
Reference#
- Zig Build System Internals by Mitchell Hashimoto
- Zig Build Explained: Part I
- Zig Build Explained: Part II
- Zig Build System Wiki Entry
- Relevant source files
- std/build.zig for
Builder
,LibExeObjStep
and some other steps - std/build/ for various other steps
- std/special/init-exe/build.zig for the default application build.zig template
- std/special/init-lib/build.zig for the default library build.zig template
- std/special/build_runner.zig for the file executed when you run
zig build