この記事は「Qiita Advent Calendar 2019 DSLで自作ビルドツールを作ろう」の10日目の記事です。
10日目 もっとデバッグしやすくしたい!
これまでにRumy-Makeにいろんな機能を付けてきましたが、さて実際にこれまでMakefileなどを使用してきた経験で困ったことは何でしょう。私の経験上は、ルールが非常に複雑になった場合に、どのルールがどのような関係を持っているのかを一目で把握することが難しいことがあります。ルールが複雑に絡み合ったり、深い階層までルールが定義してある場合には追いかけるのが非常に大変で、ルールの全体像を把握するのが難しい場合があります。
そこで、ルールの完成性をグラフに表してくれるツールがあればいいんじゃないかという気持ちになりましたルールは基本的にツリー構造になっているはずなので、それを可視化することを考えます。
可視化と言っても、ルールを見ながら深さ優先探索を行っていき、タブを深くしていきながらターゲットを表示するだけです。あまり凝った作りではないのですが、とりあえずは良しと考えましょう。
いつものように、どのような仕組みにするかまずはアルゴリズムを考えます。
- 指定されたインデックスの数だけスペースを打っていき、インデントを作る。
- ターゲット名を表示する。
- 各依存するターゲットについt、インデントの数を増やして、1.に戻って実行する
という、非常に単純な仕組みです。一瞬で考えたのでいくつか抜けがあるかもしれませんが、とりあえずはこれでうまくできるはずです。
private def do_draw_target (name, spc, show_line_list, last_item) (1..spc).each_with_index {|_, i| if show_line_list.include?(i) then print "| " else print " " end } if last_item then print "\`" else print "|" end print "-> Target : " + name.to_s if not $target_list.key?(name) then puts "" return else puts "\t\t\t\t// " + $target_list[name].commands.to_s end $target_list[name].depend_targets.each_with_index{|dep_name, index| new_show_line_list = show_line_list if $target_list[name].depend_targets.length-1 != index then new_show_line_list = new_show_line_list + [spc + 2] last_item = false else last_item = true end do_draw_target(dep_name, spc + 2, new_show_line_list, last_item) } end def draw_target (name) if not $target_list.key?(name) then puts "Error: target \"#{name}\" not found." exit end do_draw_target(name, 0, [], true) end
実際の実装は上記です。表示を格好良くするために少しだけ工夫をしています。例えば、
- 通常は
|->
で依存関係を表すが、最後の1行だけは`->
として次の行に繋がらないように見せる。 - 途中の
|
をが変な風に挿入されないように、どの場所でどの|
を入れるか配列で管理。
この辺の綺麗にする工夫は説明しても理解するのに時間がかかるでしょうし(私の説明が下手なので)、あまり本質ではないため省略します。とりあえずは、以下のコードを実行してみます。
source_file = "./test.c" exec_file = source_file.sub(".c", "") make_target :make_ccode do executes ["echo \"#include <stdio.h>\nint main () { printf(\\\"Hello Rumy-Make!!\\\"\); return 0; }\" > #{source_file}"] end make_target :compile_c do depends [:make_ccode, "test2.c", "test3.c"] executes ["gcc #{source_file} -o #{exec_file}"] end make_target :link_code do depends [:hogehoge] end make_target :run_c do global depends [:link_code, :compile_c] executes ["#{exec_file}"] end draw_target :run_c
依存がたくさん並んでいます。使いもしないのに:link_code
とか:hogehoge
とか意味のないラベルをたくさん入れました。これだけだと、一番トップの:run_c
からどのような依存関係で実行されるのかが分かりにくいので、draw_target
で依存関係を表示してみます。
RUBYLIB=${PWD}/../src ./draw_target.rb
こんな感じです。綺麗に可視化されました。
`-> Target : run_c // ["./test"] |-> Target : link_code // [] | `-> Target : hogehoge `-> Target : compile_c // ["gcc ./test.c -o ./test"] |-> Target : make_ccode // ... |-> Target : test2.c `-> Target : test3.c
おまけ。Makefileで依存のループを書いたらどうなる?
上記の可視化コマンドを作りながらふと思ったのですが、Makefileって依存関係にループを作るとどうなるんでしょう?やってみましょう。
test1 : test2 echo test1 test2 : test1 echo test2
make
make: Circular test2 <- test1 dependency dropped. echo test2 test2 echo test1 test1
あ、loopを検出して自動的に止まりました。どうもループを検出して無限ループに陥らない工夫が入っているようですね。
しかし、Rumy-Makeはそんな機能は入れていないので、普通にターゲットの依存でループを作ると可視化ツールが暴走してどこまでも表示してしまいました。これはおいおい直す必要がありそうです...