← Torna a Impara

Zig Build System

Quando introdurre il Zig Build System?

In molti casi sono sufficienti i comandi base zig build-exe, zig build-lib, zig build-obj e zig test. A volte però un progetto necessita di un ulteriore livello di astrazione per gestire la complessità del compilare da sorgente.

Per esempio, potrebbe verificarsi una di queste situazioni:

Se qualunque di queste è applicabile, il progetto trarrà beneficio dal Zig Build System.

Per iniziare

Eseguibile semplice

Questo build script crea un eseguibile a partire da un file Zig contenente una funzione main pubblica.

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

Installare artefatti di build

Il build system di Zig, come la maggior parte dei build system, è basato sul modellare il progetto come un grafo aciclico diretto (DAG) di "step", che vengono eseguiti in parallelo.

Di default, lo step principale nel grafo è lo step Install, il cui scopo è copiare gli artefatti di build nella loro destinazione finale. Lo step di installazione parte senza dipendenze, quindi eseguendo zig build non accade nulla. Il build script di un progetto deve aggiungere elementi all'insieme delle cose da installare, ed è ciò che fa la chiamata alla funzione installArtifact qui sopra.

Output

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

In questo output sono presenti due cartelle generate: .zig-cache and zig-out. La prima contiene file che renderanno le compilazioni successive più rapide, ma questi file non sono pensati per essere tracciati in un sistema di controllo versione; questa cartella può essere completamente eliminata in qualunque momento senza conseguenze.

La seconda cartella generata, zig-out, è un "prefisso di installazione". Questo è correlato al concetto standard di gerarchia del file system. Questa cartella non è scelta dal progetto, ma dall'utente del comando zig build con la flag --prefix (-p per abbreviare).

Tu, come responsabile del progetto, scegli cosa conterrà questa cartella, ma l'utente sceglie dove installarla nel proprio sistema. Il build script non può definire percorsi di output assoluti perchè questo ostacolerebbe il caching, concorrenza e composibilità, oltre a infastidire l'utente finale.

Aggiungere uno step per eseguire l'applicazione

È pratica comune aggiungere uno step Run, per permettere di eseguire l'applicazione principale direttamente dalla riga di comando.

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

Le basi

Opzioni fornite dall'utente

Usa b.option per rendere il build script configurabile dagli utenti finali e anche da altri progetti che dipendono dal tuo progetto come pacchetto.

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", .{});
}

Nota la presenza di queste righe:

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

Questa parte del menù di aiuto è generata automaticamente in base alla logica in build.zig. Gli utenti possono scoprire le opzioni di configurazione del build script in questo modo.

Opzioni di configurazione standard

Nella sezione precedente abbiamo usato una flag booleana per indicare una compilazione per Windows. Possiamo fare ancora di meglio.

La maggior parte dei progetti vuole rendere possibile modificare la piattaforma di destinazione e le opzioni di ottimizzazione. Per incoraggiare una convenzione standard nella nomenclatura di queste opzioni, Zig fornisce le funzioni standardTargetOptions e standardOptimizeOption.

Le opzioni di destinazione standard permettono a chi esegue zig build di scegliere la piattaforma per cui compilare. Di default è permessa qualunque piattaforma, escluderle tutte significa compilare per il sistema host (quello usato per la compilazione). Sono disponibili ulteriori opzioni per restringere l'insieme di piattaforme supportate.

Le opzioni di ottimizzazione standard permettono a chi esegue zig build di scegliere tra Debug, ReleaseSafe, ReleaseFast e ReleaseSmall. Di default il build script non considera nessuna delle opzioni di rilascio come preferibile, quindi l'utente deve prendere una decisione per poter creare una build di rilascio.

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

Ora il nostro menù --help contiene più elementi:

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

È del tutto possibile creare queste opzioni usando direttamente b.option, ma questa API fornisce una convenzione comune per i nomi di queste opzioni, che sono utilizzate di frequente.

Nell'output del nostro terminale, osserva che abbiamo passato -Dtarget=x86_64-windows -Doptimize=ReleaseSmall. In confronto al primo esempio, ora vediamo file diversi nel prefisso di installazione:

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

Opzioni per la compilazione condizionale

Per passare delle opzioni dal build script al codice Zig del progetto, usa lo step 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 questo esempio, i dati forniti da @import("config") sono noti in fase di compilazione, impedendo l'innesco di @compileError. Se avessimo passato -Dversion=0.2.3 oppure omesso l'opzione, avremmo visto la compilazione di app.zig fallire con l'errore "too old".

Libreria statica

Questo build script crea una libreria statica di codice Zig, e inoltre crea un eseguibile di altro codice Zig che usa la libreria.

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 questo caso, viene installata solo la libreria statica:

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

Però, se guardi attentamente, il build script contiene un'opzione per installare anche la demo. Se passiamo anche l'opzione -Denable-demo, allora dovremmo vedere questo nel prefisso di installazione:

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

Nota che nonostante la chiamata non condizionale a addExecutable, il build system non perde tempo compilando l'eseguibile demo a meno che sia richiesto con -Denable-demo, perchè il build system è basato su un grafo aciclico diretto (DAG) in cui gli archi rappresentano dipendenze.

Libreria dinamica

Qui usiamo gli stessi file dell'esempio sulla libreria statica, eccetto il file build.zig che ha delle modifiche.

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

Output

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

Come nell'esempio della libreria statica, per ottenere un link dell'eseguibile alla libreria, usa codice come questo:

exe.linkLibrary(libfizzbuzz);

Testing

I singoli file possono essere testati direttamente con zig test esempio.zig. Casi d'uso più complessi possono essere risolti orchestrando il testing mediante il build scriot.

Quando si usa il build script, gli unit test sono suddivisi in due step differenti nel grafo di compilazione, lo step Compile e lo step Run. Senza una chiamata a addRunArtifact, che definisce una dipendenza tra questi due step, gli unit test non verranno eseguiti.

Lo step Compile può essere configurato come qualunque eseguibile, libreria o file oggetto, per esempio mediante linking di librerie di sistema, impostando le opzioni di destinazione, o aggiungendo ulteriori unità di compilazione.

Lo step Run può essere configurato come qualunque altro step Run, per esempio si può evitare l'esecuzione quando il sistema host non può usare gli eseguibili.

Quando si utilizza il build system per eseguire gli unit test, il build runner e il test runner comunicano via stdin e stdout per eseguire più insiemi di test in parallelo, e riportare i fallimenti dei test in modo chiaro senza che i vari output siano mischiati tra loro. Questa è una delle ragioni per cui scrivere nello standard out nei test è problematico - interferisce con questo canale di comunicazione. D'altro canto, questo meccanismo permetterà di implementare una nuova funzionalità, ovvero un unit test può verificare che avvenga un panic.

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 questo caso potrebbe essere utile abilitare skip_foreign_checks per gli unit test:

@@ -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

Linking di librerie di sistema

Per dipendere da librerie in modo soddisfacente, ci sono due opzioni:

  1. Fornire le librerie mediante il Zig Build System (vedi Gestione pacchetti e Libreria statica).
  2. Usa i file forniti dal sistema host.

Per le necessità dei mantenitori di progetti upstream, ottenere queste librerie mediante il Zig Build System risulta in minori attriti e mette i poteri di configurazione in mano ai suddetti mantenitori. Chiunque compili in questo modo avrà risultati riproducibili, consistenti con ciò che ottengono gli altri, oltre a funzionare su ogni sistema operativo con persino supporto alla cross-compilazione. inoltre, questo permette al progetto di decidere con precisione chirurgica l'esatta versione dell'intero albero di dipendenze da usare per la compilazione. Questo è generalmente considerato il modo preferibile di dipendere da librerie esterne.

Invece, per le necessità di creare pacchetti software per repository come Debian, Homebrew o Nix, è obbligatorio il linking di librerie di sistema. Quindi, i build script devono rilevare la modalità di build e fare le opportune configurazioni.

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

Gli utenti di zig build possono usare --search-prefix per fornire ulteriori cartelle da considerare "directory di sistema" allo scopo di rilevare librerie statiche e dinamiche.

Generare file

Eseguire tool di sistema

Questa versione di "hello world" si aspetta di trovare un file word.txt in un certo percorso, e vogliamo usare un tool di sistema per generarlo a partire da un file JSON.

Tieni a mente che le dipendenze di sistema renderanno il tuo progetto più difficile da compilare per i tuoi utenti. Questo build script per esempio dipende da jq, che di default non è presente nella maggior parte delle distribuzioni Linux e potrebbe essere un tool poco familiare per gli utenti Windows.

La prossima sezione sostituirà jq con un tool scritto in Zig incluso nel codice sorgente del progetto, che è l'approccio preferibile.

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

Output

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

Nota come captureStdOut crea un file temporaneo con l'output dell'invocazione di jq.

Eseguire i tool del progetto

Questa versione di "hello world" si aspetta di trovare un file word.txt allo stesso percorso di prima, o vogliamo generarlo in fase di compilazione invocando un programma scritto in Zig per elaborare un file JSON.

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

Output

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

Produrre asset per @embedFile

Questa versione di "hello world" vuole usare @embedFile su un asset generato in fase di compilazione, che produrremo usando un tool scritto in Zig.

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

Output

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

Generare codice Zig

Questo build script usa un programma scritto in Zig per generare un file Zig, per poi esporlo al programma principale (main.zig) come modulo/dipendenza.

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

Output

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

Gestire uno o più file autogenerati

Lo step WriteFiles permette di generare uno o più file che condividono una cartella padre. La cartella generata si troverà dentro la cartella .zig-cache locale, e ogni file generato è individualmente disponibile come std.Build.LazyPath. Anche la cartella padre stessa è disponibile come LazyPath.

Questa API supporta la scrittura di qualunque stringa nella cartella generata. oltre al copiare file al suo interno.

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

Output

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

Modificare e sovrascrivere i file sorgente

Non capita spesso, ma a volte un progetto traccia i file generati in un sistema di controllo versione. Questo può essere utile quando i file generati vengono modificati raramente e il processo di aggiornamento richiede dipendenze di sistema scomode, ma solo durante il processo di aggiornamento.

Per questo, WriteFiles permette di soddisfare questo caso d'uso. Questa funzionalità verrà estratta da WriteFiles in uno step separato in una futura versione di Zig.

Fai molta attenzione con questa funzionalità; non dovrebbe essere utilizzata durante il normale processo di compilazione, ma come tool eseguito da uno sviluppatore con l'intenzione di aggiornare i file sorgente, che saranno dunque tracciati nel sistema di controllo versione. Se fatto durante il normale processo di compilazione, causerà bug di caching e concorrenza.

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

Dopo aver eseguito questo comando, src/protocol.zig verrà aggiornato e sovrascritto.

Altri esempi

Build per più piattaforme per creare un rilascio

In questo esempio cambieremo alcune opzioni di default durante la creazione di uno step InstallArtifact, allo scopo di mettere la build di ciascuna piattaforma in una sottocartella separata nel percorso di installazione.

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", .{});
}

Output

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