Introducción
Cuando empezamos a programar en Zig, uno de los primeros conceptos que debe entenderse es el grafo de dependencias que el sistema de construcción de Zig crea automáticamente. Este grafo describe las relaciones entre los distintos archivos y bibliotecas de un proyecto, y es la base para que la compilación sea eficiente y correcta.
Conceptos básicos del sistema de construcción de Zig
El sistema de construcción de Zig, conocido como zig build, trabaja con archivos build.zig. En este archivo se definen los pasos de construcción (build steps), los recursos (resources) y, muy importante, las dependencias entre ellos. Cada recurso puede ser, por ejemplo, un módulo de código fuente, una biblioteca, un archivo de datos o incluso un ejecutable.
¿Qué representa un grafo?
Un grafo es una estructura compuesta por nodos (los recursos) y aristas (las dependencias). En el contexto de Zig:
- Un nodo es cualquier objetivo que se puede construir (un ejecutable, una biblioteca estática, un módulo de código, etc.).
- Una arista indica que un nodo necesita la salida de otro nodo antes de poder construirse.
El grafo es dirigido y acíclico (DAG), lo que significa que no puede haber ciclos; cada nodo depende de otros nodos que están “antes” en el orden de construcción.
Construyendo el grafo paso a paso
Cuando ejecutamos zig build, el compilador analiza el contenido de build.zig y crea automáticamente el grafo de dependencias. A continuación se describe cómo se construye este grafo.
1. Declaración del proyecto
El primer paso es crear una std.Build que representa el proyecto entero.
// build.zig
const std = @import("std");
// Se crea una instancia del sistema de construcción
// El objeto build contiene el contexto y la configuración global del proyecto.
pub fn build(b: *std.Build) void {
// Configuraciones globales, por ejemplo, el modo de compilación
// (debug, releaseSafe, releaseFast, releaseSmall). Aquí usamos releaseSafe.
const mode = .ReleaseSafe;
// Se declara el objetivo principal del proyecto: un ejecutable
const exe = b.addExecutable("hello", "src/main.zig");
exe.setBuildMode(mode);
// Aquí se agregarán más dependencias más adelante
}
2. Añadiendo dependencias de módulo
Supongamos que el programa hello depende de un módulo llamado utils. Cada módulo se declara con b.addModule, y el grafo se actualiza automáticamente para indicar que el ejecutable depende de ese módulo.
// build.zig (continuación)
// Declarar el módulo utils. Este módulo se define en src/utils.zig
const utils = b.addModule("utils", "src/utils.zig");
// Agregar el módulo al ejecutable como dependencia
exe.addModule("utils", utils);
}
3. Compilación de bibliotecas estáticas
Si el proyecto requiere una biblioteca estática externa, se declara con b.addStaticLibrary y se enlaza al ejecutable.
// build.zig (ejemplo de biblioteca estática)
// Crear una biblioteca estática llamada libmath
const libmath = b.addStaticLibrary("libmath", "src/math.zig");
libmath.setBuildMode(mode);
// El ejecutable necesita la biblioteca libmath
exe.linkLibrary(libmath);
Ejemplo completo: proyecto con módulos y biblioteca
A continuación se muestra un proyecto mínimo que incluye:
- Un ejecutable
hello. - Un módulo
utils. - Una biblioteca estática
libmath. - Archivo de datos
config.jsonutilizado como recurso.
Archivo build.zig
// build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
// Modo de compilación: ReleaseSafe (el más seguro de los modos release)
const mode = .ReleaseSafe;
// 1. Ejecutable principal
const exe = b.addExecutable("hello", "src/main.zig");
exe.setBuildMode(mode);
// 2. Módulo utils
const utils = b.addModule("utils", "src/utils.zig");
exe.addModule("utils", utils);
// 3. Biblioteca estática libmath
const libmath = b.addStaticLibrary("libmath", "src/math.zig");
libmath.setBuildMode(mode);
exe.linkLibrary(libmath);
// 4. Recurso de datos (config.json)
// Este recurso se copia al directorio de salida y el ejecutable
// puede acceder a él a través de std.fs.cwd().
const config = b.addInstallFile("assets/config.json", "config.json");
// 5. Generar el ejecutable
exe.install();
}
Archivo src/main.zig
// src/main.zig
const std = @import("std");
// Importar el módulo utils
const utils = @import("utils");
// Importar la biblioteca libmath
// Se hace a través de la definición de módulo en build.zig,
// por lo que no se necesita importar explícitamente aquí.
const math = @import("libmath");
pub fn main() !void {
// 1. Leer configuración desde config.json
const cwd = std.fs.cwd();
const file = try cwd.openFile("config.json", .{ .mode = .read_only });
defer file.close();
// 2. Obtener el contenido completo del archivo
const buf = try file.readAllAlloc(std.heap.page_allocator, 1024);
defer std.heap.page_allocator.free(buf);
// 3. Procesar la configuración (ejemplo: imprimirla)
std.debug.print("Config: {s}\\n", .{buf});
// 4. Llamar a función del módulo utils
utils.printMessage("Hola desde utils!");
// 5. Llamar a función de la biblioteca libmath
const sum = math.add(3, 7);
std.debug.print("3 + 7 = {}\\n", .{sum});
}
Archivo src/utils.zig
// src/utils.zig
const std = @import("std");
pub fn printMessage(msg: []const u8) void {
std.debug.print("[UTILS] {}\\n"
