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.Stepnodes 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-nameor 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-objorzig testdepending 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.addExecutablethat 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_stepis made,exealso 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 ReleaseSafeto 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 --helpto 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
-
installstep always created and accessed viaBuilder.getInstallStep() -
InstallArtifactStepis 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.addInstallArtifactcreates a newInstallArtifactStepthat copies the compilation result ofexeto$prefix/bin(usuallyzig-out)InstallArtifactStep(implicitly) depends onexeso will buildexeas well- invoke by
zig build install(or justzig buildfor short) - uninstall the artifact by invoking
zig build uninstall - the
InstallArtifactStepregisters the output file forexein 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
runstep 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.RunStepruns any executable on the system -
RunStep.addArgwill add a single string argument to argv. -
RunStep.addArgswill add several strings at the same time -
RunStep.addArtifactArgwill add the result file of aLibExeObjStepto argv -
RunStep.addFileSourceArgwill 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:
RunStepruns executable in the compile cache directory, not install directory (e.g../zig-cache/o/b0f56fa4ce81bb82c61d98fb6f77b809/freshvszig-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.argscontains 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/addPackagePathwith 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_optionspackage - To get this value from the building user, use
Builder.option() - Supported types for
optionare Strings and Enums (-Dname=valuestyle), Booleans (-Dname,-Dname=true,-Dname=falsestyle) and list of strings (-Dname=value -Dname=value2style) - 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_optionsto communicate values frombuild.zigto your project's source code - how to use:
-
create
OptionStepinbuild.zigwith 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=valueflags. - can get the value a user provided (or
nullif they didn't, so useorelseon 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/objwith 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) voidis responsible for adding the custom build logic for module to said Builder- invoking
zig builddoes 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_runnerimports your project'sbuild.zig(it does this with a magic@import("@build"))- somewhere in its belly invokes your
pub fn build(b: *Builder)on aBuilderit created earlier - The very last thing it does is hand over to this
Builderyou got to modify usingmake() - the main workhorse is
LibExeObjStep.makewhich 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
Steps that describe different stages of a build - Provide default target and release mode for
Steps - 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: anArrayListof differentSteps that must be executed before this one (though that isn't handled byStepitself)- 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
Builderlikebuilder.addTranslateC(std.build.FileSource) - to get a quick overview of them, grep for
pub fn addwhile 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
BuildersaddXmethods 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
LibExeObjStepto run your tests as done in the default build.zig for libraries
std.zig.CrossTarget#
defines project Build Targets
build.zigtemplate 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-targetCLI option - The
ParseOptionsstruct 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,LibExeObjStepand 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