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.

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.