Diferenças no tratamento de versões entre RubyGems e npm
Gareth Visagie
14 de dezembro de 2016
0 minutos de leituraRubyGems e npm são os gerenciadores de pacotes padrão de fato para Ruby e Node.js.
À primeira vista, eles parecem semelhantes (e são mesmo!), mas, ao criar um produto que interage com ambos, é preciso levar em conta algumas diferenças sutis.
Recentemente, adicionamos suporte a projetos Ruby no Snyk. As diferenças no tratamento de versões entre RubyGems e npm trouxeram alguns desafios ao longo do caminho.
Esta publicação explica essas diferenças, os problemas que elas causaram e como os resolvemos.
Versionamento semântico
Desenvolvedores que trabalham com Node.js e Ruby interagem com o padrão de versionamento semântico principalmente por meio desses gerenciadores de pacotes. Por isso, é fácil ter a impressão de que a forma como cada gerenciador trata as versões faz parte do próprio SemVer. Como veremos, não é bem assim.
O que é SemVer?
Versionamento semântico (SemVer) é um padrão para números de versão de software que busca ajudar quem consome bibliotecas a entender o impacto das novas versões, simplesmente comparando os números de versão.
Softwares que seguem o padrão SemVer devem ser identificados por números de versão compostos. Alterações em cada parte desse número indicam diferentes tipos de mudanças no próprio software.
Citando diretamente http://semver.org:
Dado um número de versão MAJOR.MINOR.PATCH, incremente:1. a versão MAJOR quando fizer mudanças incompatíveis na API;2. a versão MINOR quando adicionar funcionalidades de maneira compatível com versões anteriores; e3. a versão PATCH quando corrigir bugs de maneira compatível com versões anteriores.Rótulos adicionais para pré-lançamentos e metadados de build estão disponíveis como extensões ao formato MAJOR.MINOR.PATCH.
O padrão SemVer também especifica regras de precedência — como comparar versões para ordená-las.
O que não é SemVer?
A maioria dos gerenciadores de pacotes, se não todos, permite especificar “intervalos” de versões. Especificar dependências de software como intervalos, por exemplo, >= 5.0 and < 6, permite usar bibliotecas de software em uma variedade maior de cenários.
Essa “especificação de intervalos” não faz parte do SemVer, embora as regras de comparação façam. Isso significa que cada gerenciador de pacotes pode usar sua própria sintaxe e suas próprias regras para especificar intervalos, por exemplo, >= 5, ~ 1.2, ^2.3.0, >5 || <2.
A sintaxe de intervalos do RubyGems e do npm é muito semelhante (falaremos mais sobre isso adiante). Por isso, se você costuma usar ambos, é fácil começar a pensar nessa sintaxe como parte do padrão — embora não seja.
Números de versão no npm e no RubyGems
npm e RubyGems têm opiniões diferentes sobre o que é um número de versão válido.
No npm, as versões devem seguir estritamente o SemVer 2.0. Qualquer número de versão que não especifique as versões major, minor e patch é inválido.
O RubyGems tem um conceito mais flexível do que é uma versão válida e permite um número ilimitado de partes. Por exemplo, 5.1.0.2.1.0 é perfeitamente válido. Quem cria gems é incentivado a seguir o padrão de versionamento semântico, mas isso não é imposto por meios programáticos.
Apesar disso, números de versão com quatro partes são bastante comuns no ecossistema RubyGems. Na Snyk, encontramos esses casos com frequência, já que muitas gems (principalmente Rails e seus componentes) usam a quarta parte para correções de segurança (por exemplo, uma vulnerabilidade em 5.0.0 é corrigida em 5.0.0.1).
Intervalos de versões no npm e no RubyGems
RubyGems e npm têm um conjunto semelhante de operadores para especificar intervalos de versões.
O tratamento de intervalos no RubyGems faz parte da biblioteca padrão do Ruby. No npm, ele é feito pelo pacote semver.
Sintaxe simples de intervalos
Os operadores simples de intervalo >, >=, <, >= e = se comportam da mesma forma no RubyGems e no npm. O RubyGems também oferece o operador !=, que não é compatível com o npm.
Sintaxe avançada de intervalos
Tanto o npm quanto o RubyGems permitem expressar intervalos complexos de forma concisa. O npm oferece mais operadores do que o RubyGems, atendendo a uma variedade um pouco maior de intervalos complexos.
Ambos oferecem o operador ~>, que especifica intervalos “pessimistas”. Em linhas gerais, ele significa “aceite qualquer versão a partir desta, até um limite que provavelmente seja seguro, conforme indicado pelo SemVer”. No entanto, o comportamento exato é sutilmente diferente no RubyGems e no npm.
O RubyGems interpreta isso como “permitir versões mais altas que alterem apenas a parte menos significativa da versão”, enquanto o npm interpreta como “permitir apenas alterações de patch se uma versão de patch for especificada; caso contrário, permitir apenas alterações de minor”. Veja um exemplo:
intervalo | Interpretação do RubyGems | Interpretação do npm |
|---|---|---|
|
| erro, versão inválida |
|
|
|
|
|
|
Que problemas isso causou?
No início, partimos do princípio de que o tratamento de versões no RubyGems era um superconjunto do npm. Por isso, usamos o pacote semver para operações com intervalos do npm e do RubyGems.
Isso levou a alguns resultados surpreendentes nos testes alfa. Quase todos os repositórios Ruby que testamos pareciam extremamente vulneráveis — até mesmo aplicações Rails completamente atualizadas.
A comparação dos relatórios de testes automatizados com os feitos manualmente revelou muitos falsos positivos (e alguns falsos negativos). Uma investigação mais aprofundada mostrou que a culpa era da diferença entre a semântica dos intervalos no RubyGems e no npm.
O problema fundamental: uma versão que atende a determinado intervalo no RubyGems não necessariamente atende ao mesmo intervalo no npm.
Precisávamos de outra abordagem.
Como resolvemos esse problema?
A Snyk começou oferecendo suporte apenas ao npm, então o código do nosso produto já usava o pacote semver do npm para operações com versões e intervalos.
Decidimos criar uma biblioteca semelhante para oferecer o tratamento de versões no estilo RubyGems, com a mesma API do pacote semver do npm.
Em seguida, nosso sistema decide qual dessas bibliotecas integrar com base no gerenciador de pacotes usado no código que estamos testando, monitorando e corrigindo.
Como implementamos o ruby-semver?
O tratamento de versões do RubyGems faz parte da biblioteca padrão do Ruby. Em vez de reimplementá-lo, decidimos usar o Opal para transpilar as partes relevantes do RubyGems para JavaScript.
Começamos tentando transpilar as classes diretamente do código-fonte Ruby. Isso se mostrou impossível devido às diferenças no suporte a expressões regulares do JavaScript e do Ruby.
No fim, copiamos duas classes da biblioteca padrão do Ruby para nosso repositório, adaptamos as expressões regulares para que fossem compatíveis com JavaScript e adicionamos um script para transpilar esse código-fonte Ruby para JavaScript.
Depois que o código transpilado funcionou em um REPL do Node.js, adicionamos um conjunto de testes razoável para a API no estilo npm-semver que queríamos. Em seguida, criamos um adaptador o mais enxuto possível entre a API SemVer do npm e o código RubyGems transpilado.
A solução funcionou bem, embora dependa de um bom transpiler de Ruby para JavaScript. Seria possível implementar o comportamento manualmente, mas isso exigiria mais esforço e, o que é mais importante, poderia deixar passar casos extremos pouco óbvios que o RubyGems já trata.
Conclusão
Adicionar suporte a Ruby na Snyk acabou revelando alguns desafios interessantes nas diferentes formas como linguagens de programação e gerenciadores de pacotes lidam com o versionamento. Os intervalos para versões SemVer são ainda mais problemáticos, pois não são definidos de forma alguma.
Entender esses desafios nos ajudou a compreender melhor o SemVer e suas limitações, fortalecendo, em última análise, a base para oferecer suporte a outros ecossistemas.
Se você trabalha com Ruby e Node.js, conheça o ruby-semver. Abrimos o código-fonte na esperança de que ele possa ser útil para outras pessoas.
E adicione a Snyk aos seus projetos Ruby. Estamos ampliando ativamente o suporte a novos tipos de projetos Ruby (incluindo gems; fique de olho).
Comece a resolver desafios de capture the flag
Aprenda a resolver desafios de capture the flag assistindo sob demanda ao nosso workshop virtual introdutório.