1. a jar-nORM-ous task
simple data-backed web app in a single jar
Ian Dees
www.ian.dees.name ● @undees
JRubyConf 2009
Hi. I’m Ian, a JRuby user. This talk is about deploying a Ruby app in a single .jar file. When you’re
leaning on big frameworks like ActiveRecord, there are a few things to remember when you package
your app.
2. CRUD - CUD
Imagine you’re handed some database credentials, and you want to whip up a web front end in
Sinatra to browse around the data. Let’s say the schema is simple and fairly normalized: the sweet
spot for ActiveRecord.
Photo (cc) flickr.com/photos/treehouse1977/1425246464
3. define “success”
images and stylesheets
A
B single .jar
C app.jar + jruby.jar
D “Install Ruby....”
F “Install....”
MRI and MySQL present some deployment challenges, even when everyone is on the same platform.
With JRuby, you can just hand out a couple of .jar files and have people run “java -jar
app.jar” (assuming everyone has a recent JVM). For bigger deployments, you can use Warbler to
make a .war file—but that requires configuring an application server: definitely overkill for small
projects.
4. 1-2-3-4
1. vendor everything
2. extract jars from your binary gems
3. let rawr do the heavy lifting
4. combine your jar with jruby-complete
The “rawr” project provides some recipes for packing Ruby code into a .jar, with a small Java
bootstrapper. Here are the four basic steps involved.
5. 1. vendor everything
# This file is auto-generated;
# use 'rake gems:vendor' to update it.
TE
%w(activerecord-2.3.4
L
activerecord-jdbc-adapter-0.9.2 E
activesupport-2.3.4
SO
activerecord-jdbcsqlite3-adapter-0.9.2
haml-2.2.13
O
jdbc-sqlite3-3.6.3.054
rack-1.0.1
B
sinatra-0.9.4).each do |dir|
$: << File.dirname(__FILE__) + "/#{dir}/lib"
end
Trying to disentangle the notion of a GEM_HOME from the weird file paths inside .jars is... nontrivial.
You can avoid the fuss by just putting your dependencies into the .jar as plain old Ruby libraries, a
bit like Rails’ vendored gems.
6. 1 (revised). use Bundler
bundle_path 'lib/ruby'
bin_path 'lib/ruby/bin'
gem 'activerecord'
gem 'activerecord-jdbc-adapter'
gem 'activerecord-jdbcsqlite3-adapter'
gem 'activesupport'
gem 'haml'
gem 'jdbc-sqlite3'
gem 'rack'
gem 'sinatra'
The Bundler library can automate this step for you.
7. 2. binary gems
desc 'Extract jars from our gems into staging area'
task :unjar => 'package/classes' do
Dir['lib/ruby/**/*.jar'].each do |jar|
path = File.expand_path(jar)
Dir.chdir 'package/classes' do
sh "jar -xf #{path}"
end
end
end
Some gems, notably including ActiveRecord database adapters, contain .jars. Java can’t load these
from within another .jar. One workaround is to extract the .class files out of the sub-.jars for
inclusion in your project. (Another approach is to use something like JarJar.)
8. 3. rawr
# build_configuration.rb
configuration do |c|
# ...
c.source_dirs = ['src', 'lib/ruby']
c.source_exclude_filter = Dir['lib/ruby/**/*.java'].map do
|file|
Regexp.new(
File.basename(file).gsub(".", ".") + "$")
end
# ...
end
The default configuration of rawr gets confused by gems that include .java files. The easiest thing to
do is tell it to skip these files. It would be nice if rawr’s configuration file took a glob-style file
pattern, but for now, you can make do with an array of regular expressions.
9. 4a. unjar
desc 'Extract app and jruby-complete
for later combining'
task :stage => 'package/bigjar/contents' do
Dir.chdir('package/bigjar/contents') do
sh 'jar -xf ../../jar/appapp.jar'
sh 'jar -xf ../../jar/lib/java/jruby-complete.jar'
end
end
At this point, you’ve got your app.jar and a stock jruby-complete.jar. That fits the “C” grade on our
success criteria, so we could stop here. But it’s not too big of an extra step to combine those into a
single .jar. That should earn us at least a “B.” First, extract them into the same directory.
10. 4b. manifest
desc 'Point the big jar manifest at our app'
task :manifest do
manifest = IO.read
'package/bigjar/contents/META-INF/MANIFEST.MF'
manifest.gsub! /^Main-Class: .+$/,
'Main-Class: org.rubyforge.rawr.Main'
File.open('package/bigjar/manifest', 'w') do
|f| f.write manifest
end
end
Then, change the main class in the manifest from the JRuby interpreter to the rawr-generated
bootstrapper.
11. 4c. combine
desc 'Combine staged app and
jruby-complete files into one jar'
task :package do
Dir.chdir('package/bigjar') do
sh 'jar -cfm appapp.jar manifest -C contents/ .'
end
end
Finally, combine them all into one jar.
12. 5-6-7-8
task :default => %w(
gems:bundle
gems:unjar
rawr:jar
app:stage app:manifest app:package)
Just to run this idea into the ground, here are those four basic steps again, expressed as Rake
tasks.
13. } /undees/appapp
An example of this technique is available on GitHub and Bitbucket. The demo app is a silly web front
end around some iPhone app store data. To run it, you’ll need to download the app store CSV data
provided by busted-loop.com and run a couple of Rake tasks to populate your database first. See
the Rakefile for details.
14. overall grade:
B-
A single .jar, with no embedded stylesheets or images, is nearly enough to earn a solid B according
to our success criteria. All that’s left to do is fix one minor quirk (the app currently has to be run
from a subfolder of the working directory).