Skip to content

Commit c2fdb38

Browse files
committed
bundler: Lock ruby fallback variants for platform gems
This fixes two issues: If a lockfile has a platform variant only bundler will install the ruby variant but then fail at setup time because it only looks for the platform version. The solution to that is to keep ruby platform variants in the lockfile even if ruby is not (or cannot) be added to the PLATFORMS. Now you will get an error if you need the ruby variant but it isn't in the lockfile, and you can actually resolve the issue by putting the ruby variant in the lockfile!
1 parent b3d52ad commit c2fdb38

8 files changed

Lines changed: 161 additions & 25 deletions

File tree

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
@@ -302,11 +302,15 @@ def all_versions_for(package)
302302
next groups if package.force_ruby_platform?
303303
end
304304

305-
platform_group = Resolver::SpecGroup.new(platform_specs.flatten.uniq)
305+
platform_specs = platform_specs.flatten.uniq
306+
platform_group = Resolver::SpecGroup.new((platform_specs + ruby_specs).uniq)
306307
next groups if platform_group == ruby_group
307308

308309
groups << Resolver::Candidate.new(version, group: platform_group, priority: 1)
309310

311+
platform_only_group = Resolver::SpecGroup.new(platform_specs)
312+
groups << Resolver::Candidate.new(version, group: platform_only_group, priority: 0) unless platform_only_group == platform_group
313+
310314
groups
311315
end
312316
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: 115 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@
262262
end
263263
end
264264

265-
it "installs the ruby variant but Bundler.setup still complains when only an incompatible platform-specific variant is locked" do
265+
it "adds and installs the ruby variant when only an incompatible platform-specific variant was locked" do
266266
build_repo4 do
267267
build_gem "nokogiri", "1.18.10"
268268
build_gem "nokogiri", "1.18.10" do |s|
@@ -294,15 +294,64 @@
294294
L
295295

296296
simulate_platform "x86_64-linux" do
297-
bundle "install --verbose", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
298-
expect(exitstatus).to eq(0)
299-
expect(out).to include("Fetching nokogiri 1.18.10\n")
300-
expect(out).to include("Installing nokogiri 1.18.10\n")
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
301350

302-
# FIXME: We should not install an alternative and then refuse to use it.
303-
ruby "require 'bundler'; Bundler.setup", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
351+
simulate_platform "x86_64-linux" do
352+
bundle "install --verbose", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
304353
expect(exitstatus).not_to eq(0)
305-
expect(err).to include("Could not find nokogiri-1.18.10-x86_64-linux in locally installed gems")
354+
expect(err).to include("nokogiri-1.18.10-x86_64-linux requires ruby version < #{Gem.ruby_version}")
306355
end
307356
end
308357
end
@@ -810,10 +859,13 @@
810859
bundle "update --conservative nokogiri"
811860
end
812861

862+
checksums.checksum gem_repo4, "nokogiri", "1.13.0"
863+
813864
expect(lockfile).to eq <<~L
814865
GEM
815866
remote: https://gem.repo4/
816867
specs:
868+
nokogiri (1.13.0)
817869
nokogiri (1.13.0-x86_64-darwin)
818870
sorbet-static (0.5.10601-x86_64-darwin)
819871
@@ -829,6 +881,61 @@
829881
L
830882
end
831883

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+
832939
it "automatically fixes the lockfile if only ruby platform is locked and some gem has no ruby variant available" do
833940
build_repo4 do
834941
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

spec/resolver/platform_spec.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,15 @@
7171
should_resolve_as %w[foo-1.0.0]
7272
end
7373

74-
it "prefers the platform specific gem to the ruby version" do
74+
it "prefers the platform specific gem to the ruby version, but keeps the ruby fallback" do
7575
@index = build_index do
7676
gem "foo", "1.0.0"
7777
gem "foo", "1.0.0", "x64-mingw-ucrt"
7878
end
7979
dep "foo"
8080
platforms "x64-mingw-ucrt"
8181

82-
should_resolve_as %w[foo-1.0.0-x64-mingw-ucrt]
82+
should_resolve_as %w[foo-1.0.0 foo-1.0.0-x64-mingw-ucrt]
8383
end
8484

8585
describe "on a linux platform" do
@@ -88,7 +88,7 @@
8888
# Gem's platform is *-linux => gem is glibc + maybe musl compatible
8989
# Gem's platform is *-linux-musl => gem is musl compatible but not glibc
9090

91-
it "favors the platform version-specific gem on a version-specifying linux platform" do
91+
it "favors the platform version-specific gem on a version-specifying linux platform, but keeps the ruby fallback" do
9292
@index = build_index do
9393
gem "foo", "1.0.0"
9494
gem "foo", "1.0.0", "x86_64-linux"
@@ -97,10 +97,10 @@
9797
dep "foo"
9898
platforms "x86_64-linux-musl"
9999

100-
should_resolve_as %w[foo-1.0.0-x86_64-linux-musl]
100+
should_resolve_as %w[foo-1.0.0 foo-1.0.0-x86_64-linux-musl]
101101
end
102102

103-
it "favors the version-less gem over the version-specific gem on a gnu linux platform" do
103+
it "favors the version-less gem over the version-specific gem on a gnu linux platform, but keeps the ruby fallback" do
104104
@index = build_index do
105105
gem "foo", "1.0.0"
106106
gem "foo", "1.0.0", "x86_64-linux"
@@ -109,7 +109,7 @@
109109
dep "foo"
110110
platforms "x86_64-linux"
111111

112-
should_resolve_as %w[foo-1.0.0-x86_64-linux]
112+
should_resolve_as %w[foo-1.0.0 foo-1.0.0-x86_64-linux]
113113
end
114114

115115
it "ignores the platform version-specific gem on a gnu linux platform" do
@@ -122,15 +122,15 @@
122122
should_not_resolve
123123
end
124124

125-
it "falls back to the platform version-less gem on a linux platform with a version" do
125+
it "falls back to the platform version-less gem on a linux platform with a version, but keeps the ruby fallback" do
126126
@index = build_index do
127127
gem "foo", "1.0.0"
128128
gem "foo", "1.0.0", "x86_64-linux"
129129
end
130130
dep "foo"
131131
platforms "x86_64-linux-musl"
132132

133-
should_resolve_as %w[foo-1.0.0-x86_64-linux]
133+
should_resolve_as %w[foo-1.0.0 foo-1.0.0-x86_64-linux]
134134
end
135135

136136
it "falls back to the ruby platform gem on a gnu linux platform when only a version-specifying gem is available" do

0 commit comments

Comments
 (0)