*はじめに [#g24116ef]

Ruby on Railsをインストールする方法として、

 gem install rails --include-dependencies

と書かれていました。この一行によって何が起こるかを見ていきましょう。

なお、今回対象としたバージョンは0.9.4です。

* [#zace7908]

 Bulk updating Gem source index for: http://gems.rubyforge.org
 Successfully installed rails-1.2.3
 Successfully installed rake-0.7.3
 Successfully installed activesupport-1.4.2
 Successfully installed activerecord-1.15.3
 Successfully installed actionpack-1.13.3
 Successfully installed actionmailer-1.3.3
 Successfully installed actionwebservice-1.2.3

$RUBY_HOME/lib/ruby/site_ruby/1.8

 $ wc rubygems.rb 
      509    1680   15196 rubygems.rb 

 $ wc rubygems/*.rb
       79     226    2048 rubygems/builder.rb
      289     833    7961 rubygems/command.rb
      152     431    4699 rubygems/command_manager.rb
      100     344    2772 rubygems/config_file.rb
      119     491    3585 rubygems/custom_require.rb
      137     485    3949 rubygems/dependency_list.rb
      139     417    4005 rubygems/doc_manager.rb
       71     244    2005 rubygems/format.rb
      273     830    7729 rubygems/gem_commands.rb
        7      12     105 rubygems/gem_open_uri.rb
       46     158    1102 rubygems/gem_openssl.rb
       43     104    1235 rubygems/gem_runner.rb
      667    2228   21364 rubygems/installer.rb
      156     554    4352 rubygems/old_format.rb
      771    2825   24486 rubygems/open-uri.rb
      855    2463   23384 rubygems/package.rb
      147     437    4030 rubygems/remote_fetcher.rb
      202     618    7018 rubygems/remote_installer.rb
        6      22     133 rubygems/rubygems_version.rb
      527    1992   16910 rubygems/security.rb
      486    1214   11875 rubygems/server.rb
      375    1182   10922 rubygems/source_index.rb
      189     619    4796 rubygems/source_info_cache.rb
       37     107     856 rubygems/source_info_cache_entry.rb
      706    2344   21814 rubygems/specification.rb
       25      74     511 rubygems/timer.rb
      262     665    6354 rubygems/user_interaction.rb
      156     528    5548 rubygems/validator.rb
      321     969    8202 rubygems/version.rb
     7343   23416  213750 total

 $ wc rubygems/commands/*.rb
       56     121    1317 rubygems/commands/build_command.rb
       82     215    2912 rubygems/commands/cert_command.rb
       73     188    2236 rubygems/commands/check_command.rb
       75     192    2309 rubygems/commands/cleanup_command.rb
       65     163    1809 rubygems/commands/contents_command.rb
      105     277    3151 rubygems/commands/dependency_command.rb
       58     184    1843 rubygems/commands/environment_command.rb
       81     244    2574 rubygems/commands/help_command.rb
      138     382    4783 rubygems/commands/install_command.rb
       32      61     621 rubygems/commands/list_command.rb
       20      43     497 rubygems/commands/outdated_command.rb
      102     306    3508 rubygems/commands/pristine_command.rb
       85     213    2657 rubygems/commands/query_command.rb
       74     156    1907 rubygems/commands/rdoc_command.rb
       34      58     624 rubygems/commands/search_command.rb
       94     239    2801 rubygems/commands/sources_command.rb
       57     155    1647 rubygems/commands/specification_command.rb
       50     106    1297 rubygems/commands/uninstall_command.rb
       75     283    2471 rubygems/commands/unpack_command.rb
      101     264    3397 rubygems/commands/update_command.rb
     1457    3850   44361 total

*rubygems/version.rb [#c992ddaa]

gemコマンドを見ると初めにRubyのバージョンチェックが行われています。

 required_version = Gem::Version::Requirement.new(">= 1.8.0")
 unless  required_version.satisfied_by?(Gem::Version.new(RUBY_VERSION))
   puts "Expected Ruby Version #{required_version}, was #{RUBY_VERSION}"
   exit(1)
 end

見てみましょう。まず、Gem::Version::Requirementです。早速おもしろいコードがあります。

 class Requirement
   
   OPS = {
     "="  =>  lambda { |v, r| v == r },
     "!=" =>  lambda { |v, r| v != r },
     ">"  =>  lambda { |v, r| v > r },
     "<"  =>  lambda { |v, r| v < r },
     ">=" =>  lambda { |v, r| v >= r },
     "<=" =>  lambda { |v, r| v <= r },
     "~>" =>  lambda { |v, r| v >= r && v < r.bump }
   }
   
   OP_RE = Regexp.new(OPS.keys.collect{|k| Regexp.quote(k)}.join("|"))

これらの英数はメソッドの外に書かれているため、Rubyインタプリタが読み込んだときにすぐ評価され演算子解析用正規表現を構築しています。

次に、Requirement#initializeメソッドとinitializeメソッドが呼んでいるparseメソッドです。

 def initialize(reqs)
   @requirements = reqs.collect do |rq|
     op, version_string = parse(rq)
     [op, Version.new(version_string)]
   end
   @version = nil   # Avoid warnings.
 end

 def parse(str)
   if md = /^?s*(#{OP_RE})?s*([0-9.]+)?s*$/.match(str)
     [md[1], md[2]]
   elsif md = /^?s*([0-9.]+)?s*$/.match(str)
     ["=", md[1]]
   elsif md = /^?s*(#{OP_RE})?s*$/.match(str)
     [md[1], "0"]
   else
     fail ArgumentError, "Illformed requirement [#{str}]"
   end
 end


渡された引数から演算子とバージョンを取り出しています。数字だけ指定すると等価比較になるようです。演算子だけ指定した場合は・・・意味があるのでしょうか?:-) 引数エラーでいいような。

以上でRequirementオブジェクトが構築できたので次にバージョンチェック部分に移ります。以下のようになっています。

 def satisfied_by?(version)
   normalize
   @requirements.all? { |op, rv| satisfy?(op, version, rv) }
 end

all?メソッドって知らないなと思ったら1.8で追加されたらしいです。ということは・・・スクリプトが実行できたら調べるまでもなく1.8ってことですよね:-)

それはともかくsatisfy?メソッドです。

 def satisfy?(op, version, required_version
   OPS[op].call(version, required_version)
 end

初めに見たOPSハッシュを参照してProcオブジェクトを呼び出しています。このような仕掛けでRubyGemsはこれ以外のバージョン要求についても処理しているようです。

ここまで呼んできてわかったこととして、RubyGemsではpublicメソッドとprivateメソッドを分けて書いているようです。個人的にはpublicメソッドから使われるprivateメソッドは使う側のすぐ下に書くとスクロール量が少なくて済むので好きなのですが。

*rubygems/gem_runner.rb [#g5fa53ac]

gemコマンドに戻って、最後の一行でGem::GemRunnerオブジェクトを作成しrunメソッドを呼び出しています。[[青木さん添削本>http://i.loveruby.net/ja/rubimabook/]]によるとGem::GemRunnerというのは少し冗長ですね。

他のメソッドで出てくるので、各インスタンス変数に何が入っているかを示しておきます。

 def initialize(options={})
   @command_manager_class = options[:command_manager] || Gem::CommandManager
   @config_file_class = options[:config_file] || Gem::ConfigFile
   @doc_manager_class = options[:doc_manager] || Gem::DocManager
 end

次にrunメソッドです。

 def run(args)
   do_configuration(args)
   cmd = @command_manager_class.instance
   cmd.command_names.each do |c|
     Command.add_specific_extra_args c, Array(Gem.configuration[c])
   end
   cmd.run(Gem.configuration.args)
 end

do_configurationは[[YAML形式>http://jp.rubyist.net/magazine/?0009-YAML]]で書かれた設定ファイルを読み込んでいます、が、設定ファイルはないので無視します。

*rubygems/command_manager.rb [#t6112996]

次にGem::CommandManagerをインスタンス化してます。単純にnewしていないところを見るとシングルトンパターンっぽいです。見てみましょう。

 def self.instance
   @command_manager ||= CommandManager.new
 end

何故これがシングルトンパターンを実装することになるかは||=演算子のからくりを知らないとわかりません。以下のような挙動になります。

:一回目|@command_managerがnullなのでCommandManager.newが実行され@command_managerに格納、@command_managerが返される
:二回目|@command_managerはnullではないのでCommandManager.newは実行されず、@command_managerが返される

さて、CommandManager#initializeメソッドに移るとRubyGemsの各コマンドが登録されているようです。登録メソッドを見てみましょう。

 def register_command(command_obj)
   @commands[command_obj] = load_and_instantiate(command_obj)
 end

 def load_and_instantiate(command_name)
   command_name = command_name.to_s
   begin
     Gem::Commands.const_get("#{command_name.capitalize}Command").new
   rescue
     require "rubygems/commands/#{command_name}_command"
     retry
   end
 end

load_and_instantiateメソッドでは各コマンドを実装するオブジェクトが作られています。Gem::Commandsの各コマンド処理クラスっていつの間に定義されたんだっけ?と一瞬思ったのですがすぐ下ですね。つまり以下のような動きをするようです。

+例えば、installコマンドを処理するGem::Commands::InstallCommandをインスタンス化しようとする
+定義されていないので下のrescueブロックが実行される
+rubygems/commands/install_command.rbが読み込まれる
+Gem::Commands::InstallCommandが定義される
+もう一度Gem::Commands::InstallCommandを作ろうとすると今度はインスタンスかできる

拡張可能なように設計するという場合のイディオムな感じですね。個人的にはinitializeメソッドで登録メソッドを呼ぶのではなくrubygems/command以下のものを勝手に登録してしまえばいいかな〜とも思うのですが。

そういえば何でクラスを取得するのにGem::Commandsの定数を取得してるんだ?と思われた方がいるかもしれませんが、それはRubyではクラスは定数として定義されているためです。

*rubygems/command_manager.rb [#ve5dc6bc]

GemRunner#runメソッドに戻って、次の行は設定ファイルに書かれているコマンド別の設定を設定しているだけなので飛ばします。

次にGem::CommandManagerクラスに移りましょう。GemRunnerから呼ばれているrunメソッドはprocess_argsメソッドを呼んでいるだけ(例外処理は例によって省きます)で、install --include-dependenciesとした場合は以下が実行されます。

 cmd_name = args.shift.downcase
 cmd = find_command(cmd_name)
 cmd.invoke(*args)

find_commandメソッドの中身がおもしろいので見てみましょう。

 def find_command(cmd_name)
   possibilities = find_command_possibilities(cmd_name)
   if possibilities.size > 1
     raise "Ambiguous command #{cmd_name} matches [#{possibilities.join(', ')}]"
   end
   if possibilities.size < 1
     raise "Unknown command #{cmd_name}"
   end
   self[possibilities.first]
 end

 def find_command_possibilities(cmd_name)
   len = cmd_name.length
   self.command_names.select { |n| cmd_name == n[0,len] }
 end

入力された文字列からどのコマンドっぽいかを検索しています。どのコマンドっぽいかというのは例えば

 gem l

と入力した場合、listだけがArray#selectメソッドのブロックでtrueになるのでこの人はlistを指定したかったんだなということです。

 gem s

だとsource, search, specificationの3つが該当します。この場合はどのコマンドを実行すればいいかわからないのでエラーになります。

*rubygems/command.rb [#dac53b6f]

というわけで実行するコマンドが判別できたら各コマンド実行クラスのinvokeメソッドが呼ばれます。今回対象とするのはGem::Commands::InstallCommandです。

invokeメソッドの前にinitializeメソッド周りを見てみましょう。

 class InstallCommand < Command
   include CommandAids
   include VersionOption
   include LocalRemoteOptions
   include InstallUpdateOptions
 
   def initialize
     super(
       'install',
       'Install a gem into the local repository',
       {
         :domain => :both, 
         :generate_rdoc => true,
         :generate_ri   => true,
         :force => false, 
         :test => false, 
         :wrappers => true,
         :version => "> 0",
         :install_dir => Gem.dir,
         :security_policy => nil,
       })
     add_version_option('install')
     add_local_remote_options
     add_install_update_options
   end

親クラスのコンストラクタを呼んでいます。第1引数はコマンド名、第2引数はコマンドの説明とすぐわかりますが第3引数はちょっとわかりません。親クラスのinitialize見てみると、

 def initialize(command, summary=nil, defaults={})
   @command = command
   @summary = summary
   @program_name = "gem #{command}"
   @defaults = defaults
   @options = defaults.dup
   @option_list = []
   @parser = nil
   @when_invoked = nil
 end

となっているので第3引数はオプションで変更可能な値のデフォルト値と推察されます。

次にadd_version_optionメソッドですがこれは上でincludeしているVersionOptionモジュール(rubygems/gem_commands.rb)で定義されています。

 def add_version_option(taskname, *wrap)
   add_option('-v', '--version VERSION', 
              "Specify version of gem to #{taskname}", *wrap) do 
                |value, options|
     options[:version] = value
   end
 end

add_local_remote_options, add_install_update_optionsについても同様の処理が行われています。ちなみに、--include-dependenciesオプションはadd_install_update_optionsメソッドで定義されるようです。add_optionメソッドはCommandクラスで定義されています。

 def add_option(*args, &handler)
   @option_list << [args, handler]
 end

それでは次にCommandManagerから呼ばれるinvokeメソッドを見てみましょう。InstallCommandクラスにはinvokeメソッドがなく親クラスのCommandクラスに定義されています。

 def invoke(*args)
   handle_options(args)
   if options[:help]
     show_help
   elsif @when_invoked
     @when_invoked.call(options)
   else
     execute
   end
 end

handle_optionsメソッドは次のようになっています。

 def handle_options(args)
   args = add_extra_args(args)
   @options = @defaults.clone
   parser.parse!(args)
   @options[:args] = args
 end

extra_argsは設定ファイルに書かれていた場合に追加の引数を指定するものなようなので無視します。次にparserですが、実はこれは変数ではなくメソッドです。う〜む、get_parserという名前にすべきだと思います。ともかくparserメソッドに移りましょう。

 def parser
   create_option_parser if @parser.nil?
   @parser
 end

パーサが作られていなかったら作成、作られていたら単純に返しています。先ほど||=を使ってシングルトンパターンを実装していたのの別パターンですね。

create_option_parserメソッドではOptionParserオブジェクトを淡々と構築しています。

パーサの構築が終わるとparse!メソッドが呼び出され、@optionsに値が設定されます。

*rubygems/commands/install_command.rb [#f8a3267d]

invokeメソッドに戻るとexecuteメソッドが呼ばれます。Commandクラスの子クラスはこのメソッドをオーバーライドして処理を実装するようです。

executeメソッドの前半はローカルにgemファイルがある場合の処理のようです。今回はリモートからgemファイルを拾ってくるので無視しましょう。とするとインストール処理を行っているのは次の部分のようです。

 installer = Gem::RemoteInstaller.new(options)
 installed_gems = installer.install(
   gem_name,
   options[:version],
   options[:force],
   options[:install_dir])
   if installed_gems
   installed_gems.compact!
   installed_gems.each do |spec|
     say "Successfully installed #{spec.full_name}"
   end
 end

*rubygems/remote_installer.rb [#y2f0ff6b]

ではGem::RemoteInstaller#installメソッドです。まず初めにRubyにあまり詳しくない方のために、

 unless version_requirement.respond_to?(:satisfied_by?)
   version_requirement = Version::Requirement.new [version_requirement]
 end

version_requirementはStringオブジェクトを渡すことも可能なのですが後の処理ではVersion::Requirementとして扱いたいのでStringオブジェクトからVersion::Requirementオブジェクトを構築しています。Version::Requirementオブジェクトかのチェックにはオブジェクトがsatisfied_by?メソッドを実装しているかでチェックしています。以前見たsoap4rではis_a?メソッドが使われていました。

さて本題の部分です。

 spec, source = find_gem_to_install(gem_name, version_requirement)
 dependencies = find_dependencies_not_installed(spec.dependencies)
 
 installed_gems << install_dependencies(dependencies, force, install_dir)
 
 cache_dir = @options[:cache_dir] || File.join(install_dir, "cache")
 destination_file = File.join(cache_dir, spec.full_name + ".gem")
 
 download_gem(destination_file, source, spec)

 installer = new_installer(destination_file)
 installed_gems.unshift installer.install(force, install_dir, install_stub)

依存関係を見てインストールしてから指定されたパッケージを入れるというセオリー通りの処理が行われています。

まずfind_gem_to_installメソッドを見てみましょう。

 def find_gem_to_install(gem_name, version_requirement)
   specs_n_sources = specs_n_sources_matching gem_name, version_requirement
 
   top_3_versions = specs_n_sources.map{|gs| gs.first.version}.uniq[0..3]
   specs_n_sources.reject!{|gs| !top_3_versions.include?(gs.first.version)}
 
   binary_gems = specs_n_sources.reject { |item|
     item[0].platform.nil? || item[0].platform==Platform::RUBY
   }
 
   # only non-binary gems...return latest
   return specs_n_sources.first if binary_gems.empty?

find_gem_to_installメソッドの残りではバイナリgemの処理が行われているようですが今回入れたパッケージは全てpure Rubyなようなので無視します。ともかく最新バージョンのパッケージ情報が返されるようです。次に、specs_n_sources_matchingメソッドに進みましょう。ところで、メソッド呼び出しなのに()を付けないのは減点1ですね:-)

 def specs_n_sources_matching(gem_name, version_requirement)
   specs_n_sources = []
 
   source_index_hash.each do |source_uri, source_index|
     specs = source_index.search(/^#{Regexp.escape gem_name}$/i,
                                 version_requirement)
     # TODO move to SourceIndex#search?
     ruby_version = Gem::Version.new RUBY_VERSION
     specs = specs.select do |spec|
       spec.required_ruby_version.nil? or
         spec.required_ruby_version.satisfied_by? ruby_version
     end
     specs.each { |spec| specs_n_sources << [spec, source_uri] }
   end
 
   specs_n_sources = specs_n_sources.sort_by { |gs,| gs.version }.reverse
 
   specs_n_sources
 end

 def source_index_hash
   return @source_index_hash if @source_index_hash
   @source_index_hash = {}
   Gem::SourceInfoCache.cache_data.each do |source_uri, sic_entry|
     @source_index_hash[source_uri] = sic_entry.source_index
   end
   @source_index_hash
 end

どうやらリモートのパッケージ一覧をキャッシュしてそこから要求されたパッケージを探しているようです。次にGem::SourceInfoCache周りを見てみましょう。ところで、gsってGemSpecの略なんですね。GhostScript?と思ってしまいました。紛らわしいので減点1:-)

*rubygems/source_info_cache.rb [#l3782b85]

Gem::SourceInfoCache.cache_dataから始まる呼び出しは以下のようになっています。refreshメソッドの最後のflushメソッドでリモートパッケージ一覧がキャッシュされているようです。

 def self.cache_data
   cache.cache_data
 end

 def self.cache
   return @cache if @cache
   @cache = new
   @cache.refresh
   @cache
 end

 def refresh
   Gem.sources.each do |source_uri|
     cache_entry = cache_data[source_uri]
     if cache_entry.nil? then
       cache_entry = Gem::SourceInfoCacheEntry.new nil, 0
       cache_data[source_uri] = cache_entry
     end
 
     cache_entry.refresh source_uri
   end
   update
   flush
 end

今回は初めてgem installしたとして、SourceInfoCache#cache_dataメソッドは単純に空ハッシュを返します(そのため、ifブロックが実行されてGem::SourceInfoCacheEntryオブジェクトが作成されます)。

Gem.sourcesってどこに定義されているんだ?と探したところ、RubyGemsをインストールしたときに同時にインストールされるsourcesパッケージで定義されていました。

*rubygems/source_index.rb [#nb885523]

Gem::SourceInfoCacheEntry#refreshメソッドです。content-lengthの値で更新されてないかを判断しています。last-modifiedを見た方がいい気がしますね。

 def refresh(source_uri)
   remote_size = Gem::RemoteFetcher.fetcher.fetch_size source_uri + '/yaml'
   return if @size == remote_size # HACK bad check, local cache not YAML
   @source_index.update source_uri
   @size = remote_size
 end

Gem::SourceIndexクラスに移りましょう。

 def update(source_uri)
   use_incremental = false
 
   begin
     gem_names = fetch_quick_index source_uri
     remove_extra gem_names
     missing_gems = find_missing gem_names
     use_incremental = missing_gems.size <= INCREMENTAL_THRESHHOLD
   rescue Gem::OperationNotSupportedError => ex
     use_incremental = false
   end
 
   if use_incremental then
     update_with_missing source_uri, missing_gems
   else
     new_index = fetch_bulk_index source_uri
     @gems.replace new_index.gems
   end
 
   self
 end

以前パッケージ情報を取得したときから増えているパッケージが一定数以下なら差分アップデートをしているようです。今回はキャッシュはないのでfetch_bulk_indexメソッドに移ります。

  def fetch_bulk_index(source_uri)
   say "Bulk updating Gem source index for: #{source_uri}"
 
   begin
     yaml_spec = fetcher.fetch_path source_uri + '/yaml.Z'
     yaml_spec = unzip yaml_spec
   rescue
     begin
       yaml_spec = fetcher.fetch_path source_uri + '/yaml'
     end
   end
 
   convert_specs yaml_spec
 end

どうやらパッケージ情報はYAMLで書かれているようです。長いのでサンプルを載せるのは止めておきます。

え〜っと・・・、ここまででパッケージ情報が取得できたので次はsearchメソッドですね。特に説明は要らないかと思います。

 def search(gem_pattern, version_requirement=Version::Requirement.new(">= 0"))
   gem_pattern = /#{ gem_pattern }/i if String === gem_pattern
   version_requirement = Gem::Version::Requirement.create(version_requirement)
   result = []
   @gems.each do |full_spec_name, spec|
     next unless spec.name =~ gem_pattern
     result << spec if version_requirement.satisfied_by?(spec.version)
   end
   result = result.sort
   result
 end

* [#rc03388a]

Gem::RemoteInstaller#installメソッドに戻ります。次はfind_dependencies_not_installedメソッドです。

 def find_dependencies_not_installed(dependencies)
   to_install = []
   dependencies.each do |dependency|
     srcindex = Gem::SourceIndex.from_installed_gems
     matches = srcindex.find_name(dependency.name, dependency.requirement_list)
     to_install.push dependency if matches.empty?
   end
   to_install
 end

そんな感じかなというところです。もちろん、必要がないのに「何やってるんだ?このコードは??」というコードを書く必要はありません。

Gem::SourceIndex#from_installed_gemsメソッドおよび呼ばれているメソッド達です(例外処理省略済み)。

 def from_installed_gems(*deprecated)
   if deprecated.empty?
     from_gems_in(*installed_spec_directories)
   else
     from_gems_in(*deprecated)
   end
 end

 def from_gems_in(*spec_dirs)
   self.new.load_gems_in(*spec_dirs)
 end

 def load_gems_in(*spec_dirs)
   @gems.clear
   specs = Dir.glob File.join("{#{spec_dirs.join(',')}}", "*.gemspec")
   specs.each do |file_name|
     gemspec = self.class.load_specification(file_name.untaint)
     add_spec(gemspec) if gemspec
   end
   self
 end

 def load_specification(file_name)
   spec_code = File.read(file_name).untaint
   gemspec = eval(spec_code)
   gemspec.loaded_from = file_name
   return gemspec
 end

gemspecファイルを見るとわかりますがgemspecファイルはRubyスクリプトでGem::Specificationオブジェクトが定義されています。

Gem::RemoteInstaller#installメソッドに戻って、次のinstall_dependenciesメソッドです。RemoteInstallerを作ってinstallメソッドを呼び出すことで依存パッケージが依存するパッケージも適切にインストールされます。

 def install_dependencies(dependencies, force, install_dir)
   return if @options[:ignore_dependencies]
   installed_gems = []
   dependencies.each do |dep|
     if @options[:include_dependencies] ||
        ask_yes_no("Install required dependency #{dep.name}?", true)
       remote_installer = RemoteInstaller.new @options
       installed_gems << remote_installer.install(dep.name,
                                                  dep.version_requirements,
                                                  force, install_dir)
     elsif force then
       # ignore
     else
       raise DependencyError, "Required dependency #{dep.name} not installed"
     end
   end
   installed_gems
 end

*rubygems/installer.rb [#z88a792c]

次にリモートからファイルをダウンロードしてくると、後はローカルにファイルがある場合と同じように処理できます。というわけで次はGem::Installerクラスです。installメソッドは例外処理とかを省くと以下のようになります。

 def install(force=false, install_dir=Gem.dir, ignore_this_parameter=false)
   format = Gem::Format.from_file_by_path @gem, security_policy
 
   # Build spec dir.
   @directory = File.join(install_dir, "gems", format.spec.full_name).untaint
   FileUtils.mkdir_p @directory
 
   extract_files(@directory, format)
   generate_bin(format.spec, install_dir)
   build_extensions(@directory, format.spec)
 
   # Build spec/cache/doc dir.
   build_support_directories(install_dir)
 
   # Write the spec and cache files.
   write_spec(format.spec, File.join(install_dir, "specifications"))
   unless File.exist? File.join(install_dir, "cache", @gem.split(/?//).pop)
     FileUtils.cp @gem, File.join(install_dir, "cache")
   end
 
   puts format.spec.post_install_message unless format.spec.post_install_message.nil?

   format.spec.loaded_from = File.join(install_dir, 'specifications', format.spec.full_name+".gemspec")
   return format.spec
 end

*rubygems/package.rb [#w849937a]

まず一行目のGem::Format.from_file_by_pathメソッドから。例外処理と旧フォーマットかのチェックの後from_ioメソッドが呼ばれています。

 def self.from_io(io, gem_path="(io)", security_policy = nil)
   format = self.new(gem_path)
   Package.open_from_io(io, 'r', security_policy) do |pkg|
     format.spec = pkg.metadata
     format.file_entries = []
     pkg.each do |entry|
       format.file_entries << [{"size", entry.size, "mode", entry.mode,
           "path", entry.full_name}, entry.read]
     end
   end
   format
 end

Gem::Packageに処理が移っています。package.rbを開いて眺めるとTar何とかと書いてあるのでgemファイルの実体はtarフォーマットと推察されます。

Gem::Package.open_from_ioメソッドは第2引数modeが"r"の場合TarInput.open_from_ioメソッドを呼び出しています。TarInput.open_from_ioメソッドはTarInputオブジェクトを作成後ブロックを呼び出しています。

TarInput#initializeメソッドでは与えられたストリームからTarReaderオブジェクトを構築して、セキュリティ周りのことがごそごそされていますがそこら辺を無視するとmetadata.gzからGem::Specificationを取得しています。metadata.gzを展開したmetadataは例によってYAMLでパッケージ情報が書かれています。

次にPackage#eachメソッドを眺めてみます。

 def each(&block)
   @tarreader.each do |entry|
     next unless entry.full_name == "data.tar.gz"
     is = zipped_stream(entry)
     begin
       TarReader.new(is) do |inner|
         inner.each(&block)
       end
     ensure
       is.close if is
     end
   end
   @tarreader.rewind
 end

というわけでPackage#eachメソッドのブロックに渡されてくるのはdata.tar.gz(インストールするパッケージの各ファイルが格納されている)内の各ファイルとなっています。

* [#q9bc9ae7]

Installer#installメソッドに戻って、extract_filesメソッドはすでにファイルは展開されているので各ファイルを書き込むだけです。その後、実行ファイルがある場合は実行ファイルのスタブを作成し、拡張ライブラリがある場合は拡張ライブラリを構築し、先ほど挙げたGem::SpeficationオブジェクトのためのRubyスクリプトを書き込んでinstallメソッドは終了です。

以上でRubyGemsのパッケージインストール処理は終了です。


トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS