読者です 読者をやめる 読者になる 読者になる

ねこのて

- nekonote -

mruby-cli で ワンバイナリなツールを作ってみた

f:id:dojineko:20160221195105j:plain

今回も長め。


メモツールの移行を思い立ったので、今までのデータを移すべくテキトーなツールをつくって見ました。
移行ツールと言いながらまだ 単一のMarkdownとBoostnote の相互変換くらいにしか対応していませんが、、、

github.com

前回のネタに続いてせっかくなのでGolangでなく、mruby-cli で作ってみました。
今回はその過程でやったことや作ってみて思ったこととかをつれつれなるままに書いてみようと思います。

dojineko.hateblo.jp

やったこと

  1. mruby-cliでひな型を作る
  2. ガシガシ実装する
  3. 開発環境向けにコンパイルして、トライ&エラー
  4. 目的の機能が完成するまで 2 と 3 を繰り返す
  5. 出来上がったら docker でクロスコンパイル
  6. Githubにreleaseを登録しておしまい

TDDとかなにそれ美味しいのみたいな感じですが、まぁそれは置いといて...

mruby-cliでひな形を作る

mruby-cli 自体はmgemでなくそれ単体で動作するバイナリとして配布されています。
これを利用することで、mrubyでワンバイナリなツールを作成するためのひな形を手軽に用意することができます。 [2016-08-09: 表現を修正]

github.com

https://github.com/hone/mruby-cli/releases から開発環境に応じたツールをダウンロードし、下記のようにひな形を作ってもらいます。

# hello という名前でひな形をつくってもらう
$ ./mruby-cli -s hello
  create  .gitignore
  create  mrbgem.rake
  create  build_config.rb
  create  Rakefile
  create  Dockerfile
  create  docker-compose.yml
  create  tools/
  create  tools/hello/
  create  tools/hello/hello.c
  create  mrblib/
  create  mrblib/hello.rb
  create  mrblib/hello/
  create  mrblib/hello/version.rb
  create  bintest/
  create  bintest/hello.rb
  create  test/
  create  test/test_hello.rb

これでOK。mruby-cli 自体の役目はここまでです。案外ドライなもんです。
このあとは、build_config.rb に使うモジュールを追加して、ツール本体の実装を進めていきます。

mrubyのモジュール追加と機能実装

mruby-cli で作成するツールのコンパイル設定は、作られたひな形の build-config.rb 編集します。
このファイル中で、利用するmrubyの機能を設定したり、あるいは後で使うクロスコンパイル用の設定を記述します。

mrubyの機能追加のために、モジュールを組み込むには、build-config.rbgem_config に設定します。
デフォルトでは何も入っていませんが、それでも puts とか基本的な機能は使えます。

def gem_config(conf)
  #conf.gembox 'default'

  # be sure to include this gem (the cli app)
  conf.gem File.expand_path(File.dirname(__FILE__))
end

ちなみに、今回 momonga の作成には下記のモジュールを利用しました。

これらを組み込むため、下記のように追加しました。

def gem_config(conf)
  #conf.gembox 'default'
  conf.gembox 'full-core'
  conf.gem :mgem => 'mruby-getopts'
  conf.gem :github => 'iij/mruby-io'
  conf.gem :github => 'iij/mruby-dir'
  conf.gem :github => 'iij/mruby-iijson'
  conf.gem :github => 'monochromegane/mruby-time-strftime'
  conf.gem :github => 'monochromegane/mruby-secure-random'

  # be sure to include this gem (the cli app)
  conf.gem File.expand_path(File.dirname(__FILE__))
end

ちなみに指定の方法によって意味が異なりますので、必要に応じて使い分けましょう。

# gembox: 複数のgemの依存をひとまとめにしたものを取り込む
# 実態は mrbgems 配下に同名の full-core.gembox がある
conf.gembox 'full-core'

# mgem: mgem で拾えるmrbgemsを指定する
conf.gem :mgem => 'mruby-getopts'

# github: githubのリポジトリを指定する
conf.gem :github => 'iij/mruby-io'

# git: リモートアクセスできる git リポジトリを指定する
conf.gem :git => 'https://github.com/iij/mruby-dir.git'

使うモジュールの設定がかけたら、次にコンパイル設定を見ておきます。

コンパイルの設定は続く、MRuby::Build.new MRuby::Build.new('*****') do |conf| などで定義されています。
開発途中に関しては、クロスコンパイルの部分は邪魔なだけなのでコメントアウトしてしまうのが良いかと思います。

# 実行中の環境向けのコンパイル設定を記述する
MRuby::Build.new do |conf|

# ↓のブロックはクロスコンパイル用なので予めコメントアウトする
# MRuby::Build.new('xxxxx') do |conf|
# MRuby::CrossBuild.new('xxxxx') do |conf|

あとはコードを書いていけばOKです。
コード自体は、mruby-cli で作られたひな形の mrblib 配下にあるファイルに記述していきます。

ここで書くコードについて注意しないといけないのは、やはりRubyのつもりで書くと、
いつも使ってるメソッドが使えないなどの問題に結構ぶつかるという点です。

momonga の実装については下記のように調べてコードを書いていきました。

  • Ruby1.8/1.9 のドキュメントを参考にする
  • すでにある mrbgems のコードを参考にする
  • "mruby" ほげほげググる

とにかく検索してもmruby自体のドキュメントにヒットしないので、
Ruby1.8/1.9 のドキュメントを参考にしたり、mrbgems のコードを読んでどんなメソッドが定義されているかを読むのが案外早かったりします。

さて、hello という名前で初期化した場合は、 mrblib/hello.rb にエントリポイントが記載されています。
また、mrblib/hello 配下に関連するソースコードを配置するとコンパイル時に一緒に組み込んでくれるようです。
ちなみにひな形生成直後は下記のような内容になっています。

mrblib/hello.rb
def __main__(argv)
  if argv[1] == "version"
    puts "v#{Hello::VERSION}"
  else
    puts "Hello World"
  end
end
mrblib/hello/version.rb
module Hello
  VERSION = "0.0.1"
end

ひとしきり気の済むところまで実装が終わったら、コンパイルしてみましょう。
ここではサンプルのままコンパイルしてみます。コンパイルにはお馴染み rake を使います。
コンパイルした結果は、作ったディレクトリの mruby/bin/アプリ名 になります。

ちなみに、シンタックスエラーなどコンパイルに影響のあるエラーはここで検知できます。
一方、実行時に型が合わないとか、渡ってきたオブジェクトにあるはずのメソッドがないとかの動的型付け特有のエラーについては成果物を実行するまでは検知できません。 そういうのはできるとこからテストコードに落としていくと良いでしょう。

# コンパイル
rake
# ずらずらとコンパイルが始まる

# 実行
./mruby/bin/hello
Hello World

./mruby/bin/hello version
0.0.1

というわけで無事にコンパイル、実行できました。

ちなみにダメ元でWindowsで何も考えずに rake してみたら gcc/clang がないということで、見事にコンパイルがこけました。そりゃそうだ。

Cygwin や Msys2 なんかを用意してあげれば、コンパイルできたかもしれませんが、Windows上でゴニョるより、VM用意するほうが早いとおもったので早々に諦めた次第です。 気が向いたら別の機会にチャレンジしてみようと思います。

Dockerによるクロスコンパイル

冒頭にちょっと出てきたクロスコンパイルについてはDockerを使った方法が便利です。
環境をきっちり整えてあげれば最初にコメントアウトしてしまったクロスコンパイルの設定をすべて戻してrakeするだけでも良さそうですが、何より不必要に環境を汚すことはしたくないものです。

幸いなことに mruby-cli ではクロスコンパイル用のコンテナを用意してくれており、なおかつ docker-compose を利用することで手軽にクロスコンパイルできるような仕組みがあります。

実際につかってみると下記のような感じになります。

# dockerVMを起動して初期化
docker-machine start dev
eval $(docker-machine env dev)

# 予め build-config.rb のコメントアウトを戻して、
vim build-config.rb

# クロスコンパイルを実行
docker-compose run compile
# ずらずらとコンパイルが行われる

# 下記にコンパイル結果が出力される
ls -la mruby/build/

ちなみに mruby-cli の README にも書いてあるように OS XLinux環境 でないと docker-compose をつかってもクロスコンパイルができません。

On Mac OS X and Windows, Docker Toolbox is the recommended way to install Docker and docker-compose (does not work on windows).
OS XWindowsでは、dockerとdocker-composeのインストールにはDocker Toolboxを利用することをおすすめします(Windowsでは動作しません。)

ここでの does not work on windows. と言うのはどういうことなんだろうとおもって docker-composeを叩いてみると下記の警告が…

$ docker-compose run compile
Interactive mode is not yet supported on Windows.
Please pass the -d flag when using `docker-compose run`.

深追いはしてないですが docker-compose の インタラクティブモードがまだ使えない様子ですね。
コンテナ内の出力をコンソールに返せないようなので -d でスルーすることはできそうです。

これで試すとコンテナの名前だけが帰ってきますが、自分の環境では、うまくコンパイルできませんでした。残念。

つかってみて思ったこと

今回は mruby-cli で簡単にコマンドラインツールを作ってみました。
開発言語にRuby(mruby)を使えるので、Railsからの流れやらで、Rubyに普段から慣れている方にはたまらない仕組みなのではないかなと思いました。

また、OS XLinux環境ではdockerを使ってクロスコンパイルもできるので、成果物を広く使ってもらえる可能性があるのが良い点かなと思います。

一方でWindowsにおいては、mruby-cliで手軽にツールを作るというには、Golangを先に使った感覚から言うと、 mruby-cli自体の問題ではないですが、gccやclangなど、比較して用意するものがちょっと大変かなと思いました。
どうしてもWindowsじゃないとダメなんだ!という場合には逆らわず、開発用のVMを用意してあげるのが幸せになる近道かと思います。

また加えて、静的型付けによるチェックや、Golangにあるコンパイラを使ったコード補完機能などはやっぱり魅力的だなというのがありました。

下記に、今回使ってみた感じからざっくり使い分けのメモを出してみます。
あくまで今時点の感想からなので、今後は変わるかもしれません。

mruby-cli を使うのが良さそうな場合

  • 開発環境がOS X、もしくはLinux環境の場合
  • mrbgems が作る機能に対してすでにそろっている場合
  • mrbgems がなくても、サクッと作れるだけのスキルが有る場合
  • 作った機能を mrbgems として機能を切り分けて、流用する可能性がある場合
  • Ruby/mruby に慣れていて、他の手段を習得するよりも目的を早く達成できる場合
  • mruby を習得するきっかけが欲しい場合

mruby-cli を使わなくても良さそうな場合

  • 開発環境がWindowsの場合
  • 目的を達成するために、Golangなど他の手段を考えられる場合
  • Ruby/mruby に特にこだわらない場合

mruby/mruby-cliも目的を達成するためにある、数ある手段のうちの1つなので、目的に応じて使い分けていくことが大切なのかなぁと思いました。

上げたちょっとつらい所については、知らないことがある部分も大きいと思うのでなにか解決策がありましたらぜひ教えていただきたいです(´・ω・`)

ではでは~


関連リンク

今回の扉ネコ

www.pakutaso.com

Boostnote

Electron製なメモツール b00st.io