RubyGemsのコードリーディングをしました。バージョンは2.7.3です。今回は gem installのパターンで追ってみます。

コマンドの実行ファイルはbin/gemです。

bin/gemはGem::GemRunner#runを呼び出します。

Gem::CommandManager#run経由でGem::Commands::InstallCommand#executeが実行されます。

#install_gemsが根幹の処理になります。get_all_gem_names_and_versionsは引数に指定したgem名の配列です。

#install_gemは以下のように定義されています。

Gem::DependencyInstaller#resolve_dependenciesで取得したGem::RequestSetに対してinstallメソッドを実行しています。

Gem::DependencyInstaller#resolve_dependenciesは以下のように定義されています。

 全部追えなかったので全部追ううと複雑で本質から外れそうなので、Gem::Resolver::InstallerSet#add_always_installのみ解説します。

#find_allは以下のように定義されています。

@remote_setはGem::Resolver::BestSetのインスタンスです。Gem::Resolver::BestSet#find_allは以下のとおりです。

pick_setsはGem.sourcesの要素であるGem::Sourceのインスタンスの#dependency_resolver_setを呼び出して@setsに追加します。

#api_uriはGem::RemoteFetcher#api_endpointを呼び出しています。

_rubygems._tcp.rubygems.orgのSRVレコード(DNS)を取得します。rubygems.orgの場合はapi.rubygems.orgが返ります。

最終的に#dependency_resolver_setはGem::Resolver::APISetのインスタンスを返します(https://api.rubygems.org/api/v1/dependencies のGETリクエストのレスポンスはresponse.uriでしか利用されないっぽい)

@setsへの設定が終わると、Gem::Resolver::BestSet#find_allはsuperを呼び出し、Gem::Resolver::ComposedSet#find_allが呼ばれます。各々の要素であるGem::Resolver::APISetに対してfind_allが呼ばれます。

Gem::Resolver::APISet#find_allは以下のように定義されています。

#versionsはGem::RemoteFetcher#fetch_pathでhttps://api.rubygems.org/api/v1/dependencies?gems={name}からGETリクエストでgemのデータを取得します。

取得したデータは以下のような構造になっています。各要素にgemの名前、バージョン、プラットフォーム、依存gemが入っています。

これらの情報を元にGem::Resolver::APISpecificationのインスタンスがセットされます。

Gem::InstallerSet#add_always_installに戻ると、foundにはGem::Resolver::APISpecificationのインスタンスの配列が入り、max_byによって最新のバージョンが選択されます。この最新バージョンのGem::Resolver::APISpecification#specが@always_installにセットされます。

Gem::Resolver::APISpecification#specはGem::Source#fetch_specを呼び出します。

https://api.rubygems.org/quick/Marshal.4.8/{name}-{version}.gemspec.rz からGETでデータを取得してrzファイルを解凍します。解凍したgemspecはキャッシュとして${HOME}/.gem/specs/api.rubygems.org%443/quick/Marshal.4.8/ ディレクトリ内に保存されます。このgemspecはMarshal.dumpされているので、最終的にMarshal.loadした結果を返します。

こうして対象のgemと依存gemがgemspecから取得できるので、それらを良い感じにソートした要素をGem::RequestSetのインスタンスに入れます。Gem::RequestSet#installは以下のように定義されています。

sorted_requestsはGem::Resolver::ActivationRequestの配列でGem::Resolver::ActivationRequest#specはGem::Resolver::APISpecificationのインスタンスです。

Gem::Resolver::ApiSpacification#installはGem::Resolver::Specification#installで以下のように定義されています。

sourceはGem::SourceのインスタンスなのでGem::Source#downloadが呼ばれます。Gem::Source#downloadはさらにGem::RemoteFetcher#downloadを呼び出します。

色々やっていますが、ざっくり言うとhttps://api.rubygems.org/gems/{name}-{version}.gemからgemをダウンロードしてきて、cacheディレクトリに保存しています。

ダウンロード後は、ダウンロードしたローカルのファイルパスをGem::Installerのインスタンスに設定して、Gem::Installer#installを呼び出します。

Gem::Installer#extrace_filesはGem::Package#extract_filesを呼び出します。

gemファイルはtarアーカイブなのでtarで解凍してdata.tar.gzを取得し、gems/{name}-{version}のディレクトリに展開します。

Gem::Installer#generate_binでは#generate_bin_scriptでspec.executablesからgems/binディレクトリに実行ファイルを展開します。

Gem::Installer#write_specはspec.to_ruby_for_cacheでgemspecファイルをspecificationsディレクトリに書き出します。

Gem::Specification#to_ruby_for_cacheはGem::Specification#to_rubyを呼び出しており、こんな感じでごりごりとファイルを生成しています。

Gem::StubSpecification::PREFIXを付けて生成することに注意してください。これはrequireする際にRubyを評価することなくgem名、バージョン、プラットフォーム、require_pathsを抽出するための仕組みになっています。

まとめ

  1. _rubygems._tcp.rubygems.orgのSRVレコード(DNS)を取得。api.rubygems.orgが返る。
  2. https://#{1のホスト名}/api/v1/dependencies?gems={name}からgemの情報(名前、バージョン、依存gemなど)を取得
  3. https://#{1のホスト名}/quick/Marshal.4.8/{name}-{version}.gemspec.rz からデータを取得してrzファイルを解凍。解凍したgemspecはキャッシュとして${HOME}/.gem/specs/api.rubygems.org%443/quick/Marshal.4.8/ ディレクトリ内に保存。
  4. 依存関係等を考慮してインストールすべきgemを決定&良い感じにソート
  5. https://api.rubygems.org/gems/{name}-{version}.gemからダウンロード
  6. gemファイルはtarアーカイブなのでtarで解凍してdata.tar.gzを取得して、gems/{name}-{version}のディレクトリにgemの中身を展開。
  7. 実行ファイル群をbinディレクトリに展開
  8. gemspecファイルをspecificationsディレクトリに書き出し。requireのstubのPREFIXを付ける。
  9. (その他インストールの色んな処理)