この記事は「Qiita Advent Calendar 2019 DSLで自作ビルドツールを作ろう」の1日目の記事です。
1日目 ビルドツールを作る動機・最初のプログラム
ビルドツールとは、誰もが聞いたことがあると思います。プログラムのビルド作業を自動化し、その作業を簡略化します。代表的なところで言うとMake、各種プログラミング言語には、その言語専用のビルドツールがついている場合もあります。最近だと、RustではCargoというビルド自動化システムが付いていますね。
このビルドツール、非常に便利で使っていない人はいません。わざわざ数百もあるソースコードをビルドするのに、コマンドラインで1つずつビルドしていく人はいません。Makefileなり何なりを使って、自動的にバイナリ作成、テストまでを行う仕組みを構築しています。
しかし、このビルドツール、お世話になったことは数知れずなのですが、その中身、実はよく勉強したことがありません。ファイルが新しくなっていなければビルドを省略する?具体的にどうやって?複数あるMakefileを渡り歩いてルールを作り上げる、どうやって?身近にあるビルドツールは、意外と秘密に溢れています。
そこで、今回ビルドツールの威力の一端を体験するために、自分でビルドツールを作ってみることにしました。といっても一から構築するのは大変なので、最近流行りのDSLの力を借ります。具体的には、DSL構築に適しているといわれるRubyの力を借ります。Rubyを用いて自作ビルドツールを構築することで、ビルドツールの基本的な考え方、そしてついでにDSLについても勉強しようというお得なシリーズです。
ちなみに、私はビルドツールの専門家でも、DSLのプロでもありません。言ってしまうならばこのシリーズを書くためにRubyのDSL構築方法について勉強し始めました。アマチュアも良いとこです。
色々と間違っていることを書いているかもしれませんが、どうかご容赦ください。よろしければ、不正確な部分は指摘して頂けると幸いです。
既存のビルドツール。有名なところをいくつか私の知っている範囲で。
Make
言わずと知れたビルドツールの代表格です。とりあえずMakefileでビルドを書いておけば誰も何も言わなくても分かるだろ、という安心感があり、非常に古くから使われているツールです。かつてはLinuxでのアプリケーションのインストールは、「インストール」と呼ばずに「make」と呼んでいたくらい、代表的なツールです。
独自の文法を採用しており、最初は取っかかりにくいように思えますが慣れるとMake以外で書けなくなります。並列ビルドにも対応しており十分な機能を持っていますが、ビルドルールは非常に一般化されたルールとして提供されており、各言語のためのルールというものは非常に限られています。
また、ヘルプ・ドキュメント作成などの近代的な機構が無く、バージョン管理機構との協調などの新しい技術は導入されていません。そういう意味では、後述するビルドツールと比較するとプロジェクトの構築、ルールの記述のしやすさという面では劣ります。
CMake
CやC++のコードをビルドするためのツールです。どちらかというとビルドツールのWrapperという理解です。様々なプロジェクトがCMakeをベースに記述されています。LLVMもその一つです。CMakeは実際のビルドに当たり複数のビルドツールを生成することができます。CMakeの記述からMakefileを生成したり、Ninja用のビルドコマンドを生成することができます。
Bazel
Googleが開発したビルドツールです。Googleの内部プロジェクトから派生したツールで、これを利用している有名プロジェクトはあまり知らないですが、現代的なツールとして個人的に注目しています。
OMake
私自身はOMakeを使った事は無いのですが、非常に便利なツールであるとして認知度があります。文法的にはMakeと似ているのでしょうか?
Cargo
Rust向けのパッケージ管理・ビルドツールです。Rustを使う場合には避けて通れません。
SBT
Scala向けのパッケージ管理・ビルドツールです。Scalaを使う場合にはよく使います。
今回作るもの"Rumy-Make"
さて、いろいろと調査したところで、今回作る独自ビルドツールの説明をします。名前を"Rumy-Make"と名付けます。Rubyの内部DSLとして記述していきます。なので「Rubyで記述した独自(my)のMake」ということで"Rumy-Make"と名付けました。1か月プロジェクトとしてわずかな時間で作ったので実装としてはToyですが、一応目標としてはある程度のプロジェクトに適用して、ビルドが実行できるものを目指します。
まとめると、今回の作成するビルドツールは以下の特徴を持ちます。
- Rubyの内部DSLとして実装する。つまり文法はRubyのものに則る
- Toyツールとして割り切る。あまりにも複雑な機能は実装しない。
- ある程度の実プロジェクトに適用できる規模を考えて実装する。
- Gitコマンドとの連携など、近代的な機能を盛り込むべく頑張る。
このプロジェクトの成果物は、以下のGitHubのサイトで公開しています。
参考資料
以下の資料を参考にしています。
最初の実装は以下を参考にしました。
以下の書籍も非常に有名ですが、難しすぎて私には理解できなかった。
- 作者: Paolo Perrotta,角征典
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/10/10
- メディア: 大型本
- この商品を含むブログ (3件) を見る
コマンドを実行する関数execute
を作る
まずは、Ruby DSLについて学んでいきます。基本的に上記の資料を見て進めていきますが、まずはexecute
関数という、コマンドを実行できる関数を作ります。このためには、Rubyのdefine_method
という関数を使います。define_method
を使うと、任意の関数を作り上げることができます。
define_method
を用いて"execute"コマンドを作ります。"execute"コマンドは引数を1つ受け取り、その内容を実行して結果(stdout)を表示します。
src/rumy-exec.rb
#/usr/bin/ruby define_method("execute") {|command| result = `#{command}` puts "Result\n" puts "====================" puts result puts "====================\n\n" }
テストを作ってみます。execute
コマンドを実行し、ls -lt
とecho Hello World
を実行しその結果を取得します。
src/rumy-exec.rb
#!/usr/bin/ruby load "rumy-exec.rb" execute "ls -lt" execute "echo Hello World"
コマンドライン上で以下のように入力して、テストを実行します。
ruby -Isrc/ tests/exec_test.rb
Result ==================== total 0 drwxrwxrwx 1 msyksphinz msyksphinz 4096 10月 20 15:42 src drwxrwxrwx 1 msyksphinz msyksphinz 4096 10月 20 15:38 tests ==================== Result ==================== Hello World ====================
実行できているようです。このようにして、まずはrubyに独自コマンドexecute
を登録し実行させることができました。
このexec_test.rb
に記述されているexecute
コマンドは、Rubyの文法からは逸脱しておらず、完全にRubyの機能を用いて記述されています。このようなDSLのことを「内部DSL」と呼ぶそうです。一方で、make
ファイルのように開発されている言語とは全く異なる言語体系で実装されているDSLを「外部DSL」と呼んで区別します。