¡Hola, entusiastas de Zig! 👋 En nuestra publicación anterior, dimos nuestros primeros pasos en el mundo de Zig. Hoy, vamos a adentrarnos en el corazón de la gestión de proyectos de Zig: el sistema de construcción de Zig. ¡Empecemos! 🎉

¿Qué es el sistema de construcción de Zig? 🤔

El sistema de construcción de Zig es una herramienta poderosa e incorporada que te ayuda a gestionar, compilar, probar y enlazar tus proyectos. Simplifica procesos de construcción complejos y proporciona una compilación cruzada sin interrupciones, permitiéndote apuntar a diferentes plataformas con facilidad.

Empezando con build.zig 🚀

Para aprovechar el sistema de construcción de Zig, necesitas crear un archivo build.zig en el directorio raíz de tu proyecto.

El conjunto de herramientas de Zig (toolchain) llama a la función build(b: *std.Build) void de build.zig. El parámetro bse utiliza para configurar y definir el proceso de construcción mediante pasos o instrucciones que no formen ciclos o bucles.

Por ejemplo, podemos crear nuestro propio paso my-step:

⚠️ ¡Atención, programador! Este post utiliza la versión 0.11.0-dev.3971 de Zig

const std = @import("std");

pub fn build(b: *std.build.Builder) void {
    const my_step = b.step("my-step", "Este paso es mi paso");
    _ = my_step;
}

Este paso aparecerá en la ayuda si ejecutamos zig build --help, y aunque no hace nada, hace mucha ilusión.

$ zig build --help

Usage: zig build [steps] [options]

Steps:
  install (default)            Copy build artifacts to prefix path
  uninstall                    Remove build artifacts from prefix path
  my-step                      Este paso es mi paso

[...]

Project-Specific Options:
  (none)

[...]

También podemos observar en la ayuda una sección Project-Specific Options, que por ahora aparece vacía.

Añadiendo el primer paso 👣

Zig viene equipado con algunos pasos listos para usar. Uno de los más interesantes es addExecutable, que permite compilar un ejecutable o librería.

Si añadimos este main.zig al directorio:

const std = @import("std");

pub fn main() !void {
    const stdout = std.io.getStdOut().writer();
    try stdout.print("Hola otra vez\n", .{});
}

Y definimos build.zig como:

const std = @import("std");

pub fn build(b: *std.Build) void {

    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "my-executable",
        .root_source_file = .{ .path = "main.zig" },
        .target = target,
        .optimize = optimize,
    });

    b.installArtifact(exe);

}

Cuando ejecutemos el comando zig build se compilará nuestro ejecutable. Estas son las líneas principales:

  1. const target = b.standardTargetOptions(.{}): Define las opciones de destino que estarán disponibles y cual será la de defecto, lo que permite al usuario seleccionar para qué plataforma se va a construir el código.

  2. const optimize = b.standardOptimizeOption(.{}): Define las opciones de optimización estándar, lo que permite al usuario seleccionar el nivel de optimización para la compilación.

  3. const exe = b.addExecutable(.): Añade un ejecutable al proceso de construcción. Los parámetros definen cómo se construirá el ejecutable, incluyendo el archivo fuente principal, las opciones de destino y las opciones de optimización.

  4. b.installArtifact(exe): Esta línea declara que el ejecutable creado en el paso anterior debe ser instalado en la ubicación estándar cuando se realiza el paso de instalación.

Si volvemos a ejecutar zig build --help observaremos como ahora la sección Project-Specific Options muestra opciones para establecer la plataforma, características de la cpu y la optimización desde línea de comandos.

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

Al lanzar zig build se generarán dos directorios importantes:

  • zig-cache: Este directorio contiene artefactos de construcción intermedios, como archivos de objeto, y es utilizado por el sistema de construcción de Zig para almacenar en caché los resultados de la construcción y acelerar las compilaciones posteriores.
  • zig-out: Este directorio almacena la salida final de tu proceso de construcción, incluyendo ejecutables, bibliotecas y otros binarios.

Compilación cruzada hecha fácil 🌉

Una de las características más poderosas del sistema de construcción de Zig es su capacidad para compilar de forma cruzada tus proyectos. Para ello simplemente proporciona la opción --target cuando ejecutes zig build:

zig build -Dtarget=aarch64-linux-gnu

Este comando compila tu proyecto para la plataforma objetivo especificada, manejando todas las complejidades por ti. 🎉

$ file zig-out/bin/executable 
zig-out/bin/executable: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, with debug_info, not stripped

Añadiendo más pasos 👣👣

Ahora que sabemos como funciona podemos añadir otros dos pasos, uno para ejecutar el programa y otro para lanzar los tests:

const std = @import("std");

pub fn build(b: *std.Build) void {

    // compilación
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

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

    b.installArtifact(exe);

    // ejecución
    const run_cmd = b.addRunArtifact(exe);
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }
    run_cmd.step.dependOn(b.getInstallStep());
    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    // tests
    const unit_tests = b.addTest(.{
        .root_source_file = .{ .path = "main_test.zig" },
        .target = target,
        .optimize = optimize,
    });
    const run_unit_tests = b.addRunArtifact(unit_tests);
    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&run_unit_tests.step);
}

En este código podemos ver

  1. El bloque if (b.args) |args| {...}: Añade cualquier argumento pasado al comando de ejecución.

  2. const run_step = b.step("run", "Run the app"): Crea un paso de construcción que ejecuta el comando de ejecución.

  3. const unit_tests = b.addTest(.) y const run_unit_tests = b.addRunArtifact(unit_tests): Crea un paso para las pruebas unitarias. Este paso construye el ejecutable de prueba pero no lo ejecuta.

  4. const test_step = b.step("test", "Run unit tests"): Este paso ejecuta las pruebas unitarias.

Para añadir los test añadimos el archivo main_test.zig:

const std = @import("std");

test "simple test" {
    try std.testing.expect(addOne(41) == 42);
}

fn addOne(number: i32) i32 {
    return number + 1;
}

Para crear un caso de prueba en Zig, usa la palabra clave test, seguida de un nombre de prueba y un bloque de código.

Para lanzar los test usamos el comando zig build test. Si todo va bien no deberíamos ver ningún error, el comando es realmente muy silencioso.

¿Se puede más fácil? 😅

Crear un archivo build.zig puede ser un poco tedioso, más aun cuando todavía no dominamos el lenguaje. Pero no tienes por qué empezar desde cero. El comando zig init-exe te generará las carpetas y archivos necesarios para compilar, ejecutar y pasar los test de un proyecto de básico de ejemplo en el directorio donde te encuentres.

¿Qué sigue? 🌟

¡Eso es todo! Ahora tienes un sólido entendimiento del sistema de construcción de Zig y puedes usarlo para gestionar, construir y compilar de forma cruzada tus proyectos con facilidad. 💡


¡Feliz codificación y nos vemos en la próxima publicación! 👩‍💻👨‍💻🚀