Zig Build-System
- Wann sollte man das Zig-Build-System einsetzen?
- Erste Schritte
- Grundlagen
- Dateien erzeugen
- Praktische Beispiele
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:
- Die Kommandozeile wird zu lang und unhandlich, und du möchtest sie aufschreiben.
- Du willst viele Dinge bauen, oder der Build-Prozess umfasst viele Schritte.
- Du möchten die Vorteile der Parallelverarbeitung und des Cachings nutzen, um die Erstellungszeit zu verkürzen.
- Du möchtest Konfigurationsoptionen für das Projekt freigeben.
- Der Build-Prozess ist je nach Zielsystem und anderen Optionen unterschiedlich.
- Du hast Abhängigkeiten zu anderen Projekten.
- Du möchtest unnötige Abhängigkeiten zu 'cmake', 'make', 'shell', 'msvc', 'python', etc. vermeiden und/oder das Projekt mehr Mitwirkenden zugänglich machen.
- Du möchtest ein Paket anbieten, das auch von Dritten genutzt werden kann.
- Du möchtest einen standardisierten Weg für Werkzeuge wie IDEs bieten, damit diese semantisch verstehen, wie das Projekt zu bauen ist.
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.
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello World!\n", .{});
}
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);
}
$ 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
).
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello World!\n", .{});
}
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);
}
$ 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.
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);
}
$ zig build--help
Usage: /home/ci/deps/zig-linux-x86_64-0.13.0/zig build [steps] [options]
Steps:
install (default) Copy build artifacts to prefix path
uninstall Remove build artifacts from prefix path
General Options:
-p, --prefix [path] Where to install files (default: zig-out)
--prefix-lib-dir [path] Where to install libraries
--prefix-exe-dir [path] Where to install executables
--prefix-include-dir [path] Where to install C header files
--release[=mode] Request release mode, optionally specifying a
preferred optimization mode: fast, safe, small
-fdarling, -fno-darling Integration with system-installed Darling to
execute macOS programs on Linux hosts
(default: no)
-fqemu, -fno-qemu Integration with system-installed QEMU to execute
foreign-architecture programs on Linux hosts
(default: no)
--glibc-runtimes [path] Enhances QEMU integration by providing glibc built
for multiple foreign architectures, allowing
execution of non-native programs that link with glibc.
-frosetta, -fno-rosetta Rely on Rosetta to execute x86_64 programs on
ARM64 macOS hosts. (default: no)
-fwasmtime, -fno-wasmtime Integration with system-installed wasmtime to
execute WASI binaries. (default: no)
-fwine, -fno-wine Integration with system-installed Wine to execute
Windows programs on Linux hosts. (default: no)
-h, --help Print this help and exit
-l, --list-steps Print available steps
--verbose Print commands before executing them
--color [auto|off|on] Enable or disable colored error messages
--prominent-compile-errors Buffer compile errors and display at end
--summary [mode] Control the printing of the build summary
all Print the build summary in its entirety
new Omit cached steps
failures (Default) Only print failed steps
none Do not print the build summary
-j<N> Limit concurrent jobs (default is to use all CPU cores)
--maxrss <bytes> Limit memory usage (default is to use available memory)
--skip-oom-steps Instead of failing, skip steps that would exceed --maxrss
--fetch Exit after fetching dependency tree
Project-Specific Options:
-Dwindows=[bool] Target Microsoft Windows
System Integration Options:
--search-prefix [path] Add a path to look for binaries, libraries, headers
--sysroot [path] Set the system root directory (usually /)
--libc [file] Provide a file which specifies libc paths
--system [pkgdir] Disable package fetching; enable all integrations
-fsys=[name] Enable a system integration
-fno-sys=[name] Disable a system integration
Available System Integrations: Enabled:
(none) -
Advanced Options:
-freference-trace[=num] How many lines of reference trace should be shown per compile error
-fno-reference-trace Disable reference trace
--build-file [file] Override path to build.zig
--cache-dir [path] Override path to local Zig cache directory
--global-cache-dir [path] Override path to global Zig cache directory
--zig-lib-dir [arg] Override path to Zig lib directory
--build-runner [file] Override path to build runner
--seed [integer] For shuffling dependency traversal order (default: random)
--debug-log [scope] Enable debugging the compiler
--debug-pkg-config Fail if unknown pkg-config flags encountered
--verbose-link Enable compiler debug output for linking
--verbose-air Enable compiler debug output for Zig AIR
--verbose-llvm-ir[=file] Enable compiler debug output for LLVM IR
--verbose-llvm-bc=[file] Enable compiler debug output for LLVM BC
--verbose-cimport Enable compiler debug output for C imports
--verbose-cc Enable compiler debug output for C compilation
--verbose-llvm-cpu-features Enable compiler debug output for LLVM CPU features
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.
const std = @import("std");
pub fn main() !void {
std.debug.print("Hello World!\n", .{});
}
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);
}
$ 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
.
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();
}
}
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;
}
$ 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.
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;
}
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();
}
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);
}
}
$ 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.
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);
}
$ 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.
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());
}
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);
}
}
$ 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);
}
}
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
$ 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:
- Stelle die Bibliotheken über das Zig Build System bereit (siehe Paketverwaltung und Statische Bibliothek).
- 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.
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);
}
$ 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": "世界"
}
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});
}
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);
}
$ 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": "世界"
}
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});
}
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);
}
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);
}
$ 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": "世界"
}
const std = @import("std");
const word = @embedFile("word");
pub fn main() !void {
try std.io.getStdOut().writer().print("Hello {s}\n", .{word});
}
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);
}
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);
}
$ 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.
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});
}
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);
}
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);
}
$ 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.
const std = @import("std");
pub fn main() !void {
std.debug.print("hello world\n", .{});
}
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);
}
$ 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.
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);
}
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});
}
pub const Header = extern struct {
magic: u64,
width: u32,
height: u32,
};
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.
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);
}
}
$ 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
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