ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

mrubyのプロジェクトで、mruby自体やmgemのリビジョンを固定したい!!1

この記事は GMOペパボ Advent Calendar 2018 の3日目の記事です... 本当に遅くなりすいません...

さて、皆さんは、固定したいですか? ペパボは、 latest が最高!という文化の会社です(この記事唯一のペパボ要素)。

とは言え現実は社会なので、そういう時もあります。じゃあやっていきましょう。

まず、 mruby のビルドエコシステム自体には今の所そういう機能はありません。貢献のチャンスが広がっていて便利ですね。一方でmrubyのビルドスイートはRubyで書かれているので(セルフビルドできるといいですね、ワンバイナリにもなるし... どなたか...)、自分でそういうビルドシステムを作ったりすることはできます。

今日はその辺りに貢献するためのヒントとなりそうな内容を雑に書いとこうと思います。

mruby自体のバージョンを固定する - Haconiwa の場合

Haconiwa というmrubyで書かれたコンテナランタイムがあるのですが、これは基本的にビルド時に特定のリビジョンのmrubyを指定するようにしています。

  • mruby_version.lock というファイルにmrubyのリビジョンを書き込む
  • rake consistent というタスクを叩く

これで、該当するバージョンのmrubyを取ってきてビルドに使うことができます。

rake consistent の中身は結構雑にこういう感じで。

if ENV["MRUBY_VERSION"] && !ENV["MRUBY_VERSION"].empty?
  MRUBY_VERSION = ENV["MRUBY_VERSION"]
else
  MRUBY_VERSION = File.read(File.expand_path "../mruby_version.lock", __FILE__).chomp
end

file :mruby do
  cmd = "git clone --depth=1 https://github.com/mruby/mruby.git"
  case MRUBY_VERSION
  when /\A[a-fA-F0-9]+\z/
    cmd << " && cd mruby"
    cmd << " && git fetch --depth=500 && git checkout #{MRUBY_VERSION}"
  when /\A\d\.\d\.\d\z/
    cmd << " && cd mruby"
    cmd << " && git fetch --tags && git checkout $(git rev-parse #{MRUBY_VERSION})"
  when "master"
    # skip
  else
    fail "Invalid MRUBY_VERSION spec: #{MRUBY_VERSION}"
  end
  sh cmd
end

task :consistent do
  match = system %q(test "$(cat ../mruby_version.lock)" = "$( git rev-parse HEAD )")
  if match
    STDERR.puts "mruby version is consistent to mruby_version.lock"
  else
    STDERR.puts "making mruby version consistent to mruby_version.lock..."
    Dir.chdir("../") do
      FileUtils.rm_rf mruby_root
      Rake::Task[:mruby].invoke
    end
  end
end

この辺を汎用的に動くようにすれば本体に取り込まれるかもしれませんね。

mgemのリビジョンを固定したい!!

こちらは少々骨が折れそうという感想を持っています。一方で、Haconiwaには haconiwa revisions というコマンドがあり、ビルド時に組み込んだmrubyとmgemのバージョンを一覧確認できます。

これをどう実現しているかというと、ビルドの時点でダウンロードされた(非同梱の)mgemのリビジョンを全て取得し、 src/REVISIONS.defs というファイルを自動生成して、それをHaconiwa内部のCのコードでincludeしてプログラムから使えるようにしているのでした。

REVISIONS.defsリポジトリには含まれません(自動生成ファイルなんで)が、こういうCの配列(の一部)です。

{"MRUBY_CORE_REVISION", "b3a181aaa13aaa85e968fd09b780c061289aea38"},
{"haconiwa", "b3363af3eba0640dc566ae7017777ba947ef70c6"},
{"mruby-apparmor", "88ae9bd9d34f1d4cb4cc80c3d26f7af4aa55201d"},
{"mruby-argtable", "35693d4d220aa2a050a9ba95d4ac08bda9112cc0"},
{"mruby-capability", "02ba73d48c448990a150462353a1cee2ec8d8ba3"},
{"mruby-cgroup", "b57392d0c1caa1bfe41213aa62f0de34e8ce22a9"},
{"mruby-cgroupv2", "e73faa0b126b5788bd6a4ffaa630c50550d143ff"},
{"mruby-criu", "22dc202c8197096e7d492cebb551bbbf608a4e36"},
{"mruby-dir", "89dceefa1250fb1ae868d4cb52498e9e24293cd1"},
{"mruby-env", "056ae324451ef16a50c7887e117f0ea30921b71b"},
// ...

こういうタスクを mrbgem.rake に定義して rake でのビルドに引っ掛けるイメージ。

DEFS_FILE = File.expand_path('../src/REVISIONS.defs', __FILE__) unless defined?(DEFS_FILE)
DEPENDENT_GEMS = Dir.glob(File.expand_path('../mruby/build/mrbgems/mruby-*', __FILE__)) unless defined?(DEPENDENT_GEMS)

MRuby::Gem::Specification.new('haconiwa') do |spec|
  def spec.save_dependent_mgem_revisions
    file DEFS_FILE => (DEPENDENT_GEMS + ["#{MRUBY_ROOT}/.git/packed-refs"]) do
      f = open(DEFS_FILE, 'w')
      corerev = `git rev-parse HEAD`.chomp
      f.puts %Q<{"MRUBY_CORE_REVISION", "#{corerev}"},>
      mygems = ["../"] # Parent of mruby/ - haconiwa.gem
      mygems += `find ./build/mrbgems -type d -name 'mruby-*' | sort`.lines
      mygems.each do |l|
        l = l.chomp
        if File.directory? "#{l}/.git"
          gemname = l.split('/').last
          gemname = "haconiwa" if gemname == '..'
          rev = `git --git-dir #{l}/.git rev-parse HEAD`.chomp
          f.puts %Q<{"#{gemname}", "#{rev}"},>
        end
      end
      f.close
      puts "GEN\t#{DEFS_FILE}"
    end

    libmruby_a = libfile("#{build.build_dir}/lib/libmruby")
    file libmruby_a => DEFS_FILE
  end

  spec.save_dependent_mgem_revisions
end

コードの通り、 MRUBY_ROOT/build/mrbgems/mruby-* という箇所に、チェックアウトされてビルドに使われるmgemのコードが眠っているので、そこからgitコマンド経由ですべてのリビジョンを取得しています。こんな感じで意外と「つかわれているすべてのmgemのリビジョンを取得する」こと自体はできるようにはなっています。

で、ここで一旦取得したmgemのリビジョンからロックファイルを作るというのがまた問題になるわけですが、「今の」ビルドシステムの二つの仕様を使えば作り込むことができるかもと思っています。

ひとつ目が、 :checksum_hash というオプションで、指定したリビジョンにmgemをチェックアウトしてから、ビルドしてくれるものです。これはWikiにないのと、「チェックサムなのか...?」という気持ちが湧くことがあるので、このオプションは将来変更されるかもしれませんよ...。

github.com

もう一つの仕様が、「gem宣言のあと勝ち」というものです。たとえば、 build_config.rb に以下のような記述をすると、 mgem側での指定よりも build_config.rbの記述の仕様が優先されます。結果として、ビルド全体では 0a32553d のリビジョンのmruby-jsonが利用されることになります。

def gem_config(conf)
  conf.gem File.expand_path(File.dirname(__FILE__))
end

MRuby::Build.new do |conf|
  toolchain :gcc

  conf.enable_bintest
  conf.enable_debug
  conf.enable_test

  conf.gembox 'default'

  gem_config(conf)
  conf.gem github: 'mattn/mruby-json', \
    checksum_hash: '0a32553d255e62e63ffaa70b12e53767c7da7240', \
    branch: 'master'
end

これらの仕様を総合すると、チェックアウト済みのmgemから以下のようなRubyコードの断片を生成できるようにし、

gem mgem: "mruby-apparmor",   checksum_hash: "88ae9bd9d34f1d4cb4cc80c3d26f7af4aa55201d"
gem mgem: "mruby-argtable",   checksum_hash: "35693d4d220aa2a050a9ba95d4ac08bda9112cc0"
gem mgem: "mruby-capability", checksum_hash: "02ba73d48c448990a150462353a1cee2ec8d8ba3"
gem mgem: "mruby-cgroup",     checksum_hash: "b57392d0c1caa1bfe41213aa62f0de34e8ce22a9"
#...

build_config.rb の最後で、そのファイルが存在した場合のみinstance_evalし、そうでなければ普通にmgemのlatestを落としてくる、というようなリビジョン固定の実装をすることが考えられます。

MRuby::Build.new do |conf|
  #...
  
  lockfile = File.expand_path('../.mgems.lock.rb', __FILE__)
  if File.exist? lockfile
    code = File.read lockfile
    conf.instance_eval code # ここでmgemの定義を全て上書きできる
  end
end

この実装は、動かすレベルまで書ききっているわけではないので、アイデアに過ぎないと言えば過ぎないですが、どうでしょうか。こんどHaconiwaに組みこもうとは思っていますが、なにせスプラトゥーンの新環境とスマブラが...。


はい。こんな感じで、mrubyのビルドシステムは基本的にRuby、Rake taskなので、いろいろなことをやっていけます。工夫していきましょう。

で、明日の記事ですが、 k1lowさんが既に書いていました。

k1low.hatenablog.com

明日とは、なんなのでしょう... 明後日とは、未来とは...