Embracing Promiscuous Gemfiles

Bundler has been around for quite some time now, but I continue to see what I consider to be be bad advice with regards to specifying version constraints in your project Gemfile. My position is simple: Do not constrain gem versions in your Gemfile until you have a good reason to do so. I typically encounter two arguments against this position. The first is misinformed, showing a critical lack of understanding of how bundler works. The second is a defensible position, but seems to offer no advantage over my approach.

Specific Versions For Everything!

Developers in the first camp routinely lock gems down to specific versions ('3.1.0') They argue that this ensures all developers and all deployment environments run the same version of the project’s dependencies. While this is true for direct dependencies listed in the Gemfile, it is not true for the dependencies of those dependencies. This position shows a fundamental misunderstanding of how bundler works. The complete versioning picture is maintained by bundler in Gemfile.lock, which is generated when bundle install is run for the first time. Subsequent runs of bundle install install only the exact versions listed in Gemfile.lock. This is why it’s critical that Gemfile.lock be checked into source control. It’s Gemfile.lock – and not Gemfile – that provides complete consistency across deployments.

This is important: the Gemfile.lock makes your application a single package of both your own code and the third-party code it ran the last time you know for sure that everything worked. Specifying exact versions of the third-party code you depend on in your Gemfile would not provide the same guarantee, because gems usually declare a range of versions for their dependencies.

Pessimistic Version Constraints for All!

Some developers lean on the use of the pessimistic version constraint (~>). In fact, the rubygems web interface encourages this explicitly on every gem page. In the absence of an existing version that breaks compatibility I find this unnecessary as well.

The pessimistic constraint is typically used to lock a gem to a specific major.minor version while allowing the patch level to increase. For instance, '~> 3.1.0' would lock the gem to version 3.1.x. The effectiveness of this approach depends on gem authors strictly following a rational or semantic versioning policy. Even well-meaning gem maintainers accidentally introduce breaking changes or performance regressions in patch number releases. A patch level release may also take new versions (major, minor, or patch level) of its dependencies which may cause issues with a different direct dependency in your gemfile.

Regardless of the constraints used in the Gemfile, the developer must still exercise caution when updating the bundle. It’s unclear to me what advantage using pessimistic version constraints by default offers.

Promiscuous Gemfiles

I opt to leave my gems unconstrained by default. Updating gems in the application bundle is done with bundle update <gemname> and will change Gemfile.lock. This is a conscious operation and is undertaken with care. If the project’s test suite or QA plan fail then it’s appropriate to add a version constraint to the offending gem or update the application to remedy the incompatibility.

I will leverage a pessimistic version constraint when, for instance, maintaining a rails 2.3.x project. The '~> 2.3.14' constraint would allow the project to update to newer security fixes while not introducing changes I know to be breaking from the 3.x releases. I will use an exact version constraint if, for example, a gem introduces breaking changes in a patch number release. The result is a less noisy Gemfile where all of the version constraints are immediately obvious and usually documented with appropriate comments.

The introduction of bundler 1.1 (currently at release candidate 3), adds the command bundle outdated which is a boon to this approach. bundle outdated returns a list of gems that would be updated if bundle update were run with no arguments. This allows me to easily see which gems have been updated and investigate the changes before running bundle update. It’s important to note that bundle outdated will not show you versions that don’t meet the constraints specified in your gemfile. If you have a gem specified with the constraint '~>2.0.0' and the maintainer releases version 2.1 bundle outdated won’t alert you to this. When paired with an unconstrained Gemfile, bundle outdated can help developers keep up with changes to dependencies that might prove useful in their project.

Conclusion

Unnecessary constraints make updating gems a hassle, particularly as your project ages or new versions of your dependencies and their dependencies are released. If you alter a constraint to pick up a new version of a direct dependency, you may find that bundle update fails until you alter additional constraints. This hoop jumping can be avoided by removing all but the strictly necessary constraints and employing bundle update with care.