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:
| Pergunta | Por 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:
| Situacao | Opcao recomendada |
|---|---|
| Um ou dois arquivos pequenos e estaveis | Vendorize em third_party/ com licenca preservada. |
| Biblioteca C com build simples | Compile os .c pelo build.zig e isole o @cImport. |
| Projeto grande com build proprio | Considere 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:
- Dependencia nova precisa de justificativa curta no PR ou commit.
build.zig.zondeve fixar origem e hash.- API externa entra por um wrapper local quando for usada por mais de um arquivo.
- Nenhuma dependencia deve esconder allocator global.
- O build precisa rodar com
zig buildlimpo em pelo menos Debug e ReleaseSafe. - 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
stdnao cobre bem? - A licenca permite o uso pretendido?
- O pacote e pequeno o suficiente para auditar?
targeteoptimizeestao sendo repassados nobuild.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:
| Area | Decisao |
|---|---|
| Argumentos de CLI | Dependencia pequena, isolada em src/cli_args.zig. |
| CSV simples | Implementacao local ou receita propria, se o formato for controlado. |
| HTTP | Usar std.http se suficiente; wrapper local para retries e timeout. |
| JSON | Comecar com std.json; so trazer biblioteca externa se houver ganho claro. |
| Validacao de dominio | Codigo 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.