zig の Build.Step の詳細

に公開

0.15.1

zig の build でちょっと凝ったことをやろうとする場合、既存の Step を組み合わせるよりも自作した方が簡単に実現できる場合がある。

  • LazyPath の中身にアクセスしたい場合
  • cache の挙動を制御したい場合(cmake の再実行を抑止するとか)

など。

LazyPath の中身にアクセスする Step

Step を作るにはイディオムのようなものがあって、
おおむね以下のような感じになります。

pub const PrintLazyPathContent = struct {
    step: std.Build.Step,
    path: std.Build.LazyPath,

    pub fn create(owner: *std.Build, path: std.Build.LazyPath) *@This() {
        const this = owner.allocator.create(@This()) catch @panic("OOM");
        this.* = .{
            .step = std.Build.Step.init(.{
                .id = .custom,
                .name = "PrintLazyPath",
                .owner = owner,
                .makeFn = make,
            }),
            .path = path,
        };
        // 👇 この step が path を待つ
        path.addStepDependencies(&this.step);
        return this;
    }

    fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) anyerror!void {
        // メンバーのポインタから持ち主を得る 👇
        // これが Step の作法になっている。
        // this は予約後ではなく普通の変数。
        const this: *@This() = @fieldParentPtr("step", step);
        const b = step.owner;

        // path解決
        const path3 = this.path.getPath3(b, step);
        // b.cache_root.handle は Dir
        const file = try b.cache_root.handle.openFile(path3.sub_path, .{});
        defer file.close();
        const content = try file.readToEndAlloc(b.allocator, std.math.maxInt(usize));
        std.log.debug("content: {s}", .{content});
    }
};

Step の依存関係

b.getInstallStep().dependOn(&install.step);

のように std.Build.Step で依存 graph を構築する。
明示的に dependOn する必要がある場合は限られていて、
std.Build.LazyPath などを介して裏で繋げてくれる関数がわりとある。

b.getInstallStep()b.step() 関数で得られる step を root として木を構築して、
末端から逆順にタスクを実行する。
zig build--summary all 引数をつけることで依存ツリーを表示するとわかりやすい。

> zig build --summary all

Build Summary: 17/17 steps succeeded
install cached
├─ install xrhandsfb cached
  └─ compile exe xrhandsfb Debug native cached 73ms MaxRSS:34M
     ├─ WriteFile  cached
  └─ WriteFile  cached
     ├─ CmakeStep cached
  └─ GetVcEnv cached
     └─ FindVcInstall success
     ├─ CmakeStep cached
  └─ GetVcEnv cached
     └─ FindVcInstall success
     └─ CmakeStep cached
        └─ GetVcEnv cached
           └─ FindVcInstall success
     ├─ compile lib SampleXrFramework Debug native cached 50ms MaxRSS:34M
  ├─ WriteFile  (+3 more reused dependencies)
  ├─ WriteFile  (+3 more reused dependencies)
  └─ WriteFile  (+3 more reused dependencies)
     ├─ WriteFile  (+3 more reused dependencies)
     └─ compile lib SampleXrFramework Debug native (+3 more reused dependencies)
├─ install generated/ cached
  └─ WriteFile  (+3 more reused dependencies)
└─ install assets/ cached

std.Build.LazyPath

https://ziglang.org/documentation/master/std/#std.Build.LazyPath

step と連携する重要な型に LazyPath というのがある。

b.path("src/main.zig")

これは union になっていて、

  • src_path
  • generated
  • cwd_relative
  • dependency

となっている。

一番単純なのが cwd_relative で文字列から直接作る。

var path = std.Build.LazyPath{ .cwd_relative = "C:/path/to/abs/path/some.exe" };

Std.Build.Step.Run と LazyPath(generated)

generated な LazyPath は動的に path が決まる。

generatedLazyPath.addStepDependencies(step) のように待つことができる。
中で dependsOn を呼んでいる。

std.Build.GeneratedFile
pub const GeneratedFile = struct {
    /// The step that generates the file
    step: *Step,

    /// The path to the generated file. Must be either absolute or relative to the build runner cwd.
    /// This value must be set in the `fn make()` of the `step` and must not be `null` afterwards.
    // 👇
    path: ?[]const u8 = null,
};

Std.Build.Step.Run で
名前に Out がついて、std.Build.LazyPath を返す関数は
generated な LazyPath を返すものが多い。

  • addOutputFileArg
  • captureStdOut

中で generatedLazyPath.addStepDependencies してくれそうなところは dependOn
呼ぶ必要が無い、というか呼ばない方がよい。
もし呼ぶと、不要な dependency が増えたり、悪くするとエラーになる。
--summary all で確認するべし。

generated を makeFn でセットする例

manifest と cache

fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) anyerror!void {
    const this: *@This() = @fieldParentPtr("step", step);
    const b = step.owner;

    // manifest
    var man = step.owner.graph.cache.obtain();
    defer man.deinit();

    // man にstepの入力に応じた byte を追加して変化させる

    if (try step.cacheHitAndWatch(&man)) {
        // 前回の実行から manifest が変化していないので
        // キャッシュを返すべし
        const digest = man.final();
        const prefix_dir = try b.cache_root.join(b.allocator, &.{ "o", &digest, "prefix" });
        this.output.path = prefix_dir;
        return;
    }

    // .zig-cache/o/XXXXXXXXXXXXXXXXX が決まる
    const digest = man.final();
    const cache_dir = b.pathJoin(&.{ "o", &digest });
    // b.cache_root.handle が std.fs.Dir なので、ここから相対パスとして扱うのが基本
    b.cache_root.handle.makePath(cache_dir) catch |err| {
        return step.fail("unable to make path '{f}{s}': {s}", .{
            b.cache_root, cache_dir, @errorName(err),
        });
    };
    // fullpath の文字列を得る例
    const cwd = b.fmt("{s}/o/{s}", .{ try b.cache_root.handle.realpathAlloc(b.allocator, ""), &digest });

    // タスク

    // キャッシュを登録する
    try step.writeManifestAndWatch(&man);
}

https://github.com/ziglang/zig/issues/18450

参考

https://smntin.github.io/blog/2025/zig-build-embed/

使用例。

Discussion