Como o Cargo Pensa Sobre Dependências e Builds
O Cargo, à primeira vista, parece um gerenciador de pacotes no estilo npm ou pip: um arquivo de manifesto lista dependências, um lockfile fixa versões e um comando compila o projeto. Por baixo dessa superfície familiar, o Cargo toma um conjunto específico de decisões de design, mais visivelmente em torno de como ele resolve features em todo um grafo de dependências e como ele trata um projeto multi-crate como uma única unidade de build, que moldam como os projetos Rust reais são estruturados.
Esta página é a âncora conceitual para o restante da seção Cargo. Ela cobre a relação manifest/lockfile, a mecânica da unificação de features (uma fonte frequente de bugs confusos do tipo "por que habilitar X aqui afetou Y ali") e workspaces como a unidade que o Cargo realmente compila e resolve, não o crate individual.
Resumo
- O Cargo resolve um grafo de dependências por workspace, registra o resultado exato em
Cargo.locke unifica as flags de features em todos os crates que esse grafo contém. - Por Que Importa: Uma resolução única e unificada mantém os builds reproduzíveis e garante que quaisquer dois crates que linkam a mesma dependência linkam a mesma versão compilada dela, o que previne uma categoria inteira de bugs de símbolo duplicado e incompatibilidade de ABI.
- Conceitos Chave: manifesto, lockfile, resolução semver, unificação de features, workspace, resolver.
- Quando Usar: Entender este modelo é mais importante ao depurar "por que essa feature está habilitada quando não a pedi", ao estruturar um projeto multi-crate, ou ao decidir se um monorepo deve ser um workspace ou vários.
- Limitações / Trade-offs: A unificação de features e a resolução de grafo único trocam algum isolamento por consistência garantida; uma feature habilitada por um crate em um workspace realmente afeta todo outro crate que compartilha essa dependência.
- Tópicos Relacionados: versionamento semântico, features do Cargo, publicação no crates.io, scripts de build.
Fundamentos
Todo projeto Cargo tem dois arquivos que parecem semelhantes, mas servem a propósitos opostos. Cargo.toml, o manifesto, expressa intenção: ele lista dependências como intervalos de versão (serde = "1.0" significa "qualquer release 1.x, compatível pelas regras semver") junto com metadados do pacote, features e perfis de build. Cargo.lock, o lockfile, registra o fato: a versão exata de cada crate, direta e transitiva, que o Cargo realmente resolveu e compilou na última vez que foi executado.
Essa divisão existe para resolver uma tensão real. Um manifesto que fixasse versões exatas em todos os lugares tornaria cada atualização de dependência uma tarefa manual, crate por crate. Um manifesto sem fixação alguma tornaria os builds não reproduzíveis, puxando silenciosamente uma dependência transitiva mais nova entre um build e o próximo. A resposta do Cargo é deixar o manifesto flexível enquanto o lockfile permanece exato, e tratar o lockfile como a fonte da verdade para o que realmente é compilado até que alguém execute deliberadamente cargo update.
O Cargo segue as convenções de versionamento semântico do Rust ao resolver um intervalo como "1.0": significa "maior ou igual a 1.0.0, menor que 2.0.0", com a suposição de que uma release 1.x nunca quebra a API pública da qual um consumidor 1.0 depende. Uma versão 0.x inicial é tratada mais estritamente, pois crates pré-1.0 usam a posição 0.MINOR.PATCH da mesma forma que crates estáveis usam MAJOR.MINOR, então "0.8" significa especificamente "compatível com 0.8.x", não "0.x.".
A convenção prática que decorre dessa divisão: aplicações e raízes de workspace quase sempre commitem Cargo.lock ao controle de versão, porque um binário implantado precisa das mesmas versões de dependência exatas a cada vez que é construído. Bibliotecas publicadas no crates.io geralmente não commitem seu próprio lockfile, porque o trabalho de uma biblioteca é compor corretamente no grafo de dependências que seu consumidor já possui, não ditar um.
Mecânicas e Interações
O resolver de dependências do Cargo não roda uma vez por crate. Ele roda uma vez por workspace, percorrendo o manifesto de cada crate membro e cada dependência transitiva, e produz um único grafo resolvido registrado em um Cargo.lock compartilhado. Este modelo de grafo único é a causa raiz do comportamento que os desenvolvedores geralmente acham mais surpreendente sobre o Cargo: unificação de features.
Se o crate A em um workspace depende de serde sem a feature derive, e o crate B depende de serde com derive habilitado, ambos os crates recebem serde compilado com derive ativado. O Cargo não compila duas cópias diferentes de serde, uma com a feature e outra sem, porque as features são aditivas por design (ativar uma não deve remover capacidade), então o resolver simplesmente pega a união de todas as requisições de feature para uma determinada dependência em todo o grafo e compila essa única configuração em todos os lugares.
# crate-a/Cargo.toml
[dependencies]
serde = "1.0" # sem features explícitas
# crate-b/Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
# Resultado: serde é compilado com `derive` habilitado
# PARA AMBOS crate-a e crate-b, mesmo que crate-a nunca o tenha pedido.Isso é um trade-off deliberado, não uma falha. Garante que quaisquer dois crates no grafo linkem a exata mesma versão compilada de uma dependência compartilhada, o que evita erros de símbolo duplicado e, para crates com código unsafe que dependem de um layout de memória específico, evita incompatibilidades de ABI genuinamente inseguras. O resolver introduzido na edição de 2021 (resolver = "2") estreitou um pouco o escopo da unificação, de modo que features requisitadas apenas por dev-dependencies ou apenas para uma plataforma de destino diferente não vazem para o build normal, mas a unificação dentro do grafo que realmente é compilado junto ainda é a regra, não a exceção.
[workspace.dependencies] existe especificamente para tornar essa unificação previsível em vez de acidental. Declarar uma dependência uma vez na raiz do workspace e ter cada membro referenciando-a com dep = { workspace = true } significa que toda a equipe vê um lugar onde um conjunto de versão ou features é decidido, em vez de descobrir a configuração unificada real apenas depois que o Cargo a resolve.
Considerações Avançadas e Aplicações
Um workspace é a unidade de build coordenada do Cargo, e entendê-lo como um conceito de primeira classe, não apenas "uma pasta com múltiplos crates dentro", explica a maioria das decisões estruturais em projetos Rust multi-crate reais. Um Cargo.toml raiz de workspace com uma tabela [workspace] lista os crates membros; se esse arquivo raiz não tiver uma seção [package] própria, ele é um workspace virtual, comum em monorepos que distribuem vários binários e bibliotecas independentes sem um "crate principal" único.
Todo membro de um workspace compartilha um Cargo.lock, um grafo de dependências resolvido e um diretório de saída de build target/. Isso tem consequências operacionais reais: cargo build --workspace compila todos os membros juntos, compartilhando artefatos de dependências já compilados entre eles, o que é tipicamente mais rápido do que compilar cada crate isoladamente, mas também significa que um erro de compilação no grafo de dependências de um membro pode impedir cargo check de ter sucesso para todo o workspace, mesmo para membros que não tocam no caminho de código quebrado.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
| Crate único + módulos | Modelo mental mais simples; um manifesto, um lockfile | Sem versionamento independente ou cadência de publicação por módulo | Apps e bibliotecas pequenas a médias |
| Workspace Cargo (repositório único) | Lockfile compartilhado garante versões consistentes; builds incrementais rápidos | Unificação de features afeta todos os membros; grafos grandes tornam rebuilds completos lentos | Crates relacionados (API, CLI, domínio compartilhado) lançados juntos |
| Múltiplos repositórios | Isolamento completo; cadência de release independente por crate | Tipos compartilhados saem de sincronia; mais difícil testar mudanças cross-crate juntas | Equipes que possuem cronogramas de release separados para produtos genuinamente separados |
| Ferramentas de monorepo poliglota (Bazel, Nx) | Lida com linguagens não-Rust junto com Rust | Perde a ergonomia nativa do Cargo; investimento em tooling mais íngreme | Monorepos de linguagens mistas além de Rust puro |
A escolha entre um workspace e repositórios separados é realmente uma escolha sobre quão acoplados os ciclos de release de um conjunto de crates devem ser. Crates que sempre precisam ser testados e implantados juntos (um servidor API e seus tipos de domínio compartilhados) se beneficiam do lockfile único de um workspace, que torna "estas versões foram testadas juntas" verdade por construção, em vez de por convenção. Crates com ciclos de vida genuinamente independentes, especialmente aqueles publicados separadamente no crates.io com seu próprio semver, frequentemente funcionam melhor como repositórios separados precisamente para evitar o acoplamento de unificação de features que um workspace introduz.
A publicação complica um pouco o modelo de workspace, porque o crates.io não tem conceito de workspace: cargo publish opera em um pacote por vez, e qualquer dependência path = "../other-crate" deve resolver para um número de versão publicado antes que esse pacote possa ser publicado. publish = false no manifesto de um membro marca crates apenas para uso interno para que automações do tipo cargo publish --workspace, ou scripts de CI que iteram sobre membros, os pulem com segurança.
Equívocos Comuns
- "
cargo updatemuda o que meu código depende" - ele apenas reescreveCargo.lockdentro dos intervalos de versão já declarados emCargo.toml; adotar uma nova versão principal ainda requer editar o manifesto manualmente. - "Cada crate em um workspace obtém sua própria resolução de dependências" - a resolução acontece uma vez para todo o workspace, que é exatamente por que habilitar uma feature em um membro pode mudar o comportamento compilado de uma dependência compartilhada usada por outro.
- "A unificação de features é um bug que o Cargo deveria corrigir" - é uma garantia de consistência deliberada; a alternativa, compilar múltiplas cópias da mesma dependência com features diferentes, arriscaria símbolos duplicados e incompatibilidades de ABI inseguras para crates
unsafe. - "Bibliotecas devem sempre commitar Cargo.lock como aplicações fazem" - bibliotecas geralmente o omitem, porque seu trabalho é compor no grafo que um consumidor já possui, não forçar um conjunto específico de versões transitivas em todos os usuários downstream.
- "Um workspace é apenas uma pasta com vários arquivos Cargo.toml dentro" - é um conceito específico do Cargo com um lockfile compartilhado, um grafo resolvido compartilhado e um diretório
target/compartilhado, o que muda o comportamento de build e de features em comparação a tratar cada crate como totalmente independente. - "Intervalos de versão como
\"1.0\"significam exatamente a versão 1.0.0" - pela convenção semver do Rust,"1.0"significa qualquer release 1.x compatível; um pin exato requer a sintaxe"=1.0.0"explicitamente.
FAQs
Qual é a diferença real entre Cargo.toml e Cargo.lock?
Cargo.toml declara intervalos de versão de dependência e metadados, a intenção. Cargo.lock registra as versões exatas que o Cargo resolveu e compilou, o fato. Builds usam o lockfile quando um está presente e ainda satisfaz os intervalos do manifesto.
Por que cargo update não mexe no Cargo.toml?
Porque seu trabalho é mais restrito: atualizar o lockfile para as versões mais novas que ainda satisfazem os intervalos já declarados no manifesto. Adotar uma versão fora desses intervalos, como uma nova versão principal, requer editar Cargo.toml diretamente.
Devo commitar Cargo.lock ao controle de versão?
Sim para aplicações e raízes de workspace, para que cada build e deploy use versões de dependência idênticas. Geralmente não para bibliotecas standalone publicadas no crates.io, já que uma biblioteca deve compor no grafo que seu consumidor já possui, não forçar um conjunto específico de versões transitivas em todos os usuários downstream.
O que exatamente é unificação de features?
Quando dois ou mais crates no mesmo grafo resolvido dependem da mesma dependência com diferentes features habilitadas, o Cargo compila uma configuração com a união de todas as features requisitadas, em vez de cópias separadas por conjunto de features.
Por que habilitar uma feature em um crate afeta outro crate que eu não toquei?
Porque o Cargo resolve um grafo de dependências por workspace, não um por crate. Se ambos os crates dependem da mesma versão de uma dependência compartilhada, eles recebem a mesma configuração compilada dela, incluindo qualquer feature que um deles requisitou.
Resolver = "2" desliga a unificação de features?
Não, ele a estreita. Ele impede que features requisitadas apenas por dev-dependencies ou apenas para outras plataformas de destino vazem para o build normal, mas crates que são realmente compilados juntos dentro de um mesmo target ainda recebem features unificadas.
O que é um workspace virtual?
Um Cargo.toml raiz de workspace contendo apenas uma tabela [workspace], sem uma seção [package] própria. Comum para monorepos que distribuem vários crates independentes sem um pacote principal único na raiz.
Membros de workspace compartilham um diretório target?
Sim, um diretório target/ na raiz do workspace contém artefatos de build para cada membro, o que permite que dependências compartilhadas compilem uma vez e sejam reutilizadas entre membros em vez de uma vez por crate.
Posso publicar um workspace inteiro no crates.io de uma vez?
Não como uma operação única; crates.io não tem conceito de workspace. cargo publish roda por pacote, e dependências path entre membros devem resolver para números de versão publicados antes que o pacote dependente possa publicar.
Quando devo usar um workspace em vez de repositórios separados?
Quando os crates envolvidos são sempre testados e lançados juntos, como um servidor API e seus tipos de domínio compartilhados, para que um lockfile único garanta "estas versões foram testadas juntas" por construção. Crates lançados independentemente frequentemente se encaixam melhor em repositórios separados.
O que workspace.dependencies realmente resolve?
Centraliza declarações de versão e features para uma dependência em um único lugar na raiz do workspace, para que os membros a referenciem com dep = { workspace = true } em vez de cada um repetir (e potencialmente discordar sobre) a mesma string de versão.
Por que um membro quebrado às vezes bloqueia todo o build do workspace?
Porque cargo check/build --workspace resolve e frequentemente compila o grafo compartilhado junto; um erro de compilação em uma dependência compartilhada por múltiplos membros pode bloquear o comando em todo o workspace mesmo para membros que não chamam o código quebrado diretamente.
Relacionados
- Cargo.toml Explicado - campos do manifesto, tabelas de dependência e sintaxe de versão
- Gerenciamento de Dependências -
cargo update,cargo audite higiene da cadeia de suprimentos - Features e Dependências Opcionais - composição de features e gating
cfg - Workspaces - layout prático de workspace e comandos
- Noções Básicas do Cargo -
new,build,teste comandos do dia a dia
Versões da Stack: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, e Polars 0.46+.