Skip to content

Commit dbd537e

Browse files
authored
Merge pull request #9556 from Shopify/rwstauner/ruby-platform-lock
Fix two issues by including the ruby platform variants in the lock file
2 parents b715e9c + 56841bb commit dbd537e

9 files changed

Lines changed: 204 additions & 18 deletions

File tree

bundler/lib/bundler/cli/outdated.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def run
7777
gemfile_specs + dependency_specs
7878
end
7979

80-
specs.sort_by(&:name).uniq(&:name).each do |current_spec|
80+
specs_for_outdated_check(specs).each do |current_spec|
8181
next unless gems.empty? || gems.include?(current_spec.name)
8282

8383
active_spec = retrieve_active_spec(definition, current_spec)
@@ -152,6 +152,12 @@ def nothing_outdated_message
152152
end
153153
end
154154

155+
def specs_for_outdated_check(specs)
156+
specs.group_by(&:name).values.filter_map do |matching_specs|
157+
MatchPlatform.select_best_platform_match(matching_specs, Bundler.local_platform).first || matching_specs.first
158+
end.sort_by(&:name)
159+
end
160+
155161
def retrieve_active_spec(definition, current_spec)
156162
active_spec = definition.resolve.find_by_name_and_platform(current_spec.name, current_spec.platform)
157163
return unless active_spec

bundler/lib/bundler/lazy_specification.rb

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ class LazySpecification
99
include ForcePlatform
1010

1111
attr_reader :name, :version, :platform, :materialization
12-
attr_accessor :source, :remote, :force_ruby_platform, :dependencies, :required_ruby_version, :required_rubygems_version, :overrides
12+
attr_accessor :source, :remote, :force_ruby_platform, :dependencies, :required_ruby_version, :required_rubygems_version
13+
attr_accessor :overrides, :locked_platforms
1314

1415
#
1516
# For backwards compatibility with existing lockfiles, if the most specific
@@ -49,6 +50,7 @@ def initialize(name, version, platform, source = nil, **materialization_options)
4950
@force_ruby_platform = default_force_ruby_platform
5051
@most_specific_locked_platform = nil
5152
@materialization = nil
53+
@locked_platforms = nil
5254
end
5355

5456
def missing?
@@ -145,7 +147,7 @@ def materialize_for_installation
145147
# Exact spec is incompatible; in frozen mode, try to find a compatible platform variant
146148
# In non-frozen mode, return nil to trigger re-resolution and lockfile update
147149
if Bundler.frozen_bundle?
148-
materialize([name, version]) {|specs| resolve_best_platform(specs) }
150+
materialize([name, version]) {|specs| resolve_best_platform(specs, locked_platforms_only: true) }
149151
end
150152
else
151153
materialize([name, version]) {|specs| resolve_best_platform(specs) }
@@ -186,12 +188,12 @@ def use_exact_resolved_specifications?
186188
# Try platforms in order of preference until finding a compatible spec.
187189
# Used for legacy lockfiles and as a fallback when the exact locked spec
188190
# is incompatible. Falls back to frozen bundle behavior if none match.
189-
def resolve_best_platform(specs)
190-
find_compatible_platform_spec(specs) || frozen_bundle_fallback(specs)
191+
def resolve_best_platform(specs, locked_platforms_only: false)
192+
find_compatible_platform_spec(specs, locked_platforms_only: locked_platforms_only) || frozen_bundle_fallback(specs)
191193
end
192194

193-
def find_compatible_platform_spec(specs)
194-
candidate_platforms.each do |plat|
195+
def find_compatible_platform_spec(specs, locked_platforms_only: false)
196+
candidate_platforms(locked_platforms_only: locked_platforms_only).each do |plat|
195197
candidates = MatchPlatform.select_best_platform_match(specs, plat)
196198
spec = choose_compatible(candidates, fallback_to_non_installable: false)
197199
return spec if spec
@@ -201,9 +203,12 @@ def find_compatible_platform_spec(specs)
201203

202204
# Platforms to try in order of preference. Ruby platform is last since it
203205
# requires compilation, but works when precompiled gems are incompatible.
204-
def candidate_platforms
206+
def candidate_platforms(locked_platforms_only: false)
205207
target = source.is_a?(Source::Path) ? platform : Bundler.local_platform
206-
[target, platform, Gem::Platform::RUBY].uniq
208+
platforms = [target, platform, Gem::Platform::RUBY].uniq
209+
return platforms unless locked_platforms_only && locked_platforms
210+
211+
platforms & locked_platforms
207212
end
208213

209214
# In frozen mode, accept any candidate. Will error at install time.

bundler/lib/bundler/materialization.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def initialize(dep, platform, candidates:)
1212
@dep = dep
1313
@platform = platform
1414
@candidates = candidates
15+
set_locked_platforms
1516
end
1617

1718
def complete?
@@ -55,5 +56,14 @@ def incomplete_specs
5556
private
5657

5758
attr_reader :dep, :platform
59+
60+
def set_locked_platforms
61+
return unless @candidates
62+
63+
platforms = @candidates.map(&:platform)
64+
@candidates.each do |candidate|
65+
candidate.locked_platforms = platforms if candidate.respond_to?(:locked_platforms=)
66+
end
67+
end
5868
end
5969
end

bundler/lib/bundler/resolver.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,15 @@ def all_versions_for(package)
305305
next groups if package.force_ruby_platform?
306306
end
307307

308-
platform_group = Resolver::SpecGroup.new(platform_specs.flatten.uniq)
308+
platform_specs = platform_specs.flatten.uniq
309+
platform_group = Resolver::SpecGroup.new((platform_specs + ruby_specs).uniq)
309310
next groups if platform_group == ruby_group
310311

311312
groups << Resolver::Candidate.new(version, group: platform_group, priority: 1)
312313

314+
platform_only_group = Resolver::SpecGroup.new(platform_specs)
315+
groups << Resolver::Candidate.new(version, group: platform_only_group, priority: 0) unless platform_only_group == platform_group
316+
313317
groups
314318
end
315319
end

spec/commands/lock_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,8 +1083,10 @@
10831083
simulate_platform("x86-mingw32") { bundle :lock }
10841084

10851085
checksums = checksums_section_when_enabled do |c|
1086+
c.checksum gem_repo4, "ffi", "1.9.14"
10861087
c.checksum gem_repo4, "ffi", "1.9.14", "x86-mingw32"
10871088
c.checksum gem_repo4, "gssapi", "1.2.0"
1089+
c.checksum gem_repo4, "mixlib-shellout", "2.2.6"
10881090
c.checksum gem_repo4, "mixlib-shellout", "2.2.6", "universal-mingw32"
10891091
c.checksum gem_repo4, "win32-process", "0.8.3"
10901092
end
@@ -1093,9 +1095,11 @@
10931095
GEM
10941096
remote: https://gem.repo4/
10951097
specs:
1098+
ffi (1.9.14)
10961099
ffi (1.9.14-x86-mingw32)
10971100
gssapi (1.2.0)
10981101
ffi (>= 1.0.1)
1102+
mixlib-shellout (2.2.6)
10991103
mixlib-shellout (2.2.6-universal-mingw32)
11001104
win32-process (~> 0.8.2)
11011105
win32-process (0.8.3)

spec/install/gemfile/platform_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
c.checksum gem_repo4, "empyrean", "0.1.0"
209209
c.checksum gem_repo4, "ffi", "1.9.23", "java"
210210
c.checksum gem_repo4, "method_source", "0.9.0"
211+
c.checksum gem_repo4, "pry", "0.11.3"
211212
c.checksum gem_repo4, "pry", "0.11.3", "java"
212213
c.checksum gem_repo4, "spoon", "0.0.6"
213214
end
@@ -220,6 +221,9 @@
220221
empyrean (0.1.0)
221222
ffi (1.9.23-java)
222223
method_source (0.9.0)
224+
pry (0.11.3)
225+
coderay (~> 1.1.0)
226+
method_source (~> 0.9.0)
223227
pry (0.11.3-java)
224228
coderay (~> 1.1.0)
225229
method_source (~> 0.9.0)

spec/install/gemfile/specific_platform_spec.rb

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,99 @@
261261
expect(the_bundle).not_to include_gem("nokogiri 1.18.10 x86_64-linux")
262262
end
263263
end
264+
265+
it "adds and installs the ruby variant when only an incompatible platform-specific variant was locked" do
266+
build_repo4 do
267+
build_gem "nokogiri", "1.18.10"
268+
build_gem "nokogiri", "1.18.10" do |s|
269+
s.platform = "x86_64-linux"
270+
s.required_ruby_version = "< #{Gem.ruby_version}"
271+
end
272+
end
273+
274+
gemfile <<~G
275+
source "https://gem.repo4"
276+
277+
gem "nokogiri"
278+
G
279+
280+
lockfile <<-L
281+
GEM
282+
remote: https://gem.repo4/
283+
specs:
284+
nokogiri (1.18.10-x86_64-linux)
285+
286+
PLATFORMS
287+
x86_64-linux
288+
289+
DEPENDENCIES
290+
nokogiri
291+
292+
BUNDLED WITH
293+
#{Bundler::VERSION}
294+
L
295+
296+
simulate_platform "x86_64-linux" do
297+
bundle "install --verbose"
298+
expect(out).to include("Installing nokogiri 1.18.10")
299+
expect(the_bundle).to include_gem("nokogiri 1.18.10")
300+
end
301+
302+
expect(lockfile).to eq <<~L
303+
GEM
304+
remote: https://gem.repo4/
305+
specs:
306+
nokogiri (1.18.10)
307+
nokogiri (1.18.10-x86_64-linux)
308+
309+
PLATFORMS
310+
x86_64-linux
311+
312+
DEPENDENCIES
313+
nokogiri
314+
315+
BUNDLED WITH
316+
#{Bundler::VERSION}
317+
L
318+
end
319+
320+
it "fails at install time when only an incompatible platform-specific variant is locked" do
321+
build_repo4 do
322+
build_gem "nokogiri", "1.18.10"
323+
build_gem "nokogiri", "1.18.10" do |s|
324+
s.platform = "x86_64-linux"
325+
s.required_ruby_version = "< #{Gem.ruby_version}"
326+
end
327+
end
328+
329+
gemfile <<~G
330+
source "https://gem.repo4"
331+
332+
gem "nokogiri"
333+
G
334+
335+
lockfile <<-L
336+
GEM
337+
remote: https://gem.repo4/
338+
specs:
339+
nokogiri (1.18.10-x86_64-linux)
340+
341+
PLATFORMS
342+
x86_64-linux
343+
344+
DEPENDENCIES
345+
nokogiri
346+
347+
BUNDLED WITH
348+
#{Bundler::VERSION}
349+
L
350+
351+
simulate_platform "x86_64-linux" do
352+
bundle "install --verbose", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
353+
expect(exitstatus).not_to eq(0)
354+
expect(err).to include("nokogiri-1.18.10-x86_64-linux requires ruby version < #{Gem.ruby_version}")
355+
end
356+
end
264357
end
265358

266359
it "doesn't discard previously installed platform specific gem and fall back to ruby on subsequent bundles" do
@@ -766,10 +859,13 @@
766859
bundle "update --conservative nokogiri"
767860
end
768861

862+
checksums.checksum gem_repo4, "nokogiri", "1.13.0"
863+
769864
expect(lockfile).to eq <<~L
770865
GEM
771866
remote: https://gem.repo4/
772867
specs:
868+
nokogiri (1.13.0)
773869
nokogiri (1.13.0-x86_64-darwin)
774870
sorbet-static (0.5.10601-x86_64-darwin)
775871
@@ -785,6 +881,61 @@
785881
L
786882
end
787883

884+
it "locks ruby fallback variant dependencies without adding the ruby platform" do
885+
build_repo4 do
886+
build_gem "native_tool", "1.0" do |s|
887+
s.add_dependency "rake"
888+
end
889+
890+
build_gem "native_tool", "1.0" do |s|
891+
s.platform = "x86_64-linux"
892+
end
893+
894+
build_gem "rake"
895+
896+
build_gem "sorbet-static", "0.5.10601" do |s|
897+
s.platform = "x86_64-linux"
898+
end
899+
end
900+
901+
simulate_platform "x86_64-linux" do
902+
install_gemfile <<~G
903+
source "https://gem.repo4"
904+
905+
gem "native_tool"
906+
gem "sorbet-static"
907+
G
908+
end
909+
910+
checksums = checksums_section_when_enabled do |c|
911+
c.checksum gem_repo4, "native_tool", "1.0"
912+
c.checksum gem_repo4, "native_tool", "1.0", "x86_64-linux"
913+
c.checksum gem_repo4, "rake", "1.0"
914+
c.checksum gem_repo4, "sorbet-static", "0.5.10601", "x86_64-linux"
915+
end
916+
917+
expect(lockfile).to eq <<~L
918+
GEM
919+
remote: https://gem.repo4/
920+
specs:
921+
native_tool (1.0)
922+
rake
923+
native_tool (1.0-x86_64-linux)
924+
rake (1.0)
925+
sorbet-static (0.5.10601-x86_64-linux)
926+
927+
PLATFORMS
928+
x86_64-linux
929+
930+
DEPENDENCIES
931+
native_tool
932+
sorbet-static
933+
#{checksums}
934+
BUNDLED WITH
935+
#{Bundler::VERSION}
936+
L
937+
end
938+
788939
it "automatically fixes the lockfile if only ruby platform is locked and some gem has no ruby variant available" do
789940
build_repo4 do
790941
build_gem("sorbet-static-and-runtime", "0.5.10160") do |s|

spec/lock/lockfile_spec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,13 +1324,15 @@
13241324
G
13251325

13261326
checksums = checksums_section_when_enabled do |c|
1327+
c.checksum gem_repo2, "platform_specific", "1.0"
13271328
c.checksum gem_repo2, "platform_specific", "1.0", "universal-java-16"
13281329
end
13291330

13301331
expect(lockfile).to eq <<~G
13311332
GEM
13321333
remote: https://gem.repo2/
13331334
specs:
1335+
platform_specific (1.0)
13341336
platform_specific (1.0-universal-java-16)
13351337
13361338
PLATFORMS

0 commit comments

Comments
 (0)