Build System do Zig: Guia Completo do build.zig
O build system de Zig é único — em vez de arquivos declarativos como Makefiles ou CMakeLists.txt, você escreve a lógica de build em Zig puro. Isso significa que tem acesso a toda a expressividade da linguagem: condicionais, loops, comptime e até chamadas de rede, tudo dentro do build script.
Neste guia, vamos explorar o build.zig em profundidade: desde a estrutura básica até dependências externas com build.zig.zon, steps customizados e integração com projetos C/C++.
Se é novo em Zig, comece com Por que aprender Zig e o cheatsheet do build system.
Estrutura Básica do build.zig
Todo projeto Zig começa com um build.zig na raiz:
const std = @import("std");
pub fn build(b: *std.Build) void {
// Opções padrão de target e otimização
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Definir executável principal
const exe = b.addExecutable(.{
.name = "meu-app",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// Instalar o artefato
b.installArtifact(exe);
// Step de "run" para executar o programa
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Executar o aplicativo");
run_step.dependOn(&run_cmd.step);
}
Execute com:
zig build # Compila
zig build run # Compila e executa
zig build -Doptimize=ReleaseFast # Build otimizado
O
standardTargetOptionsé o que habilita a poderosa cross-compilation do Zig — basta passar-Dtarget=aarch64-linux.
Steps: O Grafo de Build
O build system funciona como um grafo direcionado acíclico (DAG) de steps. Cada step pode depender de outros, e o Zig executa apenas o que é necessário:
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Step 1: Compilar a lib
const lib = b.addStaticLibrary(.{
.name = "minha-lib",
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
});
// Step 2: Compilar o executável (depende da lib)
const exe = b.addExecutable(.{
.name = "meu-app",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe.linkLibrary(lib);
// Step 3: Testes unitários
const unit_tests = b.addTest(.{
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
});
const run_tests = b.addRunArtifact(unit_tests);
// Steps acessíveis pela CLI
b.installArtifact(exe);
const test_step = b.step("test", "Rodar testes unitários");
test_step.dependOn(&run_tests.step);
const run_step = b.step("run", "Executar o app");
const run_exe = b.addRunArtifact(exe);
run_exe.step.dependOn(b.getInstallStep());
run_step.dependOn(&run_exe.step);
}
Agora zig build test roda testes e zig build run executa. Cada step só compila o necessário — muito mais rápido que Makefiles tradicionais.
Para mais sobre testes, veja o guia completo de testes em Zig.
Build Options: Configuração Flexível
Defina opções customizáveis via CLI para compilação condicional:
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Opção booleana
const enable_logging = b.option(
bool,
"enable-logging",
"Habilitar logs detalhados",
) orelse false;
// Opção de enum
const Backend = enum { sqlite, postgres, mysql };
const backend = b.option(
Backend,
"backend",
"Selecionar backend de banco de dados",
) orelse .sqlite;
const exe = b.addExecutable(.{
.name = "meu-app",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// Passar opções para o código via build options
const options = b.addOptions();
options.addOption(bool, "enable_logging", enable_logging);
options.addOption(Backend, "backend", backend);
exe.root_module.addOptions("config", options);
b.installArtifact(exe);
}
No código Zig:
// src/main.zig
const config = @import("config");
pub fn main() void {
if (config.enable_logging) {
// Logs detalhados habilitados
std.debug.print("Logging ativado\n", .{});
}
switch (config.backend) {
.sqlite => initSqlite(),
.postgres => initPostgres(),
.mysql => initMysql(),
}
}
Use na CLI:
zig build -Denable-logging=true -Dbackend=postgres
Isso substitui o #ifdef do C e os feature flags do Cargo (Rust) — tudo resolvido em comptime com type safety. Se você vem de Python, pense nas build options como variáveis de ambiente do setup.py, mas com verificação em tempo de compilação.
Dependências com build.zig.zon
O build.zig.zon é o gerenciador de pacotes do Zig. Diferente do go.mod ou Cargo.toml, usa a sintaxe ZON (Zig Object Notation):
// build.zig.zon
.{
.name = "meu-projeto",
.version = "0.1.0",
.dependencies = .{
.zap = .{
.url = "https://github.com/zigzap/zap/archive/refs/tags/v0.7.0.tar.gz",
.hash = "1220aabff7e77a3a21e46738bd8e4016e8d0347ae0b5a8f6e3e5e68b1f42a1e35c3f",
},
.clap = .{
.url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.8.0.tar.gz",
.hash = "122062d301569d9b5288ac2ea13c24ca7b3f41398cba2f41d73947f7a5e624e78b49",
},
},
.paths = .{"."},
}
Para detalhes sobre ZON, veja o glossário de zon e o package manager do Zig.
Depois, use as dependências no build.zig:
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Buscar dependência
const zap = b.dependency("zap", .{
.target = target,
.optimize = optimize,
});
const exe = b.addExecutable(.{
.name = "meu-servidor",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// Adicionar módulo da dependência
exe.root_module.addImport("zap", zap.module("zap"));
b.installArtifact(exe);
}
Para adicionar uma dependência:
zig fetch --save https://github.com/zigzap/zap/archive/refs/tags/v0.7.0.tar.gz
Saiba mais sobre zig fetch no glossário.
Integração com Projetos C/C++
O build system brilha na interoperabilidade com C. Você pode compilar código C/C++ diretamente:
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "app-hibrido",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// Compilar arquivos C junto com Zig
exe.addCSourceFiles(.{
.files = &.{
"vendor/utils.c",
"vendor/parser.c",
},
.flags = &.{
"-std=c11",
"-Wall",
"-Wextra",
"-O2",
},
});
// Adicionar include paths
exe.addIncludePath(b.path("vendor/include"));
// Linkar com bibliotecas do sistema
exe.linkSystemLibrary("pthread");
exe.linkLibC();
b.installArtifact(exe);
}
Isso torna o Zig uma alternativa ao GCC/Clang como compilador C — muitos projetos usam zig cc como drop-in replacement. Veja como isso se aplica na migração de projetos C para Zig.
Steps Customizados
Crie steps que executam scripts, geram código ou fazem validações:
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "meu-app",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
// Step customizado: gerar documentação
const docs_step = b.step("docs", "Gerar documentação");
const docs_cmd = b.addSystemCommand(&.{
"zig", "doc", "src/lib.zig",
});
docs_step.dependOn(&docs_cmd.step);
// Step customizado: limpar artefatos
const clean_step = b.step("clean", "Limpar build artifacts");
const clean_cmd = b.addRemoveDirTree(b.path("zig-out"));
clean_step.dependOn(&clean_cmd.step);
// Step de benchmark
const bench = b.addExecutable(.{
.name = "bench",
.root_source_file = b.path("src/bench.zig"),
.target = target,
.optimize = .ReleaseFast,
});
const bench_run = b.addRunArtifact(bench);
const bench_step = b.step("bench", "Rodar benchmarks");
bench_step.dependOn(&bench_run.step);
}
Agora:
zig build docs # Gera documentação
zig build clean # Limpa artefatos
zig build bench # Roda benchmarks
Compilação Condicional por Plataforma
O build system tem acesso completo às informações do target:
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "meu-app",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// Linking condicional por OS
if (target.result.os.tag == .linux) {
exe.linkSystemLibrary("io_uring");
} else if (target.result.os.tag == .macos) {
exe.linkFramework("CoreFoundation");
} else if (target.result.os.tag == .windows) {
exe.linkSystemLibrary("ws2_32");
}
// Arquivo fonte condicional
if (target.result.cpu.arch == .aarch64) {
exe.addCSourceFile(.{
.file = b.path("src/neon_simd.c"),
.flags = &.{},
});
}
b.installArtifact(exe);
}
Isso é muito mais legível que os #ifdef _WIN32 do C ou condicionais complexas em CMake. Para SIMD especificamente, veja nosso guia de SIMD em Zig.
Para comparação, o sistema de build do Go (
go build) é mais simples mas menos flexível. Saiba mais em golang.com.br. Já o Cargo do Rust oferece features semelhantes — explore em rustlang.com.br.
Múltiplos Artefatos
Projetos maiores produzem vários binários e bibliotecas:
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Biblioteca compartilhada
const lib = b.addSharedLibrary(.{
.name = "minha-lib",
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(lib);
// Biblioteca estática
const static_lib = b.addStaticLibrary(.{
.name = "minha-lib",
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(static_lib);
// Executável CLI
const cli = b.addExecutable(.{
.name = "minha-cli",
.root_source_file = b.path("src/cli.zig"),
.target = target,
.optimize = optimize,
});
cli.linkLibrary(static_lib);
b.installArtifact(cli);
// Servidor
const server = b.addExecutable(.{
.name = "meu-servidor",
.root_source_file = b.path("src/server.zig"),
.target = target,
.optimize = optimize,
});
server.linkLibrary(static_lib);
b.installArtifact(server);
}
Dicas e Boas Práticas
- Use
standardTargetOptionsestandardOptimizeOption— habilitam flags padrão de CLI - Separe lib e exe — facilita testes e reutilização
- Documente seus steps — a descrição aparece em
zig build --help - Use
build.zig.zonpara dependências — não faça git submodules - Teste o build para múltiplos targets — aproveite a cross-compilation
- Use release modes adequados —
ReleaseFastpara performance,ReleaseSafepara produção com safety checks
Para ferramentas de debug e profiling, veja o ecossistema de debug tools e profiling.
Conclusão
O build system do Zig é poderoso porque é código Zig real — não uma DSL limitada. Você define targets, options, steps e dependências com a mesma linguagem que escreve seu software. Combinado com a interoperabilidade C nativa e o gerenciamento de pacotes via build.zig.zon, é uma das experiências de build mais coesas do ecossistema de linguagens de sistemas.
Para se aprofundar, explore o overview do build system, o glossário de build.zig e o cheatsheet do build system. E se está planejando sua carreira com Zig, confira o roadmap de desenvolvedor e as ferramentas de produtividade.