Skip to content

Commit 9904eca

Browse files
Merge pull request #7527 from rubygems/resolver-improvements
Fix resolver error message when it runs out of versions due to `--strict --patch` filtering out everything (cherry picked from commit a384c33)
1 parent 6283eb9 commit 9904eca

File tree

5 files changed

+129
-79
lines changed

5 files changed

+129
-79
lines changed

bundler/lib/bundler/gem_version_promoter.rb

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,37 @@ def level=(value)
4545

4646
# Given a Resolver::Package and an Array of Specifications of available
4747
# versions for a gem, this method will return the Array of Specifications
48-
# sorted (and possibly truncated if strict is true) in an order to give
49-
# preference to the current level (:major, :minor or :patch) when resolution
50-
# is deciding what versions best resolve all dependencies in the bundle.
48+
# sorted in an order to give preference to the current level (:major, :minor
49+
# or :patch) when resolution is deciding what versions best resolve all
50+
# dependencies in the bundle.
5151
# @param package [Resolver::Package] The package being resolved.
5252
# @param specs [Specification] An array of Specifications for the package.
53-
# @return [Specification] A new instance of the Specification Array sorted and
54-
# possibly filtered.
53+
# @return [Specification] A new instance of the Specification Array sorted.
5554
def sort_versions(package, specs)
56-
specs = filter_dep_specs(specs, package) if strict
55+
locked_version = package.locked_version
5756

58-
sort_dep_specs(specs, package)
57+
result = specs.sort do |a, b|
58+
unless package.prerelease_specified? || pre?
59+
a_pre = a.prerelease?
60+
b_pre = b.prerelease?
61+
62+
next 1 if a_pre && !b_pre
63+
next -1 if b_pre && !a_pre
64+
end
65+
66+
if major? || locked_version.nil?
67+
b <=> a
68+
elsif either_version_older_than_locked?(a, b, locked_version)
69+
b <=> a
70+
elsif segments_do_not_match?(a, b, :major)
71+
a <=> b
72+
elsif !minor? && segments_do_not_match?(a, b, :minor)
73+
a <=> b
74+
else
75+
b <=> a
76+
end
77+
end
78+
post_sort(result, package.unlock?, locked_version)
5979
end
6080

6181
# @return [bool] Convenience method for testing value of level variable.
@@ -73,9 +93,18 @@ def pre?
7393
pre == true
7494
end
7595

76-
private
96+
# Given a Resolver::Package and an Array of Specifications of available
97+
# versions for a gem, this method will truncate the Array if strict
98+
# is true. That means filtering out downgrades from the version currently
99+
# locked, and filtering out upgrades that go past the selected level (major,
100+
# minor, or patch).
101+
# @param package [Resolver::Package] The package being resolved.
102+
# @param specs [Specification] An array of Specifications for the package.
103+
# @return [Specification] A new instance of the Specification Array
104+
# truncated.
105+
def filter_versions(package, specs)
106+
return specs unless strict
77107

78-
def filter_dep_specs(specs, package)
79108
locked_version = package.locked_version
80109
return specs if locked_version.nil? || major?
81110

@@ -89,32 +118,7 @@ def filter_dep_specs(specs, package)
89118
end
90119
end
91120

92-
def sort_dep_specs(specs, package)
93-
locked_version = package.locked_version
94-
95-
result = specs.sort do |a, b|
96-
unless package.prerelease_specified? || pre?
97-
a_pre = a.prerelease?
98-
b_pre = b.prerelease?
99-
100-
next -1 if a_pre && !b_pre
101-
next 1 if b_pre && !a_pre
102-
end
103-
104-
if major? || locked_version.nil?
105-
a <=> b
106-
elsif either_version_older_than_locked?(a, b, locked_version)
107-
a <=> b
108-
elsif segments_do_not_match?(a, b, :major)
109-
b <=> a
110-
elsif !minor? && segments_do_not_match?(a, b, :minor)
111-
b <=> a
112-
else
113-
a <=> b
114-
end
115-
end
116-
post_sort(result, package.unlock?, locked_version)
117-
end
121+
private
118122

119123
def either_version_older_than_locked?(a, b, locked_version)
120124
a.version < locked_version || b.version < locked_version
@@ -133,13 +137,13 @@ def post_sort(result, unlock, locked_version)
133137
if unlock || locked_version.nil?
134138
result
135139
else
136-
move_version_to_end(result, locked_version)
140+
move_version_to_beginning(result, locked_version)
137141
end
138142
end
139143

140-
def move_version_to_end(result, version)
144+
def move_version_to_beginning(result, version)
141145
move, keep = result.partition {|s| s.version.to_s == version.to_s }
142-
keep.concat(move)
146+
move.concat(keep)
143147
end
144148
end
145149
end

bundler/lib/bundler/resolver.rb

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -50,26 +50,26 @@ def setup_solver
5050
specs[name] = matches.sort_by {|s| [s.version, s.platform.to_s] }
5151
end
5252

53+
@all_versions = Hash.new do |candidates, package|
54+
candidates[package] = all_versions_for(package)
55+
end
56+
5357
@sorted_versions = Hash.new do |candidates, package|
54-
candidates[package] = if package.root?
55-
[root_version]
56-
else
57-
all_versions_for(package).sort
58-
end
58+
candidates[package] = filtered_versions_for(package).sort
5959
end
6060

61+
@sorted_versions[root] = [root_version]
62+
6163
root_dependencies = prepare_dependencies(@requirements, @packages)
6264

6365
@cached_dependencies = Hash.new do |dependencies, package|
64-
dependencies[package] = if package.root?
65-
{ root_version => root_dependencies }
66-
else
67-
Hash.new do |versions, version|
68-
versions[version] = to_dependency_hash(version.dependencies.reject {|d| d.name == package.name }, @packages)
69-
end
66+
dependencies[package] = Hash.new do |versions, version|
67+
versions[version] = to_dependency_hash(version.dependencies.reject {|d| d.name == package.name }, @packages)
7068
end
7169
end
7270

71+
@cached_dependencies[root] = { root_version => root_dependencies }
72+
7373
logger = Bundler::UI::Shell.new
7474
logger.level = debug? ? "debug" : "warn"
7575

@@ -156,9 +156,15 @@ def parse_dependency(package, dependency)
156156
end
157157

158158
def versions_for(package, range=VersionRange.any)
159-
versions = range.select_versions(@sorted_versions[package])
159+
versions = select_sorted_versions(package, range)
160160

161-
sort_versions(package, versions)
161+
# Conditional avoids (among other things) calling
162+
# sort_versions_by_preferred with the root package
163+
if versions.size > 1
164+
sort_versions_by_preferred(package, versions)
165+
else
166+
versions
167+
end
162168
end
163169

164170
def no_versions_incompatibility_for(package, unsatisfied_term)
@@ -247,7 +253,7 @@ def all_versions_for(package)
247253
locked_requirement = base_requirements[name]
248254
results = filter_matching_specs(results, locked_requirement) if locked_requirement
249255

250-
versions = results.group_by(&:version).reduce([]) do |groups, (version, specs)|
256+
results.group_by(&:version).reduce([]) do |groups, (version, specs)|
251257
platform_specs = package.platforms.map {|platform| select_best_platform_match(specs, platform) }
252258

253259
# If package is a top-level dependency,
@@ -274,8 +280,6 @@ def all_versions_for(package)
274280

275281
groups
276282
end
277-
278-
sort_versions(package, versions)
279283
end
280284

281285
def source_for(name)
@@ -334,6 +338,21 @@ def raise_not_found!(package)
334338

335339
private
336340

341+
def filtered_versions_for(package)
342+
@gem_version_promoter.filter_versions(package, @all_versions[package])
343+
end
344+
345+
def raise_all_versions_filtered_out!(package)
346+
level = @gem_version_promoter.level
347+
name = package.name
348+
locked_version = package.locked_version
349+
requirement = package.dependency
350+
351+
raise GemNotFound,
352+
"#{name} is locked to #{locked_version}, while Gemfile is requesting #{requirement}. " \
353+
"--strict --#{level} was specified, but there are no #{level} level upgrades from #{locked_version} satisfying #{requirement}, so version solving has failed"
354+
end
355+
337356
def filter_matching_specs(specs, requirements)
338357
Array(requirements).flat_map do |requirement|
339358
specs.select {| spec| requirement_satisfied_by?(requirement, spec) }
@@ -357,12 +376,8 @@ def requirement_satisfied_by?(requirement, spec)
357376
requirement.satisfied_by?(spec.version) || spec.source.is_a?(Source::Gemspec)
358377
end
359378

360-
def sort_versions(package, versions)
361-
if versions.size > 1
362-
@gem_version_promoter.sort_versions(package, versions).reverse
363-
else
364-
versions
365-
end
379+
def sort_versions_by_preferred(package, versions)
380+
@gem_version_promoter.sort_versions(package, versions)
366381
end
367382

368383
def repository_for(package)
@@ -379,12 +394,19 @@ def prepare_dependencies(requirements, packages)
379394

380395
next [dep_package, dep_constraint] if name == "bundler"
381396

382-
versions = versions_for(dep_package, dep_constraint.range)
397+
dep_range = dep_constraint.range
398+
versions = select_sorted_versions(dep_package, dep_range)
383399
if versions.empty? && dep_package.ignores_prereleases?
400+
@all_versions.delete(dep_package)
384401
@sorted_versions.delete(dep_package)
385402
dep_package.consider_prereleases!
386-
versions = versions_for(dep_package, dep_constraint.range)
403+
versions = select_sorted_versions(dep_package, dep_range)
387404
end
405+
406+
if versions.empty? && select_all_versions(dep_package, dep_range).any?
407+
raise_all_versions_filtered_out!(dep_package)
408+
end
409+
388410
next [dep_package, dep_constraint] unless versions.empty?
389411

390412
next unless dep_package.current_platform?
@@ -393,6 +415,14 @@ def prepare_dependencies(requirements, packages)
393415
end.compact.to_h
394416
end
395417

418+
def select_sorted_versions(package, range)
419+
range.select_versions(@sorted_versions[package])
420+
end
421+
422+
def select_all_versions(package, range)
423+
range.select_versions(@all_versions[package])
424+
end
425+
396426
def other_specs_matching_message(specs, requirement)
397427
message = String.new("The source contains the following gems matching '#{requirement}':\n")
398428
message << specs.map {|s| " * #{s.full_name}" }.join("\n")

bundler/lib/bundler/resolver/candidate.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class Resolver
1515
# considered separately.
1616
#
1717
# Some candidates may also keep some information explicitly about the
18-
# package the refer to. These candidates are referred to as "canonical" and
18+
# package they refer to. These candidates are referred to as "canonical" and
1919
# are used when materializing resolution results back into RubyGems
2020
# specifications that can be installed, written to lock files, and so on.
2121
#

bundler/spec/bundler/gem_version_promoter_spec.rb

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ def sorted_versions(candidates:, current:, name: "foo", locked: [])
3333

3434
it "numerically sorts versions" do
3535
versions = sorted_versions(candidates: %w[1.7.7 1.7.8 1.7.9 1.7.15 1.8.0], current: "1.7.8")
36-
expect(versions).to eq %w[1.7.7 1.7.8 1.7.9 1.7.15 1.8.0]
36+
expect(versions).to eq %w[1.8.0 1.7.15 1.7.9 1.7.8 1.7.7]
3737
end
3838

3939
context "with no options" do
4040
it "defaults to level=:major, strict=false, pre=false" do
4141
versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
42-
expect(versions).to eq %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0]
42+
expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0]
4343
end
4444
end
4545

@@ -51,25 +51,25 @@ def sorted_versions(candidates:, current:, name: "foo", locked: [])
5151

5252
it "keeps downgrades" do
5353
versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
54-
expect(versions).to eq %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0]
54+
expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0]
5555
end
5656
end
5757

5858
context "when level is minor" do
5959
before { gvp.level = :minor }
6060

61-
it "removes downgrades and major upgrades" do
61+
it "sorts highest minor within same major in first position" do
6262
versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
63-
expect(versions).to eq %w[0.3.0 0.3.1 0.9.0]
63+
expect(versions).to eq %w[0.9.0 0.3.1 0.3.0 1.0.0 2.1.0 2.0.1 0.2.0]
6464
end
6565
end
6666

6767
context "when level is patch" do
6868
before { gvp.level = :patch }
6969

70-
it "removes downgrades and major and minor upgrades" do
70+
it "sorts highest patch within same minor in first position" do
7171
versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
72-
expect(versions).to eq %w[0.3.0 0.3.1]
72+
expect(versions).to eq %w[0.3.1 0.3.0 0.9.0 1.0.0 2.0.1 2.1.0 0.2.0]
7373
end
7474
end
7575
end
@@ -82,25 +82,25 @@ def sorted_versions(candidates:, current:, name: "foo", locked: [])
8282

8383
it "orders by version" do
8484
versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
85-
expect(versions).to eq %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0]
85+
expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0]
8686
end
8787
end
8888

8989
context "when level is minor" do
9090
before { gvp.level = :minor }
9191

92-
it "favors downgrades, then upgrades by major descending, minor ascending, patch ascending" do
92+
it "favors minor upgrades, then patch upgrades, then major upgrades, then downgrades" do
9393
versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
94-
expect(versions).to eq %w[0.2.0 2.0.1 2.1.0 1.0.0 0.3.0 0.3.1 0.9.0]
94+
expect(versions).to eq %w[0.9.0 0.3.1 0.3.0 1.0.0 2.1.0 2.0.1 0.2.0]
9595
end
9696
end
9797

9898
context "when level is patch" do
9999
before { gvp.level = :patch }
100100

101-
it "favors downgrades, then upgrades by major descending, minor descending, patch ascending" do
101+
it "favors patch upgrades, then minor upgrades, then major upgrades, then downgrades" do
102102
versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
103-
expect(versions).to eq %w[0.2.0 2.1.0 2.0.1 1.0.0 0.9.0 0.3.0 0.3.1]
103+
expect(versions).to eq %w[0.3.1 0.3.0 0.9.0 1.0.0 2.0.1 2.1.0 0.2.0]
104104
end
105105
end
106106
end
@@ -110,7 +110,7 @@ def sorted_versions(candidates:, current:, name: "foo", locked: [])
110110

111111
it "sorts regardless of prerelease status" do
112112
versions = sorted_versions(candidates: %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0], current: "1.8.0")
113-
expect(versions).to eq %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0]
113+
expect(versions).to eq %w[2.0.0 2.0.0.pre 1.8.1 1.8.1.pre 1.8.0 1.7.7.pre]
114114
end
115115
end
116116

@@ -119,16 +119,16 @@ def sorted_versions(candidates:, current:, name: "foo", locked: [])
119119

120120
it "deprioritizes prerelease gems" do
121121
versions = sorted_versions(candidates: %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0], current: "1.8.0")
122-
expect(versions).to eq %w[1.7.7.pre 1.8.1.pre 2.0.0.pre 1.8.0 1.8.1 2.0.0]
122+
expect(versions).to eq %w[2.0.0 1.8.1 1.8.0 2.0.0.pre 1.8.1.pre 1.7.7.pre]
123123
end
124124
end
125125

126126
context "when locking and not major" do
127127
before { gvp.level = :minor }
128128

129-
it "keeps the current version last" do
129+
it "keeps the current version first" do
130130
versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.1.0 2.0.1], current: "0.3.0", locked: ["bar"])
131-
expect(versions.last).to eq("0.3.0")
131+
expect(versions.first).to eq("0.3.0")
132132
end
133133
end
134134
end

0 commit comments

Comments
 (0)