woshidan's blog

あいとゆうきとITと、とっておきの話。

Gemfileに書いてあるgemのコードはどこから取得しているのか

Bundlerの動作を少し詳しく調べてみようと思ったのですが、その前にGemfileの内容について 少し調べました。

内容

  • Gemfileとは
  • Gemfileに書いてあるgemのコードはどこから取得しているのか
    • global source lines
    • sourceオプション
    • pathオプション
    • gitオプション
    • ローカルリポジトリを取得先に指定した場合のファイルの更新内容の反映について
  • gem探索の際の各種オプションの優先順位

参考

Gemfile(5) - A format for describing gem dependencies for Ruby programs

http://memo.yomukaku.net/entries/IpCSQmo

Bundler: The best way to manage a Ruby application's gems

Gemfileとは

GemfileはRubyのコードを動かすために必要なgemの依存性を書いておくためのファイルです。 gemの依存性を管理したいプロジェクトのルートディレクトリに置いておきます。

たとえば、 RailsではRakefile(Railsの起動コマンドを叩くファイル)と Gemfile(railsrubyのバージョンをgemの形で管理している)を同じトップディレクトリに置いています。

Gemfile

#
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '4.1.0'

Rakefile

# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require File.expand_path('../config/application', __FILE__)

Rails.application.load_tasks

Gemfileに書いてあるgemのコードはどこから取得しているのか

global source lines

gemに対して、特に:source:git, :pathオプションの指定をしなければGemfileの先頭行に書いてあるsourceメソッドの引数のサイトから取得されます。

source "https://rubygems.org"

ほぼ、https://rubygems.orgのはずです。

作成したgemを外部から利用されたい場合は、このサイトに作成したgemのgitリポジトリ等を登録すればよさそうです。

実は、sourceメソッド複数行記述することが可能ですが、Bundlerのバージョンが1.7から非推奨になっております。 (また、このsourceメソッドの行はマニュアル中でglobal source linesと呼ばれることがあります)

いろんなサイトから複数のバージョンが登録されていたりすることがあるため、 意図せぬライブラリを読み込まないためにするためだとおもいますが、 たとえば、社内ライブラリなので社内サーバから取得したいんだけど...という場合もありますよね。

そう言う場合は下記の各種オプションから適切なものを使えばよさそうです。 個人的に良く見かけているのはgitです。

sourceオプション

下記のように、:sourceオプションを利用してgemを検索してくるサイトを指定することが出来ます。

gem "some_internal_gem", :source => "https://gems.example.com"

ただし、指定したサイトにgemがなければbundlerはsourceオプションで指定したサイトからgemを探すので注意が必要です(後述)。

pathオプション

ファイルシステム内の特定のパスを指定して、gemの取得先とする事が出来ます。

指定するディレクトリとしては取得したいgemに対応する.gemspecファイルが そのディレクトリのルートに含まれているか、 bundlerが使うgemのバージョンを指定する必要があります。

また、いくつかの同じディレクトリ下に入っているgemのパスを指定したい場合は、

path 'components' do
  gem 'admin_ui'
  gem 'public_ui'
end

のようにブロックでそのディレクトリを与えると、gemの名前に対応するサブディレクトリの .gemspecを指定します。

gitオプション

また、これらのオプションはgroupのブロックを使って一括して書く事が出来ます。 ブロックに対して与えたオプション(ref, tag, branchなど)は、ブロック内の各行に対して継承されます。

source "https://gems.example.com" do
  gem "some_internal_gem"
  gem "another_internal_gem"
end

git "https://github.com/rails/rails.git" do
  gem "activesupport"
  gem "actionpack"
end

platforms :ruby do
  gem "ruby-debug"
  gem "sqlite3"
end

group :development do
  gem "wirble"
  gem "faker"
end

ローカルリポジトリを取得先に指定した場合のファイルの更新内容の反映について

リモートのサイトからgemを取得する場合、 リモートのサイトのgitに登録されているgemのバージョンで指定されているコミットの内容を取得します。

しかし、gemの指定先にローカルのブランチを指定した場合、 デフォルトでは、.gemspecにおいてs.files = [ ... ]に含まれているファイルの内容が更新されているかを調べ、 ローカルのブランチにコミットしなくてもファイルに変更を保存するたびに、 その更新内容を反映させます。

たとえばRails::Engineの内容を元のRailsプロジェクトからローカルから取得するようにしたとき、

gem 'some_engine', :path => "~/some_engine"

some_engine内のコードの加えられたコードの変更を反映させたい時は、 元のプロジェクト内のModelのコードの変更を反映させる時と同じように、railsコンソール上で、

reload!

コマンドを実行するかコンソールを再起動すれば、リモートへプッシュしたりローカルのgitにコミットされていなくても 変更を反映させる事が出来ます。

このローカルのgemの更新の検査、更新内容の反映を止めさせたい場合は、

bundle config disable_local_branch_check false

とbundlerに設定を追加します。

gem探索の際の各種オプションの優先順位

  1. そのgemに個別にオプションが設定されていれば、そのオプション(:source:git, :path)の設定を利用します
  2. Gemfileに書いたgemが依存している等、暗黙的に指定されているgemを、そのgemを利用するGemfileに書いたgemの設定に従って、取り込みます。有名な例としては、Railsが利用しているActiveSupportのgemがあります。ActiveSupportrubygems.orgに別のバージョンが公開されていますが、Railsをインストールしたときにはrailsのgemspecで指定されているrailsリポジトリ内のActiveSupportのコードが利用されます。
  3. 最後に1行目にglobal source linesとして指定したデフォルトのsourceのURLからgemを探します。

実は、これ以外にbundler自身のオプションも読み込まれるみたいで、 bundler自体のconfigに特定のgemのconfigを下記のように設定しておくと

bundle config local.GEM_NAME /path/to/local/git/repository

と打ち込んだ上で、Gemfileを

gem 'rack', :github => 'rack/rack', :branch => 'master' # branchの部分を追加!

上記の通りにbundler自身に設定してしまうと、その設定はパソコン内のbundlerでglobalな設定なのでやや注意が必要です。 また、

bundle config --delete local.GEM_NAME

も打ち込まないといけず、gem自身を置き換えたいならGemfileだけで十分かも、という気がしています。