FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

DSLでビルドツールを自作する (8日目 cleanコマンドを作る)

この記事は「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コマンドで、中間生成物を認識して削除してくれました。とりあえず、問題はなさそうです。