はじめに

Ruby on Railsをインストールする場合、以下のコマンドを入力することになります。

gem install rails --include-dependencies

この一行によって何が起こるかを見ていきましょう。ちなみに、私はgemを使ったのは初めてだったので以下の出力が表示されました。

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

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

Gem::Version::Requirement(rubygems/version.rb)

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

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

バージョンチェック処理

次にバージョンチェック部分に移ります。以下のようになっています。

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メソッドは使う側のすぐ下に書くとスクロール量が少なくて済むので好きなのですが。

Gem::GemRunner(rubygems/gem_runner.rb)

gemコマンドに戻って、最後の一行でGem::GemRunnerオブジェクトを作成しrunメソッドを呼び出しています。青木さん添削本によると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形式で書かれた設定ファイルを読み込んでいます。が、設定ファイルはないので無視します。

Gem::CommandManager(rubygems/command_manager.rb)

オブジェクトの構築

次に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の各コマンド処理クラスっていつの間に定義されたんだっけ?と一瞬思ったのですがすぐ下ですね。つまり以下のような動きをするようです。

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

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

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

コマンドの検索

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

次にGem::CommandManager#runメソッドに移ります。runメソッドはprocess_argsメソッドを呼んでいるだけ(例外処理はしていますが)で、install --include-dependenciesとした場合、process_argsメソッドでは以下のコードが実行されます。

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つが該当します。この場合はどのコマンドを実行すればいいかわからないのでエラーになります。

Gem::Command(rubygems/command.rb)

というわけで実行するコマンドが判別できたら各コマンド実行クラスの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オブジェクトを淡々と構築しています。

Gem::Commands::InstallCommand(rubygems/commands/install_command.rb)

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

Gem::RemoteInstaller(rubygems/remote_installer.rb)

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

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

version_requirementはStringオブジェクトを渡すことも可能なのですが後の処理ではGem::Version::Requirementとして扱いたいのでStringオブジェクトからGem::Version::Requirementオブジェクトを構築しています。Gem::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の処理が行われているようですが今回入れたパッケージは全てバイナリgemではないので無視します。ともかく最新バージョンのパッケージ情報が返されるようです。次に、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:-)

Gem::SourceInfoCache(rubygems/source_info_cache.rb)

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パッケージで定義されていました。

Gem::SourceIndex(rubygems/source_index.rb)

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

再びGem::RemoteInstaller

インストールされていない依存パッケージの検索

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

つまりこういうことです。

  1. aパッケージをインストール。b, cに依存(RemoteInstaller.install('a'))
    1. bパッケージインストール。d, eに依存(RemoteInstaller.install('b'))
      1. dパッケージをインストール(RemoteInstaller.install('d'))
      2. eパッケージをインストール(RemoteInstaller.install('e'))
    2. cパッケージをインストール(RemoteInstaller.install('c'))

Gem::Installer(rubygems/installer.rb)

次にリモートからファイルをダウンロードしてくると、後はローカルにファイルがある場合と同じように処理できます。というわけで次は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

Gem::Package(rubygems/package.rb)

まず一行目の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(インストールするパッケージの各ファイルが格納されている)内の各ファイルとなっています。

追補Gem::Installer

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

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

おわりに

今回はRubyGemsのインストール処理を読みました。これだ!というものは特にないのですがとてもRuby的なコードになっていると思いました。

それではみなさんもよいコードリーディングを。


トップ   編集 凍結解除 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2007-08-13 (月) 22:15:06 (4498d)