Build System do Zig: Guia Completo do build.zig

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

  1. Use standardTargetOptions e standardOptimizeOption — habilitam flags padrão de CLI
  2. Separe lib e exe — facilita testes e reutilização
  3. Documente seus steps — a descrição aparece em zig build --help
  4. Use build.zig.zon para dependências — não faça git submodules
  5. Teste o build para múltiplos targets — aproveite a cross-compilation
  6. Use release modes adequados — ReleaseFast para performance, ReleaseSafe para 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.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.