Skip to content

Commit 9efc31a

Browse files
Merge pull request #8489 from rubygems/deivid-rodriguez/fix-conservativeness-when-lockfile-has-incorrect-deps
Fix dependency locking when Bundler finds incorrect lockfile dependencies
2 parents c56ebfd + b8e5508 commit 9efc31a

File tree

10 files changed

+133
-66
lines changed

10 files changed

+133
-66
lines changed

bundler/lib/bundler/definition.rb

Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,28 @@ def self.build(gemfile, lockfile, unlock)
5858
# @param ruby_version [Bundler::RubyVersion, nil] Requested Ruby Version
5959
# @param optional_groups [Array(String)] A list of optional groups
6060
def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [], gemfiles = [])
61-
if [true, false].include?(unlock)
61+
unlock ||= {}
62+
63+
if unlock == true
64+
@unlocking_all = true
6265
@unlocking_bundler = false
6366
@unlocking = unlock
67+
@sources_to_unlock = []
68+
@unlocking_ruby = false
69+
@explicit_unlocks = []
70+
conservative = false
6471
else
72+
@unlocking_all = false
6573
@unlocking_bundler = unlock.delete(:bundler)
6674
@unlocking = unlock.any? {|_k, v| !Array(v).empty? }
75+
@sources_to_unlock = unlock.delete(:sources) || []
76+
@unlocking_ruby = unlock.delete(:ruby)
77+
@explicit_unlocks = unlock.delete(:gems) || []
78+
conservative = unlock.delete(:conservative)
6779
end
6880

6981
@dependencies = dependencies
7082
@sources = sources
71-
@unlock = unlock
7283
@optional_groups = optional_groups
7384
@prefer_local = false
7485
@specs = nil
@@ -97,17 +108,15 @@ def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, opti
97108
@originally_locked_specs = SpecSet.new(@locked_gems.specs)
98109
@locked_checksums = @locked_gems.checksums
99110

100-
if unlock != true
101-
@locked_specs = @originally_locked_specs
102-
@locked_sources = @locked_gems.sources
103-
else
104-
@unlock = {}
111+
if @unlocking_all
105112
@locked_specs = SpecSet.new([])
106113
@locked_sources = []
114+
else
115+
@locked_specs = @originally_locked_specs
116+
@locked_sources = @locked_gems.sources
107117
end
108118
else
109-
@unlock = {}
110-
@locked_gems = nil
119+
@locked_gems = nil
111120
@locked_platforms = []
112121
@most_specific_locked_platform = nil
113122
@platforms = []
@@ -131,21 +140,18 @@ def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, opti
131140
@sources.merged_gem_lockfile_sections!(locked_gem_sources.first)
132141
end
133142

134-
@sources_to_unlock = @unlock.delete(:sources) || []
135-
@unlock[:ruby] ||= if @ruby_version && locked_ruby_version_object
143+
@unlocking_ruby ||= if @ruby_version && locked_ruby_version_object
136144
@ruby_version.diff(locked_ruby_version_object)
137145
end
138-
@unlocking ||= @unlock[:ruby] ||= (!@locked_ruby_version ^ !@ruby_version)
146+
@unlocking ||= @unlocking_ruby ||= (!@locked_ruby_version ^ !@ruby_version)
139147

140148
@current_platform_missing = add_current_platform unless Bundler.frozen_bundle?
141149

142150
converge_path_sources_to_gemspec_sources
143151
@path_changes = converge_paths
144152
@source_changes = converge_sources
145153

146-
@explicit_unlocks = @unlock.delete(:gems) || []
147-
148-
if @unlock[:conservative]
154+
if conservative
149155
@gems_to_unlock = @explicit_unlocks.any? ? @explicit_unlocks : @dependencies.map(&:name)
150156
else
151157
eager_unlock = @explicit_unlocks.map {|name| Dependency.new(name, ">= 0") }
@@ -373,7 +379,7 @@ def lock(file_or_preserve_unknown_sections = false, preserve_unknown_sections_or
373379

374380
def locked_ruby_version
375381
return unless ruby_version
376-
if @unlock[:ruby] || !@locked_ruby_version
382+
if @unlocking_ruby || !@locked_ruby_version
377383
Bundler::RubyVersion.system
378384
else
379385
@locked_ruby_version
@@ -434,7 +440,7 @@ def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false)
434440
changed << "* #{name} from `#{lockfile_source_name}` to `#{gemfile_source_name}`"
435441
end
436442

437-
reason = nothing_changed? ? "some dependencies were deleted from your gemfile" : change_reason
443+
reason = resolve_needed? ? change_reason : "some dependencies were deleted from your gemfile"
438444
msg = String.new
439445
msg << "#{reason.capitalize.strip}, but the lockfile can't be updated because frozen mode is set"
440446
msg << "\n\nYou have added to the Gemfile:\n" << added.join("\n") if added.any?
@@ -450,7 +456,7 @@ def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false)
450456
"freeze by running `#{suggested_command}`." if suggested_command
451457
end
452458

453-
raise ProductionError, msg if added.any? || deleted.any? || changed.any? || !nothing_changed?
459+
raise ProductionError, msg if added.any? || deleted.any? || changed.any? || resolve_needed?
454460
end
455461

456462
def validate_runtime!
@@ -619,7 +625,7 @@ def resolution_packages
619625
@resolution_packages ||= begin
620626
last_resolve = converge_locked_specs
621627
remove_invalid_platforms!
622-
packages = Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: @gems_to_unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local)
628+
packages = Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: @unlocking_all || @gems_to_unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local, new_platforms: @new_platforms)
623629
packages = additional_base_requirements_to_prevent_downgrades(packages, last_resolve)
624630
packages = additional_base_requirements_to_force_updates(packages)
625631
packages
@@ -762,6 +768,7 @@ def add_current_platform
762768
@most_specific_non_local_locked_ruby_platform = find_most_specific_locked_ruby_platform
763769
return if @most_specific_non_local_locked_ruby_platform
764770

771+
@new_platforms << local_platform
765772
@platforms << local_platform
766773
true
767774
end
@@ -783,7 +790,7 @@ def change_reason
783790
unlock_reason = if unlock_targets
784791
"#{unlock_targets.first}: (#{unlock_targets.last.join(", ")})"
785792
else
786-
@unlock[:ruby] ? "ruby" : ""
793+
@unlocking_ruby ? "ruby" : ""
787794
end
788795

789796
return "bundler is unlocking #{unlock_reason}"
@@ -853,8 +860,6 @@ def converge_locals
853860
end
854861

855862
def check_lockfile
856-
@missing_lockfile_dep = nil
857-
858863
@locked_spec_with_invalid_deps = nil
859864
@locked_spec_with_missing_deps = nil
860865

@@ -872,10 +877,6 @@ def check_lockfile
872877
@locked_specs.delete(missing)
873878

874879
@locked_spec_with_missing_deps = missing.first.name
875-
elsif !@dependency_changes
876-
@missing_lockfile_dep = current_dependencies.find do |d|
877-
@locked_specs[d.name].empty? && d.name != "bundler"
878-
end&.name
879880
end
880881

881882
if invalid.any?
@@ -936,21 +937,30 @@ def converge_sources
936937
end
937938

938939
def converge_dependencies
940+
@missing_lockfile_dep = nil
939941
changes = false
940942

941-
@dependencies.each do |dep|
943+
current_dependencies.each do |dep|
942944
if dep.source
943945
dep.source = sources.get(dep.source)
944946
end
945947

946-
unless locked_dep = @locked_deps[dep.name]
947-
changes = true
948-
next
948+
name = dep.name
949+
950+
dep_changed = @locked_deps[name].nil?
951+
952+
unless name == "bundler"
953+
locked_specs = @originally_locked_specs[name]
954+
955+
if locked_specs.any? && !dep.matches_spec?(locked_specs.first)
956+
@gems_to_unlock << name
957+
dep_changed = true
958+
elsif locked_specs.empty? && dep_changed == false
959+
@missing_lockfile_dep = name
960+
end
949961
end
950962

951-
# We already know the name matches from the hash lookup
952-
# so we only need to check the requirement now
953-
changes ||= dep.requirement != locked_dep.requirement
963+
changes ||= dep_changed
954964
end
955965

956966
changes
@@ -1015,11 +1025,6 @@ def converge_specs(specs)
10151025
end
10161026
end
10171027

1018-
if dep.nil? && requested_dep = requested_dependencies.find {|d| name == d.name }
1019-
@gems_to_unlock << name
1020-
deps << requested_dep
1021-
end
1022-
10231028
converged << s
10241029
end
10251030

bundler/lib/bundler/gem_version_promoter.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,6 @@ def segments_do_not_match?(a, b, level)
132132
# Specific version moves can't always reliably be done during sorting
133133
# as not all elements are compared against each other.
134134
def post_sort(result, unlock, locked_version)
135-
# default :major behavior in Bundler does not do this
136-
return result if major?
137135
if unlock || locked_version.nil?
138136
result
139137
else

bundler/lib/bundler/resolver/package.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class Resolver
1515
class Package
1616
attr_reader :name, :platforms, :dependency, :locked_version
1717

18-
def initialize(name, platforms, locked_specs:, unlock:, prerelease: false, prefer_local: false, dependency: nil)
18+
def initialize(name, platforms, locked_specs:, unlock:, prerelease: false, prefer_local: false, dependency: nil, new_platforms: [])
1919
@name = name
2020
@platforms = platforms
2121
@locked_version = locked_specs.version_for(name)
@@ -24,10 +24,14 @@ def initialize(name, platforms, locked_specs:, unlock:, prerelease: false, prefe
2424
@top_level = !dependency.nil?
2525
@prerelease = @dependency.prerelease? || @locked_version&.prerelease? || prerelease ? :consider_first : :ignore
2626
@prefer_local = prefer_local
27+
@new_platforms = new_platforms
2728
end
2829

2930
def platform_specs(specs)
30-
platforms.map {|platform| GemHelpers.select_best_platform_match(specs, platform, prefer_locked: !unlock?) }
31+
platforms.map do |platform|
32+
prefer_locked = @new_platforms.include?(platform) ? false : !unlock?
33+
GemHelpers.select_best_platform_match(specs, platform, prefer_locked: prefer_locked)
34+
end
3135
end
3236

3337
def to_s
@@ -55,7 +59,7 @@ def hash
5559
end
5660

5761
def unlock?
58-
@unlock.empty? || @unlock.include?(name)
62+
@unlock == true || @unlock.include?(name)
5963
end
6064

6165
def ignores_prereleases?

bundler/spec/bundler/gem_version_promoter_spec.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ def build_candidates(versions)
2020
end
2121
end
2222

23-
def build_package(name, version, locked = [])
24-
Bundler::Resolver::Package.new(name, [], locked_specs: Bundler::SpecSet.new(build_spec(name, version)), unlock: locked)
23+
def build_package(name, version, unlock)
24+
Bundler::Resolver::Package.new(name, [], locked_specs: Bundler::SpecSet.new(build_spec(name, version)), unlock: unlock)
2525
end
2626

27-
def sorted_versions(candidates:, current:, name: "foo", locked: [])
27+
def sorted_versions(candidates:, current:, unlock: true)
2828
gvp.sort_versions(
29-
build_package(name, current, locked),
29+
build_package("foo", current, unlock),
3030
build_candidates(candidates)
3131
).flatten.map(&:version).map(&:to_s)
3232
end
@@ -127,7 +127,7 @@ def sorted_versions(candidates:, current:, name: "foo", locked: [])
127127
before { gvp.level = :minor }
128128

129129
it "keeps the current version first" do
130-
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"])
130+
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", unlock: [])
131131
expect(versions.first).to eq("0.3.0")
132132
end
133133
end

bundler/spec/commands/lock_spec.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -645,13 +645,13 @@
645645
it "single gem updates dependent gem to minor" do
646646
bundle "lock --update foo --patch"
647647

648-
expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-1.4.5 bar-2.1.1 qux-1.0.0].sort)
648+
expect(the_bundle.locked_specs).to eq(%w[foo-1.4.5 bar-2.1.1 qux-1.0.0].sort)
649649
end
650650

651651
it "minor preferred with strict" do
652652
bundle "lock --update --minor --strict"
653653

654-
expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-1.5.0 bar-2.1.1 qux-1.1.0].sort)
654+
expect(the_bundle.locked_specs).to eq(%w[foo-1.5.0 bar-2.1.1 qux-1.1.0].sort)
655655
end
656656

657657
it "shows proper error when Gemfile changes forbid patch upgrades, and --patch --strict is given" do
@@ -674,25 +674,25 @@
674674
it "defaults to major" do
675675
bundle "lock --update --pre"
676676

677-
expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort)
677+
expect(the_bundle.locked_specs).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort)
678678
end
679679

680680
it "patch preferred" do
681681
bundle "lock --update --patch --pre"
682682

683-
expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-1.4.5 bar-2.1.2.pre qux-1.0.1].sort)
683+
expect(the_bundle.locked_specs).to eq(%w[foo-1.4.5 bar-2.1.2.pre qux-1.0.1].sort)
684684
end
685685

686686
it "minor preferred" do
687687
bundle "lock --update --minor --pre"
688688

689-
expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-1.5.1 bar-3.1.0.pre qux-1.1.0].sort)
689+
expect(the_bundle.locked_specs).to eq(%w[foo-1.5.1 bar-3.1.0.pre qux-1.1.0].sort)
690690
end
691691

692692
it "major preferred" do
693693
bundle "lock --update --major --pre"
694694

695-
expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort)
695+
expect(the_bundle.locked_specs).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort)
696696
end
697697
end
698698
end
@@ -734,7 +734,7 @@
734734
it "adds the latest version of the new dependency" do
735735
bundle "lock --minor --update sequel"
736736

737-
expect(the_bundle.locked_gems.specs.map(&:full_name)).to eq(%w[sequel-5.72.0 bigdecimal-99.1.4].sort)
737+
expect(the_bundle.locked_specs).to eq(%w[sequel-5.72.0 bigdecimal-99.1.4].sort)
738738
end
739739
end
740740

bundler/spec/install/gemfile/gemspec_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@
420420

421421
simulate_new_machine
422422
simulate_platform("jruby") { bundle "install" }
423+
expect(lockfile).to include("platform_specific (1.0-java)")
423424
simulate_platform("x64-mingw32") { bundle "install" }
424425
end
425426

bundler/spec/lock/lockfile_spec.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1875,6 +1875,62 @@
18751875
L
18761876
end
18771877

1878+
it "automatically fixes the lockfile when it has incorrect deps, keeping the locked version" do
1879+
build_repo4 do
1880+
build_gem "net-smtp", "0.5.0" do |s|
1881+
s.add_dependency "net-protocol"
1882+
end
1883+
1884+
build_gem "net-smtp", "0.5.1" do |s|
1885+
s.add_dependency "net-protocol"
1886+
end
1887+
1888+
build_gem "net-protocol", "0.2.2"
1889+
end
1890+
1891+
gemfile <<~G
1892+
source "#{file_uri_for(gem_repo4)}"
1893+
gem "net-smtp"
1894+
G
1895+
1896+
lockfile <<~L
1897+
GEM
1898+
remote: #{file_uri_for(gem_repo4)}/
1899+
specs:
1900+
net-protocol (0.2.2)
1901+
net-smtp (0.5.0)
1902+
1903+
PLATFORMS
1904+
#{lockfile_platforms}
1905+
1906+
DEPENDENCIES
1907+
net-smtp
1908+
1909+
BUNDLED WITH
1910+
#{Bundler::VERSION}
1911+
L
1912+
1913+
bundle "install"
1914+
1915+
expect(lockfile).to eq <<~L
1916+
GEM
1917+
remote: #{file_uri_for(gem_repo4)}/
1918+
specs:
1919+
net-protocol (0.2.2)
1920+
net-smtp (0.5.0)
1921+
net-protocol
1922+
1923+
PLATFORMS
1924+
#{lockfile_platforms}
1925+
1926+
DEPENDENCIES
1927+
net-smtp
1928+
1929+
BUNDLED WITH
1930+
#{Bundler::VERSION}
1931+
L
1932+
end
1933+
18781934
shared_examples_for "a lockfile missing dependent specs" do
18791935
it "auto-heals" do
18801936
build_repo4 do

0 commit comments

Comments
 (0)