Let’s say I have a Zig project that uses build.zig which provides various steps, like zig build test. The steps take various config options like:
zig build test -Drelease
zig build example -Dexample-name="hello world"
Now, let’s say I have my CI script, which invokes a certain sequence of steps:
- zig buid test
- zig build test -Drelease
- zig build example -Dexample-name="hello world"
- zig build example -Dexample-name="hello world" -Dtarget=x86_64-windwos
That is horrible yaml with is painful to work with. Ideally, I’d love to reduce my ci script to just
- zig build ci
and have that ci step call appropriate steps with various arguments. What’s the best way to do that? The problem here is that, in build.zig, only one set o arguments exists at the same time.
I can call addSystemCommand myself a bunch, which I think would work:
const step_ci = b.step("ci", "Run full suite of checks");
step_ci.dependOn(
&b.addSystemCommand(.{ b.graph.zig_exe, "build", "test", "-Drelease" }).step,
);
step_ci.dependOn(
&b.addSystemCommand(.{ b.graph.zig_exe, "build", "test" }).step,
);
but one problem here is that I don’t get a unified build graph, so this won’t necessary use my CPU cores most efficiently and will have suboptimal progress reporting.
If the step definition is complex, you get a lot of copy-paste. Which you can battle with extract step-creating function, but that’s more complex, and you’d still have to cal the function twice.
Because the resulting steps are different, you don’t get caching. Eg, if you run zig build test, and then zig build ci, if the ci re-creates test step by following debug/release matrix, it wouldn’t notice that the test were being run already.
/// Causes the Run step to be considered to have side-effects, and therefore
/// always execute when it appears in the build graph.
/// It also means that this step will obtain a global lock to prevent other
/// steps from running in the meantime.
/// The step will fail if the subprocess crashes or returns a non-zero exit code.
inherit,
The best way, as tensorusch mentioned, would be to do dependency injection and pass on all configurations you want through some function. If this seems too hard for your case, then you may be interested in this hack where you create a nested ci/build.zig/ci/build.zig.zon:
// ci/build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const install_step = b.getInstallStep();
const release_modes: []const bool = &.{ false, true };
for (release_modes) |release_mode| {
// if you want more targets then you could add another loop
const something = b.dependency("something", .{
.release = release_mode,
.@"example-name" = @as([]const u8, "hello world"),
});
install_step.dependOn(&something.builder.top_level_steps.get("test").?.step);
install_step.dependOn(&something.builder.top_level_steps.get("example").?.step);
// and if you want to store/save executables somewhere:
_ = something.artifact("exe");
}
}
However, this has quite a few drawbacks because you must do cd ci && zig build. I’m also unsure if the builder.top_level_steps should be used this way from separate builders. Therefore, if possible, I would try to use the original build.zig.
if you abstract the meat of your steps into a function that takes its config as parameters, it should be relatively easy to make a ci step that calls a bunch of configurations
I was thinking that running zig bulild test and then zig build ci would compile tests in debug mode twice, because there will be separate std.Build.Step.Compile compiles. But it looks (obvious in retrospect) that caching is determined “by value”, not by identity.
That is, the snippet above creates three distinct (in terms of pointer equality) std.Build.Step.Compiles, two of which are equal. So the cacing works as expected:
λ ~/zig-0.14/zig build --summary all test
Build Summary: 3/3 steps succeeded; 1/1 tests passed
test success
└─ run test 1 passed 1ms MaxRSS:1M
└─ zig test Debug native cached 35ms MaxRSS:36M
matklad@ahab ~/tmp
λ ~/zig-0.14/zig build --summary all ci
Build Summary: 5/5 steps succeeded; 2/2 tests passed
ci success
├─ run test 1 passed 1ms MaxRSS:1M
│ └─ zig test Debug native cached 51ms MaxRSS:36M
└─ run test 1 passed 382ms MaxRSS:1M
└─ zig test ReleaseSafe native success 4s MaxRSS:288M
Ok, so then my complained is reduced to just “I have to refactor my entire build script”, which is something I can live with, thanks!