Dependencias em Zig: Guia Pratico com build.zig.zon

Zig evita a cultura de instalar cinquenta pacotes antes de escrever a primeira linha de codigo. Isso e uma vantagem, mas tambem cria uma duvida pratica: quando um projeto cresce, como trazer uma biblioteca externa sem transformar o build.zig em uma caixa-preta? A resposta nao e “nunca use dependencias”. A resposta e tratar dependencia como parte do design do binario.

Essa pergunta aparece com frequencia em comunidades Zig: como usar projetos que nao nasceram com build.zig, onde colocar resolved_target e optimize, quando criar um modulo, quando compilar C junto e quando simplesmente copiar um arquivo pequeno para dentro do repositorio. Para um time brasileiro que esta avaliando Zig em CLI interna, gateway HTTP, ferramenta de dados ou componente embarcado, essa decisao pesa mais que a sintaxe.

Este guia mostra um caminho pragmatico para dependencias em Zig com build.zig.zon, b.dependency, modulos explicitos e uma politica simples de revisao. Se voce ainda nao domina o build system, leia primeiro Zig Build System: guia completo. Para contexto de projetos reais, veja tambem Zig para programadores C, OpenAPI em Zig e Zig com systemd.

O criterio antes do comando

Antes de adicionar qualquer pacote, responda quatro perguntas:

PerguntaPor que importa
A dependencia reduz risco real ou so economiza algumas linhas?Zig valoriza superficie pequena e previsivel.
Ela precisa alocar memoria?O chamador deve enxergar allocator e tempo de vida.
Ela funciona no alvo de deploy?Linux, Windows, macOS, musl, WASM e embarcado mudam a resposta.
Ela tem API pequena o bastante para isolar?Um wrapper local evita espalhar detalhes externos pelo projeto.

Uma biblioteca de TLS, parser complexo, driver de banco ou binding C pode valer muito. Uma funcao de split, slugify ou cor de terminal talvez nao valha o custo de auditoria, atualizacao e compatibilidade. Em Zig, a dependencia ideal e aquela que voce consegue explicar no README e remover sem reescrever o sistema inteiro.

A forma moderna: build.zig.zon

O arquivo build.zig.zon descreve o pacote e suas dependencias. Ele fica ao lado do build.zig e deve ser versionado.

.{
    .name = .minha_cli,
    .version = "0.1.0",
    .minimum_zig_version = "0.14.0",
    .dependencies = .{
        .zig_args = .{
            .url = "https://example.com/zig-args/archive/v0.2.0.tar.gz",
            .hash = "1220...",
        },
    },
    .paths = .{
        "build.zig",
        "build.zig.zon",
        "src",
    },
}

O ponto importante nao e decorar esse formato. E entender que a dependencia passa a ser declarativa, revisavel e presa a um hash. Quando alguem roda zig build, o build system sabe o que baixar, validar e colocar no cache.

Na pratica, voce costuma usar zig fetch para obter a URL e o hash inicial, depois revisa o diff manualmente:

zig fetch --save https://example.com/zig-args/archive/v0.2.0.tar.gz
git diff build.zig.zon

Nao aceite um diff de dependencia sem ler o nome, a origem, a versao e o escopo. Esse e o equivalente Zig de revisar go.mod, Cargo.toml ou package-lock.json, so que normalmente bem menor.

Ligando a dependencia no build.zig

Depois de declarar a dependencia, voce decide como ela entra no build. O caso comum e expor um modulo para o executavel.

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "minha-cli",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    const args_dep = b.dependency("zig_args", .{
        .target = target,
        .optimize = optimize,
    });

    exe.root_module.addImport("args", args_dep.module("args"));

    b.installArtifact(exe);
}

A regra pratica: passe target e optimize para dependencias que compilam codigo. Isso evita uma classe comum de erro em que o projeto principal esta em ReleaseSafe para Linux musl, mas a dependencia foi resolvida com outra configuracao. Quando uma biblioteca documenta campos como resolved_target, siga a API daquela versao, mas mantenha a intencao: todos os componentes devem compilar para o mesmo alvo e perfil.

No codigo da aplicacao, o import fica explicito:

const args = @import("args");

Evite importar a dependencia diretamente em dezenas de arquivos. Prefira um modulo local pequeno, por exemplo src/cli_args.zig, que traduz a API externa para tipos do seu projeto. Se voce trocar a biblioteca depois, a mudanca fica concentrada.

Quando o projeto externo nao tem build.zig

Nem tudo no ecossistema vem empacotado para Zig. Voce pode querer usar uma biblioteca C pequena, um arquivo .c legado, uma tabela gerada ou um projeto que so publica fonte.

Nesses casos, ha tres opcoes saudaveis:

SituacaoOpcao recomendada
Um ou dois arquivos pequenos e estaveisVendorize em third_party/ com licenca preservada.
Biblioteca C com build simplesCompile os .c pelo build.zig e isole o @cImport.
Projeto grande com build proprioConsidere manter fora do binario Zig ou criar um wrapper separado.

Exemplo com um arquivo C local:

const exe = b.addExecutable(.{
    .name = "minha-cli",
    .root_source_file = b.path("src/main.zig"),
    .target = target,
    .optimize = optimize,
});

exe.addCSourceFile(.{
    .file = b.path("third_party/miniparser/miniparser.c"),
    .flags = &.{ "-std=c99" },
});
exe.addIncludePath(b.path("third_party/miniparser"));
exe.linkLibC();

O wrapper Zig deve esconder o C do resto do projeto:

const c = @cImport({
    @cInclude("miniparser.h");
});

pub fn parse(input: []const u8) !Resultado {
    // converter slice Zig para o formato esperado pela biblioteca C
    // e devolver um tipo do seu dominio, nao um ponteiro C cru
}

Isso e mais importante que a mecanica do build. O risco de dependencia externa quase sempre entra pela fronteira: ownership de memoria, ponteiros nulos, strings terminadas em zero, erro retornado por inteiro e funcoes que parecem thread-safe mas nao sao.

Politica simples para times

Uma politica leve evita discussoes repetidas em cada pull request:

  1. Dependencia nova precisa de justificativa curta no PR ou commit.
  2. build.zig.zon deve fixar origem e hash.
  3. API externa entra por um wrapper local quando for usada por mais de um arquivo.
  4. Nenhuma dependencia deve esconder allocator global.
  5. O build precisa rodar com zig build limpo em pelo menos Debug e ReleaseSafe.
  6. Se o alvo de producao e musl, Windows, WASM ou embarcado, teste esse alvo antes de aceitar.

Para projetos pequenos, isso parece formal demais. Mas a disciplina paga cedo. Quando a CLI vira ferramenta de time, quando o worker entra em systemd ou quando o binario precisa rodar em container minimo, voce ja sabe de onde vem cada pedaco.

Checklist antes de publicar uma dependencia

Use esta lista antes de fazer merge:

  • A dependencia resolve um problema que a std nao cobre bem?
  • A licenca permite o uso pretendido?
  • O pacote e pequeno o suficiente para auditar?
  • target e optimize estao sendo repassados no build.zig?
  • O import externo esta isolado atras de um modulo local?
  • O codigo deixa claro quem aloca e quem libera?
  • O build passa sem rede depois do cache inicial?
  • Existe um caminho simples para remover ou substituir a dependencia?

Se voce responder “nao” para varias dessas perguntas, talvez a melhor dependencia seja nenhuma. Isso nao e purismo: e manutencao barata.

Um exemplo de decisao realista

Imagine uma empresa brasileira com uma CLI interna para validar arquivos de remessa. Ela precisa de parsing de argumentos, leitura de CSV e envio opcional para uma API.

Uma divisao sensata seria:

AreaDecisao
Argumentos de CLIDependencia pequena, isolada em src/cli_args.zig.
CSV simplesImplementacao local ou receita propria, se o formato for controlado.
HTTPUsar std.http se suficiente; wrapper local para retries e timeout.
JSONComecar com std.json; so trazer biblioteca externa se houver ganho claro.
Validacao de dominioCodigo do proprio projeto, com testes.

Essa abordagem preserva o melhor de Zig: binario previsivel, poucos acoplamentos e controle explicito. O time ainda usa bibliotecas quando elas reduzem risco, mas nao importa uma arvore enorme para resolver uma tarefa local.

Conclusao

Gerenciar dependencias em Zig e menos sobre colecionar pacotes e mais sobre desenhar fronteiras. build.zig.zon resolve a parte operacional: origem, hash, cache e reproducibilidade. O build.zig resolve a parte tecnica: alvo, otimizacao, modulos e artefatos. O seu projeto ainda precisa resolver a parte de engenharia: decidir o que merece entrar no binario.

Para a maioria dos projetos, a melhor regra e simples: comece com std, adicione dependencia quando ela reduzir risco real, isole a API externa e mantenha target/optimize coerentes em todo o build. Assim Zig continua sendo uma ferramenta de controle, nao mais um ecossistema onde ninguem sabe o que esta rodando em producao.

Continue aprendendo Zig

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