Este não é um exemplo educativo inventado para a ocasião. Cada trecho de código abaixo vem da raiz Cargo.toml do Aurabase e seus manifestos de caixa, conforme existem no repositório hoje - incluindo duas discrepâncias que encontramos ao relê-las para este artigo e que estamos documentando como estão, em vez de corrigi-las silenciosamente antes da publicação.
- O espaço de trabalho raiz Cargo do Aurabase declara 18 membros explícitos: 11 serviços, a CLI
aura-cli, 5 bibliotecas compartilhadas e o SDKaurabase-rs— emresolver = "2"e um únicoCargo.lock. [workspace.dependencies]centraliza versões compartilhadas; cada caixa herda{ workspace = true }em vez de definir seu próprio número - exceto quando uma caixa é substituída, silenciosamente.- Uma pasta em
services/não é automaticamente um membro do espaço de trabalho: a listamembersé explícita, não um glob, precisamente para poder excluir código não-Rust. [profile.release]se aplica apenas uma vez a todo o espaço de trabalho — uma escolha comopanic = "unwind"se aplica a todos os onze serviços de uma vez.
Um depósito por serviço ou apenas um Cargo.lock?
Um espaço de trabalho Cargo agrupa várias caixas em um único Cargo.lock e um único diretório de destino - a mesma funcionalidade que o gerenciador de pacotes do Rust fornece para este caso. Onze binários Rust separados, cada um em seu próprio repositório, parecem mais independentes à primeira vista. Na prática, isso significa onze Cargo.lockdistintos, onze resoluções de versão que podem divergir ao longo do tempo e nenhuma garantia de que dois serviços compilem a mesma versão deaxum ou sqlx.
Um espaço de trabalho Cargo resolve isso no nível do gerenciador de pacotes, não no nível de disciplina da equipe. Todos os membros compartilham um único Cargo.lock na raiz: uma dependência comum é resolvida uma vez, para uma versão idêntica, para todo o grafo. É também isso que torna um refatorador entre serviços (alterando uma assinatura em aura-core, por exemplo) visível para um único cargo build --workspace, em vez de ser descoberto serviço por serviço na produção. Esta é uma das opções que distingue nosso núcleo Rust 100% unificado de uma pilha heterogênea montada serviço por serviço.
A raiz Cargo.toml: resolvedor e membros
Tudo começa com a declaração [workspace] e sua lista members. Na Aurabase, esta lista é escrita à mão, agrupada por função — serviços, ferramentas, bibliotecas — em vez de gerada por um padrão genérico como services/*.
resolver = "2" não é um detalhe cosmético. O resolvedor v2 do Cargo isola a funcionalidade de build-dependencies e dependências específicas do alvo (target.'cfg(windows)', por exemplo) do resto do gráfico - elas não vazam mais para o binário final. Também unifica as versões da mesma dependência em todos os membros do espaço de trabalho que a compartilham: um único axum, apenas uma vez, e não onze resoluções independentes.
workspace.package: uma versão, uma edição, em princípio compartilhada
[workspace.package] declara uma vez os campos comuns — versão, edição, autores, licença — que cada caixa pode herdar com version.workspace = true em vez de copiá-los.
Todos os onze serviços do Aurabase seguem esse padrão. Duas caixas divergem, e esta é a primeira das duas discrepâncias encontradas durante a preparação deste artigo: aura-cli declara version = "0.2.0" em cópia impressa e a lib libs/aura-migrations declara version = "0.1.0" — ambos diferentes do 0.1.1 do espaço de trabalho.
version.workspace = true é opcional, campo por campo, caixa por caixa. Nada impede que uma caixa mantenha sua própria numeração — voluntariamente (ferramenta publicada separadamente, por exemplo) ou por esquecimento. Uma auditoria do espaço de trabalho deve verificar este campo, caixa por caixa, e não assumir herança.
workspace.dependencies: uma fonte de verdade, a menos que uma caixa a ignore
[workspace.dependencies] centraliza dependências compartilhadas por várias caixas. Cada serviço refere-se a ele com { workspace = true } em vez de definir sua própria restrição de versão.
Esta é a segunda discrepância real: o espaço de trabalho centraliza governor na versão 0.10, mas aura-gateway redeclara sua própria linha governor = "0.8" em vez de herdar - o gateway aplica sua limitação de taxa com a caixa governor simples, quando aura-auth, aura-ai, aura-db e aura-functions passam tower_governor em middleware. Um espaço de trabalho não impede a divergência: apenas a torna visível, se nos dermos ao trabalho de comparar.
Porém, nem todas as dependências merecem ser centralizadas. wasmtime não aparece em nenhum lugar em [workspace.dependencies]: apenas uma caixa, aura-functions, usa-o para seu tempo de execução WASM, portanto permanece declarado localmente. A regra que aplicamos: aumente uma dependência para o nível do espaço de trabalho a partir do momento em que duas ou mais caixas a compartilham, não antes.
libs/ para serviços/: que depende do que
As cinco bibliotecas compartilhadas (aura-core, aura-crypto, aura-db-adapters, aura-migrations, aura-telemetry) são declaradas como dependências de caminho em [workspace.dependencies] — aura-core = { path = "libs/aura-core" } — então cada serviço escolhe quais realmente precisa.
O gráfico resultante permanece legível de um arquivo para outro. aura-gateway depende apenas de três das cinco bibliotecas internas — aura-core, aura-crypto e aura-telemetry. Basta rotear, assinar um JWT para o sidecar PostgREST e exportar seus traces, sem tocar em aura-db-adapters nem aura-migrations. aura-provisioneradiciona aura-migrations: reproduz as migrações de esquema resultantes da criação de um projeto.
O caso mais restrito é aura-migrator, o binário dedicado que executa migrações de CI/CD. Entre as bibliotecas internas do Aurabase, sua produção [dependencies] lista apenasaura-migrations — aura-core só aparece em seu [dev-dependencies], para teste. O binário entregue à produção, portanto, não inclui nenhum códigoaura-core; ele só existe durante cargo test.
Uma caixa, vários binários: divisão aura em tempo real
Um espaço de trabalho não exige a criação de um novo membro sempre que você desejar um novo processo implementável. aura-realtime permanece um único membro do espaço de trabalho, mas seu Cargo.toml declara três tabelas [[bin]] distintas em torno do mesmo [lib]compartilhado.
O próprio manifesto documenta a fronteira entre os dois. cdc-worker captura alterações do Postgres por meio de pesquisa e publica em NATS no núcleo pub/sub (Client::publish_with_headers), sem nunca abrir um servidor WebSocket - JetStream, neste mesmo serviço, atende exclusivamente o KV de presença entre instâncias, não o fan-out do CDC. ws-front consome apenas NATS e mantém conexões WebSocket/SSE, sem nunca tocar no CDC — os detalhes completos estão em a documentação do mecanismo em tempo real. Ambos os binários compartilham o mesmo código de filtragem RLS via [lib], mas implantam e escalam de forma independente no Kubernetes. Este é o sinal certo para escolher vários binários em uma caixa em vez de um novo membro do espaço de trabalho: mesma lógica interna, diferentes topologias de implantação.
Um arquivo em services/ não é necessariamente um membro Cargo
A pasta aura-edge-runtime existe no repositório Aurabase. No entanto, ele não contém nenhum Cargo.toml - apenas TypeScript (index.ts, envelope.ts) e não aparece em nenhum lugar da lista members do espaço de trabalho raiz.
É exatamente por isso que a lista members do Aurabase é escrita à mão em vez de substituída por um padrão genérico como services/*. Um glob teria tentado incluir esta pasta não Rust no espaço de trabalho, resultando em uma falha de resolução. Uma lista explícita permite que pastas que não falam o mesmo idioma coexistam, sob o mesmo pai services/.
A lição generaliza: contar os serviços de um backend Rust listando as subpastas de services/ fornece um número falso. Somente a raiz Cargo.toml tem autoridade sobre o que realmente é compilado no espaço de trabalho.
[profile.release]: uma configuração única para todo o espaço de trabalho
Um [profile.release] declarado na raiz do espaço de trabalho se aplica a todos os membros compilados no modo release — apenas um local para ajustar, não onze. Cargo documenta todas as chaves disponíveis em sua referência de perfil de compilação ; Aurabase ativa apenas cinco.
A escolha de panic = "unwind" em vez de "abort" está documentada diretamente no comentário do arquivo: um pânico em um manipulador axum é interceptado por tokio, a tarefa retorna 500 e o processo continua a atender outras solicitações simultâneas. O ganho de desempenho deabort não compensa a perda do isolamento entre consultas.
Congelar o conjunto de ferramentas e os comandos importantes
Um espaço de trabalho unificado define versões de dependência, mas não a versão do compilador em si. rust-toolchain.toml, na raiz, congela o conjunto de ferramentas (channel = "1.93" hoje) para qualquer chamada direta para cargo ou rustc no repositório.
Este arquivo existe justamente porque ocorreu um desvio silencioso: uma ação de CI instalou o canal stable do dia sem ler rust-toolchain.toml, enquanto a imagem do Docker de lançamento compilava com a versão congelada. Um commit poderia passar em todos os testes com o Rust estável de hoje e depois quebrar na construção da imagem – descoberto após a fusão, não antes. rustuprespeita este arquivo para qualquer comando direto: foi a única correção necessária, sem afetar os fluxos de trabalho de CI.
Um último detalhe, para quem lê um manifesto antes de executá-lo: a caixa aura-cli compila um binário cuja tabela [[bin]] o nomeia aurabase, não aura. O wrapper npm publicado, @aurabase/cli, expõe os dois comandos – aura e aurabase ambos apontam para o mesmo script. O nome de uma carga [[bin]], o nome da caixa e o nome exposto por um wrapper npm são três coisas distintas; nenhum pode ser adivinhado pelos outros dois.
Checklist: onde declarar o quê
Cinco decisões surgem cada vez que você adiciona uma caixa a um espaço de trabalho do Cargo. Aqui é onde cada um é declarado, com base no exemplo do Aurabase abordado acima.
| Lista de membros | membros do [espaço de trabalho] | Root — lista explícita, nunca um globo |
|---|---|---|
| Versão/edição compartilhada | [espaço de trabalho.pacote] | Root — version.workspace = true, por caixa, opcional |
| Dependência compartilhada por mais de 2 caixas | [espaço de trabalho.dependências] | Root — então {workspace = true } em cada caixa |
| Dependência de um único consumidor | [dependências] da caixa | Localmente, sem passar pelo espaço de trabalho |
| Construir perfil | [perfil.lançamento] | Apenas root – aplica-se a todos os membros |
Para auditar um espaço de trabalho existente do Cargo – o seu ou de um projeto que você está assumindo – quatro verificações são suficientes para encontrar o tipo de discrepância documentada acima:
- Compare
[workspace.package].versioncomversionde cada caixa — um valor diferente não é necessariamente um bug, mas vale a pena documentar. - Compare
[workspace.dependencies]com as dependências declaradas localmente por cada caixa — identifique os nomes presentes em ambos os lados com versões diferentes. - Compare a lista
membersna raizCargo.tomlcom as subpastas reais no repositório - uma pasta ausente emmembersnão é necessariamente um descuido. - Verifique o nome real de cada binário compilado (
[[bin]] name) em vez de assumir que ele corresponde ao nome da caixa ou ao nome exposto por um possível wrapper npm.