30
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

クービックAdvent Calendar 2015

Day 9

Bundler入門 - setupとrequire を読み解く

Posted at

BundlerはRubyを使う上では欠かせないものですが、ブラックボックスな人は意外と多いのかなと思います。(自分もそうでした)

ブラックボックスで済んでしまうのはツールとしての完成度が高いことの裏返しなので一概に悪いことではありませんが、仕組みを知っておくと、トラブルシューティングの時に役立つ(かもしれない)ので、コードを読みながら少し掘り下げてみたいと思います。

bundle exec

$ bundle exec command

おまじないのように使う bundle exec ですが、gem が require される仕組みを追い掛けてみます。

まず、bin/bundle で bundler/cli が requireされ...
https://github.com/bundler/bundler/blob/v1.10.6/bin/bundle#L19-L20

  require 'bundler/cli'
  Bundler::CLI.start(ARGV, :debug => true)

bundler/cli/exec が require されます。
https://github.com/bundler/bundler/blob/v1.10.6/lib/bundler/cli.rb#L277-L280

    def exec(*args)
      require 'bundler/cli/exec'
      Exec.new(options, args).run
    end

runの内部では、SharedHelpers.set_bundle_envirionmentが呼び出されており、ここでRUBYOPTがセットされます。

    def set_bundle_environment
      # Set PATH
      paths = (ENV["PATH"] || "").split(File::PATH_SEPARATOR)
      paths.unshift "#{Bundler.bundle_path}/bin"
      ENV["PATH"] = paths.uniq.join(File::PATH_SEPARATOR)

      # Set RUBYOPT
      rubyopt = [ENV["RUBYOPT"]].compact
      if rubyopt.empty? || rubyopt.first !~ /-rbundler\/setup/
        rubyopt.unshift %|-rbundler/setup|
        ENV["RUBYOPT"] = rubyopt.join(' ')
      end

      # Set RUBYLIB
      rubylib = (ENV["RUBYLIB"] || "").split(File::PATH_SEPARATOR)
      rubylib.unshift File.expand_path('../..', __FILE__)
      ENV["RUBYLIB"] = rubylib.uniq.join(File::PATH_SEPARATOR)
    end

http://docs.ruby-lang.org/ja/2.2.0/doc/spec=2fenvvars.html によれば、RUBYOPT環境変数は「Rubyインタプリタにデフォルトで渡すオプションを指定します。」とのこと。

すなわち、bundle exec を実行すると、デフォルトで "-rbundler/setup" を RUBYOPT に 指定したことになり、以下のコードに辿り着きます。

if Bundler::SharedHelpers.in_bundle?
  require 'bundler'

  if STDOUT.tty? || ENV['BUNDLER_FORCE_TTY']
    begin
      Bundler.setup
    rescue Bundler::BundlerError => e
      ...
      exit e.status_code
    end
  else
    Bundler.setup
  end

Bundler.setup とは?

http://bundler.io/v1.10/bundler_setup.html に書かれている通り、Bundler.setup は Gemfile の依存関係を require できるよう load path を設定します。

少しコードを追いかけてみます。

    def setup(*groups)
      # Just return if all groups are already loaded
      return @setup if defined?(@setup)

      definition.validate_ruby!

      if groups.empty?
        # Load all groups, but only once
        @setup = load.setup
      else
        load.setup(*groups)
      end
    end

    ...

    def load
      @load ||= Runtime.new(root, definition)
    end

となっており、Runtimeのインスタンスを生成し、setupメソッドを実行していることが分かります。

さらに bundler/runtime.rbを覗いてみると、、

    def setup(*groups)
      groups.map! { |g| g.to_sym }

      # Has to happen first
      clean_load_path

      specs = groups.any? ? @definition.specs_for(groups) : requested_specs

      setup_environment
      Bundler.rubygems.replace_entrypoints(specs)

      # Activate the specs
      specs.each do |spec|
        ...
        
        Bundler.rubygems.mark_loaded(spec)
        load_paths = spec.load_paths.reject {|path| $LOAD_PATH.include?(path)}
        $LOAD_PATH.unshift(*load_paths)
      end

      setup_manpath

      lock(:preserve_bundled_with => true)

      self
    end

specをロードするパスを、$LOAD_PATH に追加していることが分かります。
spec が具体的にどんなインスタンスなのかは、例えば 以下のようにすればよいです。

[5] pry(main)> Bundler.definition.specs_for([:development]).first

Gem::Specification.new do |s|
  s.name = "rake"
  s.version = Gem::Version.new("10.4.2")
  s.installed_by_version = Gem::Version.new("0")
  s.date = Time.utc(2015, 4, 7)
  s.executables = ["rake"]
  s.files = ["bin/rake",
   ...
   ...
   "rake/version.rb",
   "rake/win32.rb"]
  s.require_paths = ["lib"]
  s.rubygems_version = "2.4.5"
  s.specification_version = 4
  s.summary = "This rake is bundled with Ruby"
end

Bundler.require とは?

ここまでで load path がセットされる仕組みは分かったので、次は require です。この役割は Bundler.require が担います。

    def setup(*groups)
      # Just return if all groups are already loaded
      return @setup if defined?(@setup)

      ...
    end

    def require(*groups)
      setup(*groups).require(*groups)
    end
    

細かい説明を飛ばして結論だけ書くと、setup インスタンス変数は Runtime インスタンスを保持しており、requireメソッドを呼び出すことになります。

コードは https://github.com/bundler/bundler/blob/master/lib/bundler/runtime.rb#L57 を見るとよいです。

まとめ

  • Bundler.setup は $LOAD_PATH の解決をし、Bundler.require は Gemfile の group に設定された gem を requireする
  • bundle exec は 内部で RUBYOPT に -rbundler/setup を設定している

See Also

本投稿は http://kotaroito.hatenablog.com/entry/2014/11/12/104146Coubic Advent Calendar 2015 に向けて、加筆・再編したものです。

30
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?