Caching compiled Rubies with RVM

NOTE This guide is superseded by the documentation here…

We get requests to install different Ruby versions into our images. It takes some time to update, test, and roll out images into our build fleet, and our current Ubuntu 12 image has 81 preinstalled Ruby versions, in response to user requests.

If we don’t have a pre-installed ruby version you want, there’s an easy way to make this quickly available for your builds. Here’s an example of compiling Ruby 2.3.3 and caching it.

machine:
  post:
    - "echo Removing project .rvmrc, contents were: &&
        cat $CIRCLE_PROJECT_REPONAME/.rvmrc"
    - "rm $CIRCLE_PROJECT_REPONAME/.rvmrc"

    - "echo Removing project .ruby-version, contents were: &&
      cat $CIRCLE_PROJECT_REPONAME/.ruby-version"
    - "rm $CIRCLE_PROJECT_REPONAME/.ruby-version"

    - |
      if [[ -e ~/rvm_binaries/ruby-2.3.3.tar.bz2 ]]
      then
        rvm mount ~/rvm_binaries/ruby-2.3.3.tar.bz2
      else
        mkdir -p ~/rvm_binaries
        rvm install 2.3.3
        cd ~/rvm_binaries && rvm prepare 2.3.3
      fi
    - rvm --default use 2.3.3

dependencies:
  cache_directories:
    - ~/rvm_binaries

test:
  override:
    - gem list
    - ruby --version

Ruby installs could be ~30 seconds or 4-5 minutes (as is the case with ruby 2.3.3), depending on whether RVM has a pre-compiled binary available for your platform (Ubuntu 12/14, x86_64). Once Ruby is compiled, rvm prepare makes a .tar.bz2 file that can be loaded by rvm mount.

A build with the above circle.yml will take 4-5 minutes to install Ruby the first time, and about 30 seconds after that.

This configuration also ensures that any .rvmrc and .ruby-version files are removed from the project before the dependencies phase.

Credit goes to this blog post for researching the nuances of RVM subcommands.

Edit: cleaned up circle.yml file to only run commands if they’re necessary

1 Like

Thanks! Will give this a try.

Could it also work to simply add ~/.rvm to cache_directories?

Unfortunately, cache restore happens in the dependencies phase. If CircleCI spots a ruby-version file, rvmrc file, or a ruby version section in Circle.yml, it will attempt to install that version in the machine phase. Also, rvm needs either mount or install to be aware of a Ruby version, AFAIK.

Here is a much better version that reads the ruby version from .ruby-version.

machine:
  post:
    - "mv $CIRCLE_PROJECT_REPONAME/.ruby-version $CIRCLE_PROJECT_REPONAME/.ruby-version.ci"

dependencies:
  override:
    - |
      RUBY_VERSION=$(cat .ruby-version.ci)
      if [[ -e ~/rvm_binaries/ruby-$RUBY_VERSION.tar.bz2 ]]
      then
        rvm mount ~/rvm_binaries/ruby-$RUBY_VERSION.tar.bz2
      else
        mkdir ~/rvm_binaries || true
        rvm install $RUBY_VERSION
        gem install bundler
        cd ~/rvm_binaries && rvm prepare $RUBY_VERSION
        ls ~/rvm_binaries
      fi
1 Like

hey so this is cool! I am curious though, it look like this is just causing my ruby build to be triggered twice…? It happens once when the command is loaded into the container, and then again once it hits the if statement. Is this intended? I have removed the .ruby_versionm we don’t have a .rvmrc file, and I don’t declare a ruby version when setting up the machine portion of the yml file. I am confused why CircleCI is trying to build ruby 2.3.3 before that if statement is ever run?

udpate on this: if you run the command as a post command on the machine portion of your yml file, this works a lot better

I’m not sure why you saw that behavior–I don’t have enough info to debug it without seeing the build. But glad you found a solution that works for you!

@Dorian for some reason your snippet caused problems with the rake commands in the database building phase. Also I don’t think we need to rename ruby-version, so I simplified things a bit:

machine:
  post:
    - |
      RUBY_VERSION=$(cat $CIRCLE_PROJECT_REPONAME/.ruby-version)
      if [[ -e ~/rvm_binaries/ruby-$RUBY_VERSION.tar.bz2 ]]
      then
        rvm mount ~/rvm_binaries/ruby-$RUBY_VERSION.tar.bz2
      else
        mkdir -p ~/rvm_binaries
        rvm install $RUBY_VERSION
        cd ~/rvm_binaries && rvm prepare $RUBY_VERSION
      fi
    - rvm --default use $RUBY_VERSION

dependencies:
  pre:
    - gem install bundler --pre
  cache_directories:
    - ~/rvm_binaries

test:
  override:
    - gem list
    - ruby --version