cargo-zigbuild: Cross-Compile Rust sem Dor | Rust Brasil

Guia prático de cargo-zigbuild em Rust: cross-compile para Linux musl/gnu, ARM64, macOS e Windows sem toolchains manuais. CI, Docker e comparativo com cross.

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:

  1. linker do alvo (ld, lld, link.exe, ld64);
  2. C runtime e libc (glibc vs musl, versões diferentes);
  3. headers e .so/.a de dependências nativas (OpenSSL, libpq, zlib);
  4. build scripts (build.rs) que chamam pkg-config no 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:

  1. intercepta o build do projeto e das crates;
  2. configura o Rust para o --target pedido;
  3. usa o Zig como linker (zig cc / zig c++) e, quando necessário, como compilador C para cc crates;
  4. 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

AbordagemComo funcionaPrósContrasQuando preferir
cargo-zigbuildZig como linker/toolchain CRápido de instalar; ótimo no Mac→Linux; CI enxutaCrates C complexas ainda falham; depende do ZigCLIs, agentes, APIs sem OpenSSL do sistema
crossDocker com toolchain por targetAmbientes maduros; muitas libs nativasImagens pesadas; precisa Docker; mais lento no notebookMonorepos com deps nativas difíceis
Build na imagem de destinocargo build dentro do container final/baseMáxima fidelidade ao runtimeCache pior; CI mais lenta; multi-arch via QEMUQuando “igual produção” > velocidade
QEMU + rustc no alvoEmula a CPU do targetFunciona até em edge casesMuito 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?

TargetLibcBinário típicoBom paraCuidado
x86_64-unknown-linux-gnuglibcdinâmicoUbuntu/Debian “normal”Versão mínima de glibc do host de build vs produção
x86_64-unknown-linux-muslmuslestático (na prática)Alpine, scratch, distroless, “um binário e pronto”Algumas crates C se comportam diferente
aarch64-unknown-linux-*gnu/muslARM64Graviton, Raspberry, edge, K8s ARMMeç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/git e, se couber, sccache;
  • matrix só quando os targets tiverem features diferentes — dois cargo zigbuild sequenciais no mesmo job costumam ser mais simples;
  • rode cargo test no 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:

  1. stage builder (qualquer arch de CI) gera o binário com cargo zigbuild --target <arch-unknown-linux-musl>;
  2. stage final só copia o binário para gcr.io/distroless/static, scratch ou 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-config retorna 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 -dev da 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:

  1. app Axum mínima + rustls;
  2. cargo zigbuild para x86_64 e aarch64 musl;
  3. Dockerfile distroless;
  4. 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.

  1. rode com RUST_LOG/-v se disponível no wrapper;
  2. confirme zig version e o triple exato;
  3. tente o mesmo crate sem dependências nativas (reproduz o mínimo);
  4. se só uma crate C falha, isole com features;
  5. 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:

Quando o artefato deixa de ser o gargalo, sobra tempo para o que importa: código claro, testes honestos e serviço no ar.