FPGA開発日記

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

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

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

6日目 細かな調整2

前回だけではやりたい調整が終わりませんでした。まだまだ直したい部分があります。

同一ターゲット内で複数のexecutesコマンドを受け付けるように

Makefileは以下のような記述ができます。つまり、1つのターゲットに対して、実行コマンドを2つ以上指定することができます。

all: test.c test2.c
   gcc -o test test1.c test2.c   # 1つ目の実行コマンド
   objdump test > test.dmp       # 2つ目の実行コマンド

さすがに1ターゲットに1つしかコマンドが書けないとターゲット量が増えてしまい読みにくくなるので、1つのターゲットで複数の実行コマンドを指定できるようにします。以下のように記述するイメージです。

make_target :compile do
    executes "gcc -o test test1.c test2.c"   # 1つ目の実行コマンド
    executes "objdump test > test.dmp"       # 2つ目の実行コマンド

この実装はとても簡単で、make_targetでTargetクラスをインスタンスするときにcommandsリストを空で初期化しておき、あとはexecutesが呼ばれるたびに配列にコマンドを追加していくだけです。実行するときはリスト内のコマンドを順番に実行すれば良いでしょう。

  • src/rumy-exec.rb
class Target
  def initialize(name)
  ...
    @commands = []   # コマンド配列は空で初期化
  end
  
  def executes(commands)
    if not commands.kind_of?(Array) then
      puts "ERROR: \"executes args\" should be specified as List. Did you forget to add '[', ']'?"
      exit
    end
    @commands += commands # 単純にコマンドを配列に追加していくだけ
  end
  ...
end   

def exec_target (name)
    ...
    # Execute commands!
    result = ""
    target.commands.each {|command|
      result = `#{command}`  # 配列内のコマンドを取り出して1つずつ実行するだけ。
      puts result
    }
end

そんな感じで、以下のように記述ができます。

load "rumy-exec.rb"

make_target :multiple_targets do
  executes ["echo Hello, First Target!"]
  executes ["echo Hello, Second Target!!"]
end

exec_target :multiple_targets

実行すると以下のようになりました。問題なさそうです。

ruby ../tests/multiple_executes.rb
[DEBUG] : Target Created  = multiple_targets, Depends = , Commands = ["echo Hello, First Target!", "echo Hello, Second Target!!"]
Hello, First Target!
Hello, Second Target!!

外部から隠ぺいするターゲットルールと、外部から直接実行できるルール

Makefileを読んでいると、時々悩むことがあります。さて、このルールは直接makeコマンド実行時に指定してよいものかしら?それとも、内部でいろんな設定が行われたうえで内部ルーチンから呼び出すべきものかしら?

例えば、あるファイルをコンパイルして実行するMakeルールを作った時に、ヘッダファイルを自動生成するルールを書いたとします。基本的にこのルールは外部から直接呼び出して欲しくありません。外とするソースコードコンパイルする直前にのみ実行されれば十分で、ユーザが直接指定する必要のないルールがあります。このようなルールはユーザから隠ぺいして、明示的に使えなくしたいのです。

そこで、Rumy-Makeではデフォルトで作成したターゲットは外部から呼び出せないにしてしまいましょう。明示的に参照可能に設定したターゲットのみ、実行できるようにします。それ以外のルールはすべてルール内で呼び出しあうものとします。そこで、ターゲットルールの中にglobalという属性を追加しました。この属性は負デフォルトでFalseです。つまり外部から参照することはできません。global=Trueに設定されたターゲットルールのみ、実行が許可されます。

# このターゲットは、基本的に外部から実行できない。
make_target :hidden_rule do
    executes "..."
end
# このターゲットは、外部から実行できる。
make_target :explicit_rule do
    global
    depends [:hidden_rule]
    executes "..."
end

これも簡単。Targetクラスにglobalメンバを追加して、globalが呼び出されるとTrueに設定します。そして、実行時にglobal=Falseのルールは直接指定できないようにします。

class Target
  def initialize(name)
...
     @global = false
  end
  ...
  def global
    @is_global = true
  end
...

exec_targetは少し改造します。まず、最初のコマンド受付のためのexec_targetと、内部で実行用のdo_targetを分離します。do_targetexec_targetからしか呼ばれません。private属性を付けておきます。exec_targetis_globalの判定を行いますが、do_targetは最初のis_globalの判定が通った後に実行されるので、全く無関係に実行します。

# まずはexec_targetでコマンド受付け。ここは`is_global`の判定を行う。
def exec_target (name)
...
  target = @target_list[name]
  if target.is_global != true then
    puts "Error: target \"#{name}\" is not global. You can't specify the target directly"
    exit
  end

  do_target(name) # 内部のdo_target呼び出し。
end

# do_targetは外からは使わない。private属性。中身はこれまでのexec_targetとほぼ一緒。
private def do_target (name)
  if @target_list.key?(name) then
    target = @target_list[name]
...

例えば、4日目に作ったtests/multiple_depends.rbはトップの:run_cしか呼ばれてほしくないので、:runのみglobal属性を付け、それ以外は付けません。

#!/usr/bin/ruby

load "rumy-exec.rb"

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]
  executes ["gcc #{source_file} -o #{exec_file}"]
end

make_target :run_c do
  global
  depends [:compile_c]
  executes ["#{exec_file}"]
end

exec_target :run_c

こうすることで、:compile_cターゲットなどを呼び出そうとしてもエラーとなり、ユーザに見せたくないターゲットを隠すことができるようになります。