gemspecとGemfileの役割をはっきりさせておく

要約

GemfileGemfile.lockは依存関係を厳密に指定するのが目的なので、アプリケーションを開発するときはレポジトリにチェックインすべき。 一方Gemを開発するときは依存関係を緩やかに定義し柔軟性を持たせることが重要なポイントなので、GemfileGemfile.lockはレポジトリにチェックインしてはいけない。 他、Gemfileとgemspecをうまく組み合わせて使う方法など。

短い方の説明

アプリケーションもgemも「依存関係」という同じような概念を持っているように思えるかも知れませんが、この言葉はそれぞれにとって重要な違いがあります。 Gemは他のgemとそのバージョンに依存しますが、それらがどこから来るのかは気にしません。 一方アプリケーションは厳密にデプロイメントを調整する必要があります。

Gemを開発するときは、Gemfileにはgemspecを使って重複を回避しましょう。 普通、GemfileにはRubygemsのソースとgemspecの一行だけが書かれるべきです。 Gemfile.lockはバージョン管理にチェックインしてはいけません。 Gemコマンドで扱われないような精密さを要求してしまうからです。 精密さが満たされるとしても、利用者側からすると依存関係のバージョンがズレていると使えないことになってしまって不便です。

アプリケーションを開発するときは、Gemfile.lockはチェックインすべきです。 Bundlerによってどの環境でも同じソースの同じGemが使えるのは大変ありがたいからです。

以前この件について記事を書いて以来、bundlerで使われるGemfileとRubygemsで使われる.gemspecの役割の違いについて色々と質問を受けてきました。 多くの人はこの二つの役目が重複していると感じて、重複を解決するためにまた別のツールを導入したりしてしまっているようです。

Rubyのライブラリ

Rubyのライブラリはgemという形式にまとめてrubygems.orgにて配布されるのが一般的です。 gemには以下のような有用な情報が含まれます。

最後の「依存関係」というのがGemfileと重なるところですね。 Gemが依存関係を記述するときは、名前とバージョン範囲をリストアップします。 個々で重要なのは依存先ライブラリのソースについては気にしないということです。 ということは、依存先のGemとして自分でミラーしたものやハックしたものを使うことができるわけです。 要するにRubygemの依存というのはシンボリックなものであって、インターネットのどの場所にあるどのGem、という厳密な依存ではないのです。

他の要素と相まって、このことがRubygemsというシステムをロバストなものにしています。

また、Gemの作者は依存先ライブラリのバージョンを範囲で指定します。 開発時に利用していた依存先が、利用時には別のものになっていることも考慮して作らなければいけません。 特に依存先の依存先についてはよくあることです。

Rubyアプリケーション

Rubyアプリケーション(Railsアプリなど)は大抵、依存するサードパーティ製コードが複雑かつ精密なものになりがちです。 開発に着手したときにどのバージョンのnokogiriを使ってるかは気にしなくても、開発時とデプロイ時で同じものを使う必要はあります。 Gemの作者と違ってアプリの作者はデプロイ環境を意のままにするので、そのアプリをサードパーティが使うかもということは考えなくてよく、依存関係はとにかく精度の高いものにすることになります。

結果的に、アプリケーションが使うGemがインターネットのどこに置かれているかまで厳密に指定するわけです。 長期にわたって影響なく使い続けられる事を考えなければいけないGem作者とは対照的な立場です。

また、アプリの作者は公開されているGemをハックしたり、未公開の"edge"バージョンを使ったりする必要があったりもします。 Bundlerではそのためにgemの場所を特定のgitレポジトリとして指定することも可能です。 Gemの作者は依存関係をシンボリックに定義しているので、アプリの作者は独自のミラー(gitなど)でその依存関係を満たすという選択ができます。

このように、永続性と柔軟性を重視するGem開発者と、どの環境においても同じ依存先を使うことを重視するアプリ開発者とでは全く必要とするものが違ってくるのです。

RubygemsとBundler

Rubygemsというライブラリ(そのCLIとAPI)は、Gem作者の要求を満たすよう設計されています。 特にgemspecはrubygems.orgにデプロイするgemの標準的なデータ書式です。 上に述べた理由により、gemspecでは依存先の場所という過渡的な情報は保持しません。

一方Bundlerはアプリ開発者の要求を満たすよう設計されています。 Gemfileはメタデータ、ファイルリストなどは含みません。 なぜなら依存関係を厳密に定義するという目的から外れるからです。 そのかわり、依存関係については本当に厳密に定義できます。

Git上の「Gem」

アプリケーションはまだリリースされていないようなgemに依存することもあると言いました。 Bundlerではgitレポジトリの中の.gemspecファイルを見て、まるでgemのように扱うことができます。

Gemを開発するときは

Gemを開発していくとこの二つの世界の狭間に陥ることがあります。 理由は

開発中のgemはいずれ成長して普通のgemとして公開するわけですから、.gemspecを仕様に従って書く必要があります。 Gemfileに書いているだけではいけません。

Gemの開発においてGemfileとgemspecに同じような内容を書かなくてすむよう、Bundlerにはこういう設定項目があります(bundlerのドキュメント):

source "http://www.rubygems.org"
gemspec

このディレクティブを書くと、Bundlerは近くの.gemspecファイルを探してくれます。 そしてbundleを実行するとそれをGemとみなし、その依存関係を解決してくれます。 load pathにも追加してくるので、記述の重複なしにgemspecとbundlerを使うことができるわけです。

まだリリースされていないGemを使う必要があるときは(たとえばRailsではRack, Arel, Mailなどのプレリリースをよく使いますが)、Gemfileにそれぞれのgemの場所を書いていきます。 依存の解決のためには.gemspecを見ますが、その取得先についてはGemfileを尊重してくれます。

source "http://www.rubygems.org"
gemspec
# このgitレポジトリにあるrackが.gemspecで指定されたバージョンと
# ズレていたら、Bundlerがエラーを吐く
gem "rack", :git => "git://github.com/rack/rack.git"

この情報はいずれ.gemspecを公開することを考えると.gemspecには書きたくありません。 繰り返しますが.gemspecではこの情報を指定しないことによって柔軟性を確保し、Gemfileでは(開発時に限って)厳密に指定することができています。

Gemfile.lockをレポジトリにチェックインするなというのはこういう理由です。 Bundlerによって自動生成されるこのファイルには必要となる全てのgemの厳密な場所が書かれていますが、それによってGemとしての柔軟性が失われるからです。

.gemspecで指定したどんな依存先でも問題なく使えるという保証にはなりませんが、とにかく、そのバージョンを指定してしまうことによるメリットはありません。

追記

確認ですが、アプリケーションの開発ではGemfile.lockをチェックインすべきです。 このファイルには使用するgemの正確なバージョンと場所が書かれています。 これはアプリケーション開発では望ましいことですが、Gem開発では嬉しくありません。