=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を実行してロード ]>