overwatering.org

blog

about

A couple of long standing problems in the world of Ruby have been dealing with all the gems that are so easy to toss into your project and the differences between Ruby implementations. They do differ: between 1.8.6 and 1.8.7 standard library APIs changed. Slow clap.

In the world of adults the solution to these problems is known as dependency management. There are now two tools to help Ruby: rvm and Bundler. These are both opinionated pieces of software that try to do a lot to help. Often far too much. After digging quite deeply into Bundler with Nick Drew on a recent project, here are some things that we learnt that I’ve tried to distill into a set of guides on good ways to use Bundler and rvm. This is just what I’ve discovered, if there are any tips or better ways to use these tools, or other tools that help, please share.

This is going to assume that you already know something about both.

  • Firstly, use rvm and Bundler. If you have used them in the past and were unhappy, try again. The early versions had some truly horrible bugs. Development has moved quickly and things are a lot better now. If you’re using Rails 3 then you have little choice about Bundler. But for any projects you should use these.

  • Use rvm to install your Ruby, don’t use the system Ruby. Create at least one gemset and use that.

  • Create a .rvmrc for your project and check it into source control. Naming the gemset after your project, it should probably look something like:

      rvm_install_on_use_flag=1
      rvm --create use ruby-1.8.7-p330@project
    
  • Use rvm on your build box. But not in production.

  • Create a personal gem set and toss the gems you use on simple little scripts into that.

  • Install Bundler into the global gemset for the Ruby you are using.

  • Include development and test groups in your Gemfile. Be strict about the difference between a gem required to run the build system (development), used in tests (test — obviously) and a gem required to run the application.

  • Don’t check the .bundle/config file into source control. Bundler often chooses to remember settings in this file. This will cause much irritation. This is an example of being more helpful than is helpful.

  • Don’t use the --deployment switch to bundle install unless you really mean that. As well as producing a local bundle of gems, installing in deployment mode will freeze the Gemfile. This is remembered and will cause much frustration when you’re trying to add new gems. If this does happen you will get an opaque error complaining that you have not checked your Gemfile.lock into source control. To get around this, just delete your .bundle/config. You’re doing that to remove the remembered setting about freezing your gems.

  • Your build script (the one you run before committing) should call bundle package. In fact, it should look quite a lot like:

      bundle package && bundle install && rake spec features
    
  • Check the gem files in vendor/cache into source control.

  • Definitely use bundle install --deployment --local --without="development test" either as part of your Capistrano tasks or as part of packaging for a release. Every one of those switches except the first should not be required. But they are. So there you go.

  • Bundler attempts to take over your world. This is intensely irritating. Expect to be annoyed.

  • If you ever need to start a Ruby program from your Ruby program but in a different Bundler world, you will need to do some cleaning of your environment. In particular you will need to clean the environment variables BUNDLE_GEMFILE, BUNDLE_PATH, BUNDLE_BIN_PATH and most surprisingly RUBYOPT. You may find the following function helpful.

      def without_bundler_env
        original_env = ENV.to_hash
        ENV.delete("BUNDLE_GEMFILE")
        ENV.delete("BUNDLE_PATH")
        ENV.delete("BUNDLE_BIN_PATH")
        ENV.delete("RUBYOPT")
        yield
      ensure
        ENV.replace(original_env.to_hash)
      end
    
  • If you need to examine the Gemfile from another Ruby project in your Bundler controlled world, you will think that you can use Bundler::Definition.build("<path to Gemfile>", "<path to Gemfile.lock>", nil). That would be a good start. You will also need to set ENV["BUNDLE_GEMFILE"] to point to that file and call resolve_with_cache!, however. Try the following:

      ENV['BUNDLE_GEMFILE'] = 'Gemfile'
      bundle_def = Bundler::Definition.build('Gemfile',
                                             'Gemfile.lock',
                                             nil)
      bundle_def.resolve_with_cache!
    

Ultimately, the goal of Bundler and rvm are noble. Dependency management is a hard problem. And there are no alternatives if you are choosing to use Ruby. Are there things that could perhaps be done better? Absolutely. But there is probably nothing so bad that some patterns couldn’t fix. So please, let’s all work out how to get the most out of these tools.