Using Bundler within Bundler without going crazy

We recently completed a project where we were provisioning new Spree apps on demand for users. This meant having to create – and boot – a new Rails app from within an existing app – which was not without its problems.

Rails templates

Fortunately, creating the skeleton for the Spree stores was relatively easy, thanks to Rails app templates (the -m option you give to rails new). We were able to use the knowledge gained from writing our own team template to set up a basic Spree store, with the necessary overrides and initialiser files we needed.

To ensure that each store always had the same set of gem versions, we used our template to create a new Rails app, then copied across the generated Gemfile.lock into our template, meaning that bundle install in the newly-created Rails app would skip the dependency resolution from the Gemfile and use the versions specified in the Gemfile.lock. This meant we could rely on Rubygems (or a local cache) for our gems, rather than having to fork various gems on Github and use the forks as our gem source (via the github: option in the Gemfile). Updates to gems would be as simple as creating a new Gemfile.lock, copying it across to the stores, and re-bundling.

So far, so good.

Running rails new from a Rails app

To actually kick off the store provisioning, we wrote a service object that wrapped the setup of the rails new command to be run, so that we could set the app name, database credentials, directory etc. easily, with the service object concerned with how we actually passed these to the new app (we used environment variables).

We then use Kernel#spawn – with its incredible array of options you can pass – to spawn a separate process to run the rails new command, with the appropriate logging of STDOUT/STDERR and the right ENV variables.

Since we don’t want to wait for the installation process (as it would slow down our webserver process for everyone else, leaving us unable to service other incoming requests), we call Process.detach to indicate that we’re not interested in knowing when the process finishes (the template notifies the user itself – via email – when the process completes successfully).

This worked fine, until we reached bundle install, when we enter…

Bundleception

Inception cafe explosion

Turns out Bundler – in a Rails app – works part of its magic through altering several environment variables, which are used by Ruby to locate the correct version of gems to load, as well as specifying which Gemfile to use. Kernel#spawn will, by default, pass all these environment variables on to your new process, which means that our newly created Rails app will then be trying to use the Gemfile of our master app, and will promptly blow up as it gets gem version conflicts.

We initially tried hand-crafting our own environment variables, unsetting the ones Bundler introduces and resetting the ones it rewrites. This was a major headache. However, after a consultation of the Bundler documentation, we found:

Bundler.with_clean_env

Fortunately, the Bundler team have a nice way out of our predicament. The Bundler.with_clean_env method takes a block, in which all of Bundler’s alterations of the ENV are undone, then restored outside of the block. We could thus turn this:

pid = Kernel.spawn("rails new #{store_name}")
Process.detach(pid)

into this:

Bundler.with_clean_env do
  pid = Kernel.spawn("rails new #{store_name}")
  Process.detach(pid)
end

and our new Rails app will have no knowledge of the app that created it. Success!

You mustn't be afraid to dream a little bigger, darling

Photo by Alex Eylar on Flickr