Unterschiede beim Umgang mit Versionen in RubyGems und npm
Gareth Visagie
14. Dezember 2016
0 Min. LesezeitRubyGems und npm sind die De-facto-Standard-Paketmanager für Ruby und Node.js.
Auf den ersten Blick wirken sie ähnlich (und das sind sie auch!). Doch wenn Sie ein Produkt entwickeln, das mit beiden interagiert, müssen einige subtile Unterschiede berücksichtigt werden.
Vor Kurzem haben wir Ruby-Projekte zu Snyk hinzugefügt. Die Unterschiede beim Umgang mit Versionen in RubyGems und npm stellten uns dabei vor einige Herausforderungen.
In diesem Blogbeitrag beschreiben wir diese Unterschiede, die dadurch entstandenen Probleme und wie wir sie gelöst haben.
Semantic Versioning
Entwicklerinnen und Entwickler, die mit Node.js und Ruby arbeiten, kommen vor allem über diese Paketmanager mit dem Semantic-Versioning-Standard in Kontakt. Daher entsteht leicht der Eindruck, dass der Umgang der einzelnen Paketmanager mit Versionen Teil von SemVer ist. Wie wir sehen werden, ist das nicht der Fall.
Was ist SemVer?
Semantic Versioning (SemVer) ist ein Standard für Softwareversionsnummern. Er soll Nutzerinnen und Nutzern von Bibliotheken helfen, die Auswirkungen neuer Versionen einfach anhand der Versionsnummern einzuschätzen.
Software, die dem SemVer-Standard folgt, muss durch zusammengesetzte Versionsnummern gekennzeichnet werden. Änderungen an den einzelnen Bestandteilen stehen für unterschiedliche Arten von Änderungen an der Software selbst.
Direkt zitiert von http://semver.org:
Bei einer Versionsnummer MAJOR.MINOR.PATCH wird Folgendes erhöht:1. die MAJOR-Version bei inkompatiblen API-Änderungen,2. die MINOR-Version, wenn Funktionen auf abwärtskompatible Weise hinzugefügt werden, und3. die PATCH-Version bei abwärtskompatiblen Fehlerkorrekturen.Zusätzliche Kennzeichnungen für Vorabversionen und Build-Metadaten sind als Erweiterungen des Formats MAJOR.MINOR.PATCH verfügbar.
Der SemVer-Standard legt außerdem Rangfolgeregeln fest – also, wie Versionen beim Sortieren miteinander verglichen werden sollen.
Was gehört nicht zu SemVer?
Die meisten (wenn nicht alle) Paketmanager ermöglichen die Angabe von Versionsbereichen. Werden Softwareabhängigkeiten als Bereiche angegeben, z. B. >= 5.0 and < 6, können Softwarebibliotheken in mehr Szenarien verwendet werden.
Diese „Bereichsangabe“ ist nicht Teil von SemVer, auch wenn die Vergleichsregeln dazugehören. Jeder Paketmanager kann daher seine eigene Syntax und eigene Regeln für die Angabe von Bereichen verwenden, z. B. >= 5, ~ 1.2, ^2.3.0, >5 || <2.
Die Bereichssyntax von RubyGems und npm ist sehr ähnlich (mehr dazu später). Wenn Sie häufig mit beiden arbeiten, kann man daher leicht annehmen, dass diese Syntax zum Standard gehört – obwohl das nicht der Fall ist.
Versionsnummern in npm und RubyGems
npm und RubyGems haben unterschiedliche Vorstellungen davon, was eine gültige Versionsnummer ist.
Bei npm müssen Versionen strikt Semantic Versioning 2.0 entsprechen. Jede Versionsnummer ohne Angabe einer Major-, Minor- und Patch-Version ist ungültig.
RubyGems legt weniger strenge Maßstäbe an gültige Versionen an und lässt eine unbegrenzte Anzahl von Bestandteilen zu. So ist beispielsweise 5.1.0.2.1.0 vollkommen gültig. Gem-Autoren werden ermutigt, dem Semantic-Versioning-Standard zu folgen, doch dies wird nicht programmgesteuert durchgesetzt.
Trotzdem sind vierteilige Versionsnummern im RubyGems-Ökosystem recht verbreitet. Bei Snyk begegnen sie uns häufig, da viele Gems (insbesondere Rails und seine Bestandteile) den vierten Teil für Sicherheitskorrekturen verwenden (z. B. wird eine Sicherheitslücke in 5.0.0 mit 5.0.0.1 behoben).
Versionsbereiche in npm und RubyGems
RubyGems und npm bieten eine ähnliche Auswahl an Operatoren zur Angabe von Versionsbereichen.
Die Bereichsverarbeitung in RubyGems ist Teil der Ruby-Standardbibliothek. In npm übernimmt das semver-Paket diese Aufgabe.
Einfache Bereichssyntax
Die einfachen Bereichsoperatoren >, >=, <, >= und = verhalten sich in RubyGems und npm identisch. RubyGems bietet außerdem den Operator !=, der von npm nicht unterstützt wird.
Erweiterte Bereichssyntax
Sowohl npm als auch RubyGems ermöglichen die kompakte Darstellung komplexer Bereiche. npm bietet mehr Operatoren als RubyGems und unterstützt dadurch eine etwas größere Auswahl komplexer Bereiche.
Beide bieten den Operator ~> für „pessimistische“ Bereiche. Vereinfacht gesagt bedeutet er: „Jede Version ab dieser Version ist zulässig, bis zu einem Punkt, der laut SemVer wahrscheinlich sicher ist.“ Das genaue Verhalten ist in RubyGems und npm jedoch leicht unterschiedlich.
RubyGems interpretiert ihn als „höhere Versionen zulassen, bei denen sich nur der niedrigstwertige Versionsbestandteil ändert“. npm versteht darunter hingegen: „Nur Patch-Änderungen zulassen, wenn eine Patch-Version angegeben ist, andernfalls nur Minor-Änderungen“. Das lässt sich am besten anhand eines Beispiels veranschaulichen:
Bereich | Interpretation durch RubyGems | Interpretation durch npm |
|---|---|---|
|
| Fehler: ungültige Version |
|
|
|
|
|
|
Welche Probleme hat das verursacht?
Zunächst gingen wir davon aus, dass der Umgang mit Versionen in RubyGems eine Obermenge von npm ist. Deshalb verwendeten wir das semver-Paket für Operationen mit npm- und RubyGems-Bereichen.
Das führte beim Alpha-Test zu einigen überraschenden Ergebnissen. Fast jedes von uns getestete Ruby-Repository schien extrem anfällig zu sein – sogar vollständig aktuelle Rails-Anwendungen.
Der Vergleich automatisierter Testberichte mit manuell erstellten Berichten zeigte zahlreiche falsch positive Ergebnisse (und einige falsch negative). Weitere Untersuchungen ergaben, dass die unterschiedlichen Bedeutungen von Bereichen in RubyGems und npm die Ursache waren.
Das grundlegende Problem: Eine Version, die in RubyGems einem bestimmten Bereich entspricht, entspricht nicht unbedingt demselben Bereich in npm.
Wir brauchten einen anderen Ansatz.
Wie haben wir das Problem gelöst?
Snyk unterstützte zunächst nur npm. Daher verwendete unser Produktcode bereits das SemVer-Paket von npm für Operationen mit Versionen und Bereichen.
Wir beschlossen, eine ähnliche Bibliothek zu entwickeln, die den Umgang mit Versionen wie RubyGems ermöglicht und dieselbe API wie das npm-SemVer-Paket bietet.
Unser System wählt dann anhand des Paketmanagers im Code, den wir testen, überwachen und korrigieren, die passende Bibliothek aus.
Wie haben wir ruby-semver implementiert?
Der Umgang mit Versionen in RubyGems ist Teil der Ruby-Standardbibliothek. Anstatt ihn selbst neu zu implementieren, beschlossen wir, mit Opal die entsprechenden Teile von RubyGems nach JavaScript zu transpilieren.
Zunächst versuchten wir, die Klassen direkt aus dem Ruby-Quellcode zu transpilieren. Das erwies sich aufgrund der Unterschiede bei der Unterstützung regulärer Ausdrücke in JavaScript und Ruby als unmöglich.
Letztlich kopierten wir zwei Klassen aus der Ruby-Standardbibliothek in unser Repository, passten die regulären Ausdrücke an JavaScript an und fügten ein Skript hinzu, das den Ruby-Quellcode nach JavaScript transpiliert.
Nachdem der transpilierte Code in einer Node.js-REPL funktionierte, ergänzten wir eine Reihe solider Tests für die gewünschte API im Stil von npm-semver. Anschließend schrieben wir einen möglichst schlanken Adapter zwischen der npm-SemVer-API und dem transpilierten RubyGems-Code.
Das funktionierte gut, setzt aber einen leistungsfähigen Ruby-zu-JavaScript-Transpiler voraus. Eine manuelle Implementierung des Verhaltens wäre möglich gewesen, hätte jedoch mehr Aufwand bedeutet und – was noch wichtiger ist – möglicherweise nicht offensichtliche Sonderfälle übersehen, die RubyGems bereits berücksichtigt.
Fazit
Durch die Unterstützung von Ruby in Snyk stießen wir auf interessante Herausforderungen dabei, wie verschiedene Sprachen und Paketmanager mit Versionen umgehen. Bereichsangaben für SemVer-Versionen sind noch problematischer, da sie überhaupt nicht definiert sind.
Die Auseinandersetzung mit diesen Herausforderungen hat unser Verständnis von SemVer und seinen Einschränkungen vertieft. Dadurch wurde die Grundlage gestärkt, auf der wir die Unterstützung weiterer Ökosysteme aufbauen können.
Wenn Sie sowohl mit Ruby als auch mit Node.js arbeiten, sehen Sie sich ruby-semver an. Wir haben es als Open Source veröffentlicht und hoffen, dass es auch anderen nützlich ist.
Und integrieren Sie Snyk in Ihre Ruby-Projekte. Wir erweitern aktiv die Unterstützung für neue Arten von Ruby-Projekten (einschließlich Gems – bleiben Sie dran).
Starten Sie mit Capture-the-Flag
Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Challenges lösen.

