[ Content | View menu ]

Gemspec: Loading Dependent Gems Based On The User’s System

Mark Mzyk | May 21, 2012

It’s a scenario that shouldn’t be hard. When installing a gem, have that gem load dependent gems based on what state the system is in.

Yet RubyGems provide no mechanism for doing this. You won’t find mention of it on Rubygems.org.

When you create a gem your gemspec is executed at creation time. When the user installs the gem, nothing that could react to the user’s system is run.

With one exception: extensions.

If your gem has an extension component (most commonly C), then it has to be compiled on the user’s system.

This process can be used to make your gem install dependencies or execute other ruby code at install time. I discovered this when looking for a way to install dependent gems on a user’s system based on the ruby version they were running.

The wikibook on RubyGems mentions this process (“How to install different version of gems depending on which version of ruby the installee is using”), but doesn’t do a great job going into detail on how it works or exactly what is needed.

This is a hack, in the sense that we’re using a system to do something other than what it was designed for. Due to this, if you use this process, it will appear to the user that your gem is installing an extension, when in fact it isn’t.

To go along with the explanation that follows, example code can be found on github: gem_dependency_example. You can install the gem gem_dep_example to see the process in action. This is the gem built from the example github repo. The gem will install itself along with a dependent gem based on the ruby version it detects on your system. It will install the gem gem_dep_shine if you have Ruby 1.9 or greater and it will install the gem gem_dep_polish if you have Ruby 1.8.7 (or older).

Let’s step through how this is accomplished. Looking at the code in the github repo, you’ll see in the gem_dep_example folder that it is a very simple gem. There is the standard gemspec and lib folder. There is also the ext, or extension, folder, which you normally don’t see unless a gem is installing an extension.

Look at the gemspec first. Nothing in the gemspec should seem surprising, except for a line near the bottom.

s.extensions = ["ext/mkrf_conf.rb"]

This line tells RubyGems that it has an extension to install and that the extension can be found in the file mkrf_conf.rb in the ext folder. This is a RubyGem convention, where it looks for this file in this location to know that an extension should be installed. In this case, the name of the file also tells RubyGems it is installing a Ruby extension and not a C extension.

Go the the ext folder and open mkrf_conf.rb, the only file there. You’ll notice some boiler plate code you’ll have to include, where you load the RubyGem’s dependency installer. If it doesn’t find this it will fail with an error.

After loading the installer, write your code to do what you want. This will run on the user’s system, so you can inspect the system and take conditional action based on what you find. In this case the code picks which gem to install based on the user’s Ruby version.

begin
  if RUBY_VERSION < "1.9"
    installer.install "gem_dep_polish", ">=0"
  else 
    installer.install "gem_dep_shine", ">=0"
  end

  rescue
    #Exit with a non-zero value to let rubygems know something went wrong
    exit(1)
end  

The extension file needs to end with these lines:

f = File.open(File.join(File.dirname(__FILE__), "Rakefile"), "w")
f.write("task :default\n")
f.close

The reason for this goes back to this being a hack. RubyGems thinks it is installing a native extension. To finish installing the extension, it will look for the make or rake file that it assumes was generated and run that to complete the installation. In this case, the relevant code has already run, but RubyGems will error out unless it finds a rake file to run. To make RubyGems happy, we create an empty rake file with an empty default task for RubyGems to run so it will exit normally.

Since it isn’t entirely clear to the user installing the gem what is happening, I recommend you avoid using this trick unless you have no other choice. I wish RubyGems included a more intuitive way of achieving this, but it doesn’t, so this will do for now.

A final note: you shouldn’t use this if you’re looking to install different gems based on the user’s platform (Windows, JRuby, Ruby, etc). In this case the accepted best practice is to create different gems for each platform and leave it to the user to install the appropriate one.