El sistema de build de Zig: reemplazar CMake y Make con zig build

CMake tiene fama de ser difícil. Make tiene fama de ser críptico. Autotools tiene fama de los dos. Durante décadas, la parte más frustrante de trabajar con proyectos C y C++ ha sido el sistema de build. Zig incluye uno propio desde el principio, escrito en Zig puro, que resuelve la mayoría de los problemas sin añadir dependencias externas.

La filosofía del sistema de build de Zig

El sistema de build de Zig no es un lenguaje de dominio específico como CMakeLists.txt ni un formato de configuración como los Makefiles. Es código Zig real que se ejecuta en tiempo de compilación. El fichero build.zig es un programa Zig con una función build que recibe un objeto *std.Build y define los artefactos, las dependencias y los pasos de build.

Esto tiene una ventaja práctica enorme: puedes usar todas las herramientas del lenguaje para escribir tu build script. Bucles, condicionales, funciones, imports. No necesitas aprender un DSL con su propia sintaxis y sus propias reglas.

Un build.zig mínimo

const std = @import("std");

pub fn build(b: *std.Build) void {
    // Target y modo de optimización configurables desde la línea de comandos
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // Definir el ejecutable
    const exe = b.addExecutable(.{
        .name = "mi-programa",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    // Instalar el ejecutable en zig-out/bin/
    b.installArtifact(exe);

    // Paso 'run' para ejecutar directamente con 'zig build run'
    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());
    const run_step = b.step("run", "Ejecutar el programa");
    run_step.dependOn(&run_cmd.step);
}

Con este build.zig puedes:

  • zig build: compilar en modo debug por defecto
  • zig build -Doptimize=ReleaseFast: compilar con optimizaciones máximas
  • zig build run: compilar y ejecutar directamente
  • zig build -Dtarget=x86_64-windows-gnu: cross-compilar para Windows

Añadir tests

const std = @import("std");

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

    const exe = b.addExecutable(.{
        .name = "mi-programa",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(exe);

    // Tests unitarios
    const unit_tests = b.addTest(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    const run_unit_tests = b.addRunArtifact(unit_tests);

    const test_step = b.step("test", "Ejecutar tests unitarios");
    test_step.dependOn(&run_unit_tests.step);
}

Ahora zig build test compila y ejecuta los tests. Los tests en Zig se escriben directamente en el fichero fuente con bloques test "descripción" { ... }.

Bibliotecas estáticas y dinámicas

const std = @import("std");

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

    // Biblioteca estática
    const lib = b.addStaticLibrary(.{
        .name = "mi-lib",
        .root_source_file = b.path("src/lib.zig"),
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(lib);

    // Ejecutable que usa la biblioteca
    const exe = b.addExecutable(.{
        .name = "mi-programa",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    exe.linkLibrary(lib);
    b.installArtifact(exe);
}

Enlazar con bibliotecas C

Si tu proyecto usa bibliotecas C del sistema, enlazarlas es directo:

const std = @import("std");

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

    const exe = b.addExecutable(.{
        .name = "mi-programa",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    // Enlazar con la libc del sistema
    exe.linkLibC();

    // Enlazar con openssl, por ejemplo
    exe.linkSystemLibrary("ssl");
    exe.linkSystemLibrary("crypto");

    b.installArtifact(exe);
}

Pasos de build personalizados

El sistema de build de Zig permite definir pasos arbitrarios: generar código, ejecutar herramientas externas, copiar ficheros. Todo forma un grafo de dependencias que Zig ejecuta en el orden correcto, en paralelo cuando es posible.

const std = @import("std");

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

    // Paso para generar código antes de compilar
    const gen = b.addSystemCommand(&.{ "python3", "scripts/generar.py" });

    const exe = b.addExecutable(.{
        .name = "mi-programa",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    // El ejecutable depende del paso de generación
    exe.step.dependOn(&gen.step);

    b.installArtifact(exe);
}

Gestión de dependencias con zig fetch

Desde versiones recientes, Zig tiene un gestor de dependencias integrado. Las dependencias se declaran en build.zig.zon, un fichero de metadatos del proyecto:

.{
    .name = "mi-proyecto",
    .version = "0.1.0",
    .dependencies = .{
        .zap = .{
            .url = "https://github.com/zigzap/zap/archive/refs/tags/v0.9.0.tar.gz",
            .hash = "1220...",
        },
    },
}

Y en build.zig añades la dependencia:

const zap = b.dependency("zap", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("zap", zap.module("zap"));

Comparación con CMake y Make

Característica

Make

CMake

zig build

Lenguaje

Makefile DSL

CMakeLists DSL

Zig

Cross-compilation

Manual

Toolchain files

Un flag

Dependencias externas

No incluye

FetchContent

build.zig.zon

Tests integrados

Manual

CTest

zig build test

Curva de aprendizaje

Alta

Muy alta

Media

El sistema de build de Zig no está libre de complejidad, especialmente cuando el proyecto crece, pero tiene una ventaja que no tiene ninguno de los otros: es un lenguaje real que ya sabes usar. No hay que aprender otro DSL con sus propias reglas. Verás más aplicaciones prácticas del sistema de build en el artículo sobre cross-compilation con Zig, donde la integración de targets es central, y en el artículo sobre interop con C de esta misma serie.

Imagen: Pexels / Daniil Komov

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP