DEV Community

Vladimir Dementyev
Vladimir Dementyev

Posted on

Faster RuboCop runs for Rails apps

Recently, Nate Berkopec shared an interesting observation: running bundle exec whatever could take seconds to boot if the Gemfile is huge (even when the executable itself requires a handful of dependencies).

That could be explained by the fact that Bundler has to verify the Gemfile.lock file consistency (all the gems are installed). Thus, that's an expected behaviour (that doesn't mean we shouldn't try to improve it; see, for example, Matthew Draper's Gel).

Rails developers usually put all the deps in the Gemfile, including dev tools, such as RuboCop. RuboCop is a linter, and linters must be fast. RuboCop itself complies with this statement but running it via Bundler may not.

How can we overcome this? Using a separate Gemfile!

I've been using this technique for a long time for gems development—to speed up CI RuboCop runs (by installing only the linter dependencies). Here is my typical rubocop.gemfile:

# gemfiles/rubocop.gemfile
source "https://rubygems.org" do
  gem "rubocop-md", "~> 1.0"
  gem "rubocop-rspec"
  gem "standard", "~> 1.0"
end
Enter fullscreen mode Exit fullscreen mode

To use it with Bundler, we need to specify the BUNDLER_GEMFILE env variable:

# first, install the deps
BUNDLE_GEMFILE=gemfiles/rubocop.gemfile bundle install

# then, run the executable
BUNDLE_GEMFILE=gemfiles/rubocop.gemfile bundle exec rubocop
Enter fullscreen mode Exit fullscreen mode

This verbose approach works well enough for machines (CI), but not for humans: maintaining a separate lockfile and using env vars in development is far from the perfect user experience.

For Rails applications development, we came up with the following trick to run commands backed by custom gemfiles—adding a simple bin/whatever wrapper. Here is our bin/rubocop:

#!/bin/bash

cd $(dirname $0)/..

export BUNDLE_GEMFILE=./gemfiles/rubocop.gemfile
bundle check > /dev/null || bundle install

bundle exec rubocop $@
Enter fullscreen mode Exit fullscreen mode

The magic $@ argument proxies everything you pass to bin/rubocop, thus, making this wrapper quack like RuboCop.

We also do bundle check || bundle install to make sure all the deps are present (so, you don't need to run bundle install yourself).

That's it.

P.S. Why not use inline gemfiles (as Xavier Noria suggested)? We could write our bin/rubocop like this:

require 'bundler/inline'

gemfile(true, quiet: true) do
  gem "rubocop-md", "~> 1.0"
  gem "rubocop-rspec"
  gem "standard", "~> 1.0"
end

require 'rubocop'
RuboCop::CLI.new.run
Enter fullscreen mode Exit fullscreen mode

However, with this approach, there is no lockfile at all. We want to make sure everyone is using the same versions of dependencies (to avoid "works on my computer" situations). Of course, we can use the exact version in the gemfile do ... end block, but, IMO, managing deps with Bundler is more convenient (e.g., you can run bundle update).

P.P.S. One of the benefits of this approach is the ability to run linters (and other tools, e.g., Kuby) locally while using Docker for application development; no need to spin up containers to run RuboCop. It's especially helpful if you want to use Git hooks or editor integrations.

Discussion (0)