Let's see how to compile ruby scripts into standalone binaries.
The final app will be a single executable file that can run on systems without a ruby interpreter installed.
I developed a gem to automate this process. Check it out: https://github.com/loureirorg/rb2exe (opens new window)
gem install rb2exe
echo "puts 'Hello world'" > test.rb
rb2exe test.rb
./test
Hello world
mkdir hello
cd hello
echo "puts 'Hello world'" > test.rb
ruby test.rb
Hello world
This method uses Ruby Traveler and pack everything into a self-extract file.
Ruby Traveler is just a static folder with a standalone "ruby" executable. The plan here is to add Ruby Traveler's folder and binaries to our project and compress everything into a single self-extractable zip file. This self-extractable file will extract the compressed data into a temporary folder and execute the main ruby script (ex. test.rb
).
As this is based on Ruby Traveler (by Phusion), which seems to be a stalled project, we are locked to the only versions they support – the latest one is 2.2.2. 😦
TIP
If you want to use different Ruby versions, other than 2.2.2, I recommend adapting this script to zw693's Traveling Ruby, which supports ALL Ruby versions: https://github.com/zw963/traveling-ruby (opens new window)
2.2.2
, 64 bitsecho $RUBY_VERSION
ruby-2.2.2
WARNING
If your version is different, please install the 2.2.2 (e.g. rvm use 2.2.2
).
This will NOT work with 2.2.0, 2.2.3, 2.1, etc.
# ~/hello/
cd ..
cp -r -pa hello app
cd hello
.package/payload/lib/ruby
sub-folder# ~/hello/
mkdir -p .package/payload/lib/ruby
.package/payload/lib
# ~/hello/
mv ../app .package/payload/lib
tree -a
.
├── .package
│ └── payload
│ └── lib
│ ├── app
│ │ └── test.rb
│ └── ruby
└── test.rb
.package/payload/lib/ruby
# ~/hello/
cd .package/payload/lib/ruby
# ~/hello/.package/payload/lib/ruby/
wget http://d6r77u77i8pq3.cloudfront.net/releases/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz
tar -xf traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz
rm traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz
.package/payload/
# ~/hello/.package/payload/lib/ruby/
cd ../..
pwd
~/hello/.package/payload
installer.sh
)# ~/hello/.package/payload/
nano installer.sh
#!/bin/bash
set -e
# Figure out where this script is located.
SELFDIR="`dirname \"$0\"`"
SELFDIR="`cd \"$SELFDIR\" && pwd`"
## GEMFILE
if [ -f "$SELFDIR/lib/vendor/Gemfile" ]
then
# Tell Bundler where the Gemfile and gems are.
export BUNDLE_GEMFILE="$SELFDIR/lib/vendor/Gemfile"
unset BUNDLE_IGNORE_CONFIG
# Run the actual app using the bundled Ruby interpreter, with Bundler activated.
exec "$SELFDIR/lib/ruby/bin/ruby" -rbundler/setup "$SELFDIR/lib/app/test.rb"
else
exec "$SELFDIR/lib/ruby/bin/ruby" "$SELFDIR/lib/app/test.rb"
fi
# ~/hello/.package/payload/
chmod +x installer.sh
Replace the exec
lines with your actual command to start the application. Eg. for Rails apps, it should be:
RAILS_ENV=production exec "$SELFDIR/lib/ruby/bin/ruby" -rbundler/setup "$SELFDIR/lib/app/bin/rails" server
TIP
The previous script is based on the "traveling ruby" tutorial. If you want to understand this process better, you can follow the official "traveling ruby" instructions.
If your project has a Gemfile, you need to follow these extra steps:
tmp
folder on .package/payload/lib
# ~/hello/.package/payload/
cd lib
# ~/hello/.package/payload/lib/
mkdir tmp
cd tmp
Gemfile
to tmp
# ~/hello/.package/payload/lib/tmp/
cp ../app/Gemfile* .
lib/vendor
folder# ~/hello/.package/payload/lib/tmp/
BUNDLE_IGNORE_CONFIG=1 bundle install --path ../vendor --without development
tmp
folder# ~/hello/.package/payload/lib/tmp/
cd ..
rm -Rf tmp
vendor
folder# ~/hello/.package/payload/lib/
rm -f vendor/*/*/cache/*
Gemfile
to the vendor
folder# ~/hello/.package/payload/lib/
cp app/Gemfile* vendor/
# ~/hello/.package/payload/lib/
mkdir vendor/.bundle/
cd vendor/.bundle/
# ~/hello/.package/payload/lib/vendor/.bundle/
nano config
BUNDLE_PATH: .
BUNDLE_WITHOUT: development
BUNDLE_DISABLE_SHARED_GEMS: '1'
This part is based on Jeff Parent's article (opens new window).
# ~/hello/.package/
nano decompress.sh
The .package/decompress.sh
:
#!/bin/bash
export TMPDIR=`mktemp -d /tmp/selfextract.XXXXXX`
ARCHIVE=`awk '/^__ARCHIVE_BELOW__/ {print NR + 1; exit 0; }' $0`
tail -n+$ARCHIVE $0 | tar -xz -C $TMPDIR
CDIR=`pwd`
cd $TMPDIR
./installer.sh
cd $CDIR
rm -rf $TMPDIR
exit 0
__ARCHIVE_BELOW__
# ~/hello/.package/
nano build.sh
The .package/build.sh
:
#!/bin/bash
cd payload
tar cf ../payload.tar ./*
cd ..
if [ -e "payload.tar" ]; then
gzip payload.tar
if [ -e "payload.tar.gz" ]; then
cat decompress.sh payload.tar.gz > output.sh
else
echo "payload.tar.gz does not exist"
exit 1
fi
else
echo "payload.tar does not exist"
exit 1
fi
chmod +x output.sh
echo "Self-extract file created"
exit 0
# ~/hello/.package/
chmod +x decompress.sh
chmod +x build.sh
./build.sh
Self-extract file created
And that's it. You can now rename and distribute the generated "output.sh" file 😃