Bundler and Gemfile best practices
Do you remember the time before Bundler existed? That was fun wasn’t it? Bundler was released in 2010 and it’s hard to overstate how important it has become for the Ruby ecosystem. Everyone is using it for their Ruby apps and their Ruby open source libraries.
But are you actually specifying your Gemfile correctly? Investing a little bit of time and effort will decrease the likelihood of ending up with a outdated web of dependencies where no one knows why you’re using all these gems in the first place.
Here are a few recommendations:
Gemfile.lock is for apps, not libraries
This seems commonly known by now: If you’re working on a application, like a Rails app, you want to have the exact same dependencies on your developer machine, the CI server and the production environment. That’s what the Gemfile.lock is for. After updating your Gemfile, always run bundle install. And after bundling, always check your Gemfile.lock into version control.
If you writing a library you want it to work with a wide range of dependency versions. As that allows users of your library to install it without getting conflicts. So a Gemfile.lock is the opposite of what you want. Put the Gemfile.lock into your .gitignore to make sure you don’t push it.
Only specify top-level dependencies
Most gems that you declare in your Gemfile have dependencies themselves. Bundler automatically resolves and installs them so there is no need for you to declare them yourself. You’ll make Bundlers life much harder once you start declaring sub-dependencies, especially when you specify a version. You’ll very likely run into more and more Bundler::DependencyErrors and it will become much harder to upgrade dependencies at all. Bundler is very good at finding a set of versions that fit all the requirements your dependencies specify, there is no need for you to do Bundlers job yourself.
Once you start using one of these already installed sub-dependencies directly in your own code, you should add it to your Gemfile. This makes it explicit you’re depending on that gem and everything will continue to work even if the top-level dependency stops using it.
Use Gemfile groups
A modern Rails app has a lot of dependencies, but quite a few of them are only relevant to speed up the development process or provide tools for debugging and testing. None of them are needed when running the app on production. Put all those dependencies in a development group and make sure your deployment script runs bundler in the right way for production (Capistrano and Heroku do the right thing by default).
But don’t overdo it with how many groups you declare. Special groups for development and test make sense (even though you could also argue for merging them together). But more groups than that rarely help. The main goal is to minimize the amount of code that needs to be loaded in the production environment. The more groups you have the more you have to think which group a gem belongs to and the harder it is to understand which gem gets loaded when.
What’s valid for your code is also valid for your Gemfile: keep the formatting and syntax consistent. For example:
- Prefer blocks over single line group definitions:
- Use proper indentation so that groups are easy to identify
- Use single quotes as there is no need for string substitutions
- Keep your whitespace in check
Are you the type of person that sorts their bookshelf alphabetically, by genre or by color? Then sorting the gems in your Gemfile might be nice as well. You can use logical groups like “3rd party APIs”, “data access” and “logging/performance” or anything else you can think of. It makes it easier for other developers to see what a gem is used for and if they add a gem they know where to put it.
Resist the urge to Ruby
The Gemfile is a Ruby DSL, which means you can use all language features of Ruby. But just because you can, doesn’t mean you should. The Gemfile declares your dependencies, let’s keep it simple and easy to reason about. Just because this
seems a bit repetitive doesn’t mean there is a need to “DRY” it up like this
Yes it’s a single line now, but it falls down as soon as you need to specify versions, set require paths or when you want to move the gems into groups. It also makes it harder to get an overview which gems are declared as you loose the uniformity. I think NPM got this right when they made their manifest file pure JSON.
Minimize git dependencies
Rubygems allows you to depend on a Rubygem directly via Git (and Github). It’s handy for when you have to fork a gem and don’t want to release a real Rubygem or if you need an unreleased feature or bugfix from the official repository. Since this is so easy it’s very tempting to do it all the time, but specifying dependencies as git urls is problematic for a number of reasons:
First, it can become a maintenance nightmare, as your fork slowly becomes out of date or merging in the upstream changes becomes much more work than creating an orderly pull request to the original project. Second, you’re effectively switching from using easy to understand and follow version numbers to git commit hashes. So updating a git dependency usually involves comparing the lastest version on master or a branch against the hash you have in your bundle. (
bundle show shows you git hashes for git dependencies) instead of just glancing at version numbers. Instead of semantic versioning, this is more like cryptographic versioning.
And also, of course you can show that you’re a good open source citizen by taking that bug that made you fork and use the git url, write some regresssion tests for it and make that pull request. People are always quite happy when that happens.
Do you really need that gem?
Adding a new dependency to your project should be a very deliberate choice. It comes with a cost and not always is the benefit of not having to write that code yourself big enough to justify introducing a dependency.
Every dependency in your application has the potential to bloat your app, to destabilize your app, to inject odd behavior via monkeypatching or buggy native code.
There is much more to this topic than we can cover in a paragraph here, luckily Mike Perham already wrote a great post about it: Kill Your Dependencies.
Support Ruby Together
A lot of companies depend on Bundler and RubyGems — would your app run without it? Yet very few realize most of the work to support this critical Ruby infrastructure has been largely done by volunteers. Ruby Together is a grassroots initiative that is trying to make this work sustainable. Membership dues directly fund work that benefits everyone using Ruby and are usually tax-deductible. Let’s improve Ruby, together!
It’s very easy for your Gemfile to accumulate more and more cruft. Especially in a team with multiple developers. You can end up in a web of outdated dependencies, old gems that are actually not needed anymore and Rails processes taking 100s of megabytes of RAM. It only takes a little bit of effort to improve things. Taking care of your Gemfile with basic hygiene and some ongoing love will make life for future you much easier.