O cargo-zigbuild é o caminho mais curto em 2026 para cross-compilar Rust sem montar uma fazenda de toolchains: ele usa o Zig como linker e como toolchain de C, e você continua no fluxo familiar do Cargo. Em vez de instalar gcc-aarch64-linux-gnu, musl-tools e headers de meia dúzia de distros, você faz rustup target add, cargo install cargo-zigbuild e roda cargo zigbuild --release --target x86_64-unknown-linux-musl. O resultado é um binário pronto para VPS Linux, Alpine, edge, Raspberry Pi ou release multi-arch — gerado a partir de um Mac, de um notebook Ubuntu ou de um runner barato de CI.
Este guia cobre o que é o cargo-zigbuild, quando ele ganha do cross e do QEMU, como instalar, como gerar binários musl/gnu/ARM64, como encaixar na CI e no Docker, e quais armadilhas de crates nativas (OpenSSL, SQLx, bindgen) ainda exigem cuidado. Se o seu gargalo atual é só o tempo de link no mesmo SO, veja também o mold e o sccache. Aqui o problema é outro: produzir o binário certo para a máquina errada.
Por que cross-compile dói em Rust (e por que 2026 mudou o jogo)
Rust em si é excelente para multiplataforma: o mesmo código compila para dezenas de targets. O que dói não é o rustc — é o ecossistema de C ao redor:
- linker do alvo (ld, lld, link.exe, ld64);
- C runtime e libc (glibc vs musl, versões diferentes);
- headers e
.so/.ade dependências nativas (OpenSSL, libpq, zlib); - build scripts (
build.rs) que chamampkg-configno host em vez do target.
No dia a dia brasileiro isso aparece assim:
- você desenvolve no macOS e precisa publicar um binário para Ubuntu 22.04 na VPS;
- o time gera releases de CLI para Linux x86_64 + aarch64 e não quer manter três runners;
- o Dockerfile multi-stage precisa de binário estático musl para imagem
scratch/distroless; - o firmware edge ou o agente de observabilidade roda em ARM64 e o notebook é Intel.
A solução clássica era: instalar cross-toolchains na mão, ou usar o projeto cross (Docker com toolchains prontas), ou compilar dentro da imagem de destino (lento, mas simples). O cargo-zigbuild entrou como terceira via: o Zig já sabe cross-compilar C e linkar para muitos triples; o wrapper só ensina o Cargo a usá-lo.
O que é cargo-zigbuild
cargo-zigbuild é um subcomando do Cargo (cargo zigbuild) que:
- intercepta o build do projeto e das crates;
- configura o Rust para o
--targetpedido; - usa o Zig como linker (
zig cc/zig c++) e, quando necessário, como compilador C paracccrates; - evita que você precise de um GCC/Clang específico por arquitetura no host.
Ele não substitui o Cargo, não baixa crates e não resolve magicamente FFI impossível. Ele remove a parte mais chata: “cadê o linker e a libc desse alvo?”.
cargo-zigbuild vs cross vs Docker nativo
| Abordagem | Como funciona | Prós | Contras | Quando preferir |
|---|---|---|---|---|
| cargo-zigbuild | Zig como linker/toolchain C | Rápido de instalar; ótimo no Mac→Linux; CI enxuta | Crates C complexas ainda falham; depende do Zig | CLIs, agentes, APIs sem OpenSSL do sistema |
| cross | Docker com toolchain por target | Ambientes maduros; muitas libs nativas | Imagens pesadas; precisa Docker; mais lento no notebook | Monorepos com deps nativas difíceis |
| Build na imagem de destino | cargo build dentro do container final/base | Máxima fidelidade ao runtime | Cache pior; CI mais lenta; multi-arch via QEMU | Quando “igual produção” > velocidade |
| QEMU + rustc no alvo | Emula a CPU do target | Funciona até em edge cases | Muito lento | Último recurso |
Regra prática: se o seu serviço usa Axum + Tokio + Serde + rustls e evita OpenSSL do sistema, cargo-zigbuild costuma ser a melhor DX. Se você depende de libssl-dev, libpq-dev e bindgen gerado contra headers da distro, avalie cross ou build na imagem.
Instalação
1. Instale o Zig
O cargo-zigbuild precisa do binário zig no PATH. Em 2026, use uma versão estável recente (consulte a documentação do cargo-zigbuild para a faixa testada).
# Exemplo: tarball oficial (Linux x86_64)
# Ajuste versão/arch conforme o host
curl -fsSL -o /tmp/zig.tar.xz \
"https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz"
sudo tar -xJf /tmp/zig.tar.xz -C /opt
export PATH="/opt/zig-linux-x86_64-0.13.0:$PATH"
zig version
No macOS, muitos times usam o gerenciador de pacotes da casa (brew install zig) ou o mesmo tarball oficial. Em CI, pin a versão do Zig no workflow para builds reproduzíveis.
2. Instale o cargo-zigbuild
cargo install cargo-zigbuild --locked
cargo zigbuild --help
3. Adicione os targets Rust
rustup target add x86_64-unknown-linux-gnu
rustup target add x86_64-unknown-linux-musl
rustup target add aarch64-unknown-linux-gnu
rustup target add aarch64-unknown-linux-musl
# se for publicar CLI multiplataforma:
rustup target add aarch64-apple-darwin
rustup target add x86_64-pc-windows-gnu
O rustup target add só entrega a stdlib e o suporte do rustc. O Zig cobre o resto do link.
Primeiro build cross em 60 segundos
Suponha uma CLI simples ou um serviço Axum sem OpenSSL do sistema:
# No diretório do projeto
cargo zigbuild --release --target x86_64-unknown-linux-musl
# Artefato típico:
# target/x86_64-unknown-linux-musl/release/meu_binario
file target/x86_64-unknown-linux-musl/release/meu_binario
Para um alvo gnu (glibc), troque o triple:
cargo zigbuild --release --target x86_64-unknown-linux-gnu
Atalho útil: vários targets no mesmo job
for t in \
x86_64-unknown-linux-musl \
aarch64-unknown-linux-musl
do
cargo zigbuild --release --target "$t"
done
Em monorepos com workspaces Cargo, use -p nome_da_crate para não recompilar o mundo.
musl vs gnu: qual target escolher?
| Target | Libc | Binário típico | Bom para | Cuidado |
|---|---|---|---|---|
x86_64-unknown-linux-gnu | glibc | dinâmico | Ubuntu/Debian “normal” | Versão mínima de glibc do host de build vs produção |
x86_64-unknown-linux-musl | musl | estático (na prática) | Alpine, scratch, distroless, “um binário e pronto” | Algumas crates C se comportam diferente |
aarch64-unknown-linux-* | gnu/musl | ARM64 | Graviton, Raspberry, edge, K8s ARM | Meça performance no hardware real |
Para releases de CLI e agentes de observabilidade, musl costuma ser o default mental: copiou o binário, rodou. Para serviços que precisam de plugins dinâmicos ou de uma distro corporativa travada em glibc, gnu ainda manda.
Se o deploy for container, combine com Docker otimizado para Rust e, quando fizer sentido, cargo-chef na stage de build.
Configuração que evita dor: .cargo/config.toml e features
Preferir rustls e backends portáteis
O maior motivo de falha no cross não é o Zig — é o OpenSSL do sistema. Prefira stacks que já usamos em produção moderna:
# Cargo.toml (exemplo de serviço HTTP)
[dependencies]
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "tls-rustls", "postgres"] }
# ou SQLite embutido em CLIs/edge — veja o guia de SQLite
Leitura relacionada: rustls em produção, SQLx / Diesel / SeaORM e SQLite embutido.
Linker e flags por target (quando precisar)
Na maioria dos casos o cargo-zigbuild já configura o linker. Ainda assim, monorepos grandes às vezes fixam flags em .cargo/config.toml para alvos nativos (por exemplo mold no Linux host) e deixam o zigbuild cuidar dos alvos cross:
# .cargo/config.toml — exemplo: mold só no host Linux local
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
Não force mold no target cross via zigbuild sem medir: o valor do Zig é exatamente ser o linker cross correto.
CI: GitHub Actions / Gitea Actions
Fluxo mínimo para publicar um binário Linux musl a partir de qualquer runner:
name: release-linux
on:
push:
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-musl,aarch64-unknown-linux-musl
- name: Install Zig
run: |
curl -fsSL -o /tmp/zig.tar.xz \
"https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz"
sudo tar -xJf /tmp/zig.tar.xz -C /usr/local
echo "/usr/local/zig-linux-x86_64-0.13.0" >> "$GITHUB_PATH"
- name: Install cargo-zigbuild
run: cargo install cargo-zigbuild --locked
- name: Build musl x86_64 + aarch64
run: |
cargo zigbuild --release --target x86_64-unknown-linux-musl
cargo zigbuild --release --target aarch64-unknown-linux-musl
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: linux-musl-bins
path: |
target/x86_64-unknown-linux-musl/release/meu_binario
target/aarch64-unknown-linux-musl/release/meu_binario
Dicas de CI que se pagam sozinhas:
- pin da versão do Zig e do
cargo-zigbuild; - cache de
~/.cargo/registry,~/.cargo/gite, se couber, sccache; - matrix só quando os targets tiverem features diferentes — dois
cargo zigbuildsequenciais no mesmo job costumam ser mais simples; - rode
cargo testno host (ou com target nativo) e reserve o zigbuild para o artefato de release.
O mesmo esqueleto funciona em Gitea Actions com runners Linux. Se o seu espelho de workflows em .github/ e .gitea/ divergir, alinhe os dois — drift de CI é dívida cara em sites e em produtos.
Docker multi-arch sem QEMU em todo cargo build
Padrão moderno:
- stage builder (qualquer arch de CI) gera o binário com
cargo zigbuild --target <arch-unknown-linux-musl>; - stage final só copia o binário para
gcr.io/distroless/static,scratchou Alpine mínima.
# syntax=docker/dockerfile:1
FROM rust:1.85-bookworm AS builder
WORKDIR /app
RUN cargo install cargo-zigbuild --locked \
&& curl -fsSL -o /tmp/zig.tar.xz \
"https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz" \
&& tar -xJf /tmp/zig.tar.xz -C /usr/local \
&& ln -s /usr/local/zig-linux-x86_64-0.13.0/zig /usr/local/bin/zig
COPY . .
ARG TARGET=x86_64-unknown-linux-musl
RUN rustup target add ${TARGET} \
&& cargo zigbuild --release --target ${TARGET} \
&& cp target/${TARGET}/release/meu_binario /meu_binario
FROM gcr.io/distroless/static-debian12
COPY --from=builder /meu_binario /meu_binario
USER nonroot:nonroot
ENTRYPOINT ["/meu_binario"]
Para publicar linux/amd64 e linux/arm64, construa duas vezes com TARGET diferente (ou use buildx com args) e faça o manifest merge. Isso é bem mais previsível do que emular cargo build inteiro via QEMU.
Armadilhas reais (e como contornar)
1. cc crate e build.rs host-centric
Crates que compilam C na build usam a crate cc. O cargo-zigbuild costuma injetar o ambiente certo, mas scripts que chamam pkg-config direto no host quebram. Sinais:
- erro de header não encontrado (
openssl/ssl.h,libpq-fe.h); pkg-configretorna paths do Ubuntu do runner em vez do alvo.
Mitigações: features vendored, trocar para backend Rust (rustls), ou isolar a crate nativa em um crate opcional.
2. glibc “too old” em produção
Binário *-linux-gnu linkado contra glibc nova não sobe em distro antiga. Se o cliente ainda roda Ubuntu 20.04, teste o artefato lá ou prefira musl estático.
3. Windows e macOS como target
Zig cobre muitos triples, mas o custo de validação sobe. Para macOS, codesign/notarização continuam sendo o trabalho chato (não é culpa do zigbuild). Para Windows, x86_64-pc-windows-gnu costuma ser o caminho de menor atrito; MSVC é outro universo de toolchains.
4. Procedural macros e build scripts
Macros procedurais sempre rodam no host. Isso é normal. O que quebra é build script que assume o host = target. Mantenha build.rs honestos com CARGO_CFG_TARGET_OS / CARGO_CFG_TARGET_ARCH.
5. Tamanho do binário e stripping
Cross bem-sucedido não significa binário pequeno. Continue com:
strip target/x86_64-unknown-linux-musl/release/meu_binario
# ou
cargo zigbuild --release --target x86_64-unknown-linux-musl
# com profile release: lto, codegen-units=1, strip=true
Veja também release engineering de binários e CLIs e otimização de tempo de compilação.
Checklist de decisão: cargo-zigbuild é para o meu projeto?
Use cargo-zigbuild se a maior parte for verdadeira:
- publicamos CLI/agente/serviço para Linux a partir de Mac ou CI única;
- preferimos
rustls/ SQLite / stacks sem OpenSSL do sistema; - queremos binário musl para Alpine/scratch;
- não queremos puxar imagens Docker gigantes só para linkar;
- o time aceita pinar versão do Zig na CI.
Prefira cross ou build na imagem se:
- dependemos de libpq/OpenSSL/outros
-devda distro; - temos bindgen pesado contra headers do sistema;
- o binário precisa ser bit-a-bit idêntico ao build “dentro” do SO de produção.
Onde isso se encaixa na carreira e no mercado BR
Cross-compile deixa de ser “truque de release engineer” e vira expectativa em vagas de plataforma, DevOps e backend sênior. Em empresas que usam Rust no Brasil e em times remotos que entregam agentes, CLIs internas e sidecars, a pergunta de entrevista deixa de ser “você conhece o borrow checker?” e passa a ser “como você gera o artefato Linux ARM a partir do pipeline?”.
Se você está montando portfólio, um repositório público com:
- app Axum mínima + rustls;
cargo zigbuildparax86_64eaarch64musl;- Dockerfile distroless;
- workflow de release com checksums;
…pesa mais em vagas Rust do que mais um CRUD sem pipeline. Combine com o guia de carreira Rust 2026 e com os números de salário Rust no Brasil.
Perguntas frequentes
O cargo-zigbuild substitui o rustup?
Não. Você continua instalando a toolchain Rust com rustup e adicionando targets. O zigbuild entra na etapa de link/C interop.
Posso usar cargo-zigbuild só em release e cargo build no dia a dia?
Sim — e é o fluxo mais comum. Desenvolvimento local no target nativo (cargo run, cargo test) e zigbuild no job de release/CD.
E testes no target cruzado?
Testes unitários rodam no host. Para rodar o binário do target, use QEMU user-mode, uma VM, ou um runner da arquitetura certa. Cross-compile resolve build, não magia de test hardware.
cargo-zigbuild serve para WebAssembly?
Para Wasm o caminho usual continua sendo wasm32-unknown-unknown / WASI com toolchains próprias. Veja nossos guias de WebAssembly em 2026 e WASI / component model. O zigbuild brilha em alvos native (Linux/macOS/Windows), não como substituto do pipeline Wasm.
Como debugar um erro de link obscuro?
- rode com
RUST_LOG/-vse disponível no wrapper; - confirme
zig versione o triple exato; - tente o mesmo crate sem dependências nativas (reproduz o mínimo);
- se só uma crate C falha, isole com features;
- compare com um
cross build --target …— se ambos falham, o problema é a crate, não a ferramenta.
Conclusão
cargo-zigbuild não é hype de ferramenta nova por ser nova: é a peça que faltava entre “Rust compila em qualquer lugar” e “eu consigo publicar o binário certo na sexta às 18h”. Com Zig no PATH, targets no rustup e um cargo zigbuild --release --target …, você encurta o caminho de notebooks brasileiros até VPS Linux, nós ARM e imagens distroless — sem carregar uma oficina de cross-gcc na mochila.
Próximos passos recomendados no Rust Brasil:
- acelerar o ciclo local com mold e sccache;
- estabilizar Docker com cargo-chef;
- fechar o pipeline de artefatos com release engineering;
- revisar se TLS e HTTP estão em rustls para o cross ficar chato só na dose certa.
Quando o artefato deixa de ser o gargalo, sobra tempo para o que importa: código claro, testes honestos e serviço no ar.