FPGA開発日記

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

DSLでビルドツールを自作する (10日目 もっとデバッグしやすくしたい!)

この記事は「Qiita Advent Calendar 2019 DSLで自作ビルドツールを作ろう」の10日目の記事です。

10日目 もっとデバッグしやすくしたい!

これまでにRumy-Makeにいろんな機能を付けてきましたが、さて実際にこれまでMakefileなどを使用してきた経験で困ったことは何でしょう。私の経験上は、ルールが非常に複雑になった場合に、どのルールがどのような関係を持っているのかを一目で把握することが難しいことがあります。ルールが複雑に絡み合ったり、深い階層までルールが定義してある場合には追いかけるのが非常に大変で、ルールの全体像を把握するのが難しい場合があります。

そこで、ルールの完成性をグラフに表してくれるツールがあればいいんじゃないかという気持ちになりましたルールは基本的にツリー構造になっているはずなので、それを可視化することを考えます。

可視化と言っても、ルールを見ながら深さ優先探索を行っていき、タブを深くしていきながらターゲットを表示するだけです。あまり凝った作りではないのですが、とりあえずは良しと考えましょう。

いつものように、どのような仕組みにするかまずはアルゴリズムを考えます。

  1. 指定されたインデックスの数だけスペースを打っていき、インデントを作る。
  2. ターゲット名を表示する。
  3. 各依存するターゲットについ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はそんな機能は入れていないので、普通にターゲットの依存でループを作ると可視化ツールが暴走してどこまでも表示してしまいました。これはおいおい直す必要がありそうです...