トップ «前の日記(2014-02-17) 最新 次の日記(2014-02-26)» 編集

Route 477



2014-02-19

[ruby] Rakefileを分割するときに気をつけること

Railsプロジェクトの場合lib/tasks/*.rakeにタスク定義を書きますが、non-RailsなプロジェクトでRakeを使った場合、開発が進むにつれてRakefileが肥大化するケースがあります。

この場合、lib/tasks/*.rakeを読むようにする修正は特に難しくなくて、以下のようにDir.[]で検索してそれぞれloadしてやれば済みます。*1

PROJ_ROOT = __dir__  # Ruby 1.xの場合はFile.dirname(__FILE__)
Dir["#{PROJ_ROOT}/lib/tasks/**/*.rake"].each do |path|
  load path
end

あとは好きな粒度(例えばnamespaceごととか)でlib/tasks/foo.rakeを設置し、タスク定義をそっちに移動すればOKです。

ただし、単純に定義を移動するだけだとタスクが動かなくなるケースがあります。以下は代表的な例とその対処方法です。

1. トップレベルの変数定義

以下のようにRakefileのトップレベルでconfigという変数を定義している場合、ファイルを分けるとタスク:fooから変数configが参照できず、エラーになります。

config = File.read("somewhere/config.yml")

task :foo do
  puts config[:foo]
  ...
end

最も簡単な対処方法は、変数定義をトップレベルのメソッドにすることです。

def config
  File.read("somewhere/config.yml")
end

これでとりあえず動くと思いますが、configを参照するたびにFile.readが走るので、CONFIGのようなトップレベルの定数にするか、MyApp.config

2. _FILE__および_dir

「Rakefileがある場所からの相対パス」を取得するために__FILE____dir__を使用するのはよくあるケースです。ファイルを分けると__FILE__および__dir__の値が変わってしまうため、対処が必要です。

少し面倒ですが、Rakefileの先頭でPROJ_ROOTのような定数を宣言し、これを「Rakefileがある場所の絶対パス」として使うのが良いと思います。*2

# プロジェクトのトップディレクトリの絶対パス
PROJ_ROOT = File.expand_path(__dir__)

タスク中の__dir__は、PROJ_ROOTに置き換えられます。タスク中の__FILE__はFile.join(PROJ_ROOT, "Rakefile")に置き換えれば良いですが、Rakefile中で__FILE__を使っている場合、本当に欲しいのはRakefileのパスではなくディレクトリの方(つまりPROJ_ROOT)であることが多いと思うので、適宜置き換えてください。以下は代表的な例です。

# before
lib_path = File.expand_path("lib/", File.dirname(__FILE__))
# after
lib_path = File.join(PROJ_ROOT, "lib")

3. タスクのロード位置

lib/tasks/以下のファイルを読み込むコードを冒頭に貼りましたが、これをRakefileのどこに置くかは多少の注意が必要です。

結論からいうと以下のように定数やヘルパーを定義したあとでloadするようにすれば大丈夫です。loadを先に置いてしまうと、タスク定義時に何かするコードがあった場合に未定義の定数を参照してNameError、みたいなケースがあるかも知れません。

# Rakeタスク全体で使う定数定義
CONFIG = ...

# Rakeタスク全体で使うヘルパーメソッド定義
def foobar_path
  ...
end

# lib/tasks/以下のファイルを読み込む
PROJ_ROOT = __dir__
Dir["#{PROJ_ROOT}/lib/tasks/**/*.rake"].each do |path|
  load path
end

余談

2.はともかく1.と3.は、静的型付け言語であれば実行前にエラーになるので、動的言語の弱点を感じます。対処法としては以下の2点が考えられます:

  • a) 各タスクの中身はメソッドを1つ呼び出すだけ、みたいな感じにしてそれらに対するユニットテストをちゃんと書く
  • b) とりあえず動かしてみて動かなかったら直す

が、Rakeタスクはだいたい開発者が使うもので、かつ往々にしてテスト書くのが面倒な処理をすることが多いので、b)で済ますことも多そうな気がします (rakeタスクがプロダクトの一部に含まれるようなケースはちゃんとa)の方向で行きましょう)。

*1 requireじゃなくてloadなのは、拡張子をRailsに合わせて.rakeにすることを想定したためです。.rbにする場合はrequireで良いです

*2 Ruby 1.9には__dir__がないので、File.dirname(__FILE__)とするかRuby 2.xに移行してください