- O sistema de gerenciamento de dependências do Rust facilita o desenvolvimento, mas a quantidade e a qualidade das dependências são uma preocupação
- Até Crates bem utilizadas podem não estar atualizadas, então às vezes é melhor implementar por conta própria
- Depois de adicionar Crates conhecidas como Axum e Tokio, o total de linhas de código incluindo dependências chegou a 3,6 milhões, algo difícil de administrar
- O código que realmente escrevi tem só cerca de 1.000 linhas, mas na prática é impossível revisar e auditar o código ao redor
- Não existe uma solução clara sobre expandir a biblioteca padrão do Rust ou sobre como implementar a infraestrutura central, e toda a comunidade precisa pensar junto no equilíbrio entre desempenho, segurança e manutenção
Visão geral do problema de dependências no Rust
- Rust é minha linguagem favorita, e a comunidade e a usabilidade da linguagem são excelentes
- A produtividade no desenvolvimento é alta, mas recentemente surgiram preocupações no aspecto de gerenciamento de dependências
Vantagens dos Crates do Rust e do Cargo
- Com o Cargo, é possível fazer gerenciamento de pacotes e automação do processo de build, o que melhora muito a produtividade
- É fácil migrar entre vários sistemas operacionais e arquiteturas, sem precisar se preocupar com gerenciar arquivos manualmente ou configurar ferramentas de build
- Dá para começar a escrever código imediatamente, sem ter que pensar em gerenciamento de pacotes à parte
Desvantagens do gerenciamento de Crates no Rust
- Como se pensa menos no gerenciamento de pacotes, acaba-se dando menos atenção à estabilidade
- Por exemplo, usei o crate dotenv, mas descobri por meio de um Security Advisory que sua manutenção foi descontinuada
- Considerei um crate alternativo (
dotenvy), mas acabei implementando diretamente só a parte de que precisava, em cerca de 35 linhas
- Como problemas de pacotes sem manutenção acontecem com frequência em várias linguagens, a essência do problema está em situações em que a dependência é inevitável
O aumento explosivo de volume de código causado pelas dependências
- Estou usando pacotes importantes e de boa qualidade do ecossistema Rust, como Tokio e Axum
- Como dependências, adicionei Axum, Reqwest, ripunzip, serde, serde_json, tokio, tower-http, tracing e tracing-subscriber
- O objetivo principal é ter um servidor web, descompressão de arquivos e logging, então o projeto em si é simples
- Usei o recurso Cargo vendor para baixar localmente todos os crates dependentes
- Ao analisar as linhas de código com o tokei, cheguei a cerca de 3,6 milhões de linhas incluindo dependências (ou cerca de 11.136 linhas sem contar os crates vendorizados)
- Como referência, diz-se que o kernel Linux inteiro tem cerca de 27,8 milhões de linhas, então meu pequeno projeto equivale a aproximadamente um sétimo disso
- O código que eu realmente escrevi tem só cerca de 1.000 linhas
- Na prática, é impossível monitorar e auditar tanta linha de código de dependências
Reflexões sobre uma solução
- No momento, não há uma solução clara
- Alguns defendem expandir a biblioteca padrão como no Go, mas isso também traz novos problemas, como o peso de manutenção
- Como o Rust busca alto desempenho, segurança e modularidade, além de competir com embarcados e C++, a expansão da biblioteca padrão precisa ser tratada com cautela
- Por exemplo, até um runtime sofisticado como o Tokio é mantido de forma muito ativa no GitHub e no Discord
- Na prática, implementar por conta própria infraestruturas centrais como runtime assíncrono ou servidor web está além do alcance de um desenvolvedor individual
- Até um serviço de grande porte como a Cloudflare usa dependências de tokio e crates.io como estão, e não está claro com que frequência faz auditorias
- A Clickhouse também menciona problemas relacionados ao tamanho dos binários e à quantidade de crates
- Com o Cargo, é difícil identificar com precisão quais linhas de código entram no binário final, e há a limitação de incluir também código desnecessário para cada plataforma
- No fim, a realidade é que só resta pedir uma resposta à comunidade como um todo
3 comentários
Ao rodar o Trivy, ele mostra muito menos vulnerabilidades high ou critical, e parece mais seguro do que o ecossistema de JS com NPM ou de Java com Maven; então o que exatamente esse texto quer afirmar sobre Rust?
Opiniões no Hacker News
import foolib, e ninguém se importa com o que tem lá dentro. Em cada etapa, você precisa de só uns 5% das funcionalidades, mas quanto mais profunda a árvore, mais código inútil se acumula. No fim, um binário simples vira 500MiB, e você acaba puxando uma dependência por causa de uma única formatação numérica. Go e Rust incentivam colocar tudo em um único arquivo, então fica difícil quando você quer usar só uma parte. A verdadeira solução no longo prazo seria um rastreamento ultrafino de símbolos/dependências, em que toda função/tipo declara exatamente o que precisa, usa-se só o código necessário e o resto é descartado. Pessoalmente não gosto muito dessa ideia, mas não consigo pensar em outra forma de resolver o sistema atual, que sai varrendo o universo inteiro da árvore de dependênciasfeature flag, mas quase todas as dependências realmente precisavam continuar por causa das funcionalidades necessárias (vulkan, decodificação de PNG, unicode shaping etc.). As dependências desnecessárias eram, em geral, muito pequenas, e só removiserde_jsoncom uma pequena modificação. Dependências maiores (winit/wgpuetc.) exigem mudanças estruturais, então não dá para tirar facilmente.opara cada função e juntava-se tudo em um arquivo.a, e o linker puxava só as funções necessárias. O namespacing também era feito comofoolib_do_thing(). Hoje, com um padrão tipo god object, todas as funções ficam em um objeto de topo, então ao importarfoolibvocê arrasta tudo. Nesse estado, fica difícil para o linker saber quais funções são realmente necessárias. Em compensação, Go é excelente em remoção de código morto, então o que não é usado é cortado do resultado da compilaçãomin-sized-rustisEven,isOddeleftpaddo npm, bibliotecas grandes e genéricas mantidas por equipes federadas oferecem muito mais garantia de futuro e continuidade--gc-sectionsglobdeveria ser só uma função simples de globbing, mas o autor também empacota uma ferramenta de linha de comando e adiciona um parser grande como dependência. Isso gera alertas frequentes de "dependency out-of-date". Além disso, o escopo de responsabilidade da própria bibliotecaglobtambém é discutível. Fazer apenas matching de padrões de string é um design mais flexível (facilita testes e abstração de sistema de arquivos). Muita gente quer bibliotecas onipotentes de "faz tudo", mas quanto mais elas crescem, maiores os efeitos colaterais. Imagino que Rust não seja tão diferentestdlib::data_structures::automata::weighted_finite_state_transducer. Como a própria linguagem embute versionamento e compatibilidade retroativa, dá para esperar melhorias no futuroglobdo POSIX realmente percorre o sistema de arquivos. Para matching de strings, existefnmatch. O ideal seria manterfnmatchem um módulo separado e fazer com que ele fosse uma dependência deglob. Implementarglobpor conta própria é bem difícil, porque há exigências complexas como estrutura de diretórios, brace expansion etc., então é preciso uma boa composição de funções bem projetadasglobcapability systempara isolar com segurança toda a árvore de bibliotecas. Por exemplo, ao projetar uma biblioteca de carregamento de imagens, ela deveria processar apenas um stream, e não arquivos diretamente, ou então declarar explicitamente que "não tem permissão para abrir arquivos", bloqueando em tempo de compilação o uso de funções perigosas. Isso não é fácil nos ecossistemas atuais, mas, se fosse feito direito, reduziria ao mínimo a superfície de ataque. Uma cultura de minimizar dependências também não resolve isso na raiz, e linguagens como Go também não estão livres de ataques à supply chain#![deny(unsafe_code)], é possível transformar o uso de código unsafe em erro de compilação e informar isso ao usuário. Não é uma verificação forçada em todos os casos, e se você permitir explicitamente ainda pode usar código unsafe. Dá para imaginar umcapability systemque controle transitivamente recursos da biblioteca padrão, como umafeatureflagtls,x509,base64etc.) doem na hora de escolher e gerenciar bibliotecasimporte fazer tudo por injeção de dependência. Se você não injetar algo como um subsistema de IO, o código de terceiros nunca consegue acessar essa área. Se quiser conceder só leitura, pode injetar um wrapper com apenas leitura. Em programação de sistemas, porém, há limitações (por causa deunsafe codeetc.)dom0, cada biblioteca em uma VM de template separada e a comunicação feita por namespaces de rede. Em setores sensíveis, isso pode ser práticoblessed.rsrecomenda uma lista de bibliotecas úteis que são difíceis de colocar na biblioteca padrão. Gosto disso porque, com esse sistema, a maioria dos pacotes fica limitada a objetivos específicos e mais fácil de gerenciarcargo-vettambém vale muito a recomendação. Ele permite rastrear e definir pacotes confiáveis, desde políticas em que certos pacotes exigem auditoria de especialistas antes da importação até algo semi-YOLO como "pacotes mantidos pelos mantenedores do tokio a gente simplesmente confia". É mais formal queblessed.rse muito bom para compartilhar uma lista oficial ou semi-oficial dentro da equipeleftpad, ficou uma imagem negativa dos package managers. Coisas comotokiosão, na prática, funcionalidades de nível de linguagem, então seria irrealista esperar que o OP quisesse auditar por conta própria o Go inteiro ou até o V8 do Nodetokiocontinuamente. Não são muitas pessoas, mas ainda assim alguém faz issocargoincluir duas versões quando duas dependências usam versões diferentes é um suporte bastante característico delefeature flagsem pacotescargosão realmente ótimos. Eu frequentemente abro PRs para esconder dependências desnecessárias atrás desses flags. Dá para ver a árvore de dependências facilmente comcargo tree. Uma visão de linhas de código que realmente entram no binário não significa muita coisa; quando funções são inlined, em geral quase tudo vai parar dentro demainfeature flagsno npm. Fico curioso se existe algum package manager que já suporte isso. Eu gostaria de isolar, dentro de bibliotecas internas, código dependente de certos frameworks para poder expandi-lasasyncpode ser implementado do jeito que você quiser. Você não fica preso a uma implementação específicaNão é um problema exclusivo do Rust.
É uma vantagem comum e, ao mesmo tempo, um problema potencial compartilhado por todas as linguagens que têm repositórios públicos de pacotes e gerenciadores de pacotes com suporte a dependências transitivas.
No fim das contas, quem usa é que precisa usar direito...
Mesmo depois do caso do leftpad no Node&npm, nada mudou.