npm、Cargo、Composer、pipはすべて、同じsemverの短縮表記を異なる方法で解釈します。私がテストした13種類の範囲指定式のうち、これら4つすべてで全く同じ挙動を示すものは、わずか2つだけでした。

各マネージャーに対して個別のパーサーを作成し、公式仕様に従って、すべての範囲指定式を19個のバージョン番号のマトリックスに対して実行しました。13×19のグリッド(247セル)が描き出したのは、断片的な結果でした。すべてのツールが同じ「yes」または「no」の回答を出したのは、わずか17セルのみであり、その17セルのほとんどは満場一致の「no」でした。端的に言えば、これらのツールは「yes」よりも「no」において一致する頻度がはるかに高いのです。

なぜツール間で差異が生じるのか

4つのマネージャーすべてがSemantic Versioningに従っていると主張していますが、それぞれが独自の短縮表記ルールを追加しています。

  • Caret (^) と dot-x – npmはcaretをサポートしていますが、pipはcaretとdot-xの両方を無視します。それだけで、npmとpipの間には49セルの不一致が生じます。
  • Bare versions – Cargoは単なる 1.2.3 をcaret範囲として扱い、「1.xと互換性がある」ことを意味します。npmとComposerは同じ文字列を完全一致として読み取り、1.2.3 のみを受け入れます。
  • Tilde (~) – Cargoのチルダはマイナーバージョンを固定します(~1.21.2.* には一致しますが、1.3.0 には一致しません)。Composerのチルダはメジャーバージョンを固定します(~1.21.* に一致します)。この2つの解釈は、マイナーコンポーネントが変更されるバージョンにおいて食い違います。

プリリリースの扱いも、もう一つの驚きをもたらします。^1.2.3 のような範囲は、数値部分が上限よりも低かったとしても、2.0.0-rc.1 を含みません。ルールは、「範囲内でプリリリース識別子が明示的に言及されていない限り、プリリリースは除外される」というものです。

ツール間で共通して安全な構文

実験の結果、>=1.2.0 <2.0.0 のような明示的な不等号による範囲指定は、npm、Cargo、Composer、pipにおいて同じように動作することがわかりました。すべてのマネージャーが、2つの境界をリテラルな数値の制限として扱い、隠れたcaretやチルダのセマンティクスは適用されません。

「少なくとも1.2である任意の1.xバージョン」を表現する必要がある場合は、省略せずにすべて記述してください。文字数は少し増えますが、コードがどこで実行されても予測可能な解決(resolution)を保証できます。

短縮表記が依然として意味を持つ場合

単一言語のプロジェクトであれば、短縮表記は依然として魅力的です。純粋なnpmエコシステム内であれば、^1.2.3 は「将来のマイナーリリースおよびパッチリリースと互換性がある」ことを簡潔に表せます。使用するツールが変わらないことがわかっている場合、CargoのcaretやComposerのチルダについても同様のことが言えます。危険が生じるのは、同じマニフェストが言語の境界を越えて再利用される場合や、CIジョブが複数のエコシステムから依存関係を取り込む場合です。

まとめ

言語間の互換性を確保するためにsemverの短縮表記に頼るのは、ギャンブルです。バージョン制約がどこでも同じ意味を持つことを保証する唯一の確実な方法は、明示的な不等号を書くことです。簡潔さが必要なときは単一のエコシステム内に留まり、一貫性が必要なときは短縮表記を捨てて明快さを選びましょう。