Sistema de Armado de Zig
- ¿Cómo sacarle provecho al Sistema de Armado(build system) de Zig?
- Iniciando
- Lo Básico
- Generar archivos
- Ejemplos útiles
¿Cómo sacarle provecho al Sistema de Armado(build system) de Zig?
Los comandos fundamentales zig build-exe
, zig build-lib
, zig build-obj
, y zig test
en el mayor de los casos son suficientes. Sin embargo, hay ocasiones en las que un proyecto necesita otra capa de abstracción para manejar la complejidad de construir desde las fuentes.
Por ejemplo, puede que alguna de estas situaciones aplique:
- La línea de comandos se hace muy larga y difícil de manejar, y deseas un lugar donde quede escrita.
- Quieres armar muchos artefactos, o el proceso de armado contiene muchos pasos.
- Quieres aprovechar concurrencia y caché para reducir el tiempo de armado.
- Quieres exponer las opciones de configuración para el proyecto.
- El proceso de armado es diferente dependiendo del sistema objetivo(target) y otras opciones.
- Tienes dependencias de otros proyectos.
- Quieres evitar dependencias innecesarias con cmake, make, shell, msvc, python, etc., haciendo tu proyecto asequible a más contribuyentes
- Quieres proveer un paquete que otros puedan consumir.
- Quieres proveer una forma estandarizada para que herramientas tales como IDEs puedan entender semánticamente cómo armar el proyecto.
Si cualquiera de estas aplican, el proyecto se beneficiará al usar el sistema de armado de Zig.
Iniciando
Un ejecutable sencillo
Este guión de armado(build script) crea un ejecutable desde un archivo Zig que contiene la definición de una función pública main
.
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
Instalar artefactos armados
El sistema de armado de Zig, como la mayoría de sistemas de armado se basa en modelar el proyecto como un grafo acíclico dirigido(DAG) de pasos que son independientes y se ejecutan concurrentemente.
De forma predeterminada el paso principal en el grafo es el paso Install, cuyo propósito es copiar los artefactos en el lugar donde deberían quedar. El paso de instalación comienza sin dependencias, y por lo tanto nada sucederá cuando se ejecute zig build
. El guión de armado de un proyecto debe incluir el conjunto de cosas a instalar, que es lo que hace la función installArtifact
.
Resultado
├── build.zig
├── hello.zig
├── .zig-cache
└── zig-out
└── bin
└── hello
Hay dos directorios generados en este resultado: .zig-cache
y zig-out
. El primero contiene archivos que harán subsecuentes armados más rápidos, aunque estos archivos no se espera que estén incluidos en el control de versiones(source-control) y este directorio puede eliminarse completamente en cualquier momento sin consecuencias.
El segundo, zig-out
, es un "prefijo de instalación". Se mapea al concepto de la jerarquía del sistema estándar de archivos. Este directorio no lo elige el proyecto y si por el usuario de zig build
con la opción bandera(flag) --prefix
(-p
en corto).
Tú, como el mantenedor del proyecto elige lo que se debe colocar en este directorio, pero el usuario elige dónde instalarlo en su sistema. El guión de armado no puede establecer los paths de salida porque esto rompería el caché, la concurrencia y la componibilidad molestando al usuario final.
Añadir un paso útil para ejecutar la Aplicación
Es común añadir el paso Run
para ofrecer una forma de ejecutar nuestra aplicación directamente desde el comando de armado.
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
Lo Básico
Opciones dadas por el usuario
Use b.option
para hacer que el guión de armado sea configurable para usuarios finales y otros proyectos que dependan del proyecto como un paquete.
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", .{});
}
Por favor fija tu atención a estas líneas:
Project-Specific Options:
-Dwindows=[bool] Target Microsoft Windows
Esta parte del menú de ayuda es auto-generada basada en la ejecución de la lógica de build.zig
. Los usuarios pueden descubrir las opciones de configuración del guión de armado de esta forma.
Opciones estándar de configuración
Previamente usamos la bandera booleana para indicar el armado para Windows. Sin embargo, podemos hacerlo mejor.
La mayoría de proyectos desean ofrecer la habilidad de cambiar el objetivo(arquitectura objetivo) y los ajustes de optimización. Para estimular la convención de nombramiento estándar de estas opciones Zig provee las funciones de ayuda standardTargetOptions
y standardOptimizeOption
.
Las opciones estándar de arquitectura le permiten a la persona que ejecuta zig build
elegir la arquitectura para generar. De forma predeterminada, se permite cualquier arquitectura y no elegir significa apuntar al sistema en el que se ejecuta(host system). Se proveen otras opciones para restringir las arquitecturas soportadas.
Las opciones de optimizaciones estándar le permiten a la persona que ejecuta zig build
seleccionar entre Debug
, ReleaseSafe
, ReleaseFast
, y ReleaseSmall
. De forma predeterminada ninguna de las opciones versión(release) se considera una opción preferible por el guión de armado, y el usuario debe tomar una decisión para crear una versión.
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
Ahora nuestro menú --help
contiene más entradas:
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 totalmente posible crear estas opciones vía b.option
directamente, pero este API provee una convención comúnmente usada para nombrar estas opciones frecuentemente usadas.
En nuestra salida de la terminal, observa que pasamos -Dtarget=x86_64-windows -Doptimize=ReleaseSmall
. Comparado con el primer ejemplo, ahora vemos archivos diferentes en el prefijo de instalación(installation prefix):
zig-out/
└── bin
└── hello.exe
Opciones para compilación condicional
Para pasar opciones desde el guión de armado y en el código del proyecto Zig, usa el paso 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
En este ejemplo, el dato proveído por @import("config")
se sabe en tiempo de compilación (comptime-known), previendo que se lance @compileError
. Si hubiéramos pasado la opción -Dversion=0.2.3
u omitido la opción, hubiéramos visto fallar la compilación de app.zig fallar con el error "too old".
Biblioteca estática
Este guión de armado crea una biblioteca estática de código Zig, y también un ejecutable de otro código Zig que la consume.
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
En este caso, solamente se instala la librería estática.
zig-out/
└── lib
└── libfizzbuzz.a
Sin embargo, si mira atentamente, el guión de armado contiene una opción para instalar también el demo. Si pasamos adicionalmente -Denable-demo
, entonces vemos lo siguiente en el prefijo de instalación:
zig-out/
├── bin
│ └── demo
└── lib
└── libfizzbuzz.a
Nota que a pesar de la llamada incondicional a addExecutable
, el sistema de armado no gasta tiempo alguno generando el ejecutable demo
a menos que sea requerido con -Denable-demo
, porque el sistema de armado se basa en un Grafo Acíclico dirigido con dependencias.
Biblioteca estática
Aquí tenemos los mismos archivos del ejemplo de la biblioteca estática excepto por el cambio de 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);
}
$ 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
Resultado
zig-out
└── lib
├── libfizzbuzz.so -> libfizzbuzz.so.1
├── libfizzbuzz.so.1 -> libfizzbuzz.so.1.2.3
└── libfizzbuzz.so.1.2.3
Como en el ejemplo de la biblioteca estática, para crear un enlace ejecutable hacia esta, usa un código como el siguiente:
exe.linkLibrary(libfizzbuzz);
Pruebas
Puedes probar archivos individualmente con zig test foo.zig
, sin embargo, puedes resolver casos de uso más complejos orquestando las pruebas con el guión de armado.
Cuando usas el guión de armado, las pruebas unitarias se parten en dos pasos diferentes en el grafo de armado, el paso Compile y el paso Run. Sin el llamado a addRunArtifact
, que establece una arista de dependencia entre los dos pasos, no se ejecutarán las pruebas unitarias(unit tests).
El paso de compilación puede configurarse de la misma manera que con cualquier ejecutable, librería o código objeto(object code), por ejemplo al enlazar hacia bibliotecas del sistema, establecer opciones de arquitectura, o añadir unidades de compilación adicionales.
Puedes configurar el paso Run
de la misma manera que cualquier paso Run, por ejemplo dejando de lado la ejecución cuando el sistema base(host) no tiene capacidad de ejecutar el binario.
Cuando se usa el sistema de armado para ejecutar pruebas unitarias, el ejecutor de armado(build runner) y el ejecutor de pruebas(test runner) se comunican vía stdin
y stdout
para ejecutar varios conjuntos de pruebas(test suites) concurrentemente y reportar fallos de los test de forma coherente sin que se mezclen sus salidas. Esta es una razón por la cual escribir a la salida estándar en pruebas unitarias es problemático - interferirá con este canal de comunicación. Por otro lado, este mecanismo permitirá una característica que viene, la habilidad de una prueba unitaria de esperar un panic.
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
En este caso puede ser bueno ajustar skip_foreign_checks
para las pruebas unitarias:
@@ -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
Enlazar hacia bibliotecas del sistema
Para satisfacer dependencia de bibliotecas hay dos opciones:
- Proveer estas bibliotecas vía el sistema de armado de Zig (ver Manejo de paquetes y Bibliotecas estáticas).
- Usar los archivos proveídos por el sistema base.
Para el caso de mantenedores de proyectos upstream(upstream projects), obtener tales librerías vía el Sistema de Armado de Zig provee la fricción mínima y pone el poder de configuración en manos de tales mantenedores. Cualquiera que lo arme de esta forma tendrá resultados tan reproducibles y consistentes como cualquier otro y funcionará en cualquier sistema operativo soportando incluso compilación cruzada. Más aún, le permite al proyecto decidir con perfecta precisión las versiones exactas de su árbol de dependencias entero frente al que quiere armar. Esta se espera que sea la forma preferida en general al depender de bibliotecas externas.
Sin embargo, en el caso de uso de repositorios de empaquetamiento de programas como Debian, Homebrew, o Nix, en donde es obligatorio enlazar hacia bibliotecas del sistema, los guiones de armado deben detectar el modo y configurar acordemente.
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
Los usuarios de zig build
pueden usar --search-prefix
para proveer directorios adicionales que se consideran "directorios de sistema" para el propósito de encontrar bibliotecas estáticas y dinámicas.
Generar archivos
Ejecutar herramientas del sistema
Esta versión de hello world espera encontrar un archivo word.txt
en el mismo directorio y queremos usar una herramienta del sistema para generarlo a partir de un archivo JSON.
Ten en cuenta que las dependencias del sistema hará que tu proyecto sea más difícil de armar para tus usuarios. Este guión de armado depende de jq
, por ejemplo, que no está presente en la mayoría de las distribuciones Linux y puede ser una herramienta poco familiar para los usuarios Windows.
La siguiente sección reemplazará jq
por una herramienta Zig incluida en el árbol de fuentes, la aproximación preferida.
words.json
{
"en": "world",
"es": "mundo",
"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
Resultado
zig-out
├── hello
└── word.txt
Note como captureStdOut
crea un archivo temporal con el resultado de la invocación de jq
.
Ejecutar herramientas del proyecto
Esta versión de hola mundo espera encontrar un archivo word.txt
en el mismo directorio, y queremos producirlo en tiempo de armado al invocar el programa Zig con un archivo JSON.
tools/words.json
{
"en": "world",
"es": "mundo",
"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
Resultado
zig-out
├── hello
└── word.txt
Producir insumos para @embedFile
Esta versión de hello world quiere embeber @embedFile
un recurso generado en tiempo de armado, que vamos a producir usando una herramienta escrita en Zig.
tools/words.json
{
"en": "world",
"es": "mundo",
"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
Resultado
zig-out/
└── bin
└── hello
Generar código fuente Zig
Este guión de armado usa un programa Zig para generar un archivo Zig y después lo expone al programa principal como un módulo de dependencia.
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
Resultado
zig-out/
└── bin
└── hello
Tratar con uno o más archivos generados
El paso WriteFiles ofrece una forma de generar uno o más archivos que comparten un directorio padre. El directorio generado está dentro del .zig-cache
local y cada archivo generado está disponible como un std.Build.LazyPath
. El directorio padre también está disponible como un LazyPath.
Esta API soporta escribir cadenas arbitrarias en el directorio generado así como copiar archivos en el mismo.
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
Resultado
zig-out/
└── project.tar.gz
Mutar archivos directamente(in place)
Es poco común, pero sucede que un proyecto incluya archivos generados en el sistema de control de versiones. Puede ser útil cuando los archivos generados son actualizados en pocas oportunidades y tienen dependencias complejas en el proceso de actualización, pero solamente durante el proceso de actualización.
Por esto, WriteFiles provee una forma de lograr esta tarea. Es una característica que será movida de WriteFiles a su propio paso de compilación en una versión futura de Zig.
Ten cuidado con esta funcionalidad; no deberías usarla durante el proceso normal de armado, pero es una utilidad ejecutada por un desarrollador con intención de actualizar archivos fuente que serán incluidos en el control de versiones. Si se hace durante el proceso normal de armado causará bugs de caché y concurrencia.
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
Después de ejecutar este comando, src/protocol.zig
se actualizará.
Ejemplos útiles
Armar múltiples arquitecturas al crear una versión
En este ejemplo vamos a cambiar algunos predeterminados al crear un paso InstallArtifact
para colocar el resultado de cada arquitectura en un directorio separado en la ruta(path) de instalación.
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", .{});
}
Resultado
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