ERROR: While executing gem … (Gem::DependencyResolutionError) in Ruby 3.1


Gem::DependencyResolutionError trigger from gem install

While trying to gem install on locally built chef.gem and its dependencies chef-utils.gem and chef-config.gem, I was seeing the following error:

Running `gem install /Users/powell/projects/chef/pkg/chef-18.0.160.gem` failed with the following output:

ERROR:  While executing gem ... (Gem::DependencyResolutionError)
    conflicting dependencies chef-utils (< 19, >= 16.0) and chef-utils (= 18.0.160)
  Activated chef-utils-18.0.160
  which does not match conflicting dependency (< 19, >= 16.0)

  Conflicting dependency chains:
    chef (= 18.0.160), 18.0.160 activated, depends on
    chef-utils (= 18.0.160), 18.0.160 activated

  versus:
    chef (= 18.0.160), 18.0.160 activated, depends on
    ohai (~> 18.0), 18.0.14 activated, depends on
    chef-utils (< 19, >= 16.0)

  Gems matching chef-utils (< 19, >= 16.0):
    chef-utils-18.0.160

Note that the “conflict” is (<19, >= 16.0) vs. (= 18.0.160). That shouldn’t be a problem, right? 18 is definitely less than 19 and greater than or equal to 16!

Ok, so let’s find the source of the error… I’m using rbenv with ruby 3.1.2 installed, so the rubygems root is at ~/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems so we’ll search for a DependencyResolutionError from there. (Using the_silver_searcher for searching or apt install silversearcher-ag on Ubuntu)

Gem::DependencyResolutionError search using ag
ag DependencyResolutionError search results

Tracing symptoms

Tracing down several layers, we get to requirement.rb and Gem::Requirement#satisfied_by? From there, I added a tap block and output some code on the result from requirements.all?

  ##
  # True if +version+ satisfies this Requirement.

  def satisfied_by?(version)
    raise ArgumentError, "Need a Gem::Version: #{version.inspect}" unless
      Gem::Version === version
    requirements.all? {|op, rv| OPS[op].call version, rv }.tap do |result|
      # tap block for debugging requirements
      unless result 
        puts "Version #{version}" # what version are we trying
        puts caller[0..10] # how are we getting here??
        # what are our requirements
        requirements.each do |op, rv|
          puts "#{op} #{rv} #{OPS[op].call version, rv}"
        end
      end
    end
  end

Now we go back and rake install 2>&1 | gvim - to see the output of the debugging. Most notable is the versions being validated

 Version 17.10.0
<internal:kernel>:90:in `tap'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/requirement.rb:244:in `satisfied_by?'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/dependency.rb:238:in `match?'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/api_set.rb:57:in `block in find_all'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/api_set.rb:56:in `each'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/api_set.rb:56:in `find_all'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/composed_set.rb:54:in `block in find_all'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/composed_set.rb:53:in `map'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/composed_set.rb:53:in `find_all'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/best_set.rb:30:in `find_all'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/installer_set.rb:175:in `find_all'
= 18.0.162 false
Version 17.10.0
<internal:kernel>:90:in `tap'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/requirement.rb:244:in `satisfied_by?'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/dependency.rb:238:in `match?'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/api_set.rb:57:in `block in find_all'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/api_set.rb:56:in `each'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/api_set.rb:56:in `find_all'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/composed_set.rb:54:in `block in find_all'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/composed_set.rb:53:in `map'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/composed_set.rb:53:in `find_all'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/best_set.rb:30:in `find_all'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/resolver/installer_set.rb:175:in `find_all'
= 18.0.162 false
Version 16.18.0
<internal:kernel>:90:in `tap'
/home/thomas/.rbenv/versions/3.1.2/lib/ruby/3.1.0/rubygems/requirement.rb:244:in `satisfied_by?'

We are validating two sets of conditions: <19, >=16.0 and = 18.0.162. It is the latter condition that is tripping us up. The versions being used to fulfill the dependencies for gem install that are in the gemspec are remote versions and chef-utils stops at 17.10.0 on rubygems

chef-utils 17.10.0
chef-utils on rubygems

Since our gemspec is looking for an exact version match on a version that’s not published, we can’t use gem install yet… in this case, the version is pinned to use the same version as the dependency.

  s.add_dependency "chef-config", "= #{Chef::VERSION}"
  s.add_dependency "chef-utils", "= #{Chef::VERSION}"

Conclusion / Workarounds / Solutions

The Gem::DependencyResolutionError is a bit misleading in this case, but looking through the resolution mechanism, there’s not an easy path to determine that the resolution failed due to the exact version match not existing. This will happen on any gem install in which the gem you’re installing is looking for versions that don’t match dependencies that are published (whether RubyGems or internal gem server.) You don’t have to be looking for an exact match. If you’re looking for >= 18 and only 17 and earlier is published, you’ll still have a problem.

Clearly, if you’re juggling multiple “internal” gems at the same time, your development process will likely run into this if you try to gem install the gem. How do you avoid it? bundle install doesn’t have this limitation, as you can look for candidate gems locally and/or specify local repos. Install the dependencies first and then bundle install on the parent project. Once you’re preparing to launch the gem, you can publish prerelease versions of the dependencies and pin to those.


Leave a Reply

%d bloggers like this: