Rakeを徹底解剖 - その1 "Rakeの実行から初期化まで"
はじめに
先日とあるハッカソンに参加した際に、rake の task を拡張したメソッドを追加する必要がありました。
単なるメソッド追加だろうと思っていたら、それが思いの外大変。
なかなかうまくいかず、どうやったら拡張できるのかを探るために rake の仕組みを調べてみることに。
ただ、この仕組みがなかなか複雑ですぐに理解できるものでもなく、その時は設計やコードの綺麗さを捨て、動くものを作る感じで乗り切りました。
後日、その時書いたコードに納得がいかず、"もっといい書き方はないのか?"と悶々と気になりだし、「そもそも 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
に関しては、ブロック内で発生したException
をcatch
してる模様。
今回は、初期化にフォーカスを当てるため、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