kei-p3’s blog

kei-pによる技術共有と思考整理

Rakeを徹底解剖 - その1 "Rakeの実行から初期化まで"

はじめに

先日とあるハッカソンに参加した際に、rake の task を拡張したメソッドを追加する必要がありました。
単なるメソッド追加だろうと思っていたら、それが思いの外大変。

なかなかうまくいかず、どうやったら拡張できるのかを探るために rake の仕組みを調べてみることに。
ただ、この仕組みがなかなか複雑ですぐに理解できるものでもなく、その時は設計やコードの綺麗さを捨て、動くものを作る感じで乗り切りました。

後日、その時書いたコードに納得がいかず、"もっといい書き方はないのか?"と悶々と気になりだし、「そもそも rake がどういう仕組みを調べてみよう」と思い、ソースリーディングをしてみました。

内容

内容としては、

  • rakeの仕組み
    • タスクの読み込み
    • タスクの定義
    • タスクの実行
  • 読んでいて気になった ruby テクニック
  • 余裕があったら、簡易版 rake の作成 -> 車輪の再発明 

あたりを取り上げていくつもりです。

なお、まとめ方としては、全体的な内容をまとめてからちゃんと構成立てるのがベストだと思う。
がしかし、個人的にモチベーション続かなそうなので、その日調べて気づいたことをその時の分だけまとめていく感じで書いていくつもりです。
そのため、後々響いてくるような複雑な箇所は若干読み飛ばし気味ながらまとめていくつもりです。

Rake実行および読み込み

最初はまず、bin/rakeなりbundle execなどで実行した時に下記のようなコードが読まれる。

## ext/rake
require 'rake'

Rake.application.run

ここで注意するのが、require 'rake'内でのrequire 'rake/dsl_definition'という箇所。 このrequire 'rake/dsl_definition'では、module Rake::DSLを定義するついでに、なんと

self.extend Rake::DSL

と、selfを拡張していること。

ここでの、self = mainを拡張してる。
正直かなり危なっかしいやり方ではあるが、なにか理由があってこの方法を取っているのであろう。
そしてまさに、これこそが今回のrakeの仕組みを調べようと思ったきっかけである。

Rakeの初期化

実行は、以下のように呼び出されている。

Rake.application.run

ここで、Rake.applicationは、Rake::Applicationインスタンス生成(シングルトン)をしている。

## rake/rake_module.rb

def application
  @application ||= Rake::Application.new
end

Rake::Application#initializeでは、読み込むファイルの拡張子やファイル名を定義。 あとは、読み込む際のクラスを指定してるっぽい。

## rake/application.rb

def initialize
  super
  @name = 'rake'
  @rakefiles = DEFAULT_RAKEFILES.dup
  @rakefile = nil
  @pending_imports = []
  @imported = []
  @loaders = {}
  @default_loader = Rake::DefaultLoader.new
  @original_dir = Dir.pwd
  @top_level_tasks = []
  add_loader('rb', DefaultLoader.new)
  add_loader('rf', DefaultLoader.new)
  add_loader('rake', DefaultLoader.new)
  @tty_output = STDOUT.tty?
  @terminal_columns = ENV['RAKE_COLUMNS'].to_i
end

そして、Rake::Application#runが呼ばれる。

## rake/application.rb

def run
  standard_exception_handling do
    init                    # 初期化
    load_rakefile               # taskの読み込み
    top_level               # コマンドの実行
  end
end

なお、standard_exception_handlingに関しては、ブロック内で発生したExceptioncatchしてる模様。

今回は、初期化にフォーカスを当てるため、initの中を読んでいく。

## rake/application.rb

def init(app_name='rake')
  standard_exception_handling do
    @name = app_name
    args = handle_options
    collect_command_line_tasks(args)
  end
end

ここのhandle_optionsでは、rakeで実行するタスクだけを抽出している模様。

オプションの一覧は、standard_rake_optionsに記述されている。

## rake/application.rb

def standard_rake_options # :nodoc:
  sort_options(
    [
      ['--all', '-A',
        "Show all tasks, even uncommented ones (in combination with -T or -D)",
        lambda { |value|
          options.show_all_tasks = value
        }
     ],
     ...
    ]
  )
end

オプションに指定されているものは、optionsに追加されていき、漏れたものが配列として返却されているようだ。

そして、argsは、collect_command_line_tasks(args)へと渡されて残りはのちに実行するタスクとして、登録されていく。

## rake/application.rb

def collect_command_line_tasks(args) # :nodoc:
  @top_level_tasks = []
  args.each do |arg|
    if arg =~ /^(\w+)=(.*)$/m
      ENV[$1] = $2
    else
      @top_level_tasks << arg unless arg =~ /^-/
    end
  end
  @top_level_tasks.push(default_task_name) if @top_level_tasks.empty?
end

なお、初めて知ったのだが基本的に環境変数は、引数と混ざらないようにコマンドの前に書くようにしていた。 $ HOGE=1 bin/rake task # こんな感じ

だが、どうやら rake は後ろに書く書き方でも、対応できるようだ。

if arg =~ /^(\w+)=(.*)$/m
  ENV[$1] = $2
else
  @top_level_tasks << arg unless arg =~ /^-/
end

とはいっても、コマンドによって対応していたりしていなかったりすることを考慮すると、やはり前に書くように気をつけておく方がいいと思う。

以上。ここまでがinitまでの処理の流れだ。

まとめ

今回はとりあえず、"rake実行から初期化まで"を徹底解剖してみました。
まだまだ、Rakeのコアの部分には触れてはいないものの、メソッドのまとめ方や細かなテクニックなどなかなか気になる箇所は多かったです。

次回は、load_rakefileを深ほって、Rakeのタスク読み取りの箇所を読み取っていきます!

おまけ

セーフティーなメソッド追加

なかったらメソッド追加? セーフティーなメソッド追加。

## rake/ext/core.rb

class Module
  def rake_extension(method) # :nodoc:
    if method_defined?(method)
      $stderr.puts "WARNING: Possible conflict with Rake extension: " +
        "#{self}##{method} already exists"
    else
      yield
    end
  end
end

使う時はこんな感じ。

## rake/ext/pathname.rb

class Pathname
  rake_extension("ext") do
    def ext(newext='')
      Pathname.new(Rake.from_pathname(self).ext(newext))
    end
  end
end

エラークラスの定義

なかなかユニークな書き方を発見。

## rake/application.rb

CommandLineOptionError = Class.new(StandardError)

同じワンラインで書くとするなら、自分ならこう書いてました。

class CommandLineOptionError < StandardError; end

クラスすらもインスタンス(クラスインスタンス)。まさにそれを体現した書き方。
「あ、それもありなのねw」っていう衝撃。