bundler (1.16.1)のコードリーディングをしました。今回は bundle exec
のコードを追ってみます。
exe/bundleはBundler::CLI.startを呼び出します
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler"
# ...
require "bundler/friendly_errors"
Bundler.with_friendly_errors do
require "bundler/cli"
# Allow any command to use --help flag to show help for that command
help_flags = %w[--help -h]
help_flag_used = ARGV.any? {|a| help_flags.include? a }
args = help_flag_used ? Bundler::CLI.reformatted_help_args(ARGV) : ARGV
Bundler::CLI.start(args, :debug => true)
end
Bundler::CLIはThorを継承しています
require "bundler"
require "bundler/vendored_thor"
module Bundler
class CLI < Thor
def self.start(*)
super
rescue Exception => e
Bundler.ui = UI::Shell.new
raise e
ensure
Bundler::SharedHelpers.print_major_deprecations!
end
execコマンドを実行すると#execが実行されます。#execはBundler::CLI::Exec#runを実行します。
desc "exec [OPTIONS]", "Run the command in context of the bundle"
method_option :keep_file_descriptors, :type => :boolean, :default => false
long_desc <<-D
Exec runs a command, providing it access to the gems in the bundle. While using
bundle exec you can require and call the bundled gems as if they were installed
into the system wide RubyGems repository.
D
map "e" => "exec"
def exec(*args)
require "bundler/cli/exec"
Exec.new(options, args).run
end
Bundler::CLI::Exec#runはBundler::SharedHelpers.set_bundle_environmentで環境変数を操作してから、Bundler.whichでコマンドのパスを検索してkernel_execで実行します(rubyのシェバンが付いていてdisable_exec_loadがfalseの場合はkernel_loadを実行します)
# frozen_string_literal: true
module Bundler
class CLI::Exec
def run
validate_cmd!
SharedHelpers.set_bundle_environment
if bin_path = Bundler.which(cmd)
if !Bundler.settings[:disable_exec_load] && ruby_shebang?(bin_path)
return kernel_load(bin_path, *args)
end
# First, try to exec directly to something in PATH
if Bundler.current_ruby.jruby_18?
kernel_exec(bin_path, *args)
else
kernel_exec([bin_path, cmd], *args)
end
else
# exec using the given command
kernel_exec(cmd, *args)
end
end
Bundler.whichは引数のファイルが存在しているか、実行可能かどうかを現在のパスや環境変数PATHから判別して存在すれば引数のファイル名をそのまま返します。
module Bundler
class << self
def which(executable)
if File.file?(executable) && File.executable?(executable)
executable
elsif paths = ENV["PATH"]
quote = '"'.freeze
paths.split(File::PATH_SEPARATOR).find do |path|
path = path[1..-2] if path.start_with?(quote) && path.end_with?(quote)
executable_path = File.expand_path(executable, path)
return executable_path if File.file?(executable_path) && File.executable?(executable_path)
end
end
end
kernel_execはその名の通りexec、kernel_loadはloadします。
def kernel_exec(*args)
ui = Bundler.ui
Bundler.ui = nil
Kernel.exec(*args)
# ...
end
def kernel_load(file, *args)
args.pop if args.last.is_a?(Hash)
ARGV.replace(args)
$0 = file
Process.setproctitle(process_title(file, args)) if Process.respond_to?(:setproctitle)
ui = Bundler.ui
Bundler.ui = nil
require "bundler/setup"
signals = Signal.list.keys - RESERVED_SIGNALS
signals.each {|s| trap(s, "DEFAULT") }
Kernel.load(file)
# ...
end
Bundler::SharedHelpers.set_bundle_environmentは以下の環境変数の値を書き換えます
- BUNDLE_BIN_PATH
- BUNDLE_GEMFILE
- BUNDLER_VERSION
- PATH
- RUBYOPT
- RUBYLIB
module Bundler
module SharedHelpers
def set_bundle_environment
set_bundle_variables
set_path
set_rubyopt
set_rubylib
end
def set_bundle_variables
begin
Bundler::SharedHelpers.set_env "BUNDLE_BIN_PATH", Bundler.rubygems.bin_path("bundler", "bundle", VERSION)
rescue Gem::GemNotFoundException
Bundler::SharedHelpers.set_env "BUNDLE_BIN_PATH", File.expand_path("../../../exe/bundle", __FILE__)
end
# Set BUNDLE_GEMFILE
Bundler::SharedHelpers.set_env "BUNDLE_GEMFILE", find_gemfile(:order_matters).to_s
Bundler::SharedHelpers.set_env "BUNDLER_VERSION", Bundler::VERSION
end
def set_path
validate_bundle_path
paths = (ENV["PATH"] || "").split(File::PATH_SEPARATOR)
paths.unshift "#{Bundler.bundle_path}/bin"
Bundler::SharedHelpers.set_env "PATH", paths.uniq.join(File::PATH_SEPARATOR)
end
def set_rubyopt
rubyopt = [ENV["RUBYOPT"]].compact
return if !rubyopt.empty? && rubyopt.first =~ %r{-rbundler/setup}
rubyopt.unshift %(-rbundler/setup)
Bundler::SharedHelpers.set_env "RUBYOPT", rubyopt.join(" ")
end
def set_rubylib
rubylib = (ENV["RUBYLIB"] || "").split(File::PATH_SEPARATOR)
rubylib.unshift bundler_ruby_lib
Bundler::SharedHelpers.set_env "RUBYLIB", rubylib.uniq.join(File::PATH_SEPARATOR)
end
Bundler::SharedHelpers.set_envメソッドでは元の値をプレフィックス(EnvironmentPreserver::BUNDLER_PREFIX)を付けて環境変数に保存してから、ENVを上書きしています。
def set_env(key, value)
raise ArgumentError, "new key #{key}" unless EnvironmentPreserver::BUNDLER_KEYS.include?(key)
orig_key = "#{EnvironmentPreserver::BUNDLER_PREFIX}#{key}"
orig = ENV[key]
orig ||= EnvironmentPreserver::INTENTIONALLY_NIL
ENV[orig_key] ||= orig
ENV[key] = value
end
PATHに #{Bundler.bundle_path}/bin
が設定されることで、bundle installしたライブラリの実行ファイルを実行できます。また、RUBYOPTに -rbundler/setup
が設定されるので、ruby実行時にbundler/setupがrequireされることになります。
bundler/setupはBundler.setupを呼び出します
# frozen_string_literal: true
require "bundler/shared_helpers"
if Bundler::SharedHelpers.in_bundle?
require "bundler"
if STDOUT.tty? || ENV["BUNDLER_FORCE_TTY"]
begin
Bundler.setup
rescue Bundler::BundlerError => e
# ...
else
Bundler.setup
end
# Add bundler to the load path after disabling system gems
bundler_lib = File.expand_path("../..", __FILE__)
$LOAD_PATH.unshift(bundler_lib) unless $LOAD_PATH.include?(bundler_lib)
Bundler.ui = nil
end
Bundler.setupはBundler::Runtime#setupを呼び出します。#definitionを呼び出したタイミングでBundler.configureが呼ばれ、GEM_PATHとGEM_HOMEが設定されています。
module Bundler
class << self
def setup(*groups)
# Return if all groups are already loaded
return @setup if defined?(@setup) && @setup
definition.validate_runtime!
SharedHelpers.print_major_deprecations!
if groups.empty?
# Load all groups, but only once
@setup = load.setup
else
load.setup(*groups)
end
end
Bundler::Runtime#setupでは$LOAD_PATHの操作を行います。
# frozen_string_literal: true
module Bundler
class Runtime
def setup(*groups)
# ...
SharedHelpers.set_bundle_environment
Bundler.rubygems.replace_entrypoints(specs)
# Activate the specs
load_paths = specs.map do |spec|
# ...
spec.load_paths.reject {|path| $LOAD_PATH.include?(path) }
end.reverse.flatten
# See Gem::Specification#add_self_to_load_path (since RubyGems 1.8)
if insert_index = Bundler.rubygems.load_path_insert_index
# Gem directories must come after -I and ENV['RUBYLIB']
$LOAD_PATH.insert(insert_index, *load_paths)
else
# We are probably testing in core, -I and RUBYLIB don't apply
$LOAD_PATH.unshift(*load_paths)
end
setup_manpath
lock(:preserve_unknown_sections => true)
self
end
specsはGemfileから取得した依存ライブラリの情報です。ここからload_pathsを取得し、$LOAD_PATHに追加します。これでRubyスクリプト上でbundlerでインストールした依存ライブラリのrequireができるようになります。
おまけ1:Bundler.with_clean_env
Bundler.with_clean_envを使うとbundle execやbundler/setupで設定した環境変数をリセットしてスクリプトを実行できます。def with_clean_env
with_env(clean_env) { yield }
end
original_envはENVのbundlerの設定前の値になります。設定前の判別はプレフィックス EnvironmentPreserver::BUNDLER_PREFIX
がついている EnvironmentPreserver::BUNDLER_KEYS
の環境変数から復元します。
def clean_env
Bundler::SharedHelpers.major_deprecation(2, "`Bundler.clean_env` has weird edge cases, use `.original_env` instead")
env = original_env
if env.key?("BUNDLER_ORIG_MANPATH")
env["MANPATH"] = env["BUNDLER_ORIG_MANPATH"]
end
env.delete_if {|k, _| k[0, 7] == "BUNDLE_" }
if env.key?("RUBYOPT")
env["RUBYOPT"] = env["RUBYOPT"].sub "-rbundler/setup", ""
end
if env.key?("RUBYLIB")
rubylib = env["RUBYLIB"].split(File::PATH_SEPARATOR)
rubylib.delete(File.expand_path("..", __FILE__))
env["RUBYLIB"] = rubylib.join(File::PATH_SEPARATOR)
end
env
end
Bundler::EnvironmentPreserver#restoreで元のENV値を取得しています
# frozen_string_literal: true
module Bundler
class EnvironmentPreserver
# @return [Hash]
def restore
env = @original.clone
@keys.each do |key|
value_original = env[@prefix + key]
next if value_original.nil? || value_original.empty?
if value_original == INTENTIONALLY_NIL
env.delete(key)
else
env[key] = value_original
end
env.delete(@prefix + key)
end
env
end
end
end
with_envは与えられたハッシュ値でENVをreplaceしブロックをyield後、backupからもとの環境変数に復元します。
# @param env [Hash]
def with_env(env)
backup = ENV.to_hash
ENV.replace(env)
yield
ensure
ENV.replace(backup)
end
おまけ2:bundleで設定される環境変数を確認
$ bundle exec 'echo $PATH' | grep -o ${PWD}/vendor/bundle/ruby/2.5.0/bin:
# => /path/to/project/vendor/bundle/2.5.0/bin:
$ bundle exec 'echo $BUNDLE_GEMFILE'
# => /path/to/project/Gemfile
$ bundle exec 'echo $BUNDLER_VERSION'
# => 1.16.1
$ bundle exec 'echo $BUNDLE_BIN_PATH'
# => ${HOME}/.rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/bundler-1.16.1/exe/bundle
$ bundle exec 'echo $RUBYOPT'
# => -rbundler/setup
$ bundle exec 'echo $RUBYLIB'
# => ${HOME}/.rbenv/versions/2.5.0/lib/ruby/gems/2.5.0/gems/bundler-1.16.1/lib:
# ${HOME}/.rbenv/plugins/gem-src/lib:
# ${HOME}/.rbenv/rbenv.d/exec/gem-rehash
$ bundle exec ruby -e 'puts ENV["GEM_HOME"]'
# => /path/to/project/vendor/bundle/ruby/2.5.0