普段自分が何気なく実行しているgem install
が実際にどのように行われれているのか気になったのでコードを読んで流れを追ってみました👀
※今回読んだのはrubygems
のstableなブランチを思われる3.0
のコードです。
GitHub - rubygems/rubygems at 3.0
gem installの流れ
実行コマンドの実行
まずgem install hoge
を実行するとbin/gem
が実行されコマンドライン引数(ARGV
)にinstall
とhoge
が渡されます。
そしてGem::GemRunner#run
がコマンドライン引数を引数に実行されます。
gem install hoge
Gem::GemRunner#run
が何をしているかというと引数をもとにconfig
を実行時の引数を生成し、Gem::CommandManager#run
を呼び出します。
class Gem::GemRunner
def initialize(options={})
@command_manager_class = options[:command_manager] || Gem::CommandManager
@config_file_class = options[:config_file] || Gem::ConfigFile
end
def run(args)
cmd.run Gem.configuration.args, build_args
end
Gem::CommandManager#run
では、process_args
を実行し引数から実行コマンドを特定しGem::Commands::InstallCommand
のオブジェクトを取得してinvoke_with_build_args
で実行します。
class Gem::CommandManager
def run(args, build_args=nil)
process_args(args, build_args)
end
def process_args(args, build_args=nil)
case args.first
when '-h', '--help' then
else
cmd_name = args.shift.downcase
cmd = find_command cmd_name
cmd.invoke_with_build_args args, build_args
end
end
invoke_with_build_args
は、Gem::Commands::InstallCommand
のスーパークラスのGem::Command
に定義されています。
やっていることはコマンドライン引数から実行時オプションまわりを設定や、コールバックの実行を行いGem::Commands::InstallCommand#execute
を実行します。
class Gem::Command
def invoke_with_build_args(args, build_args)
handle_options args
options[:build_args] = build_args
if options[:help]
show_help
elsif @when_invoked
@when_invoked.call options
else
execute
end
ensure
end
Gem::Commands::InstallCommand#execute
では、install先やversion周りの指定値のチェック等の前処理を行ったあと、Gem::Commands::InstallCommand#install_gem
を実行します。
class Gem::Commands::InstallCommand < Gem::Command
def execute
check_install_dir
check_version
exit_code = install_gems
show_installed
terminate_interaction exit_code
end
Gem::Commands::InstallCommand#install_gems
ではコマンドライン引数で指定したgemの数だけGem::Commands::InstallCommand#install_gem
を実行します。
例) gem install hoge fuga
の場合install_gem "hoge"
、install gem "fuga"
が実行される。
class Gem::Commands::InstallCommand < Gem::Command
def install_gems
exit_code = 0
get_all_gem_names_and_versions.each do |gem_name, gem_version|
begin
install_gem gem_name, gem_version
end
end
exit_code
end
gemのインストール処理
Gem::Commands::InstallCommand#install_gem
では、大きくわけて下記の2つが実行されます。
Gem::DependencyInstaller#resolve_dependencies
による依存gemの解決
Gem::RequestSet#install
によるgemのinstall
class Gem::Commands::InstallCommand < Gem::Command
def install_gem(name, version)
req = Gem::Requirement.create(version)
if options[:ignore_dependencies]
install_gem_without_dependencies name, req
else
inst = Gem::DependencyInstaller.new options
request_set = inst.resolve_dependencies name, req
if options[:explain]
else
@installed_specs.concat request_set.install options
end
show_install_errors inst.errors
end
end
Gem::DependencyInstaller#resolve_dependencies
による依存gemの解決
Gem::DependencyInstaller#resolve_dependencies
での依存gemの解決では、Gem::RequestSet
とGem::Dependency
とGem::Resolver::InstallerSet
のオブジェクトを生成して、Gem::Resolver::InstallerSet#always_install
でinstall対象の依存gemを取得し、最終的にGem::RequestSet
のオブジェクトにまとめて返却しています。
class Gem::Commands::InstallCommand < Gem::Command
def resolve_dependencies(dep_or_name, version)
request_set = Gem::RequestSet.new
installer_set = Gem::Resolver::InstallerSet.new @domain
dependency =
if spec = installer_set.local?(dep_or_name)
Gem::Dependency.new spec.name, version
elsif String === dep_or_name
Gem::Dependency.new dep_or_name, version
else
dep_or_name
end
dependency.prerelease = @prerelease
request_set.import [dependency]
installer_set.add_always_install dependency
request_set.always_install = installer_set.always_install
request_set
end
Gem::Resolver::InstallerSet#always_install
では、Gem::Resolver::DependencyRequest
のオブジェクトを生成して、find_all
を実行、そして@remote_set.find_all
でGem::Resolver::BestSet#find_all
が実行される。
class Gem::Resolver::InstallerSet < Gem::Resolver::Set
def initialize(domain)
@remote_set = Gem::Resolver::BestSet.new
@specs = {}
end
def add_always_install(dependency)
request = Gem::Resolver::DependencyRequest.new dependency, nil
found = find_all request
newest = found.max_by do |s|
[s.version, s.platform == Gem::Platform::RUBY ? -1 : 1]
end
@always_install << newest.spec
end
def find_all(req)
res = []
res.concat @remote_set.find_all req if consider_remote?
res
end
Gem::Resolver::BestSet#find_all
では、Gem::Resolver::BestSet#pick_sets
が実行され、@sources
の数だけGem::Source#dependency_resolver_set
が実行される。
class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet
def pick_sets
@sources.each_source do |source|
@sets << source.dependency_resolver_set
end
end
def find_all(req)
pick_sets if @remote and @sets.empty?
super
rescue Gem::RemoteFetcher::FetchError => e
replace_failed_api_set e
retry
end
※Gem::Source
はhttps://rubygems.org
等のgemの取得元を表す。
irb(main):001:0> Gem.sources
=>
Gem::Source#dependency_resolver_set
では、Gem::RemoteFetcher#fetch_path
でhttps://rubygems.org/api/v1/dependencies
にリクエストを投げてresponseを取得し、Gem::Resolver::APISet
のオブジェクトを生成します。
class Gem::Source
def dependency_resolver_set
return Gem::Resolver::IndexSet.new self if 'file' == uri.scheme
bundler_api_uri = uri + './api/v1/dependencies'
begin
fetcher = Gem::RemoteFetcher.fetcher
response = fetcher.fetch_path bundler_api_uri, nil, true
rescue Gem::RemoteFetcher::FetchError
Gem::Resolver::IndexSet.new self
else
if response.respond_to? :uri
Gem::Resolver::APISet.new response.uri
else
Gem::Resolver::APISet.new bundler_api_uri
end
end
end
Gem::Resolver::BestSet#pick_sets
にGem::Resolver::APISet.new
の@sets
にGem::Resolver::APISet
のオブジェクトを設定したあと、
super
を実行してGem::Resolver::ComposedSet#find_all
を実行します。
class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet
def find_all(req)
pick_sets if @remote and @sets.empty?
super
rescue Gem::RemoteFetcher::FetchError => e
replace_failed_api_set e
retry
end
Gem::Resolver::ComposedSet#find_all
では@sets
の各要素に対してfind_all
を呼び出すので、Gem::Resolver::APISet#find_all
が実行されます。
class Gem::Resolver::ComposedSet < Gem::Resolver::Set
def find_all(req)
@sets.map do |s|
s.find_all req
end.flatten
end
Gem::Resolver::APISet#find_all
はGem::Resolver::APISet#versions
を呼び出して、https://rubygems.org/api/v1/dependencies?gems=hoge
にリクエストを送信し、結果から依存等を管理するGem::Resolver::APISpecification
のオブジェクトを生成します。
class Gem::Resolver::APISet < Gem::Resolver::Set
def find_all(req)
res = []
versions(req.name).each do |ver|
if req.dependency.match? req.name, ver[:number]
res << Gem::Resolver::APISpecification.new(self, ver)
end
end
res
end
def versions(name)
uri = @dep_uri + "?gems=#{name}"
str = Gem::RemoteFetcher.fetcher.fetch_path uri
Marshal.load(str).each do |ver|
@data[ver[:name]] << ver
end
@data[name]
end
実際にhttps://rubygems.org/api/v1/dependencies?gems=name
にアクセスしてみるとこんな感じにgemの情報を取得できます。
require 'net/http'
require 'uri'
url = URI.parse('https://rubygems.org/api/v1/dependencies?gems=makanai')
res = Net::HTTP.get(url)
Marshal.load(res)
最初に戻ると最終的にはrequest_set
のalways_install
にinstall対象のgemのGem::Resolver::APISpecification
オブジェクトが格納されます。
class Gem::Commands::InstallCommand < Gem::Command
def resolve_dependencies(dep_or_name, version)
request_set = Gem::RequestSet.new
installer_set = Gem::Resolver::InstallerSet.new @domain
dependency =
if spec = installer_set.local?(dep_or_name)
Gem::Dependency.new spec.name, version
elsif String === dep_or_name
Gem::Dependency.new dep_or_name, version
else
dep_or_name
end
dependency.prerelease = @prerelease
request_set.import [dependency]
installer_set.add_always_install dependency
request_set.always_install = installer_set.always_install
request_set
end
Gem::RequestSet#install
によるgemのinstall
Gem::RequestSet#install
では、マルチスレッドでの
- Gemのダウンロード(
req.spec.download
)
- Gemのインストール(
req.spec.install
)
が実行されます。
class Gem::RequestSet
def install(options, &block)
sorted_requests.each do |req|
download_queue << req
end
threads = Gem.configuration.concurrent_downloads.times.map do
download_queue << :stop
Thread.new do
while req = download_queue.pop
break if req == :stop
req.spec.download options unless req.installed?
end
end
end
threads.each(&:value)
sorted_requests.each do |req|
if req.installed?
end
spec =
begin
req.spec.install options do |installer|
yield req, installer if block_given?
end
rescue Gem::RuntimeRequirementNotMetError => e
end
requests << spec
end
requests
end
Gemのダウンロード(req.spec.download
)
まずはreq.spec
で何が行われているかを確認します。まずreq
はGem::DependencyInstaller#resolve_dependencies
による依存gemの解決で格納されたGem::Resolver::APISpecification
のオブジェクトです。
Gem::Resolver::APISpecification#spec
を見てみましょう。内部ではsource.fetch_spec
を実行してGem::Source#fetch_spec
を実行しています。
class Gem::Resolver::APISpecification < Gem::Resolver::Specification
def spec
@spec ||=
begin
tuple = Gem::NameTuple.new @name, @version, @platform
source.fetch_spec tuple
rescue Gem::RemoteFetcher::FetchError
raise if @original_platform == @platform
tuple = Gem::NameTuple.new @name, @version, @original_platform
source.fetch_spec tuple
end
end
Gem::Source#fetch_spec
では、fetcher.fetch_path source_uri
を実行してhttps://api.rubygems.org/quick/Marshal.4.8/makanai-0.1.0.gemspec.rz
のようなURLにリクエストを投げて、(.rz
)形式で取得してパースする、
class Gem::Source
def fetch_spec(name_tuple)
fetcher = Gem::RemoteFetcher.fetcher
spec_file_name = name_tuple.spec_name
source_uri = uri + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}"
source_uri.path << '.rz'
spec = fetcher.fetch_path source_uri
spec = Gem::Util.inflate spec
TODO
Marshal.load spec
end
実際にMarshal.load spec
の箇所を再現すると、どんな値が設定されているかがわかる。
irb(main):007:0> pp Marshal.load(Gem::Util.inflate(File.read('/Users/user_name/Downloads/makanai-0.1.0.gemspec.rz')))
=>
なので、パースしたSpecification
のオブジェクトに基づいてダウンロード処理が実行されるはず・・・。
※このへんGem::Specification
にdownload
メソッドが無くて、req.spec.downlaod
が追えなくなってしまった。。。。
おそらくこの辺が呼ばれてgemの本体がダウンロードされている・・・?
rubygems/specification.rb at 9c142f5d9d7576012a440dceb8335c3139fcc16f · rubygems/rubygems · GitHub
rubygems/source.rb at 9c142f5d9d7576012a440dceb8335c3139fcc16f · rubygems/rubygems · GitHub
Gemのインストール(req.spec.install
)
パースしたSpecification
のオブジェクトに基づいてインストール処理が実行されるはず・・・。
※このへんGem::Specification
にinstall
メソッドが無くて、req.spec.install
が追えなくなってしまった。。。。
おそらくこの辺が呼ばれてgemの本体がインストールされている・・・?
rubygems/specification.rb at 9c142f5d9d7576012a440dceb8335c3139fcc16f · rubygems/rubygems · GitHub
おわりに
今回はgem install
を実行したときにrubygems
がどのようにgemをインストールしているかを実際の実装を読んで追ってみました。
※ダウンロードやインストール周りの処理は最後まで終えませんでしたが、どうやってgemspecを取得して依存関係を探るか等は勉強になりました。
考えてみたら当たり前ですが、rubygems.org
にリクエストを投げて依存関係等を取得しているんですね👀
なのでgemspec
に記載しているメタ情報を元にgemのインストールが行われるので、ちゃんとメンテナンスしておかないと使う人がgem installしたときに、依存gemがinstallされてなくて動かない、またちゃんとrubygemsにアップロードしないと修正が反映されない等が発生してしまうので注意ですね⚠
またrubygemsのコードは比較的pureなrubyで書かれていてtestを動かすのも簡単で処理は複雑でしたが読みやすかったです💎
次はbundlerのinstallの流れを追ってみようかなと思います。
参考
github.com
blog.freedom-man.com