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 defectozig build -Doptimize=ReleaseFast: compilar con optimizaciones máximaszig build run: compilar y ejecutar directamentezig 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
