Skip to content

Commit 8dd10fe

Browse files
authored
Merge pull request #9215 from ruby/release/4.0.3
Prepare RubyGems 4.0.3 and Bundler 4.0.3
2 parents 07029c3 + 28c66ec commit 8dd10fe

23 files changed

Lines changed: 183 additions & 36 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## 4.0.3 / 2025-12-23
4+
5+
### Enhancements:
6+
7+
* Installs bundler 4.0.3 as a default gem.
8+
9+
### Documentation:
10+
11+
* Fix broken documentation links. Pull request
12+
[#9208](https://github.com/ruby/rubygems/pull/9208) by eileencodes
13+
314
## 4.0.2 / 2025-12-17
415

516
### Enhancements:

bundler/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 4.0.3 (2025-12-23)
4+
5+
### Enhancements:
6+
7+
- Fall back to ruby platform gem when precompiled variant is incompatible [#9211](https://github.com/ruby/rubygems/pull/9211)
8+
39
## 4.0.2 (2025-12-17)
410

511
### Enhancements:

bundler/lib/bundler/lazy_specification.rb

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -138,24 +138,16 @@ def materialize_for_installation
138138
source.local!
139139

140140
if use_exact_resolved_specifications?
141-
materialize(self) do |matching_specs|
142-
choose_compatible(matching_specs)
143-
end
144-
else
145-
materialize([name, version]) do |matching_specs|
146-
target_platform = source.is_a?(Source::Path) ? platform : Bundler.local_platform
147-
148-
installable_candidates = MatchPlatform.select_best_platform_match(matching_specs, target_platform)
149-
150-
specification = choose_compatible(installable_candidates, fallback_to_non_installable: false)
151-
return specification unless specification.nil?
141+
spec = materialize(self) {|specs| choose_compatible(specs, fallback_to_non_installable: false) }
142+
return spec if spec
152143

153-
if target_platform != platform
154-
installable_candidates = MatchPlatform.select_best_platform_match(matching_specs, platform)
155-
end
156-
157-
choose_compatible(installable_candidates)
144+
# Exact spec is incompatible; in frozen mode, try to find a compatible platform variant
145+
# In non-frozen mode, return nil to trigger re-resolution and lockfile update
146+
if Bundler.frozen_bundle?
147+
materialize([name, version]) {|specs| resolve_best_platform(specs) }
158148
end
149+
else
150+
materialize([name, version]) {|specs| resolve_best_platform(specs) }
159151
end
160152
end
161153

@@ -190,6 +182,39 @@ def use_exact_resolved_specifications?
190182
!source.is_a?(Source::Path) && ruby_platform_materializes_to_ruby_platform?
191183
end
192184

185+
# Try platforms in order of preference until finding a compatible spec.
186+
# Used for legacy lockfiles and as a fallback when the exact locked spec
187+
# is incompatible. Falls back to frozen bundle behavior if none match.
188+
def resolve_best_platform(specs)
189+
find_compatible_platform_spec(specs) || frozen_bundle_fallback(specs)
190+
end
191+
192+
def find_compatible_platform_spec(specs)
193+
candidate_platforms.each do |plat|
194+
candidates = MatchPlatform.select_best_platform_match(specs, plat)
195+
spec = choose_compatible(candidates, fallback_to_non_installable: false)
196+
return spec if spec
197+
end
198+
nil
199+
end
200+
201+
# Platforms to try in order of preference. Ruby platform is last since it
202+
# requires compilation, but works when precompiled gems are incompatible.
203+
def candidate_platforms
204+
target = source.is_a?(Source::Path) ? platform : Bundler.local_platform
205+
[target, platform, Gem::Platform::RUBY].uniq
206+
end
207+
208+
# In frozen mode, accept any candidate. Will error at install time.
209+
# When target differs from locked platform, prefer locked platform's candidates
210+
# to preserve lockfile integrity.
211+
def frozen_bundle_fallback(specs)
212+
target = source.is_a?(Source::Path) ? platform : Bundler.local_platform
213+
fallback_platform = target == platform ? target : platform
214+
candidates = MatchPlatform.select_best_platform_match(specs, fallback_platform)
215+
choose_compatible(candidates)
216+
end
217+
193218
def ruby_platform_materializes_to_ruby_platform?
194219
generic_platform = Bundler.generic_local_platform == Gem::Platform::JAVA ? Gem::Platform::JAVA : Gem::Platform::RUBY
195220

bundler/lib/bundler/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: false
22

33
module Bundler
4-
VERSION = "4.0.2".freeze
4+
VERSION = "4.0.3".freeze
55

66
def self.bundler_major_version
77
@bundler_major_version ||= gem_version.segments.first

bundler/spec/bundler/plugin/events_spec.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22

33
RSpec.describe Bundler::Plugin::Events do
44
context "plugin events" do
5-
before { Bundler::Plugin::Events.send :reset }
5+
before do
6+
@old_constants = Bundler::Plugin::Events.constants.map {|name| [name, Bundler::Plugin::Events.const_get(name)] }
7+
Bundler::Plugin::Events.send :reset
8+
end
9+
10+
after do
11+
Bundler::Plugin::Events.send(:reset)
12+
Hash[@old_constants].each do |name, value|
13+
Bundler::Plugin::Events.send(:define, name, value)
14+
end
15+
end
616

717
describe "#define" do
818
it "raises when redefining a constant" do

bundler/spec/bundler/plugin_spec.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@
279279
s.write "plugins.rb", code
280280
end
281281

282+
@old_constants = Bundler::Plugin::Events.constants.map {|name| [name, Bundler::Plugin::Events.const_get(name)] }
282283
Bundler::Plugin::Events.send(:reset)
283284
Bundler::Plugin::Events.send(:define, :EVENT1, "event-1")
284285
Bundler::Plugin::Events.send(:define, :EVENT2, "event-2")
@@ -291,6 +292,13 @@
291292
allow(index).to receive(:load_paths).with("foo-plugin").and_return([])
292293
end
293294

295+
after do
296+
Bundler::Plugin::Events.send(:reset)
297+
Hash[@old_constants].each do |name, value|
298+
Bundler::Plugin::Events.send(:define, name, value)
299+
end
300+
end
301+
294302
let(:code) { <<-RUBY }
295303
Bundler::Plugin::API.hook("event-1") { puts "hook for event 1" }
296304
RUBY
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe Bundler::URINormalizer do
4+
describe ".normalize_suffix" do
5+
context "when trailing_slash is true" do
6+
it "adds a trailing slash when missing" do
7+
expect(described_class.normalize_suffix("https://example.com", trailing_slash: true)).to eq("https://example.com/")
8+
end
9+
10+
it "keeps the trailing slash when present" do
11+
expect(described_class.normalize_suffix("https://example.com/", trailing_slash: true)).to eq("https://example.com/")
12+
end
13+
end
14+
15+
context "when trailing_slash is false" do
16+
it "removes a trailing slash when present" do
17+
expect(described_class.normalize_suffix("https://example.com/", trailing_slash: false)).to eq("https://example.com")
18+
end
19+
20+
it "keeps the value unchanged when no trailing slash exists" do
21+
expect(described_class.normalize_suffix("https://example.com", trailing_slash: false)).to eq("https://example.com")
22+
end
23+
end
24+
end
25+
end

bundler/spec/install/gemfile/specific_platform_spec.rb

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@
157157
end
158158

159159
context "when running on a legacy lockfile locked only to ruby" do
160+
# Exercises the legacy lockfile path (use_exact_resolved_specifications? = false)
161+
# because most_specific_locked_platform is ruby, matching the generic platform.
162+
# Key insight: when target (arm64-darwin-22) != platform (ruby), the code tries
163+
# both platforms before falling back, preserving lockfile integrity.
164+
160165
around do |example|
161166
build_repo4 do
162167
build_gem "nokogiri", "1.3.10"
@@ -192,13 +197,69 @@
192197
end
193198

194199
it "still installs the generic ruby variant if necessary" do
195-
bundle "install --verbose"
196-
expect(out).to include("Installing nokogiri 1.3.10")
200+
bundle "install"
201+
expect(the_bundle).to include_gem("nokogiri 1.3.10")
202+
expect(the_bundle).not_to include_gem("nokogiri 1.3.10 arm64-darwin")
197203
end
198204

199205
it "still installs the generic ruby variant if necessary, even in frozen mode" do
200-
bundle "install --verbose", env: { "BUNDLE_FROZEN" => "true" }
201-
expect(out).to include("Installing nokogiri 1.3.10")
206+
bundle "install", env: { "BUNDLE_FROZEN" => "true" }
207+
expect(the_bundle).to include_gem("nokogiri 1.3.10")
208+
expect(the_bundle).not_to include_gem("nokogiri 1.3.10 arm64-darwin")
209+
end
210+
end
211+
212+
context "when platform-specific gem has incompatible required_ruby_version" do
213+
# Key insight: candidate_platforms tries [target, platform, ruby] in order.
214+
# Ruby platform is last since it requires compilation, but works when
215+
# precompiled gems are incompatible with the current Ruby version.
216+
#
217+
# Note: This fix requires the lockfile to include both ruby and platform-
218+
# specific variants (typical after `bundle lock --add-platform`). If the
219+
# lockfile only has platform-specific gems, frozen mode cannot help because
220+
# Bundler.setup would still expect the locked (incompatible) gem.
221+
222+
# Exercises the exact spec path (use_exact_resolved_specifications? = true)
223+
# because lockfile has platform-specific entry as most_specific_locked_platform
224+
it "falls back to ruby platform in frozen mode when lockfile includes both variants" do
225+
build_repo4 do
226+
build_gem "nokogiri", "1.18.10"
227+
build_gem "nokogiri", "1.18.10" do |s|
228+
s.platform = "x86_64-linux"
229+
s.required_ruby_version = "< #{Gem.ruby_version}"
230+
end
231+
end
232+
233+
gemfile <<~G
234+
source "https://gem.repo4"
235+
236+
gem "nokogiri"
237+
G
238+
239+
# Lockfile has both ruby and platform-specific gem (typical after `bundle lock --add-platform`)
240+
lockfile <<-L
241+
GEM
242+
remote: https://gem.repo4/
243+
specs:
244+
nokogiri (1.18.10)
245+
nokogiri (1.18.10-x86_64-linux)
246+
247+
PLATFORMS
248+
ruby
249+
x86_64-linux
250+
251+
DEPENDENCIES
252+
nokogiri
253+
254+
BUNDLED WITH
255+
#{Bundler::VERSION}
256+
L
257+
258+
simulate_platform "x86_64-linux" do
259+
bundle "install", env: { "BUNDLE_FROZEN" => "true" }
260+
expect(the_bundle).to include_gem("nokogiri 1.18.10")
261+
expect(the_bundle).not_to include_gem("nokogiri 1.18.10 x86_64-linux")
262+
end
202263
end
203264
end
204265

bundler/spec/realworld/fixtures/tapioca/Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,4 @@ DEPENDENCIES
4646
tapioca
4747

4848
BUNDLED WITH
49-
4.0.2
49+
4.0.3

bundler/spec/realworld/fixtures/warbler/Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,4 @@ DEPENDENCIES
3636
warbler!
3737

3838
BUNDLED WITH
39-
4.0.2
39+
4.0.3

0 commit comments

Comments
 (0)