Dual Booting Multiple Gem Versions with Bootboot

Introduction

Hey, I’m Nuzair (Red), a Ruby on Rails developer at Money Forward. Recently, I tackled a project that required upgrading Rails and Ruby versions. In this article, I’ll share how we used Bootboot to manage multiple gem versions and ensure a smooth upgrade process.

Overview

Managing multiple versions of gems in a single Ruby codebase can be challenging, especially when upgrading major components like Rails or Ruby. Bootboot simplifies this process by allowing developers to maintain and test different versions of dependencies without disrupting existing functionality. This blog outlines how we utilized Bootboot to upgrade Rails and Ruby smoothly.

Introduction to Bootboot

Bootboot, developed by Shopify, is a Bundler plugin designed to facilitate dual booting in Ruby applications. After setting up Bootboot, it generates a Gemfile_next.lock file, which can contain a different set of gem versions. This can be controlled by passing the flag DEPENDENCIES_NEXT = 1 as an environment variable. For detailed information, refer to the Bootboot documentation.

Setting Up Bootboot

Prerequisites

To set up Bootboot, follow these steps:

1. Add the plugin to your Gemfile:

plugin 'bootboot', '~> 0.2.2'

2. Run the following commands to install and configure Bootboot:

bundle install && bundle bootboot

This will modify your Gemfile and create a Gemfile_next.lock. The modified Gemfile should look like this:

plugin 'bootboot', '~> 0.2.2'
Plugin.send(:load_plugin, 'bootboot') if Plugin.installed?('bootboot')

if ENV['DEPENDENCIES_NEXT'].to_i == 1 && Plugin.installed?('bootboot')
  puts 'Installing next dependencies...'
  enable_dual_booting
else
  puts 'Installing current dependencies...'
end

I added the puts statements as they help in identifying which dependencies are being installed in the build logs.

Case Study: Upgrading Rails 6 to Rails 7

After setting up Bootboot, the Gemfile was modified to support both Rails 6 and Rails 7:

if ENV['DEPENDENCIES_NEXT'].to_i == 1 && Plugin.installed?('bootboot')
  puts 'Loading Rails 7 dependencies...'
  enable_dual_booting

  gem 'rails', '~> 7.1.3'
  gem 'rackup'
  gem 'vite_rails'
else
  gem 'rails', '~> 6.1.7.7'
  gem 'net-imap', require: false
  gem 'net-pop', require: false
  gem 'net-smtp', require: false
  gem 'webpacker', '~> 5.4'
  group :development, :test do
    gem 'pry-rails' # will be deprecated for Rails 7
  end
end

We upgraded to Rails 7 and included gems specific to both Rails versions. We replaced webpacker with vite. Additionally, we removed obsolete gems for Rails 7, such as net-imapnet-popnet-smtp, and pry-rails.

Running the Application

To run the Rails server for different versions:

For Rails 6:

bundle install
bundle exec rails s
bin/webpack-dev-server
bundle exec rspec [test/files/if/needed]

For Rails 7:

DEPENDENCIES_NEXT=1 bundle install
DEPENDENCIES_NEXT=1 bundle exec rails s
DEPENDENCIES_NEXT=1 bin/vite
DEPENDENCIES_NEXT=1 bundle exec rspec [test/files/if/needed]

Set DEPENDENCIES_NEXT=1 to enable Rails 7 code. Note that vite is not available for Rails 6, causing an error if attempted.

Adding New Gems

When adding new gems, ensure compatibility with Rails 7:

if ENV['DEPENDENCIES_NEXT'].to_i == 1 && Plugin.installed?('bootboot')
  puts 'Loading Rails 7 dependencies...'
  enable_dual_booting
  gem 'example_gem', 'v3'
else
  gem 'example_gem', 'v2'
  gem 'gem_only_needed_for_rails_6'
end

Common gems should be compatible with both Rails versions. If a gem is specific to a version, include it within the conditional block.

Important Commands

To update gems in both Gemfiles:

bundle install
DEPENDENCIES_NEXT=1 bundle install

Note: If you use Dependabot, it will not update Gemfile_next.lock. Manual updates are required for that.

Writing Code and Handling Deprecations

Use the DEPENDENCIES_NEXT environment flag to manage differences between Rails versions:

def preload_association(records)
  if ENV['DEPENDENCIES_NEXT'].to_i == 1
    ::ActiveRecord::Associations::Preloader.new(records: records.flatten, associations: [@association_name]).call
  else
    ::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)
  end
end

Here, we handle deprecations in ActiveRecord’s Preloader class based on the Rails version. This approach ensures compatibility with both versions. Make sure the method’s results are consistent across Rails 6 and 7.

Updating CI for Rails 7

We also modified our CI to run tests for Rails 7 along with Rails 6 tests to ensure compatibility and make sure changes in Rails 6 do not break Rails 7.

Build and Release

When deploying, ensure the DEPENDENCIES_NEXT environment flag is set during build time and runtime. This ensures that the correct dependencies are loaded based on the Rails version, which is crucial for a successful deployment and depends on your deployment strategy.

Additional Tips

Middleware Configurations

Use Bootboot to configure middlewares differently based on version.

Rails Application Default Configs

Load default configs as needed. We fixed an issue with remote forms this way.

Frontend Changes

Since we moved from webpacker to vite, we adjusted file setup based on the environment variable so that Rails views and assets work correctly.

Case Study: Upgrading Ruby 3.2.2 to Ruby 3.3.3

Similar to the Rails upgrade, we upgraded Ruby:

if ENV['DEPENDENCIES_NEXT'].to_i == 1 && Plugin.installed?('bootboot')
  puts 'Installing next dependencies...'
  enable_dual_booting

  ruby '3.3.3'
else
  puts 'Installing current dependencies...'
  ruby '3.2.2'
end

Bootboot has limitations with the .ruby-version file. Because most of our pipelines use .ruby-version, we had to create the file manually during build and testing based on the flag.

Conclusion

The Bootboot plugin is a powerful tool for managing multiple versions of gems within a single codebase. Its dual-booting capability allows for smooth upgrades and thorough testing without disrupting production. Our team successfully used Bootboot to upgrade Rails and Ruby, maintaining stability and addressing deprecations effectively. It also helped us reduce review times for PRs as the changes were incremental and well-tested.

I hope this guide helps you navigate similar upgrades not only Rails and Ruby but also other dependencies in your projects. Thank you and happy coding! 🚀

Published-date