この記事は「Qiita Advent Calendar 2019 DSLで自作ビルドツールを作ろう」の8日目の記事です。
8日目 cleanコマンドを作る
テストの実行方法をちょっと修正
これまで、rumy-exec.rb
をロードする方法が分からずに直接./src
ディレクトリに移ったうえで../tests/xxx.rb
のテストコードを動かしていましたが、これはどう考えてもおかしいのでもう少しまともなやり方を探していました。とりあえずは、RUBYLIB
という環境変数を使うことでライブラリロードの場所を指定できるようです。これからは、テストを実行する場合にはtests/
ディレクトリに移動して、RUBYLIB
っ環境変数に../src
を設定することでテストディレクトリからテストを実行できるようにします。
cd tests/ RUBYLIB=${PWD}/../src ruby ./check_timestamp.rb
デフォルトターゲットと、コマンドラインからのターゲット指定
これまでは基本的にrumyファイル内にデフォルトのターゲットを記述していました。つまり、make_target
でターゲットを作成した後、rumyファイル内の一番最後にexec_target
を記述して、実行されるターゲットを指定していました。しかし、これは少し不便です。Makefileだって、makeのオプションで自由に実行するルールを指定できます。
make test # Makefile内のtestルールを実行する
同じようなことを実現したいわけです。とりあえずは、何もオプションが指定されなかったときはrumyファイルのデフォルトのターゲットが実行され、オプションを指定したときはそのターゲットが実行されるようにします。
これはいくつか方法を考えたのですが、どうしてもrumyファイル内に記述を追加する方法しかなく、すこし格好悪いですがrumyファイルの最後に以下のような一文を追加するようにしました。
if ARGV.length == 0 then exec_target "test" else instance_eval("exec_target ARGV[0]") end
Rumyファイルを実行する際に、オプションを入れていなければexec_target "test"
が実行されます。そうでなければ、ARGV[0]
で指定される最初の引数が文字列として渡され、exec_target
の引数として実行されるようにしました。少し面倒ですが、これからはすべてのrumyファイルに上記を追加することで、楽にコマンドを実行できるようにします。
RUBYLIB=${PWD}/../src ruby ./check_timestamp.rb test # 最初に実行するのはtestターゲット
Cleanコマンドを作る
さて本題です。ビルドツールというのはどのルールをベースにして次のルールを実行するのか、という依存関係を記載しているツールですので、このルールを実行すると何のファイルが生成される、というのは基本的にすべて把握できます。
したがって、そのルールから生成されるファイルをたどっていき、中間生成物を削除していくことで、cleanコマンドが作れるはずです。本来Makeコマンドではバイナリ実行時にログやら一時ファイルやら生成されるはずで、これらもターゲット内にしっかりと定義しないと完全にCleanコマンドで消し切ることは難しいのですが、そこはとりあえずあいまいにして、依存関係のあるファイルをひたすら消していくというコマンドを作ります。
で、実装は本当に単純で、最初のルールから順に深さ優先探索でルールを探っていき、
- その依存ターゲットがラベルの場合、単純にそのルールに依存するターゲットを探索しに行くだけ。
- その依存ターゲットが文字列で、しかもルールとして明確に定義されている場合、それはさらに依存するルールをベースに生成されるファイルのはずなので、消す。
- その依存ターゲットが文字列で、ルールとして定義されていない場合、これはルールの葉に相当するファイルなので、消さない
という条件を実装していきます。
private def do_clean_target(name) if not $target_list.key?(name) then return end target = $target_list[name] target.depend_targets.each{|dep| do_clean_target(dep) } if not Symbol.all_symbols.include?(name) and File.exist?(name) then puts "[DEBUG] clean_target : remove " + name File.delete(name) end end
という訳で実装した結果は上記のようになりました。まず、削除対象の名前がターゲット名でない場合(これはもっともオリジナルになるソースファイルであると考える)、これは消さずに、単純に戻ります。
次に、依存するターゲットを探索していき、その中でファイルとして存在しておりかつシンボルでもない場合には、中間生成物であるとして削除します。
テストは以下のように作りました。
#!/usr/bin/ruby load "rumy-exec.rb" make_target "test.o" do depends ["test.c"] executes ["gcc -c test.c"] end make_target "test" do depends ["test.o"] executes ["gcc test.o -o test"] end make_target :run_test do global depends ["test"] executes ["./test"] end exec_target :run_test clean_target :run_test
中間生成物として、test
およびtest.o
が作られます。これらをビルドした後、clean_target
コマンド削除を試みます。
$ RUBYLIB=${PWD}/../src ruby ./clean_target.rb ... gcc test.o -o test ./test Hello Rumy-Make!! [DEBUG] clean_target : remove test.o [DEBUG] clean_target : remove test
target_clean
コマンドで、中間生成物を認識して削除してくれました。とりあえず、問題はなさそうです。