← Zurück zu Lernen

Zig Build-System

Wann sollte man das Zig-Build-System einsetzen?

Die grundlegenden Befehle zig build-exe, zig build-lib, zig build-obj, und zig test sind oftmals ausreichend. Manchmal benötigt man jedoch eine weitere Abstaktionsebene, um komplexere Kompilationen durchzuführen.

Wenn beispielsweise eine dieser Situationen zutrifft:

Wenn einer dieser Punkte zutrifft, wird das Projekt von der Verwendung des Zig-Build-Systems profitieren.

Erste Schritte

Einfaches ausführbares Programm

Dieses Build-Skript erzeugt ein ausführbares Programm aus einer Zig-Datei, mit einer von außen aufrufbaren Funktion.

hello.zig
const std = @import("std");

pub fn main() !void {
    std.debug.print("Hello World!\n", .{});
}
build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = b.path("hello.zig"),
        .target = b.host,
    });

    b.installArtifact(exe);
}
Shell
$ zig build--summary all

Build Summary: 3/3 steps succeeded
install cached
└─ install hello cached
   └─ zig build-exe hello Debug native cached 66ms MaxRSS:36M

Installation von Build-Artefakten

Das Zig-Build-System basiert, wie die meisten Build-Systeme, auf der Modellierung des Projekts als gerichteter azyklischer Graph (DAG) von Schritten, die unabhängig und gleichzeitig ausgeführt werden.

Standardmäßig ist der Hauptschritt im Graph der Schritt Install, dessen Zweck es ist, Build-Artefakte an ihren endgültigen Platz zu kopieren. Der Install-Schritt startet ohne Abhängigkeiten, und daher wird nichts passieren, wenn zig build ausgeführt wird. Das Build-Skript eines Projekts muss den zu installierenden Dingen etwas hinzufügen, was der obige Aufruf der Funktion installArtifact macht.

Ausgabe

├── build.zig
├── hello.zig
├── .zig-cache
└── zig-out
    └── bin
        └── hello

In der Ausgabe sind zwei Verzeichnisse enthalten: .zig-cache und zig-out. Das erste enthält Dateien, die nachfolgende Builds schneller machen, aber diese Dateien sind nicht dafür gedacht, in eine Versionskontrolle eingecheckt zu werden. Dieses Verzeichnis kann auch jederzeit ohne Konsequenzen vollständig gelöscht werden.

Das zweite, zig-out, ist ein „Installationspräfix“. Dies entspricht dem Standardkonzept der Dateisystemhierarchie. Dieses Verzeichnis wird nicht vom Projekt bestimmt, sondern vom Benutzer von zig build mittels dem --prefix-Flag (kurz -p).

Als Projektbetreuer wählst du aus, was in dieses Verzeichnis gelegt wird, aber der Benutzer wählt aus, wo er es in seinem System installieren möchte. Das Build-Skript kann die Ausgabepfade nicht fest kodieren, da dies die Zwischenspeicherung, die Gleichzeitigkeit und die Kompatibilität beeinträchtigen und den Endbenutzer verärgern würde.

Hinzufügen eines Komfortschritts zur Ausführung der Anwendung

Es ist üblich einen Run-Schritt hinzuzufügen, um eine Möglichkeit zu schaffen, die eigene Hauptanwendung direkt aus dem Build-Befehl zu starten (zig build run).

hello.zig
const std = @import("std");

pub fn main() !void {
    std.debug.print("Hello World!\n", .{});
}
build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = b.path("hello.zig"),
        .target = b.host,
    });

    b.installArtifact(exe);

    const run_exe = b.addRunArtifact(exe);

    const run_step = b.step("run", "Run the application");
    run_step.dependOn(&run_exe.step);
}
Shell
$ zig buildrun --summary all

Hello World!
Build Summary: 3/3 steps succeeded
run success
└─ run hello success 14ms MaxRSS:1M
   └─ zig build-exe hello Debug native cached 44ms MaxRSS:36M

Grundlagen

Benutzeroptionen

Verwende b.option, um das Build-Skript sowohl für Benutzer konfigurierbar zu machen, als auch für andere Projekte, die von dem Projekt als Paket abhängen.

build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const windows = b.option(bool, "windows", "Target Microsoft Windows") orelse false;

    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = b.path("example.zig"),
        .target = b.resolveTargetQuery(.{
            .os_tag = if (windows) .windows else null,
        }),
    });

    b.installArtifact(exe);
}
Shell
$ zig build--summary all

Build Summary: 3/3 steps succeeded
install cached
└─ install hello cached
   └─ zig build-exe hello Debug native cached 34ms MaxRSS:36M
example.zig
const std = @import("std");

pub fn main() !void {
    std.debug.print("Hello World!\n", .{});
}

Beachte bitte diese Zeilen:

Project-Specific Options:
  -Dwindows=[bool]             Target Microsoft Windows

Dieser Teil des Hilfemenüs wird automatisch erzeugt, wenn die Logik von build.zig ausgeführt wird. Benutzer können auf diese Weise Konfigurationsoptionen des Build-Skripts erkunden.

Standard-Konfigurationsoptionen

Bisher haben wir ein boolesches Flag verwendet, um die Erstellung für Windows anzuzeigen. Das können wir jedoch besser machen.

Die meisten Projekte möchten die Möglichkeit bieten, die Ziel- und Optimierungseinstellungen zu ändern. Um Standard-Namenskonventionen für diese Optionen zu fördern, bietet Zig die Hilfsfunktionen standardTargetOptions und standardOptimizeOption.

Mit den Standard-Zieloptionen kann die Person, die zig build ausführt, wählen, für welches Ziel (Betriebssystem/Platform) gebaut werden soll. Standardmäßig ist jedes Ziel erlaubt. Keine Auswahl bedeutet, dass als Ziel das Host-System automatisch gewählt wird. Weitere Optionen zur Einschränkung der unterstützten Zielsysteme sind ebenfalls verfügbar.

Die Standard-Optimierungsoptionen erlauben es der Person, die zig build ausführt, zwischen Debug, ReleaseSafe, ReleaseFast und ReleaseSmall zu wählen. Standardmäßig wird keine der Release-Optionen vom Build-Skript als bevorzugte Option angesehen, und der Benutzer muss eine Entscheidung treffen, um einen Release-Build zu erstellen.

hello.zig
const std = @import("std");

pub fn main() !void {
    std.debug.print("Hello World!\n", .{});
}
build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = b.path("hello.zig"),
        .target = target,
        .optimize = optimize,
    });

    b.installArtifact(exe);
}
Shell
$ zig build-Dtarget=x86_64-windows -Doptimize=ReleaseSmall --summary all

Build Summary: 3/3 steps succeeded
install cached
└─ install hello cached
   └─ zig build-exe hello ReleaseSmall x86_64-windows cached 44ms MaxRSS:36M

Jetzt enthält unser --help-Menü zusätzliche Einträge:

Project-Specific Options:
  -Dtarget=[string]            The CPU architecture, OS, and ABI to build for
  -Dcpu=[string]               Target CPU features to add or subtract
  -Doptimize=[enum]            Prioritize performance, safety, or binary size (-O flag)
                                 Supported Values:
                                   Debug
                                   ReleaseSafe
                                   ReleaseFast
                                   ReleaseSmall

Es ist durchaus möglich, diese Optionen direkt über b.option zu erstellen, aber diese API bietet eine allgemein gebräuchliche Namenskonvention für häufig verwendete Einstellungen.

In unserer Terminalausgabe siehst du, dass wir -Dtarget=x86_64-windows -Doptimize=ReleaseSmall übergeben haben. Im Vergleich zum ersten Beispiel sehen wir jetzt andere Dateien im Installationspräfix:

zig-out/
└── bin
    └── hello.exe

Optionen zur bedingten Kompilierung

Um Optionen aus dem Build-Skript in den Zig-Code des Projekts zu übertragen, verwenden wir den Schritt Options.

app.zig
const std = @import("std");
const config = @import("config");

const semver = std.SemanticVersion.parse(config.version) catch unreachable;

extern fn foo_bar() void;

pub fn main() !void {
    if (semver.major < 1) {
        @compileError("too old");
    }
    std.debug.print("version: {s}\n", .{config.version});

    if (config.have_libfoo) {
        foo_bar();
    }
}
build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "app",
        .root_source_file = b.path("app.zig"),
        .target = b.host,
    });

    const version = b.option([]const u8, "version", "application version string") orelse "0.0.0";
    const enable_foo = detectWhetherToEnableLibFoo();

    const options = b.addOptions();
    options.addOption([]const u8, "version", version);
    options.addOption(bool, "have_libfoo", enable_foo);

    exe.root_module.addOptions("config", options);

    b.installArtifact(exe);
}

fn detectWhetherToEnableLibFoo() bool {
    return false;
}
Shell
$ zig build-Dversion=1.2.3 --summary all

Build Summary: 4/4 steps succeeded
install cached
└─ install app cached
   └─ zig build-exe app Debug native cached 33ms MaxRSS:36M
      └─ options cached

In diesem Beispiel sind die Daten, die von @import("config") bereitgestellt werden zur Kompilierzeit bekannt (comptime-known), was verhindert, dass der @compileError ausgelöst wird. Wenn wir -Dversion=0.2.3 übergeben hätten, oder die Option weggelassen, dann wäre die Kompilierung von 'app.zig' mit dem Fehler "zu alt" fehlschlagen.

Statische Bibliothek

Dieses Build-Skript erstellt eine statische Bibliothek aus Zig-Code und dann auch eine ausführbare Datei aus anderem Zig-Code, der diese Bibliothek verwendet.

fizzbuzz.zig
export fn fizzbuzz(n: usize) ?[*:0]const u8 {
    if (n % 5 == 0) {
        if (n % 3 == 0) {
            return "fizzbuzz";
        } else {
            return "fizz";
        }
    } else if (n % 3 == 0) {
        return "buzz";
    }
    return null;
}
demo.zig
const std = @import("std");

extern fn fizzbuzz(n: usize) ?[*:0]const u8;

pub fn main() !void {
    const stdout = std.io.getStdOut();
    var bw = std.io.bufferedWriter(stdout.writer());
    const w = bw.writer();
    for (0..100) |n| {
        if (fizzbuzz(n)) |s| {
            try w.print("{s}\n", .{s});
        } else {
            try w.print("{d}\n", .{n});
        }
    }
    try bw.flush();
}
build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const libfizzbuzz = b.addStaticLibrary(.{
        .name = "fizzbuzz",
        .root_source_file = b.path("fizzbuzz.zig"),
        .target = target,
        .optimize = optimize,
    });

    const exe = b.addExecutable(.{
        .name = "demo",
        .root_source_file = b.path("demo.zig"),
        .target = target,
        .optimize = optimize,
    });

    exe.linkLibrary(libfizzbuzz);

    b.installArtifact(libfizzbuzz);

    if (b.option(bool, "enable-demo", "install the demo too") orelse false) {
        b.installArtifact(exe);
    }
}
Shell
$ zig build--summary all

Build Summary: 3/3 steps succeeded
install cached
└─ install fizzbuzz cached
   └─ zig build-lib fizzbuzz Debug native cached 56ms MaxRSS:36M

In diesem Fall wird am Ende nur die statische Bibliothek installiert:

zig-out/
└── lib
    └── libfizzbuzz.a

Wenn du jedoch genau hinsiehst, enthält das Build-Skript eine Option, um auch die Demo zu installieren. Wenn wir zusätzlich -Denable-demo übergeben, dann sehen wir dies im Installationspräfix:

zig-out/
├── bin
│   └── demo
└── lib
    └── libfizzbuzz.a

Beachte bitte, dass trotz des bedingungslosen Aufrufs von addExecutable, das Build-System in der Tat keine Zeit damit verschwendet, die ausführbare Datei demo zu bauen, es sei denn, dass wird mit mit -Denable-demo angefordert, denn das Build-System basiert auf einem gerichteten azyklischen Graph mit Abhängigkeits-Kanten.

Dynamische Bibliothek

Hier behalten wir alle Dateien aus dem Staische Bibliothek-Beispiel bei, außer dass die Datei build.zig geändert wird.

build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const libfizzbuzz = b.addSharedLibrary(.{
        .name = "fizzbuzz",
        .root_source_file = b.path("fizzbuzz.zig"),
        .target = target,
        .optimize = optimize,
        .version = .{ .major = 1, .minor = 2, .patch = 3 },
    });

    b.installArtifact(libfizzbuzz);
}
Shell
$ zig build--summary all

Build Summary: 3/3 steps succeeded
install cached
└─ install fizzbuzz cached
   └─ zig build-lib fizzbuzz Debug native cached 33ms MaxRSS:37M

Ausgabe

zig-out
└── lib
    ├── libfizzbuzz.so -> libfizzbuzz.so.1
    ├── libfizzbuzz.so.1 -> libfizzbuzz.so.1.2.3
    └── libfizzbuzz.so.1.2.3

Wie im Beispiel der statischen Bibliothek zu sehen, um eine ausführbare Datei mit ihr zu verknüpfen, verwendest du den folgenden Code:

exe.linkLibrary(libfizzbuzz);

Testen

Einzelne Dateien können direkt mit zig test foo.zig getestet werden, aber komplexere Anwendungsfälle können durch die Orchestrierung von Tests über das Build-Skript gelöst werden.

Wenn du das Build-Skript verwendest, werden die Unit-Tests in zwei verschiedene Schritte unterteilt, den Schritt Compile und den Schritt Run. Ohne einen Aufruf von addRunArtifact, der eine Abhängigkeit zwischen diesen beiden Schritten herstellt, werden die Unit-Tests nicht ausgeführt.

Der Kompilierschritt kann genauso konfiguriert werden wie jede ausführbare Datei, Bibliothek oder Objektdatei, zum Beispiel durch Linken gegen Systembibliotheken, Zieloptionen festlegen oder zusätzliche Kompiliereinheiten hinzufügen.

Der Ausführungsschritt kann wie jeder andere Ausführungsschritt konfiguriert werden, zum Beispiel durch Überspringen der Ausführung, wenn der Host nicht in der Lage ist, die Binärdatei auszuführen.

Wenn du das Build-System zur Ausführung von Unit-Tests verwendest, kommunizieren der Build-Runner und der Test-Runner über 'stdin' und 'stdout', um mehrere Unit-Testsuiten gleichzeitig auszuführen, und Testfehler auf sinnvolle Weise zu melden, ohne dass ihre Ausgaben durcheinander geworfen werden. Das ist ein Grund für Schreiben nach 'stdout' ist in Unit-Tests problematisch - es wird diese Kommunikationskanäle stören. Auf der anderen Seite, wird dieser Mechanismus eine zukünftige Funktion ermöglichen, nämlich die Fähigkeit eines Unit-Tests, eine Panik zu erwarten.

main.zig
const std = @import("std");

test "simple test" {
    var list = std.ArrayList(i32).init(std.testing.allocator);
    defer list.deinit();
    try list.append(42);
    try std.testing.expectEqual(@as(i32, 42), list.pop());
}
build.zig
const std = @import("std");

const test_targets = [_]std.Target.Query{
    .{}, // native
    .{
        .cpu_arch = .x86_64,
        .os_tag = .linux,
    },
    .{
        .cpu_arch = .aarch64,
        .os_tag = .macos,
    },
};

pub fn build(b: *std.Build) void {
    const test_step = b.step("test", "Run unit tests");

    for (test_targets) |target| {
        const unit_tests = b.addTest(.{
            .root_source_file = b.path("main.zig"),
            .target = b.resolveTargetQuery(target),
        });

        const run_unit_tests = b.addRunArtifact(unit_tests);
        test_step.dependOn(&run_unit_tests.step);
    }
}
Shell
$ zig buildtest --summary all
test
└─ run test failure
error: the host system (x86_64-linux.5.10...5.10-gnu.2.31) is unable to execute binaries from the target (aarch64-macos.11.7.1...14.1-none)
Build Summary: 5/7 steps succeeded; 1 failed
test transitive failure
├─ run test cached
│  └─ zig test Debug native cached 49ms MaxRSS:36M
├─ run test cached
│  └─ zig test Debug x86_64-linux cached 61ms MaxRSS:36M
└─ run test failure
   └─ zig test Debug aarch64-macos cached 38ms MaxRSS:36M
error: the following build command failed with exit code 1:
/home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/build-system/unit-testing/.zig-cache/o/25475ace5ef269b60e98416da65a5cde/build /home/ci/deps/zig-linux-x86_64-0.13.0/zig /home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/build-system/unit-testing /home/ci/actions-runner-website/_work/www.ziglang.org/www.ziglang.org/assets/zig-code/build-system/unit-testing/.zig-cache /home/ci/.cache/zig --seed 0x10a7accf -Zd5915bdf16628db4 --color on test --summary all

In diesem Fall könnte es eine nette Anpassung sein, skip_foreign_checks in Unit-Tests zu aktivieren:

@@ -23,6 +23,7 @@
         });

         const run_unit_tests = b.addRunArtifact(unit_tests);
+        run_unit_tests.skip_foreign_checks = true;
         test_step.dependOn(&run_unit_tests.step);
     }
 }

build.zig
const std = @import("std");

const test_targets = [_]std.Target.Query{
    .{}, // native
    .{
        .cpu_arch = .x86_64,
        .os_tag = .linux,
    },
    .{
        .cpu_arch = .aarch64,
        .os_tag = .macos,
    },
};

pub fn build(b: *std.Build) void {
    const test_step = b.step("test", "Run unit tests");

    for (test_targets) |target| {
        const unit_tests = b.addTest(.{
            .root_source_file = b.path("main.zig"),
            .target = b.resolveTargetQuery(target),
        });

        const run_unit_tests = b.addRunArtifact(unit_tests);
        run_unit_tests.skip_foreign_checks = true;
        test_step.dependOn(&run_unit_tests.step);
    }
}

// zig-doctest: build-system --collapseable -- test --summary all
Shell
$ zig build--summary all

Build Summary: 1/1 steps succeeded
install cached

System-Bibliotheken verlinken

Für die Erfüllung von Bibliotheksabhängigkeiten gibt es zwei Möglichkeiten:

  1. Stelle die Bibliotheken über das Zig Build System bereit (siehe Paketverwaltung und Statische Bibliothek).
  2. Verwende die vom Hostsystem bereitgestellten Dateien.

Für den Anwendungsfall von Upstream-Projektbetreuern bietet die Beschaffung dieser Bibliotheken über das Zig Build System die geringsten Reibungsverluste und legt die Konfigurationshoheit in die Hände der Betreuer. Jeder der auf diese Weise baut, erhält reproduzierbare und konsistente Ergebnisse. Es wird auf jedem Betriebssystem funktionieren und unterstützt sogar Cross-Compilation. Außerdem erlaubt es dem Projekt mit perfekter Präzision zu entscheiden, welche genauen Versionen des gesamten Abhängigkeitsbaums verwendet werden, gegen den es bauen möchte. Es wird erwartet, dass dies der allgemein bevorzugte Weg sein wird, um externen Bibliotheken einzubinden.

Für den Anwendungsfall der Paketierung von Software in Repositories wie Debian, Homebrew oder Nix, ist es zwingend erforderlich Systembibliotheken zu verlinken. Das heißt, Build-Skripte müssen den Modus erkennen und entsprechend konfigurieren.

build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "zip",
        .root_source_file = b.path("zip.zig"),
        .target = b.host,
    });

    exe.linkSystemLibrary("z");
    exe.linkLibC();

    b.installArtifact(exe);
}
Shell
$ zig build--summary all

Build Summary: 3/3 steps succeeded
install cached
└─ install zip cached
   └─ zig build-exe zip Debug native cached 95ms MaxRSS:37M

Benutzer von zig build können --search-prefix verwenden, um zusätzliche Verzeichnisse anzugeben, die als "Systemverzeichnisse" für die Suche nach statischen und dynamischen Bibliotheken mit einbezogen werden.

Dateien erzeugen

Systemwerkzeuge ausführen

Diese Version von "hello world" erwartet, dass eine word.txt-Datei im gleichen Pfad zu finden ist, und wir wollen ein Systemwerkzeug verwenden, um sie aus einer JSON-Datei zu erzeugen.

Sei dir bitte bewusst, dass Systemabhängigkeiten die Erstellung deines Projekts für Benutzer erschweren. Dieses Build-Skript hängt zum Beispiel von jq ab, das in den meisten Linux-Distributionen nicht standardmäßig vorhanden ist und für Windows-Benutzer unbekannt sein kann.

Im nächsten Abschnitt wird jq durch ein im Quellbaum enthaltenes Zig-Werkzeug ersetzt, was der bevorzugte Ansatz ist.

words.json

{
  "en": "world",
  "it": "mondo",
  "ja": "世界"
}

main.zig
const std = @import("std");

pub fn main() !void {
    var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    const self_exe_dir_path = try std.fs.selfExeDirPathAlloc(arena);
    var self_exe_dir = try std.fs.cwd().openDir(self_exe_dir_path, .{});
    defer self_exe_dir.close();

    const word = try self_exe_dir.readFileAlloc(arena, "word.txt", 1000);

    try std.io.getStdOut().writer().print("Hello {s}\n", .{word});
}
build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const lang = b.option([]const u8, "language", "language of the greeting") orelse "en";
    const tool_run = b.addSystemCommand(&.{"jq"});
    tool_run.addArgs(&.{
        b.fmt(
            \\.["{s}"]
        , .{lang}),
        "-r", // raw output to omit quotes around the selected string
    });
    tool_run.addFileArg(b.path("words.json"));

    const output = tool_run.captureStdOut();

    b.getInstallStep().dependOn(&b.addInstallFileWithDir(output, .prefix, "word.txt").step);

    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    const install_artifact = b.addInstallArtifact(exe, .{
        .dest_dir = .{ .override = .prefix },
    });
    b.getInstallStep().dependOn(&install_artifact.step);
}
Shell
$ zig build-Dlanguage=ja --summary all

Build Summary: 5/5 steps succeeded
install cached
├─ install generated to word.txt cached
│  └─ run jq cached
└─ install hello cached
   └─ zig build-exe hello Debug native cached 48ms MaxRSS:36M

Ausgabe

zig-out
├── hello
└── word.txt

Beachte bitte, wie captureStdOut eine temporäre Datei mit der Ausgabe des jq-Aufrufs erstellt.

Projektwerkzeuge ausführen

Diese Version von "hello world" erwartet, dass eine word.txt-Datei im gleichen Pfad zu finden ist, und wir wollen sie zur Erstellungszeit erzeugen, indem wir ein Zig-Programm für die JSON-Datei aufrufen.

tools/words.json

{
  "en": "world",
  "it": "mondo",
  "ja": "世界"
}

main.zig
const std = @import("std");

pub fn main() !void {
    var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    const self_exe_dir_path = try std.fs.selfExeDirPathAlloc(arena);
    var self_exe_dir = try std.fs.cwd().openDir(self_exe_dir_path, .{});
    defer self_exe_dir.close();

    const word = try self_exe_dir.readFileAlloc(arena, "word.txt", 1000);

    try std.io.getStdOut().writer().print("Hello {s}\n", .{word});
}

word_select.zig
const std = @import("std");

const usage =
    \\Usage: ./word_select [options]
    \\
    \\Options:
    \\  --input-file INPUT_JSON_FILE
    \\  --output-file OUTPUT_TXT_FILE
    \\  --lang LANG
    \\
;

pub fn main() !void {
    var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    const args = try std.process.argsAlloc(arena);

    var opt_input_file_path: ?[]const u8 = null;
    var opt_output_file_path: ?[]const u8 = null;
    var opt_lang: ?[]const u8 = null;

    {
        var i: usize = 1;
        while (i < args.len) : (i += 1) {
            const arg = args[i];
            if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
                try std.io.getStdOut().writeAll(usage);
                return std.process.cleanExit();
            } else if (std.mem.eql(u8, "--input-file", arg)) {
                i += 1;
                if (i > args.len) fatal("expected arg after '{s}'", .{arg});
                if (opt_input_file_path != null) fatal("duplicated {s} argument", .{arg});
                opt_input_file_path = args[i];
            } else if (std.mem.eql(u8, "--output-file", arg)) {
                i += 1;
                if (i > args.len) fatal("expected arg after '{s}'", .{arg});
                if (opt_output_file_path != null) fatal("duplicated {s} argument", .{arg});
                opt_output_file_path = args[i];
            } else if (std.mem.eql(u8, "--lang", arg)) {
                i += 1;
                if (i > args.len) fatal("expected arg after '{s}'", .{arg});
                if (opt_lang != null) fatal("duplicated {s} argument", .{arg});
                opt_lang = args[i];
            } else {
                fatal("unrecognized arg: '{s}'", .{arg});
            }
        }
    }

    const input_file_path = opt_input_file_path orelse fatal("missing --input-file", .{});
    const output_file_path = opt_output_file_path orelse fatal("missing --output-file", .{});
    const lang = opt_lang orelse fatal("missing --lang", .{});

    var input_file = std.fs.cwd().openFile(input_file_path, .{}) catch |err| {
        fatal("unable to open '{s}': {s}", .{ input_file_path, @errorName(err) });
    };
    defer input_file.close();

    var output_file = std.fs.cwd().createFile(output_file_path, .{}) catch |err| {
        fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
    };
    defer output_file.close();

    var json_reader = std.json.reader(arena, input_file.reader());
    var words = try std.json.ArrayHashMap([]const u8).jsonParse(arena, &json_reader, .{
        .allocate = .alloc_if_needed,
        .max_value_len = 1000,
    });

    const w = words.map.get(lang) orelse fatal("Lang not found in JSON file", .{});

    try output_file.writeAll(w);
    return std.process.cleanExit();
}

fn fatal(comptime format: []const u8, args: anytype) noreturn {
    std.debug.print(format, args);
    std.process.exit(1);
}

build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const lang = b.option([]const u8, "language", "language of the greeting") orelse "en";
    const tool = b.addExecutable(.{
        .name = "word_select",
        .root_source_file = b.path("tools/word_select.zig"),
        .target = b.host,
    });

    const tool_step = b.addRunArtifact(tool);
    tool_step.addArg("--input-file");
    tool_step.addFileArg(b.path("tools/words.json"));
    tool_step.addArg("--output-file");
    const output = tool_step.addOutputFileArg("word.txt");
    tool_step.addArgs(&.{ "--lang", lang });

    b.getInstallStep().dependOn(&b.addInstallFileWithDir(output, .prefix, "word.txt").step);

    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    const install_artifact = b.addInstallArtifact(exe, .{
        .dest_dir = .{ .override = .prefix },
    });
    b.getInstallStep().dependOn(&install_artifact.step);
}
Shell
$ zig build--summary all

Build Summary: 6/6 steps succeeded
install cached
├─ install generated to word.txt cached
│  └─ run word_select (word.txt) cached
│     └─ zig build-exe word_select Debug native cached 43ms MaxRSS:36M
└─ install hello cached
   └─ zig build-exe hello Debug native cached 83ms MaxRSS:36M

Ausgabe

zig-out
├── hello
└── word.txt

Erzeugen von Assets für @embedFile

Diese Version von "hello world" erwartet ein zur Build-Zeit erzeugtes Asset für @embededFile, das wir mit einem in Zig geschriebenen Werkzeug erzeugen werden.

tools/words.json

{
  "en": "world",
  "it": "mondo",
  "ja": "世界"
}

main.zig
const std = @import("std");
const word = @embedFile("word");

pub fn main() !void {
    try std.io.getStdOut().writer().print("Hello {s}\n", .{word});
}

word_select.zig
const std = @import("std");

const usage =
    \\Usage: ./word_select [options]
    \\
    \\Options:
    \\  --input-file INPUT_JSON_FILE
    \\  --output-file OUTPUT_TXT_FILE
    \\  --lang LANG
    \\
;

pub fn main() !void {
    var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    const args = try std.process.argsAlloc(arena);

    var opt_input_file_path: ?[]const u8 = null;
    var opt_output_file_path: ?[]const u8 = null;
    var opt_lang: ?[]const u8 = null;

    {
        var i: usize = 1;
        while (i < args.len) : (i += 1) {
            const arg = args[i];
            if (std.mem.eql(u8, "-h", arg) or std.mem.eql(u8, "--help", arg)) {
                try std.io.getStdOut().writeAll(usage);
                return std.process.cleanExit();
            } else if (std.mem.eql(u8, "--input-file", arg)) {
                i += 1;
                if (i > args.len) fatal("expected arg after '{s}'", .{arg});
                if (opt_input_file_path != null) fatal("duplicated {s} argument", .{arg});
                opt_input_file_path = args[i];
            } else if (std.mem.eql(u8, "--output-file", arg)) {
                i += 1;
                if (i > args.len) fatal("expected arg after '{s}'", .{arg});
                if (opt_output_file_path != null) fatal("duplicated {s} argument", .{arg});
                opt_output_file_path = args[i];
            } else if (std.mem.eql(u8, "--lang", arg)) {
                i += 1;
                if (i > args.len) fatal("expected arg after '{s}'", .{arg});
                if (opt_lang != null) fatal("duplicated {s} argument", .{arg});
                opt_lang = args[i];
            } else {
                fatal("unrecognized arg: '{s}'", .{arg});
            }
        }
    }

    const input_file_path = opt_input_file_path orelse fatal("missing --input-file", .{});
    const output_file_path = opt_output_file_path orelse fatal("missing --output-file", .{});
    const lang = opt_lang orelse fatal("missing --lang", .{});

    var input_file = std.fs.cwd().openFile(input_file_path, .{}) catch |err| {
        fatal("unable to open '{s}': {s}", .{ input_file_path, @errorName(err) });
    };
    defer input_file.close();

    var output_file = std.fs.cwd().createFile(output_file_path, .{}) catch |err| {
        fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
    };
    defer output_file.close();

    var json_reader = std.json.reader(arena, input_file.reader());
    var words = try std.json.ArrayHashMap([]const u8).jsonParse(arena, &json_reader, .{
        .allocate = .alloc_if_needed,
        .max_value_len = 1000,
    });

    const w = words.map.get(lang) orelse fatal("Lang not found in JSON file", .{});

    try output_file.writeAll(w);
    return std.process.cleanExit();
}

fn fatal(comptime format: []const u8, args: anytype) noreturn {
    std.debug.print(format, args);
    std.process.exit(1);
}

build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const lang = b.option([]const u8, "language", "language of the greeting") orelse "en";
    const tool = b.addExecutable(.{
        .name = "word_select",
        .root_source_file = b.path("tools/word_select.zig"),
        .target = b.host,
    });

    const tool_step = b.addRunArtifact(tool);
    tool_step.addArg("--input-file");
    tool_step.addFileArg(b.path("tools/words.json"));
    tool_step.addArg("--output-file");
    const output = tool_step.addOutputFileArg("word.txt");
    tool_step.addArgs(&.{ "--lang", lang });

    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    exe.root_module.addAnonymousImport("word", .{
        .root_source_file = output,
    });

    b.installArtifact(exe);
}
Shell
$ zig build--summary all

Build Summary: 5/5 steps succeeded
install cached
└─ install hello cached
   └─ zig build-exe hello Debug native cached 51ms MaxRSS:36M
      └─ run word_select (word.txt) cached
         └─ zig build-exe word_select Debug native cached 88ms MaxRSS:36M

Ausgabe

zig-out/
└── bin
    └── hello

Erzeugen von Zig-Quellcode

Diese Build-Datei verwendet ein Zig-Programm, um eine Zig-Datei zu erzeugen, welches sie dann dem Hauptprogramm als eine Modulabhängigkeit zur Verfügung stellt.

main.zig
const std = @import("std");
const Person = @import("person").Person;

pub fn main() !void {
    const p: Person = .{};
    try std.io.getStdOut().writer().print("Hello {any}\n", .{p});
}
generate_struct.zig
const std = @import("std");

pub fn main() !void {
    var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    const args = try std.process.argsAlloc(arena);

    if (args.len != 2) fatal("wrong number of arguments", .{});

    const output_file_path = args[1];

    var output_file = std.fs.cwd().createFile(output_file_path, .{}) catch |err| {
        fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
    };
    defer output_file.close();

    try output_file.writeAll(
        \\pub const Person = struct {
        \\   age: usize = 18,
        \\   name: []const u8 = "foo"        
        \\};
    );
    return std.process.cleanExit();
}

fn fatal(comptime format: []const u8, args: anytype) noreturn {
    std.debug.print(format, args);
    std.process.exit(1);
}
build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const tool = b.addExecutable(.{
        .name = "generate_struct",
        .root_source_file = b.path("tools/generate_struct.zig"),
        .target = b.host,
    });

    const tool_step = b.addRunArtifact(tool);
    const output = tool_step.addOutputFileArg("person.zig");

    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    exe.root_module.addAnonymousImport("person", .{
        .root_source_file = output,
    });

    b.installArtifact(exe);
}
Shell
$ zig build--summary all

Build Summary: 5/5 steps succeeded
install cached
└─ install hello cached
   └─ zig build-exe hello Debug native cached 39ms MaxRSS:36M
      └─ run generate_struct (person.zig) cached
         └─ zig build-exe generate_struct Debug native cached 53ms MaxRSS:36M

Ausgabe

zig-out/
└── bin
    └── hello

Umgang mit einer oder mehreren erzeugten Dateien

Der Schritt WriteFiles bietet eine Möglichkeit, eine oder mehrere Dateien zu erzeugen, die ein übergeordnetes Verzeichnis teilen. Das erzeugte Verzeichnis befindet sich innerhalb des lokalen .zig-cache, und jede erzeugte Datei ist unabhängig als std.Build.LazyPath verfügbar. Das übergeordnete Verzeichnis selbst ist ebenfalls als 'LazyPath' verfügbar.

Diese API unterstützt das Schreiben beliebiger Zeichenketten in das erzeugte Verzeichnis sowie das Kopieren von Dateien in das Verzeichnis.

main.zig
const std = @import("std");

pub fn main() !void {
    std.debug.print("hello world\n", .{});
}
build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "app",
        .root_source_file = b.path("src/main.zig"),
        .target = b.host,
    });

    const version = b.option([]const u8, "version", "application version string") orelse "0.0.0";

    const wf = b.addWriteFiles();
    const app_exe_name = b.fmt("project/{s}", .{exe.out_filename});
    _ = wf.addCopyFile(exe.getEmittedBin(), app_exe_name);
    _ = wf.add("project/version.txt", version);

    const tar = b.addSystemCommand(&.{ "tar", "czf" });
    tar.setCwd(wf.getDirectory());
    const out_file = tar.addOutputFileArg("project.tar.gz");
    tar.addArgs(&.{"project/"});

    const install_tar = b.addInstallFileWithDir(out_file, .prefix, "project.tar.gz");
    b.getInstallStep().dependOn(&install_tar.step);
}
Shell
$ zig build--summary all

Build Summary: 5/5 steps succeeded
install cached
└─ install generated to project.tar.gz cached
   └─ run tar (project.tar.gz) cached
      └─ WriteFile project/app cached
         └─ zig build-exe app Debug native cached 68ms MaxRSS:36M

Ausgabe

zig-out/
└── project.tar.gz

Quelldateien verändern

Es ist unüblich, aber manchmal der Fall, dass ein Projekt die generierte Dateien in die Versionskontrolle überträgt. Das kann nützlich sein, wenn die generierten Dateien selten aktualisiert werden und für den Aktualisierungsprozess lästige Systemabhängigkeiten haben, die aber nur während des Aktualisierungsprozess auftreten.

Hierfür bietet WriteFiles eine Möglichkeit, diese Aufgabe zu erfüllen. Das ist eine Funktion, wie aus WriteFiles in einen eigenen Build-Step extrahiert wird in einer zukünftigen Zig-Version.

Sei aber vorsichtig mit dieser Funktion; sie sollte nicht während des normalen Build-Prozesses verwendet werden, sondern als Dienstprogramm, das von einem Entwickler mit der Absicht ausgeführt wird, die Quelldateien zu aktualisieren, die dann an die Versionskontrolle übergeben werden. Wenn dies während des normalen Build-Prozesses durchgeführt wird, führt das zu Caching- und Gleichzeitigkeitsfehlern.

proto_gen.zig
const std = @import("std");

pub fn main() !void {
    var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();

    const args = try std.process.argsAlloc(arena);

    if (args.len != 2) fatal("wrong number of arguments", .{});

    const output_file_path = args[1];

    var output_file = std.fs.cwd().createFile(output_file_path, .{}) catch |err| {
        fatal("unable to open '{s}': {s}", .{ output_file_path, @errorName(err) });
    };
    defer output_file.close();

    try output_file.writeAll(
        \\pub const Header = extern struct {
        \\    magic: u64,
        \\    width: u32,
        \\    height: u32,
        \\};
    );
    return std.process.cleanExit();
}

fn fatal(comptime format: []const u8, args: anytype) noreturn {
    std.debug.print(format, args);
    std.process.exit(1);
}
main.zig
const std = @import("std");
const Protocol = @import("protocol.zig");

pub fn main() !void {
    const header = try std.io.getStdIn().reader().readStruct(Protocol.Header);
    std.debug.print("header: {any}\n", .{header});
}
protocol.zig
pub const Header = extern struct {
    magic: u64,
    width: u32,
    height: u32,
};
build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "demo",
        .root_source_file = b.path("src/main.zig"),
    });
    b.installArtifact(exe);

    const proto_gen = b.addExecutable(.{
        .name = "proto_gen",
        .root_source_file = b.path("tools/proto_gen.zig"),
    });

    const run = b.addRunArtifact(proto_gen);
    const generated_protocol_file = run.addOutputFileArg("protocol.zig");

    const wf = b.addWriteFiles();
    wf.addCopyFileToSource(generated_protocol_file, "src/protocol.zig");

    const update_protocol_step = b.step("update-protocol", "update src/protocol.zig to latest");
    update_protocol_step.dependOn(&wf.step);
}

fn detectWhetherToEnableLibFoo() bool {
    return false;
}

$ zig build update-protocol --summary all
Build Summary: 4/4 steps succeeded
update-protocol success
└─ WriteFile success
   └─ run proto_gen (protocol.zig) success 401us MaxRSS:1M
      └─ zig build-exe proto_gen Debug native success 1s MaxRSS:183M

Nach der Ausführung dieses Befehls wird src/protocol.zig an Ort und Stelle aktualisiert.

Praktische Beispiele

Build für mehrere Ziele, um eine Freigabe zu erstellen

In diesem Beispiel werden wir einige Voreinstellungen beim Erstellen eines InstallArtifact-Schrittes ändern, um den Build für jedes Ziel in ein eigenes Unterverzeichnis innerhalb des Installationspfades zu legen.

build.zig
const std = @import("std");

const targets: []const std.Target.Query = &.{
    .{ .cpu_arch = .aarch64, .os_tag = .macos },
    .{ .cpu_arch = .aarch64, .os_tag = .linux },
    .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu },
    .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl },
    .{ .cpu_arch = .x86_64, .os_tag = .windows },
};

pub fn build(b: *std.Build) !void {
    for (targets) |t| {
        const exe = b.addExecutable(.{
            .name = "hello",
            .root_source_file = b.path("hello.zig"),
            .target = b.resolveTargetQuery(t),
            .optimize = .ReleaseSafe,
        });

        const target_output = b.addInstallArtifact(exe, .{
            .dest_dir = .{
                .override = .{
                    .custom = try t.zigTriple(b.allocator),
                },
            },
        });

        b.getInstallStep().dependOn(&target_output.step);
    }
}
Shell
$ zig build--summary all

Build Summary: 11/11 steps succeeded
install cached
├─ install hello cached
│  └─ zig build-exe hello ReleaseSafe aarch64-macos cached 33ms MaxRSS:36M
├─ install hello cached
│  └─ zig build-exe hello ReleaseSafe aarch64-linux cached 98ms MaxRSS:36M
├─ install hello cached
│  └─ zig build-exe hello ReleaseSafe x86_64-linux-gnu cached 37ms MaxRSS:33M
├─ install hello cached
│  └─ zig build-exe hello ReleaseSafe x86_64-linux-musl cached 43ms MaxRSS:36M
└─ install hello cached
   └─ zig build-exe hello ReleaseSafe x86_64-windows cached 45ms MaxRSS:36M
hello.zig
const std = @import("std");

pub fn main() !void {
    std.debug.print("Hello World!\n", .{});
}

Ausgabe

zig-out
├── aarch64-linux
│   └── hello
├── aarch64-macos
│   └── hello
├── x86_64-linux-gnu
│   └── hello
├── x86_64-linux-musl
│   └── hello
└── x86_64-windows
    ├── hello.exe
    └── hello.pdb