この記事は「Qiita Advent Calendar 2019 DSLで自作ビルドツールを作ろう」の5日目の記事です。
5日目 細かな調整
Rumy-Makeの実装を進める前に、少し細かな調整をします。あまり本来のビルドツールとは関係ない、使いやすさの部分で少し改良を加えていくことにしました。
セルフヘルプ表示機能の実装
セルフヘルプというのは、いわゆるMakefile自体にMakeターゲットのヘルプを書けるようにするもので、Makefileはデフォルトでは各ターゲットに対してターゲットの意味を説明するためのヘルプを記述することができません。まあ上手く書けはできない事は無くて、例えば以下のサイトを参考にすればmake help
でターゲットのヘルプを自動的に生成することができます。
このようにMakefile本来の機能を使わずにヘルプメッセージを表示することも可能ですが、面倒なのでRumy-Makeではビルドターゲット自身にメッセージを付加できるようにします。make_taregt
を宣言する際に以下のようにexplain
を追加すると、これをターゲットのヘルプとして表示できるような機能を追加します。
make_target :make_obj do explain "Generate test.o" # :make_objターゲットのヘルプ。 execute "gcc -c test.c -o test.o" end
で、実行時にshow_help
を呼び出すことでファイル内のすべてのターゲットに記述されたヘルプを表示します。このために、Target
クラスにヘルプメッセージを格納するための@message
メンバ変数と、そのメンバ変数をすべてさらうためのshow_help
関数を定義しました。
src/rumy-exec.rb
class Target def initialize(name) @name = name @depend_targets = [] @help_message = "" # デフォルトではヘルプメッセージは何も入っていない。 ... end ... def explain(message) @help_message = message end ... end # すべてのターゲットからヘルプメッセージを取り出して表示する。 def show_help puts "[HELP] =============================================" @target_list.each{|key, target| if target.help_message != "" and target.is_global == true then puts "[HELP] #{key} : " + target.help_message end } puts "[HELP] =============================================" end
テストコードとして、以下の簡単な3つのターゲットを用意して、そのうち2つのターゲットにヘルプメッセージを付けました。
tests/show_help.rb
#!/usr/bin/ruby load "rumy-exec.rb" source_file = "./test.c" exec_file = source_file.sub(".c", "") make_target :make_ccode do explain "Generate #{source_file}" end make_target :compile_c do explain "Compile #{source_file}" end make_target :run_c do # explain "Run #{exec_file}" end show_help
$ ruby ../tests/show_help.rb ... [HELP] ============================================= [HELP] make_ccode : Generate ./test.c [HELP] compile_c : Compile ./test.c [HELP] =============================================
上記のように、ヘルプを追加したターゲットのみ、ヘルプメッセージを表示します。必要なターゲットにヘルプメッセージを書いておけば、このようにコマンド一覧のように確認することができるわけです。
ターゲット名にシンボルではなく文字列を渡せるようにする
これまで、基本的にターゲットの名前はRubyのシンボル機能を使って記述してきました。
make_target :compile_c do explain "Compile #{source_file}" end
しかし、冷静に考えるとこれは面倒です。ソースコードをコンパイルするのに、コンパイルターゲットとしてファイル名とは別にシンボルを用意しなければならないとなると、ファイル名以外にたくさんシンボル名を考える必要が生じてしまい、面倒になります。Makefileだって、Cコードをコンパイルするときにターゲット名としてコンパイル後のバイナリを指定できます。
test: test.c gcc test.c -o test # 実際には$@やら$^で書けるが... ターゲット名はバイナリ自身を指定できる
そこで、ターゲットの名前としてシンボル以外に文字列も受け付けることができるようにします。つまり、こういうことがしたいのです。
make_target "test.o" do depends ["test.c"] execute "gcc -c test.c -o test.o"
このように、ターゲット名として文字列つが使えると、新たなターゲット毎にシンボル名を考えずに済みますし、今後実装することになるタイムスタンプをベースにビルド処理を省略する機能も実装しやすくなります。
ターゲット命令文字列を使用すること自体は、全く問題なさそうです。しかし、ここでは簡単化のため、文字列としてdepends
に設定されたターゲット名が文字列だった場合にはそれがファイル名であると仮定し、その先のターゲットの依存関係は探しに行かないようにしておきます。これは単純に簡単化のための措置で、のちにタイムスタンプを見ながら文字列のターゲットでも依存性を元に探索することができるように改造します。
src/rumy-exec.rb
def exec_target (name) ... # Execute dependent commands at first target.depend_targets.each{|dep| # とりあえず簡単化のために、ターゲット名がシンボルではなく文字列だったら、その先のターゲットの実行は止める。 if not @target_list.key?(dep) and not Symbol.all_symbols.include?(dep) then puts "[DEBUG] : Depend Tareget \"#{dep}\" is because it's file." next end puts "[DEBUG] : Depends Target \"#{dep}\" execute." exec_target(dep) } ....
テストコードとして以下を作成しました。少しわかりにくいですが、test_1.c
とtest2.c
をコンパイルしてtest
バイナリを作成します。test_1.c
はもともと用意されていますが、test_2.c
は:test2_c
シンボルターゲットを通じてecho
コマンドを使って自動生成する仕組みになっています。依存関係としてはこんな感じです。
test (バイナリ) |-> test_1.c (既存) |-> :test_2 |-> test.c (生成)
これを記述したのが以下のビルドファイルです。
#!/usr/bin/ruby load "rumy-exec.rb" test1_c = "../tests/test1.c" test2_obj = "../tests/test2.o" test2_c = test2_obj.sub(".o", ".c") exec_file = "./test" make_target :gen_test2_c do executes "echo \"#include <stdio.h>\nint test2 () { printf(\\\"Hello Test2!!\\\"\); return 0; }\" > #{test2_c}" end make_target :gen_test2_obj do depends [:gen_test2_c] executes "gcc -c #{test2_c} -o #{test2_obj}" end make_target :compile do depends [test1_c, :gen_test2_obj] executes "gcc #{test1_c} #{test2_obj} -o #{exec_file}" end make_target :run do depends [:compile] executes exec_file end exec_target :run
- 最初に呼び出されるのは
:run
ターゲットシンボルです。:run
内では、依存するターゲットとして:compile
が呼び出されます。 :compile
内では、test1_c
と:gen_test2_obj
が依存しています。test1_c
は実体は文字列なのでこれ以上は探索しません。一方で:gen_test2_obj
はシンボルなので:gen_test2_obj
ターゲットを探しに行きます。:gen_test2_obj
ターゲット内では、:gen_test2_c
シンボルのターゲットが依存しています。:gen_test2_c
ターゲットではecho
コマンドでtest2.c
が生成されます。:gen_test2_obj
ターゲットに戻り、test2.c
をコンパイルしてオブジェクトtest2.o
を生成します。:compile
ターゲットに戻り、test1.c
とtest2.o
をコンパイルしてtest
バイナリを作ります。:run
ターゲットに戻り、test
を実行します。
それっぽく作れました!では実行してみます。
$ cat ../tests/test1.c #include <stdio.h> extern void test2(); void test1 () { printf("Hello Test1\n"); } int main () { test1(); test2(); } $ ruby ../tests/target_with_filename.rb [DEBUG] : Target Created = gen_test2_c, Depends = , Commands = echo "#include <stdio.h> int test2 () { printf(\"Hello Test2!!\"); return 0; }" > ../tests/test2.c [DEBUG] : Target Created = gen_test2_obj, Depends = , Commands = gcc -c ../tests/test2.c -o ../tests/test2.o [DEBUG] : Target Created = compile, Depends = , Commands = gcc ../tests/test1.c ../tests/test2.o -o ./test [DEBUG] : Target Created = run, Depends = , Commands = ./test [DEBUG] : Depends Target "compile" execute. [DEBUG] : Depend Tareget "../tests/test1.c" is because it's file. [DEBUG] : Depends Target "gen_test2_obj" execute. [DEBUG] : Depends Target "gen_test2_c" execute. Hello Test1 Hello Test2!!
実行できていることが確認できました。良さそうです。