Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

ruby-packerでRubyコードをシングルバイナリにコンパイルしてみた

ruby-packerとは

@pmq20さん作のruby-packerは、Rubyコードをシングルバイナリに変換して、Rubyがない環境でも実行できるようにするコンバーターです。

Evil Martiansのdipツール↓にも、ruby-packerでビルドした各種シングルバイナリ版がありますので実績はありますね。なおmacOS向けのdipのバイナリサイズは14MBでした。

docker-composeを便利にするツール「dip」を使ってみた

特徴

ruby-packerは、RubyとCのコードが半々近くで、他にもyaccやObjective-Cなどのコードをわずかに含んでいます。以下はREADMEからの引き写しです。

  • Linux、Mac、Windows向けのバイナリをそれぞれ生成できる
  • requireloadをネイティブでサポート(相対パスでもloadできる)
  • 自動アップデート機能
  • ネイティブC拡張をフルサポート
  • Railsアプリもフルサポート

試していませんが、ビルド時に----auto-update-url=でURLを指定するとオートアップデートもできるようです。

変換に必要なもの(Macの場合)

Command Line Toolsは普段からMacに入れていますが、あのバカでかいXcodeも必要とは...😢。

やってみた環境

  • macOS Catalina
  • Xcode 11.5
  • Command Line Tools for Xcode 11.5

LinuxとWindowsではやっていません。

ruby-packerをインストールする

とりあえずmacOS環境でインストールしてみました。

  • Homebrewでsquashfsをインストール
brew install squashfs
  • ruby-packerをインストール
curl -L http://enclose.io/rubyc/rubyc-darwin-x64.gz | gunzip > rubyc
chmod +x rubyc

./rubyc --help # 動作確認

自分はrubyc~/bin/の下に置きました。

単純なRubyコードファイルを変換する

こっ恥ずかしいことこの上ありませんが、大昔に練習で書いた単純なCLI Rubyコードを試しに変換してみます。gemではない単独のRubyファイルで、requireloadもありません。

試しにCommand Line ToolsのみでXcodeなしでやれるかどうかやってみました。

$ rubyc iscandar.rb
-> Project root not supplied, /Users/hachi8833/deve/ruby/iscandar assumed.
Ruby Compiler (rubyc) v0.4.0
- entrance: /Users/hachi8833/deve/ruby/iscandar/iscandar.rb
- options: {:make_args=>"-j4", :output=>"/Users/hachi8833/deve/ruby/iscandar/a.out", :tmpdir=>"/var/folders/3x/sfj972cx6vnddfqpk5yv36m00000gn/T/rubyc"}

-> mkdir -p /var/folders/3x/sfj972cx6vnddfqpk5yv36m00000gn/T/rubyc
-> cp -r "/__enclose_io_memfs__/local/vendor/zlib" "/var/folders/3x/sfj972cx6vnddfqpk5yv36m00000gn/T/rubyc/zlib"
-> cd /var/folders/3x/sfj972cx6vnddfqpk5yv36m00000gn/T/rubyc/zlib
-> Running [{"CI"=>"true", "ENCLOSE_IO_USE_ORIGINAL_RUBY"=>"1", "CFLAGS"=>" -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe ", "LDFLAGS"=>""}, "./configure --static"]
Checking for gcc...
Building static library libz.a version 1.2.11 with gcc.
Checking for size_t... Yes.
Checking for off64_t... No.
Checking for fseeko... Yes.
Checking for strerror... Yes.
Checking for unistd.h... Yes.
Checking for stdarg.h... Yes.
Checking whether to use vs[n]printf() or s[n]printf()... using vs[n]printf().
Checking for vsnprintf() in stdio.h... Yes.
Checking for return value of vsnprintf()... Yes.
Checking for attribute(visibility) support... Yes.
-> Running [{"CI"=>"true", "ENCLOSE_IO_USE_ORIGINAL_RUBY"=>"1", "CFLAGS"=>" -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe ", "LDFLAGS"=>""}, "make -j4"]
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN -I. -c -o example.o test/example.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o adler32.o adler32.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o crc32.o crc32.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o deflate.o deflate.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o infback.o infback.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o inffast.o inffast.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o inflate.o inflate.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o inftrees.o inftrees.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o trees.o trees.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o zutil.o zutil.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o compress.o compress.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o uncompr.o uncompr.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o gzclose.o gzclose.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o gzlib.o gzlib.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o gzread.o gzread.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN  -c -o gzwrite.o gzwrite.c
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN -I. -c -o minigzip.o test/minigzip.c
libtool -o libz.a adler32.o crc32.o deflate.o infback.o inffast.o inflate.o inftrees.o trees.o zutil.o compress.o uncompr.o gzclose.o gzlib.o gzread.o gzwrite.o
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN -o example example.o -L. libz.a
gcc -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe  -DHAVE_HIDDEN -o minigzip minigzip.o -L. libz.a
-> cd /Users/hachi8833/deve/ruby/iscandar
-> cp -r "/__enclose_io_memfs__/local/vendor/openssl" "/var/folders/3x/sfj972cx6vnddfqpk5yv36m00000gn/T/rubyc/openssl"
-> cd /var/folders/3x/sfj972cx6vnddfqpk5yv36m00000gn/T/rubyc/openssl
-> Running [{"CI"=>"true", "ENCLOSE_IO_USE_ORIGINAL_RUBY"=>"1", "CFLAGS"=>" -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe ", "LDFLAGS"=>""}, "./config"]
Operating system: x86_64-apple-darwinDarwin Kernel Version 19.4.0: Wed Mar 4 22:28:40 PST 2020; root:xnu-6153.101.6~15/RELEASE_X86_64
WARNING! If you wish to build 32-bit library, then you have to
         invoke 'KERNEL_BITS=32 ./config '.
         You have about 5 seconds to press Ctrl-C to abort.
"glob" is not exported by the File::Glob module
Can't continue after import errors at ./Configure line 17.
BEGIN failed--compilation aborted at ./Configure line 17.
"glob" is not exported by the File::Glob module
Can't continue after import errors at ./Configure line 17.
BEGIN failed--compilation aborted at ./Configure line 17.
This system (darwin64-x86_64-cc) is not supported. See file INSTALL for details.
-> Running [{"CI"=>"true", "ENCLOSE_IO_USE_ORIGINAL_RUBY"=>"1", "CFLAGS"=>" -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe ", "LDFLAGS"=>""}, "make -j4"]
make: *** No targets specified and no makefile found.  Stop.
Failed running [{"CI"=>"true", "ENCLOSE_IO_USE_ORIGINAL_RUBY"=>"1", "CFLAGS"=>" -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe ", "LDFLAGS"=>""}, "make -j4"]

やはりXcodeもないとダメか...。

空き容量を増やしてApp StoreでXcodeをインストールし(8GB必要)、Command Line Toolsも最新版を入れ直しました。この作業の方が時間かかりました😢。

気を取り直して再チャレンジ。READMEには「どんなコードでもコンパイルに5分はかかる」とあります。

$ rubyc -c iscandar.rb
#(めちゃめちゃ長いので省略)
compiling ext/extinit.o
make[2]: Nothing to be done for `libencs'.
generating enc.mk
compiling enc/encinit.c
linking ruby
ld: warning: could not create compact unwind for _ffi_call_unix64: does not use RBP or RSP based frame
-> cp "ruby" "/Users/hachi8833/deve/ruby/iscandar/a.out"
-> cd /Users/hachi8833/deve/ruby/iscandar

細かなエラーは出ていますが、Macbookのファンが唸りをあげて10分ほどで無事焼き上がりました。
焼き上がったa.outは11MBでした。早速実行してみます。

$ ./a.out
■イスカンダルのトーフ屋ゲーム■ (外部仕様より再現)
Copyright (C) 1978-2013 by N.Tsuda
Reference: http://vivi.dyndns.org/tofu/tofu.html
背景: あなたはイスカンダル星で遭難し、帰りの費用を稼ぐためにトーフをなるべくたくさん売ってお金を稼がなければならない。
最初に所持金1000円が与えられる。
30000円儲けることができれば、めでたくイスカンダルから脱出することができる。
トーフは製造に一個あたり10円かかり、一個あたり12円で売ることができる。
トーフは晴れの日は100個、曇りの日は50個、雨の日は10個売れる。
売れなかった分は損失となる。
あなたは天気予報を見て、明日いくつのトーフを製造するかを決めねばならない。

A Tofu vendor surviving in Iscandar
Copyright (C) 1978-2013 by N.Tsuda
Reference: http://vivi.dyndns.org/tofu/tofu.html
Background: You are a castaway in planet Iscandar in outer space, and you have to gain money by making and selling Tofu in order to go back to your mother planet.
Initially you have 1,000 yen. The goal is to gain 30,000 yen for your traveling fee.
One Tofu costs 10 yen for production, and the unit price is 12 yen.
The sales of Tofu depends on weather: you can sell 100 Tofu on a fine day, 50 on a cloudy day, and 10 on a rainy day.
Watch weather forecast and determine the quantity of Tofu you are going to make.

Now you have 1000 yen.

--------------------------------------------------------------------------------
Weather Forecast
Probability: Fine: 86%, Cloudy: 13%, Rainy: 1%.
--------------------------------------------------------------------------------

Enter the quantity of Tofu:

動きました😋。11MBならGo言語のバイナリサイズと大して変わらない感じです。よくここまで作ったと思います🙇。

気づいたこと

Ruby 2.4.1ベース

最新リリースのバイナリ版ruby-packerで使われているRubyのバージョンは2.4.1です。リポジトリはまめに更新されていますが、バイナリ版は3年前のものです。

$ rubyc --ruby-version
2.4.1

試しにruby-packerをgit cloneしてソースからビルドすると、最新のRuby 2.7.1でビルドされるようになりましたが、その代わり先のRubyコードをこれで変換すると、バイナリサイズが200MBを超えてしまいました。

-rwxr-xr-x 1 hachi8833 staff  11M  5 27 16:35 iscandar
-rwxr-xr-x 1 hachi8833 staff 216M  5 28 15:04 ruby2.7.1_iscandar

2.7.1だからこんなに大きくなるのかどうかはわかりませんが、バイナリ版のリリースをあえて更新してないのはそれが理由なのかもしれないと推測しました。

Macでのビルドにハマった

最初のビルドはすっと通りましたが、いろいろ試しているうちにビルドがコケるようになってきました😇。

とりあえずの対策としては、rubyc -cのように-cオプションを付けて一時ディレクトリを削除するか、それでもダメならビルドメッセージに出てくる/var/folders/3x/あたりに置かれる一時ディレクトリをsudo rm -rfで削除します。

他のgemやRailsのビルドはまだやれていません😢。

思いつく使いみち

一番ありそうなのは、Rubyをインストールできない環境でどうしてもRubyコードを実行したいときや、Rubyのないユーザー環境にRubyコードを配布したいときかなと思います。

ruby-packerでは事実上CRubyをコンパイルしているので、CRubyのビルド経験がないとハマりそうです。

Railsのシングルバイナリもやれるとのことですが、RDB接続やらWebpackerやらがちゃんと動くかどうかなど突破しないといけないことが多そうなので、自分はやらないつもりです😅。

まだ試していませんが、Dockerでruby-packerを試せるようにした方がいます↓。

おたより発掘


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。