Rakeビルド言語の使用

Rakeは、makeやantと同様の目的を持つビルド言語です。makeやantと同様にドメイン固有言語ですが、これら2つとは異なり、Ruby言語でプログラミングされた内部DSLです。この記事では、Rakeを紹介し、このWebサイトの構築にRakeを使用した際に得られた興味深い点について説明します。依存関係モデル、合成タスク、カスタムビルドルーチン、およびビルドスクリプトのデバッグなどです。

2014年12月29日



私は長年Rubyを広範囲に使用しています。その簡潔でありながら強力な構文と、一般的に優れたライブラリが好きです。数年前、私のWebサイトの生成の多くをXSLTからRubyに変換し、その変更に非常に満足しています。

私の記事を定期的に読んでいる方は、私のWebサイト全体が自動的に構築されていることをご存知でしょう。当初は、Java XSLプロセッサによく適合していたため、Javaの世界で一般的なビルド環境であるantを使用していました。Rubyをより多く使用するにつれて、Jim Weirichが開発したRubyベースのビルド言語であるRakeをより多く使用するようになりました。最近、私はビルドプロセスを完全に置き換え、Rakeを支持してすべてのantを削除しました。

Rakeを使い始めた初期の頃は、antを使用していた方法と同様に使用していました。しかし、今回の移行では、Rakeの興味深い機能をいくつか探求するために、異なる方法で実行してみました。その結果、これらの領域を掘り下げるためにこの記事を書くことにしました。Rakeは、私にとって3番目のビルド言語です。以前はmakeを長年使用していました(その多くを忘れてしまいましたが)。ここ6年ほどはantをかなり使用しています。Rakeには、これらの言語が持つ多くの機能に加えて、私にとって新しい要素がいくつかあります。RakeはRubyで記述されており、Ruby言語を多用していますが、自動化されたビルド処理には使用できます。簡単なビルドスクリプトではRubyを知る必要はありませんが、物事がより興味深くなり始めると、Rakeに最適なことをさせるにはRubyを知る必要があります。

これはやや偏った話です。Rakeのチュートリアルを書こうとしているのではなく、完全な網羅的な説明ではなく、私が興味深いと思う点に焦点を当てます。Ruby、Rake、または他のビルド言語について知っていることは前提としません。必要に応じて、Rubyの関連部分について説明します。これらのいずれかを試したことがある方、または異なる計算モデルに興味がある方にとっては、読みごたえのある記事になることを願っています。

依存関係ベースのプログラミング

ちょっと待ってください。前の段落で「異なる計算モデル」と言いました。これはビルド言語にとって、やや大げさな表現ではないでしょうか?いいえ、そんなことはありません。私が使用したすべてのビルド言語(make、ant(Nant)、およびRake)は、通常の命令型スタイルではなく、依存関係ベースの計算スタイルを使用しています。これにより、それらをどのようにプログラミングするかについて、異なる考え方が必要になります。ほとんどのビルドスクリプトは非常に短いため、ほとんどの人はそのように感じませんが、実際には非常に大きな違いです。

例を見てみましょう。プロジェクトを構築するプログラムを作成したいとします。このビルドにはいくつかの異なるステップがあります。

  • CodeGen:データ構成ファイルを取得し、それらを使用してデータベース構造とデータベースにアクセスするためのコードを生成します。
  • コンパイル:アプリケーションコードをコンパイルします。
  • データロード:テストデータをデータベースにロードします。
  • テスト:テストを実行します。

これらのタスクのいずれかを独立して実行し、すべてが機能することを確認する必要があります。前の手順をすべて実行するまでテストできません。コンパイルとデータロードは、最初にCodeGenを実行する必要があります。これらのルールをどのように表現すればよいでしょうか?

命令型スタイルで実行する場合、次のようになり、各タスクはRubyプロシージャになります。

# this is comment in ruby
def codeGen  #def introduces a procedure or method
  # do code gen stuff
end

def compile
  codeGen
  # do compile stuff
end

def dataLoad
  codeGen
  # do data load stuff
end

def test
  compile
  dataLoad
  #run tests
end

これには問題があることに注意してください。テストを呼び出すと、codeGenステップを2回実行します。codeGenステップは(私が想定するに)べき等であるため、これはエラーを引き起こしません。つまり、複数回呼び出すことは1回呼び出すことと違いはありません。ただし、時間がかかり、ビルドには時間がありません。

これを修正するために、次のようにステップをパブリック部分と内部部分に分離できます。

def compile
  codeGen
  doCompile
end

def doCompile
  # do the compile
end

def dataLoad
  codeGen
  doDataLoad
end

def doDataLoad
  #do the data load stuff
end

def test
  codeGen
  doCompile
  doDataLoad
  #run the tests
end

これは機能しますが、少し煩雑です。また、依存関係ベースのシステムがどのように役立つかの完璧な例です。命令型モデルでは、各ルーチンはルーチン内のステップを呼び出します。依存関係ベースのシステムでは、タスクがあり、前提条件(依存関係)を指定します。タスクを呼び出すと、前提条件があるかどうかを確認し、各前提条件タスクを1回呼び出すように手配します。したがって、単純な例は次のようになります。

task :codeGen do
  # do the code generation
end

task :compile => :codeGen do
  #do the compilation
end

task :dataLoad => :codeGen do
  # load the test data
end

task :test => [:compile, :dataLoad] do
  # run the tests
end

(これが何を意味するのか理解できることを願っています。構文については後で適切に説明します。)

コンパイルを呼び出すと、システムはコンパイルタスクを調べ、codeGenタスクに依存していることを確認します。次に、codeGenタスクを調べ、前提条件がないことを確認します。結果として、codeGenに続いてコンパイルが実行されます。これは、命令型の状況と同じです。

興味深いケースは、もちろんテストです。ここで、システムはコンパイルとデータロードの両方がcodeGenに依存していることを確認するため、codeGenが最初に実行され、その後にコンパイルとデータロード(任意の順序で)、最後にテストが実行されるようにタスクを調整します。基本的に、タスクの実際の実行順序は、ビルドスクリプトを記述するプログラマーによって設計時に決定されるのではなく、実行エンジンによって実行時に計算されます。

この依存関係ベースの計算モデルはビルドプロセスによく適合しており、これら3つすべてがそれを使用している理由です。タスクと依存関係の観点からビルドを考えるのは自然であり、ビルドのほとんどのステップはべき等であり、不必要な作業によってビルドが遅くなることは望ましくありません。ビルドスクリプトを作成する人は、ファンキーな計算モデルでプログラミングしていることに気づいていないと思いますが、それが現実です。

ビルド用のドメイン固有言語

私の3つのビルド言語はすべて、別の特性を共有しています。それらはすべてドメイン固有言語(DSL)の例です。ただし、それらは異なる種類のDSLです。以前に使用した用語では、

  • makeは、カスタム構文を使用した外部DSLです
  • ant(およびnant)は、XMLベースの構文を使用した外部DSLです
  • rakeは、Rubyを使用した内部DSLです。

Rakeが汎用言語の内部DSLであるという事実は、他の2つとの非常に重要な違いです。基本的に、Rakeスクリプトが有効なRubyであることを保証するために、いくつか奇妙に見えることを行う必要があるという犠牲を払って、必要なときにRubyのすべての能力を使用できます。Rubyは控えめな言語であるため、構文上の奇妙さはそれほど多くありません。さらに、Rubyは本格的な言語であるため、興味深いことをするためにDSLから抜け出す必要はありません。これは、makeやantを使用する上での常なる不満でした。実際、ビルド言語は、そのフル言語の能力が十分に価値があるほど頻繁に必要になるため、内部DSLに最適であると考えるようになりました。また、ビルドスクリプトを作成するプログラマー以外の人々はそれほど多くありません。

Rakeタスク

Rakeは2種類のタスクを定義します。通常のタスクはantのタスクに似ており、ファイルタスクはmakeのタスクに似ています。それらが何も意味しない場合は、心配しないでください。これから説明します。

通常のタスクは説明が最も簡単です。これは、私のテスト環境用のビルドスクリプトの1つです。

task :build_refact => [:clean] do
  target = SITE_DIR + 'refact/'
  mkdir_p target
  require 'refactoringHome'
  OutputCapturer.new.run {run_refactoring}
end
 

最初の行は、タスクの大部分を定義しています。この言語では、taskは実質的にタスク定義を導入するキーワードです。:build_refactは、タスクの名前です。名前を付けるための構文は、コロンで開始する必要があるという点で少し奇妙です。これは、内部DSLであることの結果の1つです。

タスクの名前の後、前提条件に進みます。ここには、:cleanという1つだけがあります。構文は => [:clean]です。角かっこ内にカンマで区切って複数の依存関係を一覧表示できます。前の例から、タスクが1つしかない場合は、角かっこは必要ないことがわかります。依存関係がない場合(または他の理由で)、依存関係はまったく必要ありません(後で説明する魅力的なトピックがあります)。

タスクの本体を定義するには、doend内にRubyコードを記述します。このブロック内には、好きな有効なRubyを配置できます。タスクがどのように機能するかを確認するために理解する必要はないため、このコードについてはここで説明しません。

Rakeスクリプト(またはrubyistが呼ぶようにrakefile)の優れた点は、これをビルドスクリプトとして非常に明確に読むことができることです。antで同等のものを記述すると、次のようになります。

<target name = "build_refact" depends = "clean">
<-- define the task -->
</target>

これで、これをDSLとして見てフォローできますが、内部DSLであるため、これが有効なRubyとしてどのように機能するかに興味があるかもしれません。実際には、taskはキーワードではなく、ルーチン呼び出しです。2つの引数を取ります。

最初の引数は、ハッシュ(マップまたはディクショナリと同等)です。Rubyには、ハッシュ用の特別な構文があります。一般的に、構文は{key1 => value1, key2 => value2}です。ただし、ハッシュが1つしかない場合、中かっこはオプションであるため、Rakeタスクを定義するときに中かっこは必要ありません。これにより、DSLを簡素化できます。では、キーと値は何でしょうか?ここでのキーはシンボルです。Rubyでは先頭のコロンで識別されます。他のリテラルを使用することもできます(文字列をすぐに確認できます)。また、変数と定数も使用できます。これらは非常に便利であることがわかります。値は配列です。これは、他の言語のリストとほぼ同等です。ここでは、他のタスクの名前を一覧表示します。角かっこを使用しない場合、リストの代わりに1つの値だけがあります。Rakeは、配列または単一のリテラルに対応します。非常に融通が利きます。

では、2番目の引数はどこにあるのでしょうか?それは doend の間にあるものです。それはブロックであり、Ruby におけるクロージャのことです。rakeファイルが実行されると、依存関係のリンクを通じて互いに接続され、適切なタイミングが来たときに実行するブロックを持つタスクオブジェクトのオブジェクトグラフが構築されます。すべてのタスクが作成されると、rakeシステムは依存関係のリンクを使用して、どのタスクをどの順序で実行する必要があるかを判断し、適切な順序で各タスクのブロックを呼び出します。クロージャの重要な特性は、評価されたときに実行する必要がなく、後で保存できることです。ブロックが実際に実行されるときにスコープにない変数であっても参照できるのです。

ここで重要なのは、私たちが見ているものが合法的なRubyコードであるということです。確かに非常に奇妙な方法で配置されていますが。しかし、この奇妙な方法によって、非常に読みやすいDSLを持つことができるのです。Rubyは構文が非常に最小限であるという点でも役立っています。手続きの引数に括弧が不要であるといった些細なことでも、このDSLをコンパクトに保つのに役立っています。クロージャは、内部DSLを作成する際に、代替の制御構造でコードをパッケージ化できるため、非常に重要です。

ファイルタスク

私が上で話したタスクは、antのタスクに似ています。Rakeは、makeのタスクの概念に近い、ファイルタスクと呼ばれる少し異なる種類のタスクもサポートしています。以下は、私のWebサイトのrakefileから少し簡略化した別の例です。

file 'build/dev/rake.html' => 'dev/rake.xml' do |t|
  require 'paper'
  maker = PaperMaker.new t.prerequisites[0], t.name
  maker.run
end

ファイルを使用する場合、タスク名ではなく実際のファイルを参照しています。したがって、「build/dev/rake.html」と「dev/rake.xml」は実際のファイルです。htmlファイルはこのタスクの出力であり、xmlファイルは入力です。ファイルタスクは、出力ファイルの作成方法をビルドシステムに指示するものと考えることができます。実際、これはmakeの概念とまったく同じです。必要な出力ファイルをリストし、それらの作成方法をmakeに指示します。

ファイルタスクの重要な部分は、実行する必要がある場合にのみ実行されるということです。ビルドシステムはファイルを確認し、出力ファイルが存在しないか、その変更日が入力ファイルよりも古い場合にのみタスクを実行します。したがって、ファイルタスクは、ファイル単位で物事を考える場合に非常にうまく機能します。

このタスクで異なる点の1つは、タスクオブジェクト自体をパラメータとしてクロージャに渡すことです。それが |t| が行っていることです。これで、クロージャ内でタスクオブジェクトを参照し、そのメソッドを呼び出すことができます。これは、ファイル名の重複を避けるためです。t.name でタスクの名前(つまり出力ファイル)を取得できます。同様に、t.prerequisites で前提条件のリストを取得できます。

Antにはファイルタスクに相当するものはありません。代わりに、各タスクが同じ種類の必要性チェックを自身で行います。XSLT変換タスクは、入力ファイル、スタイルファイル、および出力ファイルを受け取り、出力ファイルが存在しないか、入力ファイルのいずれよりも古い場合にのみ変換を実行します。これは、このチェックの責任をどこに置くかという問題です。ビルドシステムに入れるか、タスクに入れるかです。Antは主にJavaで記述された既製のタスクを使用していますが、makeとrakeはどちらもタスクのコードを記述するビルド作成者に依存しています。したがって、タスクの作成者が物事が最新かどうかをチェックする必要性を軽減する方が理にかなっています。

ただし、rakeタスクで最新チェックを行うのは実際には非常に簡単です。以下のようなコードになります。

task :rakeArticle do
  src = 'dev/rake.xml'
  target = 'build/dev/rake.html'
  unless uptodate?(target, src) 
    require 'paper'
    maker = PaperMaker.new src, target 
    maker.run
  end
end

Rakeは(fileutils パッケージを介して)cp, mv, rm などのファイル操作のための簡単なunixライクなコマンドを多数提供しています。また、この種のチェックに最適な uptodate? も提供しています。

ここで、2つの方法があることがわかります。ファイルタスクを使用するか、uptodate? を使用した通常のタスクを使用するかです。どちらを選択すべきでしょうか?

この質問に対する良い答えがないことを認めざるを得ません。どちらの戦術も非常にうまく機能するようです。私が新しいrakefileで行うことに決めたのは、細粒度のファイルタスクを可能な限り推進することでした。これが最善策だと知っていたからではなく、主にどうなるかを確認するためにそうしました。新しいものに出会ったときは、その境界を見つけるために使いすぎることが良いアイデアになることがあります。これは非常に合理的な学習戦略です。また、人々が初期の頃に新しいテクノロジーや技術を常に使いすぎる傾向があるのはそのためです。人々はしばしばこれを批判しますが、それは学習の自然な一部です。何かの有用性の境界を超えてプッシュしなければ、その境界がどこにあるのかどうやってわかるでしょうか?重要なのは、その境界を見つけたときに修正できるように、比較的制御された環境で行うことです。(結局のところ、試してみるまで、XMLがビルドファイルに適した構文だと思っていました。)

また、今のところ、ファイルタスクと細粒度タスクをあまりに過度に推進したことに問題は見つかっていません。1、2年後にはそう思わなくなるかもしれませんが、今のところ満足しています。

依存関係の逆定義

これまで、私は主にrakeがantやmakeで見られるものと同様の操作をどのように行うかについて説明してきました。それは素晴らしい組み合わせです。両方の機能をRubyのフルパワーで活用できますが、それだけではこの記事を書く理由にはなりませんでした。私の関心を引いたのは、rakeが行う(および許可する)いくつかの異なる点でした。その最初の1つは、複数の場所に依存関係を指定できることです。

antでは、依存関係を依存タスクの一部として記述することで定義します。これまでのrakeの例でも、このように行ってきました。

task :second => :first do
  #second's body
end

task :first do
  #first's body
end

Rake(makeと同様に)では、タスクを最初に宣言した後で、タスクに依存関係を追加できます。実際、複数の場所でタスクについて語り続けることができます。このようにして、前提タスクの近くに依存関係を追加することを決定できます。以下のように。

task :second do
  #second's body
end

task :first do
  #first's body
end
task :second => :first 

タスクがビルドファイル内で互いに隣り合っている場合は大きな違いはありませんが、長いビルドファイルでは、新しい柔軟性が加わります。基本的に、通常の方法で依存関係について考えたり、前提タスクを追加するときに追加したり、両方とは独立した3番目の場所に置いたりすることができます。

いつものように、この柔軟性によって新しい疑問が生まれます。依存関係を定義するのに最適な場所はどこでしょうか?まだ確実な答えはありませんが、私のビルドファイルでは、2つの経験則を使用しました。あるタスクが別のタスクを実行する前に実行する必要があると考えたときは、従来の方法で、依存タスクを記述するときに依存関係を定義しました。しかし、さまざまなエラータページなど、関連するタスクをグループ化するために依存関係を使用することがよくありました。グループ化(ビルドファイルを構造化するための一般的な部分)に依存関係を使用する場合、前提タスクの横に依存関係を置くことが理にかなっているように思われました。

task :main => [:errata, :articles]

#many lines of build code

file 'build/eaaErrata.html' => 'eaaErrata.xml' do
  # build logic
end
task :errata => 'build/eaaErrata.html'
    

実際には、:errata タスクを task キーワードで定義する必要はなく、:main の依存関係として記述するだけでタスクを定義するのに十分です。その後、個々のエラータファイルを追加し、それぞれをグループタスクに追加できます。この種のグループ動作では、これが合理的な方法であるように思えます(ただし、実際には、後で説明するように、私のビルドファイルではこれとまったく同じように行っていません)。

このことで生じる疑問の1つは、「ビルドファイル全体に分散している依存関係をどのように見つけるのか?」ということです。これは良い質問ですが、答えはrakeに教えてもらうことです。rake -P を使用すると、すべてのタスクとその前提条件が出力されます。

タスクの合成

タスクを定義した後で依存関係を追加できることと、Rubyをフル活用できることにより、ビルドにさらにいくつかのトリックが導入されます。

ただし、合成タスクについて説明する前に、ビルドプロセスに関するいくつかの重要な原則を紹介する必要があります。ビルドスクリプトは、クリーンビルドとインクリメンタルビルドの2種類のビルドを行う必要がある傾向があります。クリーンビルドは、出力領域が空の場合に発生します。この場合、すべてを(バージョン管理された)ソースからビルドします。これはビルドファイルが行うことができる最も重要なことであり、最優先事項は正しいクリーンビルドを行うことです。

クリーンビルドは重要ですが、時間がかかります。したがって、インクリメンタルビルドを行うと便利な場合がよくあります。ここでは、出力ディレクトリにすでにファイルが存在します。インクリメンタルビルドでは、最小限の作業量で、出力ディレクトリを最新のソースで最新の状態にする方法を把握する必要があります。ここで発生する可能性のあるエラーは2つあります。1つ目(そして最も重大な)は、再構築の欠落です。つまり、構築されるべきだったいくつかの項目が構築されなかったということです。これは、入力に実際に一致しない出力(特に、入力に対するクリーンビルドの結果)になるため、非常に悪いことです。より小さなエラーは、不必要な再構築です。これは、構築する必要のない出力要素を構築します。これは正確性のエラーではないため、それほど深刻なエラーではありませんが、インクリメンタルビルドに時間がかかるため問題です。時間だけでなく、混乱も招きます。rakeスクリプトを実行すると、変更されたものだけが構築されることを期待しますが、そうでない場合は「なぜこれが変わったのか?」と思います。

適切な依存関係構造を配置する点の多くは、インクリメンタルビルドがうまく機能するようにすることです。私は「rake」を実行するだけで、デフォルトタスクを呼び出し、自分のサイトのインクリメンタルビルドを行いたいと思っています。私は必要なものだけを構築したいのです。

これが私のニーズであり、興味深い問題は、それを私のblikiで機能させることです。私のblikiのソースは、blikiディレクトリにあるすべてのxmlファイルです。出力は、エントリごとに1つの出力ファイルと、いくつかの概要ページです。その中で、メインのblikiページが最も重要です。ソースファイルへの変更がblikiビルドを再トリガーする必要があるのです。

すべてのファイルに次のように名前を付けることでこれを行うことができます。

BLIKI = build('bliki/index.html')

file BLIKI => ['bliki/SoftwareDevelopmentAttitude.xml',
               'bliki/SpecificationByExample.xml',
               #etc etc
              ] do
  #logic to build the bliki
end

def build relative_path
 # allows me to avoid duplicating the build location in the build file
 return File.join('build', relative_path)
end

しかし、明らかにこれはひどく退屈であり、新しいファイルを追加したいときにリストへの追加を忘れることを要求しているようなものです。幸い、次のように行うことができます。

BLIKI = build('bliki/index.html')

FileList['bliki/*.xml'].each do |src|
  file BLIKI => src
end

file BLIKI do 
  #code to build the bliki
end

FileListはrakeの一部であり、渡されたglobに基づいてファイルのリストを生成します。ここでは、blikiソースディレクトリにあるすべてのファイルのリストを作成します。eachメソッドは内部イテレータであり、それらをループ処理し、それぞれをファイルタスクの依存関係として追加できます。(eachメソッドはコレクションクロージャメソッドです。)

blikiタスクで行うもう1つのことは、シンボリックタスクを追加することです。

desc "build the bliki"
task :bliki => BLIKI

これは、rake bliki を使用してblikiのみをビルドできるようにするためです。もうこれが必要なのかどうかはわかりません。すべての依存関係が適切に設定されている場合(現在は設定されています)、デフォルトのrakeを実行するだけで、不必要な再構築はありません。しかし、今のところそのままにしています。desc メソッドを使用すると、次のタスクの短い説明を定義できます。これにより、rake -T を実行すると、descが定義されているタスクのリストが表示されます。これは、利用可能なターゲットを確認するのに便利な方法です。

makeを使用したことがある場合、これはmakeの最大の機能の1つである、特定の種類のファイルを自動的に作成するためのパターンルールを指定する機能に似ていると思うかもしれません。一般的な例は、対応するfoo.cファイルでCコンパイラを実行して、foo.oファイルを作成したい場合です。

%.o : %.c
        gcc $< -o $@

%.c は「.c」で終わるすべてのファイルに一致します。$< はソース(前提条件)を参照し、@ はルールのターゲットを参照します。このパターンルールは、コンパイルルールでプロジェクト内のすべてのファイルをリストする必要がないことを意味します。代わりに、パターンルールはmakeに必要な *.o ファイルを構築する方法を指示します。(実際、makeにはこのような多くのパターンルールがパッケージ化されているため、makeファイルでこれを行う必要すらありません。)

Rakeには同様のメカニズムがあります。まだ必要がないため、存在することに言及する以外には説明しません。合成タスクで私が必要とするものすべてが機能しました。

ブロックスコープタスク

ファイル名と依存関係を使用する際に私が発見した問題の一つは、ファイル名を繰り返さなければならないことです。次の例を見てください。

file 'build/articles/mocksArentStubs.html' => 'articles/mock/mocksArentStubs.xml' do |t|
 transform t.prerequisites[0], t.name
end
task :articles => 'build/articles/mocksArentStubs.html'

上記の例では、'build/articles/mocksArentStubs.html' がコード内で2回言及されています。アクションブロック内では task オブジェクトを使用することで繰り返しを避けることができますが、全体の articles タスクへの依存関係を設定するためには、それを繰り返す必要があります。ファイル名を変更した場合に問題が発生する可能性があるため、この繰り返しは好きではありません。一度だけ定義する方法が必要です。定数を宣言することはできますが、このセクションでのみ使用する場合、rakefile のどこからでも見える定数を宣言することになります。私は変数のスコープをできるだけ小さくしたいのです。

上記で述べた FileList クラスを使用することで、この問題を処理できます。ただし、今回は単一のファイルでのみ使用します。

FileList['articles/mock/mocksArentStubs.xml'].each do |src|
  target = File.join(BUILD_DIR + 'articles', 'mocksArentStubs.html')
  file target => src do
    transform src, target
  end
  task :articles => target
end

このようにして、このコードブロック内でのみスコープされる src および target 変数を定義します。これは、:articles タスクからここで依存関係を定義する場合にのみ役立つことに注意してください。 :articles タスクの定義で依存関係を定義したい場合は、rakefile 全体で可視性を持たせるために定数が必要になります。

ジム・ウィーリックがこのドラフトを読んだとき、FileList ステートメントが冗長すぎる場合は、これを実行するためのメソッドを簡単に定義できると指摘しました。

  def with(value)
    yield(value)
  end

そして、次のように実行します。

  with('articles/mock/mocksArentStubs.xml') do |src|
    # whatever
  end

ビルドメソッド

ビルド言語をフルプログラミング言語への内部DSLにすることの本当に素晴らしいことの1つは、一般的なケースを処理するためのルーチンを記述できることです。サブルーチンはプログラムを構造化する最も基本的な方法の1つであり、便利なサブルーチンメカニズムの欠如は、特に複雑なビルドを行う場合に、antとmakeの大きな問題の1つです。

私が使用した一般的なビルドルーチンの一例を以下に示します。これは、XSLTプロセッサを使用してXMLファイルをHTMLに変換するものです。私の新しい文章はすべてrubyを使ってこの変換を行いますが、古いXSLTのものがたくさんあり、変更を急ぐ必要はありません。XSLTを処理するさまざまなタスクを作成した後、すぐに重複があることに気づいたので、ジョブ用のルーチンを定義しました。

def xslTask src, relativeTargetDir, taskSymbol, style
  targetDir = build(relativeTargetDir)
  target = File.join(targetDir, File.basename(src, '.xml') + '.html')
  task taskSymbol => target
  file target => [src] do |t|
    mkdir_p targetDir
    XmlTool.new.transform(t.prerequisites[0], t.name, style)
  end
end    

最初の2行で、ターゲットディレクトリとターゲットファイルを計算します。次に、ターゲットファイルを、指定されたタスクシンボルの依存ファイルとして追加します。次に、ターゲットディレクトリ(必要な場合)を作成し、XmlToolを使用してXSLT変換を実行する手順を含む新しいファイルタスクを作成します。これで、XSLTタスクを作成したいときは、このメソッドを呼び出すだけです。

xslTask 'eaaErrata.xml', '.', :errata, 'eaaErrata.xsl'

このメソッドは、すべての共通コードを適切にカプセル化し、現在の私のニーズに合わせて変数をパラメータ化します。ルーチンに親グループタスクを渡すことは、ルーチンが依存関係を簡単に構築するのに非常に役立つことがわかりました。これは、柔軟な依存関係指定方法のもう1つの利点です。画像やPDFなど、ソースからビルドディレクトリにファイルを直接コピーするための同様の共通タスクがあります。

def copyTask srcGlob, targetDirSuffix, taskSymbol
  targetDir = File.join BUILD_DIR, targetDirSuffix
  mkdir_p targetDir
  FileList[srcGlob].each do |f|
    target = File.join targetDir, File.basename(f)
    file target => [f] do |t|
      cp f, target
    end
    task taskSymbol => target
  end
end

copyTask は、コピーするファイルのglobを指定できるため、少し高度です。これにより、次のようなものをコピーできます。

copyTask 'articles/*.gif', 'articles', :articles

これにより、ソースの articles サブディレクトリ内のすべての gif ファイルが、ビルドディレクトリの articles ディレクトリにコピーされます。各ファイルに対して個別のファイルタスクが作成され、それらはすべて :articles タスクの依存関係となります。

プラットフォーム依存のXML処理

ant を使用してサイトを構築していたとき、Java ベースの XSLT プロセッサを使用しました。rake を使い始めたときに、ネイティブの XSLT プロセッサに切り替えることにしました。私は Windows と Unix (Debian と MacOS) の両方のシステムを使用していますが、どちらにも XSLT プロセッサが簡単に用意されています。もちろん、それらは異なるプロセッサであり、異なる方法で呼び出す必要があります。もちろん、これは rakefile に隠されており、rake を呼び出すときに私には隠されている必要があります。

ここでも、直接操作できるフル機能の言語を持つことの素晴らしいところです。プラットフォーム情報を使用して適切な処理を行う XML プロセッサを簡単に作成できます。

まず、ツールのインターフェース部分である XmlTool クラスから始めます。

class XmlTool
  def self.new
    return XmlToolWindows.new if windows?
    return XmlToolUnix.new
  end
  def self.windows?
    return RUBY_PLATFORM =~ /win32/i 
  end
end

ruby では、クラスで new メソッドを呼び出してオブジェクトを作成します。抑圧的なコンストラクタとは対照的に、これの素晴らしい点は、この新しいメソッドをオーバーライドできることです。異なるクラスのオブジェクトを返すことも可能です。そのため、この場合、XmlTool.new を呼び出すと、XmlTool のインスタンスは取得されません。代わりに、スクリプトを実行しているプラットフォームに適した種類のツールを取得します。

2つのツールの中で最も単純なのは、Unixバージョンです。

class XmlToolUnix
  def transform infile, outfile, stylefile
    cmd = "xsltproc #{stylefile} #{infile} > #{outfile}"
    puts 'xsl: ' + infile
    system cmd
  end
  def validate filename
    result = `xmllint -noout -valid #{filename}`
    puts result unless  '' == result
  end
end

ここでは、XML用に2つのメソッドがあります。1つはXSLT変換用、もう1つはXML検証用です。unixの場合、それぞれがコマンドライン呼び出しを起動します。rubyに慣れていない場合は、#{variable_name} constructで文字列に変数を挿入する優れた機能に注目してください。実際、ruby式の結果をそこに挿入することができます。これは非常に便利です。validateメソッドでは、バッククォートを使用します。これにより、コマンドラインを実行して結果を返します。putsコマンドは、rubyの標準出力に出力する方法です。

Windowsバージョンは、コマンドラインではなくCOMを使用する必要があるため、少し複雑です。

class XmlToolWindows
  def initialize
    require 'win32ole'
  end
  def transform infile, outfile, stylefile
    #got idea from http://blog.crispen.org/archives/2003/10/24/lessons-in-xslt/
    input = make_dom infile
    style = make_dom stylefile
    result = input.transformNode style
    raise "empty html output for #{infile}" if result.empty?
    File.open(outfile, 'w') {|out| out << result}
  end
  def make_dom filename, validate = false
    result = WIN32OLE.new 'Microsoft.XMLDOM'
    result.async = false
    result.validateOnParse = validate
    result.load filename
    return result
  end
  def validate filename
    dom = make_dom filename, true
    error = dom.parseError
    unless error.errorCode == 0
      puts "INVALID: code #{error.errorCode} for  #{filename} " + 
        "(line #{error.line})\n#{error.reason}"
    end
  end
end

ステートメント require 'win32ole' は、Windows COM を操作するための ruby ライブラリコードをプルインします。これはプログラムの通常の構成要素であることに注意してください。ruby では、ライブラリが必要な場合にのみロードされるように設定できます。その後、他のスクリプト言語と同様に COM オブジェクトを操作できます。

これら3つのXML処理クラス間には型の関係がないことに注意してください。XML 操作が機能するのは、Windows と Unix の両方の XmlTool が transform メソッドと validate メソッドを実装しているためです。これは、rubyist が ダックタイピングと呼ぶものです。アヒルのように歩き、アヒルのように鳴くなら、それはアヒルに違いありません。これらのメソッドが存在することを保証するためのコンパイル時チェックはありません。メソッドが間違っている場合は、実行時に失敗します。これはテストで洗い出されるはずです。動的型チェックと静的型チェックの議論全体に入ることはしません。これはダックタイピングの使用例であるとだけ指摘します。

Unixシステムを使用している場合は、使用しているパッケージ管理システムを使用して、私が使用しているunix xmlコマンドを見つけてダウンロードする必要がある場合があります(MacではFinkを使用しました)。XMLDOM DLLは通常windowsに付属していますが、設定によってはダウンロードする必要がある場合があります。

失敗する

プログラミングについて保証できることの1つは、常に問題が発生することです。どれだけ努力しても、あなたが言ったと思っていることと、コンピュータが聞いていることの間に、常に何らかの不一致があります。このrakeコードを見てください(実際に私に起こったことを簡略化したものです)。

src = 'foo.xml'
target = build('foo.html')
task :default => target
copyTask 'foo.css', '.', target
file target => src do
  transform src, target
end

バグがわかりますか?私もそうではありませんでした。私が知っていたのは、build/foo.html を構築する変換が、必要がない場合でも常に発生していたということです。不要な再構築です。私はその理由がわかりませんでした。ソースよりもターゲットが遅れていることを確認しても、両方のファイルのタイムスタンプは正しく、それでも再構築が行われます。

私の最初の調査は、rake のトレース機能 (rake --trace) を使用することでした。通常は、奇妙な呼び出しを特定するために必要なのはこれだけですが、今回はまったく役に立ちませんでした。'build/foo.html' タスクが実行されているということしか教えてくれず、その理由を教えてくれませんでした。

この時点で、デバッグツールの欠如をジムのせいにするかもしれません。少なくとも「お前の母親はクリーブランドのメスの狼で、お前の父親は濡れたニンジンだ」と呪いをかけることで、気分が良くなるかもしれません。

しかし、私にはより良い代替手段があります。Rakeはrubyであり、タスクは単なるオブジェクトです。これらのオブジェクトへの参照を取得して、それらを調べることができます。ジムはこのデバッグコードをrakeに組み込んでいないかもしれませんが、私自身で簡単に追加できます。

class Task 
  def investigation
    result = "------------------------------\n"
    result << "Investigating #{name}\n" 
    result << "class: #{self.class}\n"
    result <<  "task needed: #{needed?}\n"
    result <<  "timestamp: #{timestamp}\n"
    result << "pre-requisites: \n"
    prereqs = @prerequisites.collect {|name| Task[name]}
    prereqs.sort! {|a,b| a.timestamp <=> b.timestamp}
    prereqs.each do |p|
      result << "--#{p.name} (#{p.timestamp})\n"
    end
    latest_prereq = @prerequisites.collect{|n| Task[n].timestamp}.max
    result <<  "latest-prerequisite time: #{latest_prereq}\n"
    result << "................................\n\n"
    return result
  end
end

これがすべてどうあるべきかを確認するためのコードです。rubyist でない場合は、rake の一部であるタスククラスにメソッドを追加したことが奇妙に思えるかもしれません。この種のものは、アスペクト指向の導入と同じものですが、ruby では完全に合法です。多くの ruby のように、この機能で混乱が想像できますが、注意していれば、本当に素晴らしいものです。

これで、起動して何が起こっているかを確認できます。

src = 'foo.xml'
target = build('foo.html')
task :default => target
copyTask 'foo.css', '.', target
file target => src do |t|
  puts t.investigation
  transform src, target
end

これが印刷されます。

------------------------------
Investigating build/foo.html
class: Task
task needed: true
timestamp: Sat Jul 30 16:23:33 EDT 2005
pre-requisites:
--foo.xml (Sat Jul 30 15:35:59 EDT 2005)
--build/./foo.css (Sat Jul 30 16:23:33 EDT 2005)
latest-prerequisite time: Sat Jul 30 16:23:33 EDT 2005
................................

最初はタイムスタンプについて疑問に思いました。出力ファイルのタイムスタンプは 16:42 だったので、タスクが 16:23 と言ったのはなぜでしょうか。その後、タスクのクラスが FileTask ではなく Task であることに気づきました。Task は日付チェックを行いません。呼び出すと常に実行されます。そこで、これを試しました。

src = 'foo.xml'
target = build('foo.html')
file target
task :default => target
copyTask 'foo.css', '.', target
file target => src do |t|
  puts t.investigation 
  transform src, target
end

変更点は、後で他のタスクのコンテキストで言及する前に、タスクをファイルタスクとして宣言したことです。それで解決しました。

この教訓は、この種の内部DSLを使用すると、オブジェクト構造を調べて何が起こっているかを把握できるということです。これは、このような奇妙なことが起こったときに本当に役立ちます。不要なビルドが発生した別のケースでこのアプローチを使用しました。何が起こっているかを正確に把握するためにボンネットを開けることは本当に役立ちました。

(ちなみに、私の investigation メソッドは、出力ファイルがまだ存在しない場合、クリーンビルドなどで壊れます。ファイルがすでにそこにある場合にのみ必要だったので、修正に時間を費やしていません。)

これを書いた後、ジムはこれと非常によく似た調査メソッドをrake自体に追加しました。したがって、ここで私がしたことを行う必要はなくなりました。しかし、一般的な原則は依然として有効です。rakeがあなたの望むことをしない場合は、入ってその動作を変更できます。

Rakeを使用して非Rubyアプリケーションをビルドする

rake は ruby で書かれていますが、他の言語で書かれたアプリケーションをビルドするために使用できない理由はありません。どのビルド言語も何かをビルドするためのスクリプト言語であり、別の言語で書かれたツールを使用して1つの環境を問題なくビルドできます。(良い例は、ant を使用して Microsoft COM プロジェクトをビルドしたときです。Microsoft コンサルタントから隠す必要があっただけです。)rake の唯一の欠点は、より高度なことを行うには ruby を知っておくと便利であるということですが、どんなプロのプログラマーも、あらゆる種類の雑務を行うには、少なくとも 1 つのスクリプト言語を知っておく必要があると常に感じています。

テストの実行

Rake のライブラリを使用すると、TestTask クラスを使用して、rake システム内で直接テストを実行できます。

require 'rake/testtask'
Rake::TestTask.new do |t|
  t.libs << "lib/test"
  t.test_files = FileList['lib/test/*Tester.rb']
  t.verbose = false
  t.warning = true
end

デフォルトでは、これにより、指定されたファイル内のテストを実行する :test タスクが作成されます。複数のタスクオブジェクトを使用して、さまざまな状況に対応するテストスイートを作成できます。

デフォルトでは、テストタスクは、指定されたすべてのファイル内のすべてのテストを実行します。単一のファイル内のテストのみを実行する場合は、次のように実行できます。

        rake test TEST=path/to/tester.rb
      

「test_something」という単一のテストを実行する場合は、TESTOPTS を使用してテストランナーにオプションを渡す必要があります。

         rake test TEST=path/to/tester.rb TESTOPTS=--name=test_something
      

特定のテストを実行するための一時的な rake タスクを作成すると役立つことがよくあります。1つのファイルを実行するには、次のように使用できます。

Rake::TestTask.new do |t|
  t.test_files = FileList['./testTag.rb']
  t.verbose = true
  t.warning = true
  t.name = 'one'
end

1つのテストメソッドを実行するには、テストオプションを追加します。

Rake::TestTask.new do |t|
  t.test_files = FileList['./testTag.rb']
  t.verbose = true
  t.warning = true
  t.name = 'one'
  t.options = "--name=test_should_rebuild_if_not_up_to_date"
end

ファイルパスの操作

Rake は、便利なファイル操作式を実行するために文字列クラスを拡張します。たとえば、ソースを取得してファイル拡張子を変更してターゲットファイルを指定する場合は、次のようにできます。

"/projects/worldDominationSecrets.xml".ext("html")
# => '/projects/worldDominationSecrets.html'

より複雑な操作の場合、printf と同様のスタイルでテンプレートマーカーを使用する pathmap メソッドがあります。たとえば、テンプレート "%x" はパスのファイル拡張子を参照し、"%X" はファイル拡張子以外のすべてを参照するため、上記の例を次のように記述できます。

"/projects/worldDominationSecrets.xml".pathmap("%X.html")
# => '/projects/worldDominationSecrets.html'

もう1つの一般的なケースは、'src' のものが 'bin' に現れる場合です。これを行うには、"%{pattern、replacement}X" を使用してテンプレート内の要素を置換できます。たとえば、

"src/org/onestepback/proj/foo.java".pathmap("%{^src,bin}X.class")
# => "bin/org/onestepback/proj/foo.class"

Rakeのパス操作メソッドの完全なリストは、String.pathmapのドキュメントにあります。

私はこれらのメソッドが非常に便利だと感じており、自分のコードでファイルパスを操作する際には常に使用したいと思っています。これらを利用可能にするには、次の手順が必要です。

require 'rake/ext/string'

名前空間

大規模なビルドスクリプトを作成すると、同様の名前のタスクが多数になることがあります。Rakeには、これらを整理するのに役立つ名前空間の概念があります。名前空間は次のように作成します。

    namespace :articles do
      # put tasks inside the namespace here eg
      task :foo
    end
  

名前空間化されたタスクは、rake articles:fooで呼び出すことができます。

現在いる名前空間の外にあるタスクを参照する必要がある場合は、タスクの完全修飾名を使用します。通常、タスク名の文字列形式を使用する方が簡単です。

    namespace :other do
       task :special => 'articles:foo'
    end
  

組み込みのクリーンアップ

ビルドでよくあるニーズは、生成したファイルをクリーンアップすることです。Rakeには、これを実現するための組み込みの方法が用意されています。Rakeには、cleanとclobberの2つのレベルのクリーンアップがあります。cleanは最も穏やかなアプローチであり、最終生成物を導き出すために使用される一時ファイルのみを削除し、最終生成物は削除しません。clobberはより強力な手法で、最終生成物を含むすべての生成ファイルを削除します。基本的に、clobberはソース管理にチェックインされたファイルのみの状態に戻します。

ここには用語の混乱があります。私はよく、rakeのclobberと同等の、生成されたすべてのファイルを削除することを意味するために「clean」という言葉を使用する人を聞きます。そのため、その混乱に注意してください。

組み込みのクリーンアップを使用するには、require 'rake/clean'でrakeの組み込みクリーンアップをインポートする必要があります。これにより、cleanとclobberの2つのタスクが導入されます。ただし、現状では、タスクはどのファイルをクリーンアップする必要があるかを知りません。それを伝えるには、CLEANとCLOBBERという2つのファイルリストを使用します。その後、CLEAN.include('*.o')のような式でファイルリストに項目を追加できます。cleanタスクはcleanリスト内のすべてを削除し、clobberはcleanリストとclobberリストの両方のすべてを削除することを覚えておいてください。

その他

デフォルトでは、rakeはrakeが呼び出すコードでエラーが発生した場合、スタックトレースを出力しません。--traceフラグを付けて実行することでスタックトレースを取得できますが、通常は最初から表示させたいと思っています。それには、Rake.application.options.trace = trueをRakefileに入れることで実行できます。

同様に、FileUtilsからのファイル操作の出力が邪魔になることがあります。コマンドラインから-qオプションでオフにできますが、verbose(false)を呼び出すことでRakefileでも無効にできます。

rakeを実行するときに警告をオンにすると便利なことがよくあります。これは$VERBOSEを操作することで実行できます。Mislav Marohnićによる$VERBOSEの使用に関する優れたメモがあります。

Rakefile自体でrakeタスクオブジェクトを検索するには、Rake::Task[:aTask]を使用します。タスクの名前は、シンボルまたは文字列として指定できます。これにより、Rake::Task[:aTask].invokeを使用して、依存関係を使用せずに、あるタスクから別のタスクを呼び出すことができます。これを頻繁に行う必要はないはずですが、時々便利です。

最後に

これまでのところ、rakeは強力で使いやすいビルド言語であることがわかりました。もちろん、私がRubyに慣れていることが役立っていますが、rakeはビルドシステムが本格的な言語への内部DSLとして理にかなっていることを確信させてくれました。スクリプトは多くの点でビルドに自然であり、rakeは優れた言語の上に本当に優れたビルドシステムを提供するために十分な機能を追加します。また、Rubyが私が必要とするすべてのプラットフォームで実行されるオープンソース言語であるという利点もあります。

柔軟な依存関係指定の結果に驚きました。これにより、重複を減らすことができるいくつかのことができ、それにより、将来、ビルドスクリプトの保守をより簡単に行えるようになると思います。martinfowler.comとrefactoring.comのビルドスクリプト間で共有する、いくつかの共通の関数を別のファイルに取り出すことができました。

ビルドを自動化する場合は、rakeを検討する必要があります。Rubyだけでなく、任意の環境で使用できることを忘れないでください。


さらに読む

rakeはrubyforgeから入手できます。Rakefileの説明は、ドキュメントの最も優れたソースの1つです。Jim Weirichのブログにも、rakeを説明するのに役立つエントリがいくつかありますが、逆順(最初に投稿されたものが最初)に読む必要があります。これらは、私がここで簡単に説明した内容(ルールなど)についてより詳しく説明しています。

スクリプトを使用すると、bashでrakeのコマンドライン補完を設定できます。

Joe Whiteは、Rakeのライブラリの機能に関する優れたページを持っています。

ビルドツール用の内部DSLのアイデアが好きで、Pythonを好む場合は、SConsを調べてみてください。

謝辞

この記事の草稿に関するコメントをいただいたJason Yip、Juilian Simpson、Jon Tirsen、Jim Weirichに感謝します。公開後にいくつかの修正をしてくれたDave Smith、Ori Peleg、Dave Stantonにも感謝します。

しかし、最初にrakeを書いたJim Weirichに最大の感謝を捧げます。私のウェブサイトはあなたに感謝しています。

重要な改訂

2014年12月29日: テストの実行に関する議論を更新

2005年8月10日: 初版