I’m releasing a first version of JRubyGems. This post is documentation until I come up with something better.
JRubyGems allows you to package RubyGems on your Java application classpath. It removes the file system dependencies on locally installed gems that get awkward when using JRuby and dependent gems in a Java environment.
Use JRubyGems as a replacement for RubyGems. Instead of
require 'rubygems' gem 'activerecord'
require 'jrubygems' gem 'activerecord'
Behind the scenes JRubyGems requires RubyGems and injects behavior specific to finding gems on the Java classpath if they are not already installed.
You make JRubyGems available to your application by putting the jar on your application classpath.
Source is available from Subversion at https://svn.carbonfive.com/public/carbonfive/jruby/jrubygems/trunk/. The tests in ruby/test/test_jrubygems.rb illustrate JRubyGems’ behavior.
The jar at http://mvn.carbonfive.com/public/com/carbonfive/jruby/jrubygems/0.3/jrubygems-0.3.jar is all you need to try it out in your own scripts. With JRuby you can require jar files that are in your load path to get them on your classpath and make Ruby files in the jar available on your load path. So if I have a folder lib/ in my load path with the file lib/jrubygems-0.3.jar, I can use JRubyGems directly from a JRuby script with:
require 'jrubygems-0.3.jar' require 'jrubygems'
Most Java applications using embedded Ruby will have their own classpath management strategy. Just make sure the JRubyGems jar gets on the classpath of your application. If you are using Maven, you can use JRubyGems from the Carbon Five repository:
<repositories> <repository> <id>c5-public-repository</id> <name>Carbon Five Public Repository</name> <url>http://mvn.carbonfive.com/public</url> </repository> </repositories> <dependencies> <dependency> <groupId>com.carbonfive.jruby</groupId> <artifactId>jrubygems</artifactId> <version>0.3</version> </dependency> </dependencies>
If you are using Maven you can also require your JRuby dependency with:
<dependency> <groupId>org.jruby</groupId> <artifactId>jruby-complete</artifactId> <version>1.1RC1</version> </dependency>
JRubyGems expects a gem to be available on your application classpath as a .gem file (the distribution package for a gem) under the classpath location /gems. This means you can have a gems/ folder on your classpath with a bunch of .gem files in it, a jar with all your .gem files under /gems or a jar per .gem file with each file under /gems in each jar. (A limitation in the JDK’s ClassLoader.getResources method which only returns file system locations for a passed-in empty String prevented me from allowing .gem files in jar roots.)
For our migration application, I built a jar for each gem dependency and deployed it to our central Maven repository. My Maven build references these dependencies in my project POM just as I would a jar dependency.
<dependency> <groupId>com.carbonfive.jruby.gems</groupId> <artifactId>activerecord</artifactId> <version>1.15.6</version> </dependency>
I just grabbed the .gem files from my local JRuby installation’s gem cache dir at jruby-1.1b1/lib/ruby/gems/1.8/cache/ to build these jars for now.
I like that Raven takes exactly the opposite approach – managing jar dependencies as gems.
Christian and I have a side project at Carbon Five to create a standard mechanism for managing database migrations for our Java projects. The first implementation defines a migration as a SQL script to be run against the target database. I thought it would be cool to support ActiveRecord migrations too, especially for providing an easy way to do the data migrations that accompany a schema change.
My prototype of this idea went well (more on that some other time) but I quickly ran into an issue with the gem dependency for ‘activerecord-jdbc-adapter’ and its dependencies ‘activesupport’ and ‘activerecord’. The issue is that my Java project was nicely portable but the gems create a dependency on the runtime system having specific gems installed. If I install the gems locally, I also have to install them on other runtime systems like our continuous integration server.
I found another example of a JRuby user running into this problem when reading about this example of using the RedCloth Ruby library to format text in a Spring/Java application. Note the “ugly underbelly” footnote.
In the Ruby world, this is the way things work. Large Ruby applications (especially Rails applications) often rely on system services like cron to do their work. These applications have their own mechanisms for setting up new runtime systems. In the Java world, we expect to package our application with all of its dependencies and deploy it to little more than a JVM and webapp container.
Java has the classpath abstraction to address minimizing file system dependencies while Ruby uses the file system directly and extensively. I’ve seen a couple approaches for addressing this mismatch between Java and Ruby. All of them involve taking some set of resources packaged in a Java archive (jar, war), extracting it to a file system location at runtime and configuring Ruby to use the resources at that location. The “jruby-complete” JRuby distribution unpacks the core Ruby libraries into ~/.jruby/. GoldSpike, the Rails plugin for packaging Rails applications as war files, bundles gems in WEB-INF and configures GEM_HOME at runtime to use the gems from the unpacked war.
I decided to take a similar approach with JRubyGems – bundle gem dependencies in the application classpath and install them on the local file system on demand if they are not already available. As I expect happens with many JRuby projects, it took a little Java and a little Ruby and works quite well.
The main flow for RubyGems to load a gem that has not yet been loaded for an application is:
JRubyGems replaces the Gem.activate implementation to insert a couple steps that:
This ensures that gems and their transitive dependencies are installed and loaded.
The classpath searching is implemented in Java.