はじめに
先日とあるハッカソンに参加した際に、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