2018-05-09

RubyGemsのrequireは何をやっているか

=gemspecのスタブの中で対象のファイルが存在すれば、それを返します。 def self.find_by_path path path = path.dup.freeze spec = @@spec_with_requirable_file[path] ||= (stubs.find { |s| next unless Gem::BundlerVersionFinder.compatible?(s) s.contains_requirable_file? path } || NOT_FOUND) spec.to_spec end

stubsはspecificationsディレクトリ内にある拡張子がgemspecのもの(=インストール済みのrubygemのgemspec)を検索してGem::StubSpecificationのインスタンスを返します。 def self.stubs @@stubs ||= begin pattern = "*.gemspec" stubs = default_stubs(pattern).concat installed_stubs(dirs, pattern) stubs = uniq_by(stubs) { |stub| stub.full_name }

  _resort!(stubs)
  @@stubs_by_name = stubs.group_by(:name)
  stubs
end

end

default_stubsはspecifications/defaultのディレクトリ内にあるgemspecを拾ってStubSpecificationのインスタンスを生成します。defaultディレクトリには csv やjsonなどの標準ライブラリが入っています。 def self.default_stubs pattern base_dir = Gem.default_dir gems_dir = File.join base_dir, "gems" gemspec_stubs_in(default_specifications_dir, pattern) do |path| Gem::StubSpecification.default_gemspec_stub(path, base_dir, gems_dir) end end

installed_stubsはインストール済みの3rd partyのgemのgemspecを"#{Gem.path}/gems/specifications"から取得してインスタンスを生成します。 def self.installed_stubs dirs, pattern map_stubs(dirs, pattern) do |path, base_dir, gems_dir| Gem::StubSpecification.gemspec_stub(path, base_dir, gems_dir) end end

stubs内の#contains_requirable_file?がtrueであるGem::StubSpecificationを返しています。#contains_requirable_file?はGem::BasicSpecificationに定義されており#have_file?を呼びます。 def contains_requirable_file? file if @ignored then return false elsif missing_extensions? then @ignored = true

  warn "Ignoring #{full_name} because its extensions are not built. " +
    "Try: gem pristine #{name} --version #{version}"
  return false
end

have_file? file, Gem.suffixes

end

def have_file? file, suffixes return true if raw_require_paths.any? do |path| base = File.join(gems_dir, full_name, path.untaint, file).untaint suffixes.any? { |suf| File.file? base + suf } end

if have_extensions?
  base = File.join extension_dir, file
  suffixes.any? { |suf| File.file? base + suf }
else
  false
end

end

#have_file?で呼び出しているraw_require_pathsはdata.require_pathsを返します。

#dataは以下のように定義されています。 def data unless @data begin saved_lineno = $. open loaded_from, OPEN_MODE do |file| begin file.readline # discard encoding line stubline = file.readline.chomp if stubline.start_with?(PREFIX) then extensions = if /\A#{PREFIX}/ =~ file.readline.chomp $'.split "\0" else StubLine::NO_EXTENSIONS end

          @data = StubLine.new stubline, extensions
        end
      rescue EOFError
      end
    end
  ensure
    $. = saved_lineno
  end
end

@data ||= to_spec

end

specificationsディレクトリ内のgemspecファイルは以下のようなコメントが付いています。

-- encoding: utf-8 --

stub: rake 12.3.0 ruby lib

stubのprefixが付いている場合は、そのコメントを引数にStubLineをインスタンス化します。 class StubLine # :nodoc: all attr_reader :name, :version, :platform, :require_paths, :extensions, :full_name

...

def initialize data, extensions
  parts          = data[PREFIX.length..-1].split(" ".freeze, 4)
  @name          = parts[0].freeze
  @version       = if Gem::Version.correct?(parts[1])
                     Gem::Version.new(parts[1])
                   else
                     Gem::Version.new(0)
                   end

  @platform      = Gem::Platform.new parts[2]
  @extensions    = extensions
  @full_name     = if platform == Gem::Platform::RUBY
                     "#{name}-#{version}"
                   else
                     "#{name}-#{version}-#{platform}"
                   end

  path_list = parts.last
  @require_paths = REQUIRE_PATH_LIST[path_list] || path_list.split("\0".freeze).map! { |x|
    REQUIRE_PATHS[x] || x
  }
end

end

rake 12.3.0 ruby libの場合、nameがruby、versionが12.3.0(Gem::Versionのインスタンスが入る)、platformがruby、require_pathsがlibに設定されます。

よってrakeの場合、raw_require_pathsには["lib"]が入ります。このraw_require_pathsに対してループを回して"gems/#{name}-#{version}/lib/#{file}" のファイルを検索して存在すればtrueを返すことになります。このようにしてファイルの存在チェックを行っています。 base = File.join(gems_dir, full_name, path.untaint, file).untaint suffixes.any? { |suf| File.file? base + suf } 最終的にfind_by_pathではGem::StubSpecification#to_specが呼ばれます。 def to_spec #... @spec ||= Gem::Specification.load(loaded_from) @spec.ignored = @ignored if @spec

@spec

end

Gem::Specification.loadでgemspecファイルを読み込んでいます。stubは# stubのコメント部分だけを読んでSpecificationを作成する一方で、loadメソッドはGem::Specificationのインスタンスを生成します。

loadメソッドでやっていることはgemspecのファイルを読んでevalしてGem::Specificationのインスタンスであればそのまま返しています。 def self.load file

...

code = if defined? Encoding
         File.read file, :mode = 'r:UTF-8:-'
       else
         File.read file
       end

code.untaint

begin
  _spec = eval code, binding, file

  if Gem::Specification === _spec
    _spec.loaded_from = File.expand_path file.to_s
    LOAD_CACHE[file] = _spec
    return _spec
  end

...

ここでインスタンス化したGem::Specificationに対して#activateが呼ばれます。 def activate

...

activate_dependencies
add_self_to_load_path

Gem.loaded_specs[self.name] = self
@activated = true
@loaded = true

return true

end

add_self_to_load_pathは以下のような定義になっています。full_require_pathsはその名の通りrequire_pathsのフルパスです。$LOAD_PATH.insertでfull_require_pathsが$LOAD_PATHに追加されます。 def add_self_to_load_path return if default_gem?

paths = full_require_paths

# gem directories must come after -I and ENV['RUBYLIB']
insert_index = Gem.load_path_insert_index

if insert_index then
  # gem directories must come after -I and ENV['RUBYLIB']
  $LOAD_PATH.insert(insert_index, *paths)
else
  # we are probably testing in core, -I and RUBYLIB don't apply
  $LOAD_PATH.unshift(*paths)
end

end

ちなみにGem.load_path_insert_indexはRbConfig::CONFIG['sitelibdir']のインデックスで、$LOAD_PATHはlib/ruby/site_ruby/#{version}の直前に差し込まれることになります。 def self.load_path_insert_index $LOAD_PATH.each_with_index do |path, i| return i if path.instance_variable_defined?(:@gem_prelude_index) end

index = $LOAD_PATH.index RbConfig::CONFIG['sitelibdir']

index

end

依存関係にあるgemもGem::Specification#activate_dependenciesによって$LOAD_PATHに追加されます。 def activate_dependencies unresolved = Gem::Specification.unresolved_deps

self.runtime_dependencies.each do |spec_dep|

...

  specs = spec_dep.to_specs

  if specs.size == 1 then
    specs.first.activate
  else
    name = spec_dep.name
    unresolved[name] = unresolved[name].merge spec_dep
  end
end

unresolved.delete self.name

end

#runtime_dependenciesはgemspec内でadd_runtime_dependencyメソッドで追加したGem::Dependencyのインスタンスが入っています。

Gem::Dependency#to_specsは以下のように定義されています。 class Gem::Dependency def to_specs matches = matching_specs true

if matches.empty? then
  specs = Gem::Specification.stubs_for name

  if specs.empty?
    raise Gem::MissingSpecError.new name, requirement
  else
    raise Gem::MissingSpecVersionError.new name, requirement, specs
  end
end

# TODO: any other resolver validations should go here

matches

end

#matching_specsによって対象のGem::StubSpecificationを取得しています。

#matching_specsは以下のように定義されています。 def matching_specs platform_only = false env_req = Gem.env_requirement(name) matches = Gem::Specification.stubs_for(name).find_all { |spec| requirement.satisfied_by?(spec.version) env_req.satisfied_by?(spec.version) }.map(:to_spec)

Gem::BundlerVersionFinder.filter!(matches) if name == "bundler".freeze

if platform_only
  matches.reject! { |spec|
    spec.nil? || !Gem::Platform.match(spec.platform)
  }
end

matches

end

Gem::Specification.stubs_forで対象のgemspecを検索して、requirementsに合ったバージョンのgemを抽出し、#to_specしたものをmatches変数に格納しています。 def self.stubs_for name if @@stubs @@stubs_by_name[name] || [] else pattern = "#{name}-*.gemspec" stubs = default_stubs(pattern) + installed_stubs(dirs, pattern) stubs = uniq_by(stubs) { |stub| stub.full_name }.group_by(:name) stubs.each_value { |v| _resort!(v) }

  @@stubs_by_name.merge! stubs
  @@stubs_by_name[name] ||= EMPTY
end

end

matchesの配列が1つであれば、その要素のGem::Specification#activateを実行して$LOAD_PATHにrequire_pathを追加します。

matchesの配列が2つ以上の場合はGem.unresolved_depsにGem::Dependencyが追加されます。 def self.unresolved_deps @unresolved_deps ||= Hash.new { |h, n| h[n] = Gem::Dependency.new n } end

この場合は、各gemでrequireしたときにkernel_require.rbの以下の処理で$LOAD_PATHに追加されます。 found_specs = Gem::Specification.find_in_unresolved path

if found_specs.empty? then

...

# We found +path+ directly in an unresolved gem. Now we figure out, of
# the possible found specs, which one we should activate.
else

  # Check that all the found specs are just different
  # versions of the same gem
  names = found_specs.map(:name).uniq

  if names.size  1 then
    RUBYGEMS_ACTIVATION_MONITOR.exit
    raise Gem::LoadError, "#{path} found in multiple gems: #{names.join ', '}"
  end

  # Ok, now find a gem that has no conflicts, starting
  # at the highest version.
  valid = found_specs.find { |s| !s.has_conflicts? }

...

  valid.activate
end

Gem::Specification.find_in_unresolvedはDependencyに対して#to_specsを呼び出して#contains_requirable_file?でフィルタした結果を返します。 def self.find_in_unresolved path # TODO: do we need these?? Kill it specs = unresolved_deps.values.map { |dep| dep.to_specs }.flatten

specs.find_all { |spec| spec.contains_requirable_file? path }

end

#to_specsは#stubs_forを使っており、#stubs_forはバージョンが高いgemが先頭に来るようにソートされています。

found_specs.findで#has_conflicts?ではないgemで一番バージョンが高いgemに対して#activateしています。 まとめ

Kernel.requireを再定義。再定義したrequire内でオリジナルのrequireを呼ぶ
$LOAD_PATHから対象のファイルが見つからなければLoadErrorがraiseされる
再定義したKernel.require内でLoadErrorをrescue
stubsからマッチするgemを検索

(stubsはspecificationsディレクトリ内のgemspecファイルから生成。この時点ではgemspecを全ロードせずstubコメントから読み取り) gemspecに設定されたrequire_pathを使ってrequireの引数になっているパスの解決を試みる パスが解決できれば対象のgemspecをロードしてspecをactivate activateすると対象のgemのrequire_pathのフルパスが$LOAD_PATHに載る。依存gemも$LOAD_PATHに載る(ただし依存gemが複数バージョンある場合は依存gemがrequireされたタイミングで$LOAD_PATHに追加される) 再度オリジナルのrequireを実行してロード ]>

このエントリーをはてなブックマークに追加