in Everything

Making a Ruby executable with ruby-packer

You can make a single executable from your gem or even a Rails application. I just tried ruby-packer and it works as promised.

One of the things that I missed when writing a command line tool in Ruby was making a binary that is easy to distribute. Since Ruby is an interpreter we cannot just make a binary.

However, there are ways how to package Ruby interpreter and all the required gems together with your program as one distribution. For one there was Traveling Ruby project. Another good option is to look at how Vagrant is packaged (I did that when we were bringing Vagrant natively for Fedora back in the day). But neither approach felt quite right and the best way to distribute Ruby programs was to build them as RPM or DEB packages. That’s of course a lot of work and you end up with only one platform so not suitable for quick afternoon hacking unfortunately.

But then ruby-packer appeared on my radar and it got my hopes high! Today I finally had some time and tried to make a single executable for InvoicePrinter. Here is how it went.

Installation

First we need to install the prerequisites. The main one is SquashFS, a compressed read-only file system for Linux, the core idea behind ruby-packer. On Fedora we install squashfs-tools package:

# dnf install squashfs-tools

Then we need a C compiler, GNU Make and Ruby. If you are on Fedora like me and still don’t have those follow Fedora Developer Portal instructions.

Once done I locally fetched ruby-packer as mentioned in docs:

$ curl -L http://enclose.io/rubyc/rubyc-linux-x64.gz | gunzip > rubyc
$ chmod +x rubyc
$ ./rubyc --help

Building the executable

Everything went smoothly so without further ado let’s build InvoicePrinter as single Ruby executable:

$ ./rubyc --gem=invoice_printer --gem-version=1.2.0 invoice_printer --output invoice_printer

We are calling rubyc in a RubyGems mode by providing a gem name (--gem) and version (--gem-version), executable entry point and output.

After a while it produced invoice_printer executable (a.out in case the output name is not provided) and I was ready to give it a spin.

$ ./invoice_printer --help
Usage: invoice_printer [options]

Options:

    -l, --labels   labels as JSON
  -d, --document   document as JSON
     -s, --stamp   path to stamp
          --logo   path to logotype
          --font   path to font
     --page-size   letter or a4 (letter is the default)
  -f, --filename   output path
    -r, --render   directly render PDF stream (filename option will be ignored)

Works just as the gem version!

I am really pleasantly surprised. I now have 26.3 MB big working executable that I can distribute. The only small issue is that we have just one entry point, but InvoicePrinter comes with two (command line and server). To that end we have to create a separate program for InvoicePrinter Server:

$ ./rubyc --gem=invoice_printer --gem-version=1.2.0 invoice_printer_server --output invoice_printer_server

Conclusion: I can offer executables for my gems to make it easy to use for folks not having a Ruby runtime around. And it all took me just 10 minutes. ruby-packer surely needs a closer look.

Write a Comment

Comment

  1. Hi Josef! Nice to know ruby-packer is working. I got an error though when trying to build your invoice_printer as single Ruby executable. I think it has to do with the ruby-packer, because I got the same error when building my ruby script. I try to search google, and the results are errors related with ruby installation using rvm. I am really curious what has gone wrong. This is the error message. Does it ring a bell? Thanks in advance.

    In file included from openssl_missing.c:21:0:
    openssl_missing.h:133:41: warning: type defaults to ‘int’ in declaration of ‘X509_REQ’ [-Wimplicit-int]
    void ossl_X509_REQ_get0_signature(const X509_REQ *, const ASN1_BIT_STRING **, const X509_ALGOR **);
    ^
    openssl_missing.h:133:50: error: expected ‘;’, ‘,’ or ‘)’ before ‘*’ token
    void ossl_X509_REQ_get0_signature(const X509_REQ *, const ASN1_BIT_STRING **, const X509_ALGOR **);
    ^
    openssl_missing.c:131:1: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘{’ token
    {
    ^
    openssl_missing.c:143:1: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘{’ token
    {
    ^
    openssl_missing.c:155:1: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘{’ token
    {
    ^
    openssl_missing.c:165:36: warning: type defaults to ‘int’ in declaration of ‘X509_REQ’ [-Wimplicit-int]
    ossl_X509_REQ_get0_signature(const X509_REQ *req, const ASN1_BIT_STRING **psig,
    ^
    openssl_missing.c:165:45: error: expected ‘;’, ‘,’ or ‘)’ before ‘*’ token
    ossl_X509_REQ_get0_signature(const X509_REQ *req, const ASN1_BIT_STRING **psig,
    ^
    In file included from /tmp/rubyc/openssl/include/openssl/engine.h:23:0,
    from openssl_missing.c:14:
    /tmp/rubyc/openssl/include/openssl/bn.h:285:1: error: old-style parameter declarations in prototyped function definition
    DEPRECATEDIN_0_9_8(BIGNUM *BN_generate_prime(BIGNUM *ret, int bits, int safe,
    ^
    openssl_missing.c:172:1: error: expected ‘{’ at end of input
    }
    ^
    Makefile:304: recipe for target ‘openssl_missing.o’ failed
    make[2]: *** [openssl_missing.o] Error 1
    make[2]: Leaving directory ‘/tmp/rubyc/ruby-2.4.1-0.4.0/ext/openssl’
    exts.mk:231: recipe for target ‘ext/openssl/all’ failed
    make[1]: *** [ext/openssl/all] Error 2
    make[1]: Leaving directory ‘/tmp/rubyc/ruby-2.4.1-0.4.0’
    uncommon.mk:248: recipe for target ‘build-ext’ failed
    make: *** [build-ext] Error 2
    Failed running [{“CI”=>”true”, “ENCLOSE_IO_USE_ORIGINAL_RUBY”=>”1”, “CFLAGS”=>” -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe -I/tmp/rubyc/zlib -I/tmp/rubyc/openssl/include -I/tmp/rubyc/gdbm/build/include -I/tmp/rubyc/yaml/build/include -I/tmp/rubyc/libffi/build/lib/libffi-3.2.1/include -I/tmp/rubyc/ncurses/build/include -I/tmp/rubyc/readline/build/include “, “LDFLAGS”=>” -L/tmp/rubyc/zlib /tmp/rubyc/zlib/libz.a -L/tmp/rubyc/openssl -L/tmp/rubyc/gdbm/build/lib -L/tmp/rubyc/yaml/build/lib -L/tmp/rubyc/libffi/build/lib -L/tmp/rubyc/ncurses/build/lib -L/tmp/rubyc/readline/build/lib “, “ENCLOSE_IO_RUBYC_1ST_PASS”=>”1”, “ENCLOSE_IO_RUBYC_2ND_PASS”=>nil}, “make -j4 -j1”]

    • I didn’t come across any issue like this. I suggest to open an upstream issue and put information about your environment there. I don’t think it’s RVM issue, ruby-packer packages its own Ruby version. Maybe you can try the trunk/master version that already has Ruby 2.5. I should have mention though that I am running Fedora 27 (and system Ruby).

      • I have put an issue at ruby-packer repo. I have also try the trunk/master version and encounter another error. It’s related with miniruby now. Not sure what it is. I am running Ubuntu 16.04.

        linking miniruby
        autoupdate_autoupdate.o: In function `autoupdate’:
        /tmp/rubyc/ruby-2.5.1-0.5.0/autoupdate_autoupdate.c:957: undefined reference to `inflateInit2_’
        /tmp/rubyc/ruby-2.5.1-0.5.0/autoupdate_autoupdate.c:985: undefined reference to `inflate’
        /tmp/rubyc/ruby-2.5.1-0.5.0/autoupdate_autoupdate.c:997: undefined reference to `inflateEnd’
        squash_decompress.o: In function `sqfs_decompressor_zlib’:
        /tmp/rubyc/ruby-2.5.1-0.5.0/squash_decompress.c:34: undefined reference to `uncompress’
        collect2: error: ld returned 1 exit status
        Makefile:231: recipe for target ‘miniruby’ failed
        make: *** [miniruby] Error 1
        Failed running [{“CI”=>”true”, “ENCLOSE_IO_USE_ORIGINAL_RUBY”=>”1”, “CFLAGS”=>” -fPIC -O3 -fno-fast-math -ggdb3 -Os -fdata-sections -ffunction-sections -pipe -I/tmp/rubyc/local/include -I/tmp/rubyc/local/lib/libffi-3.2.1/include “, “LDFLAGS”=>””, “ENCLOSE_IO_RUBYC_1ST_PASS”=>”1”, “ENCLOSE_IO_RUBYC_2ND_PASS”=>nil}, “make -j4”]

        • One way you can work around this is to install Vagrant, get Fedora box and do it there. Since it’s one time thing (publishing) I think it’s a viable strategy (having always one VM around where this is working).

          Of course that’s not the right fix for your issue, but if you are curious to try it for your gem that might work.

  2. Current versions of ruby-packer leave an insecure RPATH in the binaries, anyone with write permission to `/tmp/ (usually everyone) can hijack the account of anyone running an executable compiled by ruby-packer.

    Unless you manually set the temporary path used during build with the -d parameter to point to some usually non-writable directory such as /root.

    https://github.com/pmq20/ruby-packer/issues/66