この記事は「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_target
はexec_target
からしか呼ばれません。private
属性を付けておきます。exec_target
はis_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
ターゲットなどを呼び出そうとしてもエラーとなり、ユーザに見せたくないターゲットを隠すことができるようになります。