bundler-resolutions
bundler-resolutions is a bundler
plugin that allows you to specify gem version requirements for your Gemfile
without explicitly declaring
a concrete dependency on those gems. It acts much like the
resolutions feature in
Yarn.
Warning
This is an experimental project and neither its API stability nor correctness should be assumed
Usage
Add bundler-resolutions
to your Gemfile, and add a .bundler-resolutions.yml
file to
specify the gems you want to specify versions requirements for.
Example 1
In this example the resulting Gemfile.lock
will have nokogiri locked to 1.16.5
or above, but
nokogiri will not be present in the DEPENDENCIES
section of the lock file. Also, if rails
were
to change to a version that did not depend on nokogiri, then the resolution would not be used or
appear in the lock file at all.
.bundler-resolutions.yml
:
gems:
nokogiri: ">= 1.16.5" # CVE-2024-34459
Gemfile
:
gem 'bundler-resolutions'
gem "rails"
Example 2
Here, the Gemfile.lock
from this example will not have nokogiri at all, as it is neither
explicitly declared in the Gemfile, nor brought in as a transitive dependency.
.bundler-resolutions.yml
:
gems:
nokogiri: ">= 1.16.5" # CVE-2024-34459
gem 'bundler-resolutions'
gem "thor"
Config file
The config file is a YAML file with a gems
key that contains a mapping of gem names to version
requirements. The version requirements are the same as those used in the Gemfile
.
Example:
gems:
nokogiri: ">= 1.16.5" # CVE-2024-34459
thor: ">= 1.0.1, < 2.0"
By default, bundler-resolutions
will look for a file named .bundler-resolutions.yml
in the
current directory, or the parent, and continue looking up to the root dir.
You can also specify a file location by setting the BUNDLER_RESOLUTIONS_CONFIG
ENV var.
Detail
bundler-resolutions
allows you to specify version requirements in a config file
to indicate that you have version requirements for those gems if they were to be brought
in as transitive dependencies, but that you don't depend on them yourself directly.
The big difference between doing this and just declaring it in your Gemfile is that it will only be used in resolutions (and be written to your lock file) if the gems you do directly depend on continue to use it. If they stop using it, then your resolutions will take no part in the bundler lock resolution.
The other difference is that even if it does take part in the resolutions, it will not be
present in the DEPENDENCIES
section of the lock file, as it is not a direct dependency.
Use cases
There are a number of reasons you may want to prevent the usage of some gem versions, but without declaring their direct use in Gemfiles. Also there are reasons to set versions across a monorepo of many Gemfiles, but where not all apps use all blessed versions, such as:
- You have learnt of a CVE of a gem.
- You have internal processes that mandate the usage of certain gem versions for legal or sign off reasons.
- You wish to take a paranoid approach to updating certain high value target gems. eg
devise
. - You want certain gem collections to move in lockstep. eg
sinatra
,rack
andrack-protection
, which are relatively tightly coupled. - You know of gems that are very slow to install and you have preinstalled them in internal base images. eg
rugged
orsorbet
. - You know of gems that are tightly coupled to ruby itself that shouldn't be upgraded. eg
stringio
andpsych
. - You know of gem incompatibilities with your codebase in their later versions.
- You know that different OS architectures do not work with some versions.
- You wish to prevent unintentional downgrades of dependencies when using
bundle
commands.
How it works
bundler-resolutions
works by patching the bundler
Resolver
filtered_versions_for
method to
allow for the resolution restrictions from the versions specified in the config file.
This is a very early version, and it should be considered experimental.
Future work may include relating this to bundler-audit, and other security tools, so you will automatically gain version restrictions against known CVEs.