It's more than once that I had to deal with a Gemfile
in which dependencies are a long and unsorted list with a lot of them without a version at all:
gem 'puma'
gem 'sinatra'
# ...
# a lot of other dependencies here
# ...
gem 'rake'
Sometimes I have to upgrade a dependency or maybe I just want to have a quick picture of the dependencies distribution my project is relying on.
To do that, I open the Gemfile.lock
and then scan for the specific version of the gem.
This is a time consuming task and:
I'd much rather prefer to have a Gemfile with a sorted list of gems, each of them providing a "sane" version requirement.
By "sane" I mean a requirement that is:
- not too loose (eg. no version at all or something like
> 2
) - not too strict (eg.
= 3.5.1
)
RubyGems suggests to use the pessimistic version constraint which is based on the fact that SemVer is a strongly encouraged practice we all should follow when authoring ruby gems.
Assuming that one of our dependencies is currently set to version 2.3
, I'd be tempted to set ~> 2
as the version requirement because it is based on the assumption that, if the gem author follow the SemVer specification, there shouldn't be any breaking changes until version 3.
Since we don't live in an ideal world, it's often better to be a little more conservative and let only the patch version upgrade freely every time we perform a bundle update
. This leads us to prefer ~> 2.3
instead.
Back to my needs, wouldn't it be awesome to have a little tool to help me update my Gemfile
suggesting the sane version requirement for each dependency?
A couple of hours later, after digging through DuckDuckGo, StackOverflow and the RubyDocs, here's the hackish script I came up with:
#!/usr/bin/env ruby
require 'bundler'
parser = Bundler::LockfileParser.new(Bundler.read_file(Bundler.default_lockfile))
definition = Bundler.definition
groups = {}
gems = definition.current_dependencies.each_with_object({}) do |current, memo|
memo[current.name] = {
groups: current.groups,
}
current.groups.each do |group|
groups[group] ||= []
groups[group] << current.name
end
memo
end
gem_names = gems.keys.sort
parser.specs.select do |spec|
gem_names.include?(spec.name)
end.each do |spec|
gems[spec.name][:suggested_version] = spec.version.approximate_recommendation
end
groups.each do |group_name, group_gems|
puts "group :#{group_name} do" unless group_name == :default
group_gems.each do |gem|
print "\t" unless group_name == :default
puts "gem \"#{gem}\", \"#{gems[gem][:suggested_version]}\""
end
puts "end" unless group_name == :default
end
If you call it little-gemfile-helper
and:
chmod u+x little-gemfile-helper
If you move into a project containing a Gemfile.lock
and a Gemfile
such as:
source 'https://rubygems.org'
gem 'puma'
gem 'sinatra'
gem 'rake'
group :test do
gem 'rack-test'
end
and launch our little helper:
./little-gemfile-helper
We would get displayed a useful reference to quickly update our Gemfile
:
gem "puma", "~> 3.9"
gem "sinatra", "~> 1.4"
group :test do
gem "rack-test", "~> 0.6"
end
Happy versioning! 🤓
Top comments (1)
A friend of mine pointed out that there's already a gem doing what I need and much more: pessimize.
It's good to know that the same need is shared by other people. 🙂
It's also interesting to understand how the gem achieves its goals.
Instead of relying on Bundler's parsing features, it defines a parser on its own.
Also, by overwriting the Gemfile, it follows a path that I didn't wanted to follow because for me it's more than enough to have a quick reference to update the Gemfile, manually handling corner cases.