I haven’t done a techie blog post for a while, and this “solved problem” keeps raising its head at work, so here goes.
Here’s the situation:
- A ruby web app
- Develop on Mac OS X
- Deploy on Linux
- Using bundler to control gem versions
- Using therubyracer and libv8
You build your app on your Mac, install your bundle of gems using bundler, and your tests run fine. Now you deploy the app to your production system, and there’s this error:
Some gems seem to be missing from your vendor/cache directory. Could not find libv8-220.127.116.11 in any of the sources
Or worse, this error:
Installing therubyracer (0.11.3) with native extensions Gem::Installer::ExtensionBuildError: ERROR: Failed to build gem native extension. /usr/local/bin/ruby extconf.rb --with-ruby-include=/usr/src/ruby-1.9.3-p392 checking for main() in -lpthread... yes *** extconf.rb failed *** Could not create Makefile due to some reason, probably lack of necessary libraries and/or headers. Check the mkmf.log file for more details. You may need configuration options. [...] /usr/local/lib/ruby/gems/1.9.1/gems/libv8-18.104.22.168-x86_64-linux/ext/libv8/location.rb:15:in `initialize': Permission denied - /usr/local/lib/ruby/gems/1.9.1/gems/libv8-22.214.171.124-x86_64-linux/ext/libv8/.location.yml (Errno::EACCES)
If you’re careful with your production deploys, then
- Your app does not run as root on the production servers, and
- Your app’s gems are cached in vendor/cache, thus not pulling gems from rubygems.org, or anywhere else.
Here’s the thing with libv8: it’s designed to be optimized for each platform where it runs, so the gem has native extensions with different versions for Mac OS X and Linux. Your gem cache needs to have both native versions of the gem in vendor/cache to satisfy the two platforms. When you bundle the gems on your development machine it doesn’t build the linux native version (obviously).
When you deploy the app on Linux, bundler fails because it doesn’t find the Linux custom gem.
Under the deployment conditions I described, trying to build libv8 fails trying to write to the system gems, hence the permission error I mentioned. (I have no idea what therubyracer is doing trying to write into the libv8 gem!)
So here’s how to solve the problem.
First, on your development system, do the normal bundle install, verify that the darwin (Mac OS X) version of libv8 is found in vendor/cache:
$ ls -l libv8* -rw-r--r-- 1 mjones mjones 33652224 2013-02-26 10:16 libv8-126.96.36.199-x86_64-darwin-11.gem
Now commit your changes with the new gem file, and push it to your git repository.
Next, connect to a handy Linux server. Install rvm (or rbenv) and your current Ruby, with a nice clean gemset. Be sure you have bundler installed.
(You may need some bundle options):
bundle config build.linecache19 --with-ruby-src=/usr/src/ruby-1.9.3p392 etc.
Clone your git repo, and do a bundle install. Using rvm (or rbenv) means you’ll have local copies of ruby and your gem set, and that should avoid any permissions problems.
Now check vendor/cache, and you should have a Linux version of the libv8 gem available. To deploy on both platforms, keep both in vendor/cache. Bundler has been good about keeping both versions around and not trying to clean the one you’re not using:
$ ls -l libv8* -rw-r--r-- 1 mjones mjones 33652224 2013-02-26 10:16 libv8-188.8.131.52-x86_64-darwin-11.gem -rw-r--r-- 1 mjones mjones 3144704 2013-02-26 11:29 libv8-184.108.40.206-x86_64-linux.gem
For good measure, I also often include the generic version of the gem, downloaded manually from rubygems. Add your updates to your git repository and push them to the remote origin.
You should find that your app deploys successfully on Linux now.