FPGA開発日記

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

DSLでビルドツールを自作する (16日目 ビルドルールをYAMLに書きたい)

この記事は「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の構造を利用すれば、これまでに定義した独自文法のDSLYAMLにそのまま移植できそうな気がしてきます。早速やってみましょう。

RubyYAMLの構文を取り込むためのパッケージ

何はともあれ、まずは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の構造ルールを決めておきたいと思います。これを考え始めてからちょっと思ったのですが、やはりRubyDSLを使うのに比べて、YAMLの構造は単純すぎてちょっと大変です。いろいろと試行して結局以下のように定義しました。

- ルール定義 # 生成したいファイル, ルール名など
  - type : # ビルドタイプ
    - bin / lib # 今のところどちらか。
  - src_lists : # ソースファイルのリスト
    - AAA.cpp
    - BBB.cpp
  - compile_options : # コンパイルオプション
    - "-I./"
    - "-O3"
...

結局YAMLを使ってRubyDSLで記述していたものを箇条書きしているだけです。あまり芸がないですが仕方ありません。しかもこれを定義しながら気が付きましたが、これではスクリプトなどを使って生成していたオプション(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_rulemake_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

この面倒さについては、次回もう少し考えてみることにします。