Diferencias en el manejo de versiones entre RubyGems y npm
Gareth Visagie
14 de diciembre de 2016
0 minutos de lecturaRubyGems y npm son los administradores de paquetes de facto para Ruby y Node.js.
A primera vista parecen similares (¡porque lo son!), pero al crear un producto que interactúa con ambos, hay diferencias sutiles que se deben tener en cuenta.
Hace poco agregamos compatibilidad con proyectos de Ruby en Snyk. Las diferencias entre el manejo de versiones en RubyGems y npm presentaron algunos desafíos en el proceso.
En esta publicación del blog describimos esas diferencias, los problemas que causaron y cómo los resolvimos.
Versionado semántico
Los desarrolladores de Node.js y Ruby interactúan con el estándar de versionado semántico principalmente a través de estos administradores de paquetes. Como resultado, es fácil tener la impresión de que la forma en que cada administrador de paquetes maneja las versiones forma parte de SemVer. Como veremos, no es así.
¿Qué es SemVer?
El versionado semántico (SemVer) es un estándar para los números de versión de software que busca ayudar a quienes usan bibliotecas a comprender el impacto de las nuevas versiones, simplemente comparando los números de versión.
El software que sigue el estándar SemVer debe identificarse con números de versión compuestos. Los cambios en cada parte del número compuesto indican distintos tipos de cambios en el software.
Citemos directamente http://semver.org:
Dado un número de versión MAJOR.MINOR.PATCH, incrementa:1. la versión MAJOR cuando hagas cambios incompatibles en la API;2. la versión MINOR cuando agregues funcionalidad de manera compatible con versiones anteriores; y3. la versión PATCH cuando corrijas errores de manera compatible con versiones anteriores.Hay etiquetas adicionales para versiones preliminares y metadatos de compilación, disponibles como extensiones del formato MAJOR.MINOR.PATCH.
El estándar SemVer también especifica reglas de precedencia, es decir, cómo se deben comparar las versiones al ordenarlas.
¿Qué no es SemVer?
La mayoría de los administradores de paquetes (si no todos) permiten especificar «rangos» de versiones. Especificar dependencias de software como rangos, por ejemplo, >= 5.0 and < 6, permite usar bibliotecas de software en una mayor variedad de situaciones.
Esta «especificación de rangos» no forma parte de SemVer, aunque sí lo hacen las reglas de comparación. Esto significa que cada administrador de paquetes puede usar su propia sintaxis y sus propias reglas para especificar rangos, por ejemplo, >= 5, ~ 1.2, ^2.3.0, >5 || <2.
La sintaxis de rangos de RubyGems y npm es muy similar (más información más adelante), así que, si sueles usar ambos, es fácil empezar a pensar que esa sintaxis forma parte del estándar, aunque no sea así.
Números de versión en npm y RubyGems
npm y RubyGems tienen criterios distintos sobre lo que constituye un número de versión válido.
En npm, las versiones deben cumplir estrictamente con Semantic Versioning 2.0. Cualquier número de versión que no especifique una versión major, minor y patch no es válido.
RubyGems tiene una definición más flexible de lo que es una versión válida y permite un número ilimitado de componentes; por ejemplo, 5.1.0.2.1.0 es perfectamente válido. Se recomienda que los autores de gemas sigan el estándar de versionado semántico, pero no se exige de forma programática.
A pesar de esto, las versiones de 4 componentes son bastante comunes en el ecosistema de RubyGems. En Snyk las encontramos con frecuencia, ya que muchas gemas (en especial Rails y sus componentes) usan el cuarto componente para correcciones de seguridad (por ejemplo, una vulnerabilidad en 5.0.0 se corrige en 5.0.0.1).
Rangos de versiones en npm y RubyGems
RubyGems y npm tienen un conjunto similar de operadores para especificar rangos de versiones.
El manejo de rangos en RubyGems forma parte de la biblioteca estándar de Ruby. En npm, se gestiona mediante el paquete semver.
Sintaxis de rangos simples
Los operadores de rangos simples >, >=, <, >= y = funcionan de forma idéntica en RubyGems y npm. RubyGems también ofrece el operador !=, que npm no admite.
Sintaxis de rangos avanzada
Tanto npm como RubyGems permiten expresar rangos complejos de forma concisa. npm ofrece más operadores que RubyGems, lo que permite definir una variedad un poco mayor de rangos complejos.
Ambos ofrecen un operador ~>, que especifica rangos «pesimistas». En términos generales, significa «aceptar cualquier versión desde esta hasta un límite que probablemente sea seguro, según lo indicado por semver». Sin embargo, el comportamiento exacto es sutilmente diferente en RubyGems y npm.
RubyGems lo interpreta como «permitir versiones superiores que solo cambien el componente de versión menos significativo», mientras que npm lo interpreta como «permitir solo cambios de patch si se especifica una versión patch; de lo contrario, permitir solo cambios de versión minor». Este ejemplo lo ilustra mejor:
rango | Interpretación de RubyGems | Interpretación de npm |
|---|---|---|
|
| error, versión no válida |
|
|
|
|
|
|
¿Qué problemas causó esto?
Al principio, supusimos que el manejo de versiones en RubyGems era un superconjunto del de npm, así que usamos el paquete semver para operar con los rangos de ambos.
Esto produjo algunos resultados sorprendentes durante las pruebas alfa. Casi todos los repositorios de Ruby que probamos parecían extremadamente vulnerables, incluso las aplicaciones de Rails completamente actualizadas.
Al comparar los informes de pruebas automatizadas con los elaborados manualmente, detectamos muchos falsos positivos (y algunos falsos negativos). Al investigar más, descubrimos que la causa era la diferencia entre la semántica de rangos de RubyGems y npm.
El problema de fondo: una versión que cumple un rango determinado en RubyGems no necesariamente cumple ese mismo rango en npm.
Necesitábamos otro enfoque.
¿Cómo resolvimos este problema?
Snyk comenzó ofreciendo compatibilidad solo con npm, así que el código de nuestro producto ya usaba el paquete semver de npm para las operaciones con versiones y rangos.
Decidimos crear una biblioteca similar para ofrecer un manejo de versiones al estilo de RubyGems, con la misma API que el paquete semver de npm.
Luego, nuestro sistema decide cuál de estas bibliotecas integrar según el administrador de paquetes que use el código que estamos probando, monitoreando y corrigiendo.
¿Cómo implementamos ruby-semver?
El manejo de versiones de RubyGems forma parte de la biblioteca estándar de Ruby. En lugar de volver a implementarlo, decidimos usar Opal para transpilar a JavaScript las partes correspondientes de RubyGems.
Al principio intentamos transpilar directamente las clases desde el código fuente de Ruby. Resultó imposible debido a las diferencias entre la compatibilidad con expresiones regulares de JavaScript y Ruby.
Al final, copiamos dos clases de la biblioteca estándar de Ruby a nuestro repositorio, adaptamos las expresiones regulares para que fueran compatibles con JavaScript y agregamos un script para transpilar ese código fuente de Ruby a JavaScript.
Una vez que el código transpilado funcionó en un REPL de Node.js, agregamos un conjunto de pruebas sólidas para la API al estilo de npm-semver que queríamos. Después, escribimos el adaptador más sencillo posible entre la API de npm SemVer y el código transpilado de RubyGems.
Funcionó bien, aunque depende de un buen transpilador de Ruby a JavaScript. Habría sido posible implementar el comportamiento manualmente, pero habría requerido más trabajo y, aún más importante, podríamos haber pasado por alto casos límite no evidentes que RubyGems ya maneja.
Conclusión
Agregar compatibilidad con Ruby en Snyk terminó revelando algunos desafíos interesantes relacionados con las distintas formas en que los lenguajes y los administradores de paquetes manejan el versionado. Los rangos para versiones SemVer son aún más problemáticos, ya que no están definidos en absoluto.
Comprender estos desafíos nos permitió entender mejor SemVer y sus limitaciones, y, en última instancia, reforzar la base sobre la que podemos agregar compatibilidad con otros ecosistemas.
Si trabajas con Ruby y Node.js, echa un vistazo a ruby-semver. Publicamos el código como software de código abierto con la esperanza de que le resulte útil a alguien más.
Y agrega Snyk a tus proyectos de Ruby. Estamos agregando activamente compatibilidad con nuevos tipos de proyectos de Ruby (incluidas las gemas; ¡mantente al tanto!).
Empieza con los desafíos de Capture the Flag
Aprende a resolver desafíos de captura la bandera con nuestro taller virtual introductorio a pedido.

