Skip to content
This repository has been archived by the owner on Apr 14, 2021. It is now read-only.

Make version conflict messages better #6647

Merged
merged 5 commits into from
Aug 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/bundler/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -855,8 +855,8 @@ def metadata_dependencies
concat_ruby_version_requirements(locked_ruby_version_object) unless @unlock[:ruby]
end
[
Dependency.new("ruby\0", ruby_versions),
Dependency.new("rubygems\0", Gem::VERSION),
Dependency.new("Ruby\0", ruby_versions),
Dependency.new("RubyGems\0", Gem::VERSION),
]
end
end
Expand Down
38 changes: 31 additions & 7 deletions lib/bundler/resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -302,21 +302,32 @@ def formatted_versions_with_platforms(versions_with_platforms)
end

def version_conflict_message(e)
# only show essential conflicts, if possible
conflicts = e.conflicts.dup
conflicts.delete_if do |_name, conflict|
deps = conflict.requirement_trees.map(&:last).flatten(1)
!Bundler::VersionRanges.empty?(*Bundler::VersionRanges.for_many(deps.map(&:requirement)))
end
e = Molinillo::VersionConflict.new(conflicts, e.specification_provider) unless conflicts.empty?

solver_name = "Bundler"
possibility_type = "gem"
e.message_with_trees(
:solver_name => "Bundler",
:possibility_type => "gem",
:solver_name => solver_name,
:possibility_type => possibility_type,
:reduce_trees => lambda do |trees|
# called first, because we want to reduce the amount of work required to find maximal empty sets
trees.uniq! {|t| t.flatten.map {|dep| [dep.name, dep.requirement] } }

# bail out if tree size is too big for Array#combination to make any sense
return trees if trees.size > 15
maximal = 1.upto(trees.size).map do |size|
trees.map(&:last).flatten(1).combination(size).to_a
end.flatten(1).select do |deps|
Bundler::VersionRanges.empty?(*Bundler::VersionRanges.for_many(deps.map(&:requirement)))
end.min_by(&:size)
trees.reject! {|t| !maximal.include?(t.last) } if maximal

trees = trees.sort_by {|t| t.flatten.map(&:to_s) }
trees.uniq! {|t| t.flatten.map {|dep| [dep.name, dep.requirement] } }
trees.reject! {|t| !maximal.include?(t.last) } if maximal

trees.sort_by {|t| t.reverse.map(&:name) }
end,
Expand Down Expand Up @@ -351,7 +362,11 @@ def version_conflict_message(e)
[]
end.compact.map(&:to_s).uniq.sort

o << "Could not find gem '#{SharedHelpers.pretty_dependency(conflict.requirement)}'"
metadata_requirement = name.end_with?("\0")

o << "Could not find gem '" unless metadata_requirement
o << SharedHelpers.pretty_dependency(conflict.requirement)
o << "'" unless metadata_requirement
if conflict.requirement_trees.first.size > 1
o << ", which is required by "
o << "gem '#{SharedHelpers.pretty_dependency(conflict.requirement_trees.first[-2])}',"
Expand All @@ -360,12 +375,21 @@ def version_conflict_message(e)

o << if relevant_sources.empty?
"in any of the sources.\n"
elsif metadata_requirement
"is not available in #{relevant_sources.join(" or ")}"
else
"in any of the relevant sources:\n #{relevant_sources * "\n "}\n"
end
end
end,
:version_for_spec => lambda {|spec| spec.version }
:version_for_spec => lambda {|spec| spec.version },
:incompatible_version_message_for_conflict => lambda do |name, _conflict|
if name.end_with?("\0")
%(#{solver_name} found conflicting requirements for the #{name} version:)
else
%(#{solver_name} could not find compatible versions for #{possibility_type} "#{name}":)
end
end
)
end

Expand Down
4 changes: 2 additions & 2 deletions lib/bundler/resolver/spec_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,10 @@ def metadata_dependencies(spec, platform)
return [] if !spec.is_a?(EndpointSpecification) && !spec.is_a?(Gem::Specification)
dependencies = []
if !spec.required_ruby_version.nil? && !spec.required_ruby_version.none?
dependencies << DepProxy.new(Gem::Dependency.new("ruby\0", spec.required_ruby_version), platform)
dependencies << DepProxy.new(Gem::Dependency.new("Ruby\0", spec.required_ruby_version), platform)
end
if !spec.required_rubygems_version.nil? && !spec.required_rubygems_version.none?
dependencies << DepProxy.new(Gem::Dependency.new("rubygems\0", spec.required_rubygems_version), platform)
dependencies << DepProxy.new(Gem::Dependency.new("RubyGems\0", spec.required_rubygems_version), platform)
end
dependencies
end
Expand Down
6 changes: 4 additions & 2 deletions lib/bundler/source/metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ class Source
class Metadata < Source
def specs
@specs ||= Index.build do |idx|
idx << Gem::Specification.new("ruby\0", RubyVersion.system.to_gem_version_with_patchlevel)
idx << Gem::Specification.new("rubygems\0", Gem::VERSION)
idx << Gem::Specification.new("Ruby\0", RubyVersion.system.to_gem_version_with_patchlevel)
idx << Gem::Specification.new("RubyGems\0", Gem::VERSION) do |s|
s.required_rubygems_version = Gem::Requirement.default
end

idx << Gem::Specification.new do |s|
s.name = "bundler"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,21 @@ def hash
# dependency graph?
# @return true iff there is a path following edges within this {#graph}
def path_to?(other)
equal?(other) || successors.any? { |v| v.path_to?(other) }
_path_to?(other)
end

alias descendent? path_to?

# @param [Vertex] other the vertex to check if there's a path to
# @param [Set<Vertex>] visited the vertices of {#graph} that have been visited
# @return [Boolean] whether there is a path to `other` from `self`
def _path_to?(other, visited = Set.new)
return false unless visited.add?(self)
return true if equal?(other)
successors.any? { |v| v._path_to?(other, visited) }
end
protected :_path_to?

# Is there a path from `other` to `self` following edges in the
# dependency graph?
# @return true iff there is a path following edges within this {#graph}
Expand Down
9 changes: 7 additions & 2 deletions lib/bundler/vendor/molinillo/lib/molinillo/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class NoSuchDependencyError < ResolverError
# @param [Array<Object>] required_by @see {#required_by}
def initialize(dependency, required_by = [])
@dependency = dependency
@required_by = required_by
@required_by = required_by.uniq
super()
end

Expand Down Expand Up @@ -101,9 +101,14 @@ def message_with_trees(opts = {})
printable_requirement = opts.delete(:printable_requirement) { proc { |req| req.to_s } }
additional_message_for_conflict = opts.delete(:additional_message_for_conflict) { proc {} }
version_for_spec = opts.delete(:version_for_spec) { proc(&:to_s) }
incompatible_version_message_for_conflict = opts.delete(:incompatible_version_message_for_conflict) do
proc do |name, _conflict|
%(#{solver_name} could not find compatible versions for #{possibility_type} "#{name}":)
end
end

conflicts.sort.reduce(''.dup) do |o, (name, conflict)|
o << %(\n#{solver_name} could not find compatible versions for #{possibility_type} "#{name}":\n)
o << "\n" << incompatible_version_message_for_conflict.call(name, conflict) << "\n"
if conflict.locked_requirement
o << %( In snapshot (#{name_for_locking_dependency_source}):\n)
o << %( #{printable_requirement.call(conflict.locked_requirement)}\n)
Expand Down
2 changes: 1 addition & 1 deletion lib/bundler/vendor/molinillo/lib/molinillo/gem_metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

module Bundler::Molinillo
# The version of Bundler::Molinillo.
VERSION = '0.6.5'.freeze
VERSION = '0.6.6'.freeze
end
56 changes: 51 additions & 5 deletions lib/bundler/version_ranges.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,42 @@ module VersionRanges
NEq = Struct.new(:version)
ReqR = Struct.new(:left, :right)
class ReqR
Endpoint = Struct.new(:version, :inclusive)
Endpoint = Struct.new(:version, :inclusive) do
def <=>(other)
if version.equal?(INFINITY)
return 0 if other.version.equal?(INFINITY)
return 1
elsif other.version.equal?(INFINITY)
return -1
end

comp = version <=> other.version
return comp unless comp.zero?

if inclusive && !other.inclusive
1
elsif !inclusive && other.inclusive
-1
else
0
end
end
end

def to_s
"#{left.inclusive ? "[" : "("}#{left.version}, #{right.version}#{right.inclusive ? "]" : ")"}"
end
INFINITY = Object.new.freeze
INFINITY = begin
inf = Object.new
def inf.to_s
"∞"
end
def inf.<=>(other)
return 0 if other.equal?(self)
1
end
inf.freeze
end
ZERO = Gem::Version.new("0.a")

def cover?(v)
Expand All @@ -32,6 +63,15 @@ def single?
left.version == right.version
end

def <=>(other)
return -1 if other.equal?(INFINITY)

comp = left <=> other.left
return comp unless comp.zero?

right <=> other.right
end

UNIVERSAL = ReqR.new(ReqR::Endpoint.new(Gem::Version.new("0.a"), true), ReqR::Endpoint.new(ReqR::INFINITY, false)).freeze
end

Expand All @@ -57,7 +97,7 @@ def self.for(requirement)
end.uniq
ranges, neqs = ranges.partition {|r| !r.is_a?(NEq) }

[ranges.sort_by {|range| [range.left.version, range.left.inclusive ? 0 : 1] }, neqs.map(&:version)]
[ranges.sort, neqs.map(&:version)]
end

def self.empty?(ranges, neqs)
Expand All @@ -66,8 +106,14 @@ def self.empty?(ranges, neqs)
next false if curr_range.single? && neqs.include?(curr_range.left.version)
next curr_range if last_range.right.version == ReqR::INFINITY
case last_range.right.version <=> curr_range.left.version
when 1 then next curr_range
when 0 then next(last_range.right.inclusive && curr_range.left.inclusive && !neqs.include?(curr_range.left.version) && curr_range)
# higher
when 1 then next ReqR.new(curr_range.left, last_range.right)
# equal
when 0
if last_range.right.inclusive && curr_range.left.inclusive && !neqs.include?(curr_range.left.version)
ReqR.new(curr_range.left, [curr_range.right, last_range.right].max)
end
# lower
when -1 then next false
end
end
Expand Down
3 changes: 3 additions & 0 deletions spec/bundler/version_ranges_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@
include_examples "empty?", false, ">= 1.0.0", "< 2.0.0"
include_examples "empty?", false, "~> 1"
include_examples "empty?", false, "~> 2.0", "~> 2.1"
include_examples "empty?", true, ">= 4.1.0", "< 5.0", "= 5.2.1"
include_examples "empty?", true, "< 5.0", "< 5.3", "< 6.0", "< 6", "= 5.2.0", "> 2", ">= 3.0", ">= 3.1", ">= 3.2", ">= 4.0.0", ">= 4.1.0", ">= 4.2.0", ">= 4.2", ">= 4"
include_examples "empty?", true, "!= 1", "< 2", "> 2"
include_examples "empty?", true, "!= 1", "<= 1", ">= 1"
include_examples "empty?", true, "< 2", "> 2"
include_examples "empty?", true, "< 2", "> 2", "= 2"
include_examples "empty?", true, "= 1", "!= 1"
include_examples "empty?", true, "= 1", "= 2"
include_examples "empty?", true, "= 1", "~> 2"
Expand Down
9 changes: 4 additions & 5 deletions spec/install/gems/resolving_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,15 +142,14 @@
expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000")

nice_error = strip_whitespace(<<-E).strip
Bundler could not find compatible versions for gem "ruby\0":
Bundler found conflicting requirements for the Ruby\0 version:
In Gemfile:
ruby\0 (#{error_message_requirement})
Ruby\0 (#{error_message_requirement})

require_ruby was resolved to 1.0, which depends on
ruby\0 (> 9000)
Ruby\0 (> 9000)

Could not find gem 'ruby\0 (> 9000)', which is required by gem 'require_ruby', in any of the relevant sources:
the local ruby installation
Ruby\0 (> 9000), which is required by gem 'require_ruby', is not available in the local ruby installation
E
expect(last_command.bundler_err).to end_with(nice_error)
end
Expand Down
4 changes: 2 additions & 2 deletions spec/resolver/basic_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,10 @@
s.required_ruby_version = "~> 2.0.0"
end

gem "ruby\0", "1.8.7"
gem "Ruby\0", "1.8.7"
end
dep "foo"
dep "ruby\0", "1.8.7"
dep "Ruby\0", "1.8.7"

deps = []
@deps.each do |d|
Expand Down
4 changes: 2 additions & 2 deletions spec/support/indexes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def an_awesome_index
gem "rack-mount", %w[0.4 0.5 0.5.1 0.5.2 0.6]

# --- Pre-release support
gem "rubygems\0", ["1.3.2"]
gem "RubyGems\0", ["1.3.2"]

# --- Rails
versions "1.2.3 2.2.3 2.3.5 3.0.0.beta 3.0.0.beta1" do |version|
Expand Down Expand Up @@ -413,7 +413,7 @@ def optional_prereleases_index
gem("b", %w[0.9.0 1.5.0 2.0.0.pre])

# --- Pre-release support
gem "rubygems\0", ["1.3.2"]
gem "RubyGems\0", ["1.3.2"]
end
end
end
Expand Down