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.
What You'll Learn
By the end of this lesson you will write a build.zig from scratch: declare a module, hand it to an executable and to a test compilation so both compile the same source, wire a run step and a test step onto the command line, and make the target and optimize mode options the caller chooses rather than values you hardcode. The file is an ordinary Zig program, so nothing about the language is new — what is new is the API it talks to and the graph it builds.
This is the lesson the capstone's final assembly depends on. The Record Analyzer stops being a single playground snippet and becomes a project with a build.zig at its root, built and tested with zig build and zig build test, and the addTest-needs-addRunArtifact trap below is the exact one the capstone calls out when it assembles. You arrive here able to write functions and to write test blocks and run them (Testing); what is new is the file that decides how those tests get compiled and whether they are ever actually executed.
Everything on this page is pinned to Zig 0.16.0, and the build system is one of the surfaces that moves between releases. Zig 0.17 is expected to carry breaking changes to b.args — the argument forwarding in the run step below — as well as to package management and @bitCast. The shape of the thing is stable: modules, steps, and a graph you assemble in a build() function. The exact call spellings are not, so check the release notes against this page when 0.17 lands rather than assuming a snippet still compiles.
Build File Basics
Any project built with zig build has a build.zig that defines how to compile and test it. Single-file programs you launch with zig run need no build file at all — zig build is what looks for one, and it stops with error: no build.zig file found when there is none.
Here is that file, in full. It is a real build.zig rather than an illustration — the fence is marked no-run because the playground compiles one file with zig run and has no project directory to build, but this is the text you would put at the root of a project.
// build.zig — the smallest useful build file.
const std = @import("std");
pub fn build(b: *std.Build) void {
// Let the person running the build choose; both default sensibly.
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// The module is the unit that carries source, target and optimize mode.
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
// Without this, zig build compiles the executable and then throws it away.
b.installArtifact(exe);
}
Put that beside a src/main.zig and a build.zig.zon, and zig build writes the binary to zig-out/bin/myapp. Run it against a project with no build.zig at all and the command says so rather than guessing:
info: initialize build.zig template file with 'zig init'
info: see 'zig --help' for more options
error: no build.zig file found, in the current directory or any parent directories
The playground fence below runs a different program: a summary of the targets and modes the file above makes available.
const std = @import("std");
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 = &.{"compile"},
},
.{
.name = "run",
.description = "Build and run the application",
.dependencies = &.{"compile"},
},
.{
.name = "install",
.description = "Install to zig-out/bin",
.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", .{});
}
}
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", .{});
}
Here is the build.zig that declares those steps. One module is created once and handed to both the executable and the test compilation, so zig build test tests exactly the code zig build ships rather than a second configuration that can drift away from it.
// build.zig — an executable plus a run step plus a test step.
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// One module, reused by both the executable and the test step.
const main_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{ .name = "myapp", .root_module = main_mod });
b.installArtifact(exe);
// zig build run — build the executable, then launch it.
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);
// zig build test — compile the same module in test mode, then run it.
const tests = b.addTest(.{ .root_module = main_mod });
const run_tests = b.addRunArtifact(tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_tests.step);
}
Every b.step(name, description) call adds an entry the command line can ask for, which you can see for yourself. Against the file above, zig build --list-steps prints:
install (default) Copy build artifacts to prefix path
uninstall Remove build artifacts from prefix path
run Run the app
test Run unit tests
install and uninstall are there without being declared — b.installArtifact(exe) attached the executable to the built-in install step, and install is what plain zig build runs by default. Running the test step against a module holding one passing test prints a summary that names both halves of the wiring:
Build Summary: 3/3 steps succeeded; 1/1 tests passed
test success
+- run test 1 pass (1 total) 8ms MaxRSS:4M
+- compile test Debug native success 516ms MaxRSS:134M
Read that tree bottom-up and the two-step is visible: compile test produced the binary, and run test — indented above it as the step that depends on it — is what actually executed the one test. That second step is the one people forget. Predict what happens without it.
Predict
These two build.zig fragments both define a test step. One wires addTest through addRunArtifact, the other attaches the test compilation directly. What happens when you run zig build test on the second one?
// Version A — the artifact is run
const tests = b.addTest(.{ .root_module = main_mod });
const run_tests = b.addRunArtifact(tests);
const step_a = b.step("test", "Run unit tests");
step_a.dependOn(&run_tests.step);
// Version B — the compilation itself is the dependency
const tests_b = b.addTest(.{ .root_module = main_mod });
const step_b = b.step("test", "Run unit tests");
step_b.dependOn(&tests_b.step);Version B builds the test binary and stops there. addTest returns a Compile step, whose job ends once the artifact exists, so zig build test compiles it, finds no problem, and reports success — with no test ever executed and a failing assertion never noticed. addRunArtifact adds the step that actually launches the binary, and only that step can observe a failure. The same pairing applies to executables: addExecutable builds, addRunArtifact runs.
Now wire one yourself. The lines below are the part of a build function that turns a module into a zig build test step.
Arrange the code
A target and an optimize mode are already read above these lines. The five lines below are the rest of a build function: they declare the module, build a test compilation from it, wrap that in something runnable, name a step, and connect the two. The pieces are shuffled. Reorder them so zig build test compiles the tests and then actually runs them — then answer what the order is really testing: four of these five lines produce a value the next line consumes, and exactly one does the connecting. Which is it, and what does the build do if that line is present but the one above it is missing?
test_step.dependOn(&run_tests.step);const tests = b.addTest(.{ .root_module = main_mod });const test_step = b.step("test", b.fmt("Run {s}", .{run_tests.step.name}));const run_tests = b.addRunArtifact(tests);const main_mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize });
Every position is forced by plain data dependency: each line consumes a name the previous one produced, so moving any of them up is a compile error in build.zig itself. The last line is the interesting one — b.step declares a name and addRunArtifact creates something executable, but dependOn is the only line that connects them. Drop the addRunArtifact and attach the step straight to the compilation and the build still succeeds, having compiled the tests and run none of them.
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", .{});
}
Read the safety column of that table one more time, then diagnose the program below. It encodes a release policy — which mode a stated goal should use, and whether that mode keeps Zig's runtime safety checks — and one of its two answers contradicts the table.
Debug
This program encodes a release policy and checks itself against it. The mode it picks for production is right, but the program still fails. Diagnose which of the four modes the safety table gets wrong, then fix it so the program prints the production mode with safety checks on.
const std = @import("std");
const Mode = enum { Debug, ReleaseSafe, ReleaseFast, ReleaseSmall };
/// Does this mode keep Zig's runtime safety checks (bounds, overflow) enabled?
fn keepsSafetyChecks(mode: Mode) bool {
return switch (mode) {
.Debug => true,
else => false,
};
}
/// Pick a mode for a stated goal.
fn recommend(goal: []const u8) Mode {
if (std.mem.eql(u8, goal, "development")) return .Debug;
if (std.mem.eql(u8, goal, "production")) return .ReleaseSafe;
if (std.mem.eql(u8, goal, "embedded")) return .ReleaseSmall;
return .ReleaseFast;
}
pub fn main() !void {
// A production build should be optimized AND keep its safety checks.
const prod = recommend("production");
if (prod != .ReleaseSafe) {
std.debug.print("CHECK FAILED: production should be ReleaseSafe, got {s}\n", .{@tagName(prod)});
return error.CheckFailed;
}
if (!keepsSafetyChecks(prod)) {
std.debug.print("CHECK FAILED: {s} is documented as keeping safety checks, but keepsSafetyChecks says no\n", .{@tagName(prod)});
return error.CheckFailed;
}
if (keepsSafetyChecks(.ReleaseFast)) {
std.debug.print("CHECK FAILED: ReleaseFast should NOT keep safety checks\n", .{});
return error.CheckFailed;
}
std.debug.print("production build: {s}, safety checks on: {}\n", .{ @tagName(prod), keepsSafetyChecks(prod) });
}Expected output:
production build: ReleaseSafe, safety checks on: true
The bug is else => false. It sweeps ReleaseSafe in with ReleaseFast and ReleaseSmall, when being optimized and keeping bounds and overflow checks is exactly what ReleaseSafe is for — the reason it is the recommended production mode. Naming all four tags (.Debug, .ReleaseSafe => true, .ReleaseFast, .ReleaseSmall => false,) fixes it and buys something more: with no else, adding a fifth mode later would fail to compile until you decided which side it falls on. An else over an enum you control answers that question silently, and always the same way.
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});
}
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", .{});
}
The build.zig side of cross-compilation is one line — the same standardTargetOptions call the first fence already had:
// build.zig — the target is a build option, not a hardcoded platform.
const std = @import("std");
pub fn build(b: *std.Build) void {
// Reads -Dtarget from the command line; defaults to the host.
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{ .name = "myapp", .root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}) });
b.installArtifact(exe);
}
That really is the whole configuration. Building that project on an x86-64 Linux machine three times, changing nothing but the flag, produces three different binaries:
$ zig build && file -b zig-out/bin/myapp
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
$ zig build -Dtarget=aarch64-linux-gnu && file -b zig-out/bin/myapp
ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, with debug_info, not stripped
$ zig build -Dtarget=x86_64-windows-gnu && ls zig-out/bin/
myapp myapp.exe myapp.pdb
No cross toolchain was installed to do that, and the third invocation produced a Windows executable and its debug symbols on a Linux host. Note what that last listing shows: zig-out is an install directory, not a build output that gets cleaned between runs, so the artifacts accumulate. The myapp sitting beside the .exe is the aarch64 binary from the second command, which overwrote the x86-64 one from the first — same name, different architecture, no warning. If you cross-compile for several targets in sequence, either pass -p to install each into its own prefix or delete zig-out between runs; otherwise the directory is a mix of whatever you built last for each filename. standardTargetOptions is what makes the target a decision the caller makes rather than one the build file bakes in.
build.zig is the first file in this course that is only Zig-the-configuration-language. Check what that actually means about the tools you already have.
Recall
Without scrolling up: build.zig is an ordinary Zig file containing pub fn build(b: *std.Build) void. Testing taught you to write test blocks and run them; Comptime taught you compile-time evaluation. Which row correctly describes what build.zig is, and when its code runs?
So build.zig is a real Zig program that zig build compiles and runs before your project is built, and whose job is to construct a graph of steps. That is different from comptime: comptime evaluates code inside the compiler while compiling one program, whereas build.zig is its own program with no such restrictions — which is why b.option(...) orelse false and plain if branching work. The test blocks from Testing are what addTest collects, and everything you know about Zig's control flow and collections applies unchanged.
Try It Yourself
Reading about the build system is not the same as writing a build file. This is a build task: a small program that reports its own pass/fail, and the second half of it is a real build.zig body. Two things are stubbed out. Run it as-is and it fails immediately, naming the first stub that is still empty. Implement each until it prints All checks passed.
The two stubs are checked in two different ways, and the difference is the point. TODO 1 is a model of the build graph — plain data the checks read back at run time, so a wrong answer prints a failed check. TODO 2 is the actual build() function for the capstone's record-analyzer project, written against the real std.Build API. Nothing calls it, so the comptime { _ = &build; } line below the stubs is what forces the compiler to analyse its body: with that line in place, a wrong field name or the wrong options struct is a compile error here rather than a discovery you make later in a real project. Try .root_source_file on addExecutable instead of .root_module and you will see it — error: no field named 'root_source_file' in struct 'Build.ExecutableOptions', which is exactly what Zig 0.15 removing the implicit root module means in practice.
Build
Finish the build. Two things are stubbed out and the checks fail until each is right. Run it as-is to see which check fails first, decide what that stub is missing, then implement both until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down. TODO 2 is a real build.zig body: nothing runs it, but the comptime block at the bottom makes the compiler check every std.Build call you write, so getting a field name wrong stops the program from compiling at all.
const std = @import("std");
fn expect(ok: bool, comptime msg: []const u8) !void {
if (!ok) {
std.debug.print("CHECK FAILED: {s}\n", .{msg});
return error.CheckFailed;
}
}
/// What a named step in the build graph is wired to do.
const Wiring = struct {
/// Does reaching this step launch a binary, or only produce one?
runs_artifact: bool,
/// Which step must be finished before this one can start.
needs: []const u8,
};
// TODO 1: answer, for each step name, what reaching it actually does.
// "install" -> runs_artifact false, needs "compile"
// "run" -> runs_artifact true, needs "compile"
// "test" -> runs_artifact true, needs "compile"
// Any other name — one that no b.step call in your build declared —
// returns error.UnknownStep.
// runs_artifact is the addRunArtifact question: does the step launch a binary,
// or only produce one? install copies an artifact into zig-out and stops.
fn wiring(step_name: []const u8) !Wiring {
// your code here
_ = step_name;
return error.UnknownStep;
}
// TODO 2: write the real build function for the record-analyzer project.
// This is an actual build.zig body compiled against the real std.Build API —
// the comptime block below forces the compiler to analyse it, so a wrong field
// name or a wrong options struct is a compile error right here rather than a
// surprise in a real project. Build it in this order:
// - target and optimize from b.standardTargetOptions(.{}) and
// b.standardOptimizeOption(.{}), so the caller chooses both
// - ONE module from b.createModule with .root_source_file = b.path("src/main.zig")
// plus that target and optimize, shared by the executable and the tests
// - an executable named "record-analyzer" taking .root_module = that module,
// handed to b.installArtifact
// - a "run" step: b.addRunArtifact(exe), forward b.args, then dependOn
// - a "test" step: b.addTest(.{ .root_module = ... }) wrapped in
// b.addRunArtifact, then dependOn — the wrap is what makes it execute
pub fn build(b: *std.Build) void {
// your code here
_ = b;
}
// --- Build checks: Do not edit below this line. ---
// Forces the compiler to analyse build's body even though nothing calls it.
// Without this line a wrong std.Build call would go unnoticed here.
comptime {
_ = &build;
}
pub fn main() !void {
const install = try wiring("install");
try expect(!install.runs_artifact, "install copies a built artifact; it does not launch one");
try expect(std.mem.eql(u8, install.needs, "compile"), "install needs the compile to have happened");
const run = try wiring("run");
try expect(run.runs_artifact, "run launches the binary, so it goes through a Run step");
try expect(std.mem.eql(u8, run.needs, "compile"), "run depends on the compile that produced the binary, not on install");
const t = try wiring("test");
try expect(t.runs_artifact, "a test step that does not run an artifact never executes a test");
try expect(std.mem.eql(u8, t.needs, "compile"), "the test binary must be compiled before it is run");
if (wiring("publish")) |_| {
try expect(false, "a name no b.step call declared should return error.UnknownStep");
} else |err| {
try expect(err == error.UnknownStep, "a name no b.step call declared should return error.UnknownStep");
}
const info = @typeInfo(@TypeOf(build)).@"fn";
try expect(info.params.len == 1, "build must take exactly one parameter");
try expect(info.params[0].type.? == *std.Build, "build's parameter must be *std.Build");
try expect(info.return_type.? == void, "build must return void");
std.debug.print("All checks passed.\n", .{});
std.debug.print("build(b: *std.Build) compiled against the real std.Build API.\n", .{});
std.debug.print("run needs {s}; test runs its artifact: {}\n", .{ run.needs, t.runs_artifact });
}Expected output:
All checks passed. build(b: *std.Build) compiled against the real std.Build API. run needs compile; test runs its artifact: true
Once it passes, try two variations and predict each before running:
- Break the executable options. In
build, change.root_module = main_modto.root_source_file = b.path("src/main.zig")— the pre-0.15 spelling — and predict what happens. The program stops compiling, witherror: no field named 'root_source_file' in struct 'Build.ExecutableOptions'pointing at that field. No check runs, because there is no binary to run; the mistake is caught by the type system rather than by an assertion, which is what thecomptime { _ = &build; }line buys you. - Unwire the test step. Delete all three test-step lines from
build— theb.addTest, theb.addRunArtifactand theb.step— leaving the executable and run step behind, and predict which check fires. None of them do: the program still printsAll checks passed., becausewiringis a separate model and the checks cannot see insidebuild's body. Delete only thetest_step.dependOn(...)line and you get something different and worth seeing — twoerror: unused local constantmessages namingrun_testsandtest_step, and no output at all. That is Zig refusing to let you bind a name you never use, and it is the only reason this particular unwiring is caught: it is a coincidence of the local variables, not a check on the build graph. Give those names to_and the error goes away along with the warning. In a real project the same unwiring gives you azig build testthat exits zero having run no tests, and nothing warns you at all. The only way to catch it is to make a test fail on purpose and confirm the build goes red.
build.zig is not the first build description you have met, even if it is the first written in the language it builds. Check which part of the idea carries.
Transfer
A build.zig declares steps and connects them with dependOn, and zig build <name> then walks that graph. A Makefile declares targets, each with prerequisites and a recipe, and make <target> walks that graph. Which statement names the mechanism the two genuinely share, rather than a resemblance of vocabulary?
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 workload-dependent — often negligible, since the optimizer removes checks it can prove redundant — and they catch bugs that would otherwise be silent memory corruption. Measure your own hot paths before assuming the cost is too high.
Next Steps
Your build.zig can compile and test a project -- next it needs to expose one. Next, you'll learn how to structure code into modules, declare dependencies, and lay out a project that other people can actually depend on.