FPGA開発日記

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

DSLでビルドツールを自作する (5日目 細かな調整)

この記事は「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.ctest2.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.ctest2.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!!

実行できていることが確認できました。良さそうです。