この記事は「Qiita Advent Calendar 2019 DSLで自作ビルドツールを作ろう」の16日目の記事です。
16日目 ビルドルールをYAMLに書きたい
これまでで、Rubyベースのビルトツールを作って、独自構文を定義してビルドルールを定義できるようにしてきました。
ここで、ふと思い返してみると、例えばRustのビルドツールであるCargoなどを見てみると、Tomlという共通文法を使ってビルドルールや依存パッケージを記述できるようになっています。
Rubyの内部DSLを使ってこれまでビルドルールの文法を構築してきましたが、ここにきてもう少し共通文法を使ってビルドルールを定義できてもよいのではないか、という気がしてきました。独自だったり、共通だったり、いろいろ試行してみますが、とにかくやってみます。
Cargoと同じTomlを使うのは少し芸がないので、通常通りYAMLを使ってみます。YAMLは、例えば以下のように階層構造を使ってデータを構造化する記法です。
- item1: - section1: - itemA - itemB - itemC - section2: - itemD - item2
YAMLの構造を利用すれば、これまでに定義した独自文法のDSLをYAMLにそのまま移植できそうな気がしてきます。早速やってみましょう。
RubyでYAMLの構文を取り込むためのパッケージ
何はともあれ、まずはYAMLファイルを受け取って構造化データに変換する必要があります。Rubyではこの機能はパッケージで提供されており、ソースコードの先頭で以下のパッケージをロードします。
require 'yaml'
そして、YAMLファイルを開いてそのまま取り込んでいきます。取り込んだYAMLファイルは、RubyのArrayとHashを使って格納されています。とりあえずは、以下のようなRubyのコードを組んで、取り込んだファイルがどのようなデータ構造で格納されるのか見てみることにしました。
data = open(ARGV[0], 'r') { |f| YAML.load(f) } def show_hash (data) if data.kind_of?(Hash) then puts "It's Hash" data.each {|key, value| puts "Key = " + key.to_s show_hash(value) } elsif data.kind_of?(Array) then puts "It's Array" data.each {|elem| show_hash(elem) } else puts data end end show_hash(data)
以下のYAMLファイルを食わせてみて、様子を見ます。
build.yaml
- "libriscv_cedar.a" : - src_lists : - "../src/riscv_pe_thread.cpp" - "../src/riscv_syscall.cpp" - "../src/riscv_fds.cpp" - "../src/riscv_clint.cpp" - "../src/env.cpp" - "../src/inst_category_riscv.cpp" - "../src/trace.cpp" - "../src/inst_print_riscv.cpp" - "../src/inst_mnemonic_riscv.cpp" - "../src/inst_riscv_init.cpp" - "../src/inst_decoder_riscv.cpp" - "../src/inst_riscv.cpp" - "../src/inst_ops_riscv.cpp" - "../src/inst_operand_riscv.cpp" - "../src/dec_utils_riscv.cpp" - "../src/riscv_sysreg_rw.cpp" - "../src/riscv_sysreg_impl.cpp" - "../src/riscv_sysreg_str.cpp" - "../src/riscv_page_table.cpp" - "../src/memory_block.cpp" - "../src/mem_body.cpp" - "../src/gdb_env.cpp" - compile_options : - "-DARCH_RISCV" - "-I../vendor/cmdline/" - "-I../vendor/softfloat/SoftFloat-3d/source/include/" - "-I../" - "-I./" - "-std=c++0x" - "-fPIC" - depends : - gen_riscv_arch_info - gen_riscv_csr_info
RUBYLIB=${HOME}/work/rumy-make/src ruby ${HOME}/work/rumy-make/src/rumy-yaml.rb build.yml
Key = libriscv_cedar.a It's Array It's Hash Key = src_lists It's Array ../src/riscv_pe_thread.cpp ../src/riscv_syscall.cpp ../src/riscv_fds.cpp ../src/riscv_clint.cpp ../src/env.cpp ../src/inst_category_riscv.cpp ../src/trace.cpp ../src/inst_print_riscv.cpp ../src/inst_mnemonic_riscv.cpp ../src/inst_riscv_init.cpp ../src/inst_decoder_riscv.cpp ../src/inst_riscv.cpp ../src/inst_ops_riscv.cpp ../src/inst_operand_riscv.cpp ../src/dec_utils_riscv.cpp ../src/riscv_sysreg_rw.cpp ../src/riscv_sysreg_impl.cpp ../src/riscv_sysreg_str.cpp ../src/riscv_page_table.cpp ../src/memory_block.cpp ../src/mem_body.cpp ../src/gdb_env.cpp
ちょっとわかりにくいですが取り込むことができました。
今度は、ビルドに使用するYAMLの構造ルールを決めておきたいと思います。これを考え始めてからちょっと思ったのですが、やはりRubyのDSLを使うのに比べて、YAMLの構造は単純すぎてちょっと大変です。いろいろと試行して結局以下のように定義しました。
- ルール定義 # 生成したいファイル, ルール名など - type : # ビルドタイプ - bin / lib # 今のところどちらか。 - src_lists : # ソースファイルのリスト - AAA.cpp - BBB.cpp - compile_options : # コンパイルオプション - "-I./" - "-O3" ...
結局YAMLを使ってRubyのDSLで記述していたものを箇条書きしているだけです。あまり芸がないですが仕方ありません。しかもこれを定義しながら気が付きましたが、これではスクリプトなどを使って生成していたオプション(python3.6-config -cflags
などで生成していたPython用のオプション設定)などを取り込むことができません。YAMLでシンプルに書きすぎた分、このあたりに弊害が出てきました。
しかしせっかく作ったので、これを取り込んでみたいと思います。要するに、上記のYAMLファイルを取り込んで、Rumyのビルドルールを作り上げるわけです。C++用に作ったラッパーを作っているのと、作業的には大して変わりません。
以下のようになりました。取り込んだYAMLをさらっていき、typeが"lib"であればライブラリビルドのためのRumyビルドルールを呼び出し、"bin"であればバイナリビルドのためのビルドルールを呼び出します。それ以外はとりあえずは非サポートとしました。
rumy-yaml.rb
# ルールの取得 def obtain_rule (rule_array) # すべてのルール定義を読み込む。 rule_array.each {|rule| if rule.kind_of?(Hash) rule.each {|key, value| # ルール定義であればそれを取り込む。 puts "rule = " + key # "top"は特殊ワード。topで定義したルールからビルドを始める、という印。 if key == "top" then @top_rule = value[0] elsif value[0].key?("type") then # typeがlibであれば、ライブラリビルド if value[0]["type"][0] == "lib" then make_library_rule(key, value) # typeがbinであれば、バイナリビルド elsif value[0]["type"][0] == "bin" then make_execute_rule(key, value) else puts "I don't know this type! \"" + value[0]["type"][0].to_s + "\"" ...
そして、make_library_rule
とmake_execute_rule
を定義し、それぞれC++ビルドラッパーを使ってビルドルールを定義します。大体一緒なので、make_library_rule
のみ掲載します。
rumy-yaml.rb
def make_library_rule(lib_name, option_hash) # assert(rule_hash.kind_of?(Hash)) lib_option_hash = Hash.new lib_option_hash['src_lists' ] = [] lib_option_hash['compile_options'] = [] lib_option_hash['link_options' ] = [] lib_option_hash['depends' ] = [] option_hash.each {|elem| elem.each{|key, value| lib_option_hash[key] = value } } make_library(lib_name, lib_option_hash['src_lists' ], lib_option_hash['compile_options'], lib_option_hash['link_options' ], lib_option_hash['depends' ]) end
C++ラッパーを定義したときのmake_library
を呼び出しているので、構造的には単純です。
このYAML読み込み機能を通じて、Swimmer-RISCVのビルドルールを書き直してみます。これまでに作ったbuild.rb
のRumyビルドルールを、YAMLに書き換えるだけです。とはいえ、Rumyで使っていた便利機能がYAMLにより削られてしまうので、面倒なことこの上ないのですが...
build.yaml
- "libriscv_cedar.a" : - type : - "lib" - src_lists : - "../src/riscv_pe_thread.cpp" - "../src/riscv_syscall.cpp" - "../src/riscv_fds.cpp" ... - compile_options : - "-DARCH_RISCV" ... - "-fdebug-prefix-map=/build/python3.6-tym8xC/python3.6-3.6.8=." - "-specs=/usr/share/dpkg/no-pie-compile.specs" - "-fstack-protector" - "-Wformat" - "-Werror=format-security" ...
書いてみると猛烈に面倒です。これまでRubyのシェル機能などを使って呼び出していたオプション一覧を、すべて書き下す羽目になりました。しかし、一応すべて書き下すことができたので、無事にパッケージをビルドすることができます。
RUBYLIB=${HOME}/work/rumy-make/src ruby ${HOME}/work/rumy-make/src/rumy-yaml.rb build.yml
この面倒さについては、次回もう少し考えてみることにします。