この記事は 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にないのと、「チェックサムなのか...?」という気持ちが湧くことがあるので、このオプションは将来変更されるかもしれませんよ...。
もう一つの仕様が、「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さんが既に書いていました。
明日とは、なんなのでしょう... 明後日とは、未来とは...