Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,8 @@ The following default gem is added.

The following default gems are updated.

* RubyGems 4.0.2
* bundler 4.0.2
* RubyGems 4.0.3
* bundler 4.0.3
* date 3.5.1
* delegate 0.6.1
* digest 3.2.1
Expand Down Expand Up @@ -353,8 +353,8 @@ The following bundled gems are updated.
* net-smtp 0.5.1
* matrix 0.4.3
* prime 0.1.4
* rbs 3.10.0.pre.2
* typeprof 0.31.0
* rbs 3.10.0
* typeprof 0.31.1
* debug 1.11.1
* base64 0.3.0
* bigdecimal 4.0.1
Expand All @@ -371,6 +371,7 @@ see the following links for details.
* [4.0.0 Released - RubyGems Blog](https://blog.rubygems.org/2025/12/03/4.0.0-released.html)
* [4.0.1 Released - RubyGems Blog](https://blog.rubygems.org/2025/12/09/4.0.1-released.html)
* [4.0.2 Released - RubyGems Blog](https://blog.rubygems.org/2025/12/17/4.0.2-released.html)
* [4.0.3 Released - RubyGems Blog](https://blog.rubygems.org/2025/12/23/4.0.3-released.html)

## Supported platforms

Expand Down
2 changes: 1 addition & 1 deletion doc/language/box.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ yay #=> "foo"
box = Ruby::Box.new
box.require('foo')

box.Foo.say #=> "foo"
box::Foo.say #=> "foo"

yay # NoMethodError
```
Expand Down
4 changes: 2 additions & 2 deletions gems/bundled_gems
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ net-pop 0.1.2 https://github.com/ruby/net-pop
net-smtp 0.5.1 https://github.com/ruby/net-smtp
matrix 0.4.3 https://github.com/ruby/matrix
prime 0.1.4 https://github.com/ruby/prime
rbs 3.10.0.pre.2 https://github.com/ruby/rbs badcb9165b52c1b7ccaa6251e4d5bbd78329c5a7
typeprof 0.31.0 https://github.com/ruby/typeprof
rbs 3.10.0 https://github.com/ruby/rbs
typeprof 0.31.1 https://github.com/ruby/typeprof
debug 1.11.1 https://github.com/ruby/debug
racc 1.8.1 https://github.com/ruby/racc
mutex_m 0.3.0 https://github.com/ruby/mutex_m
Expand Down
57 changes: 41 additions & 16 deletions lib/bundler/lazy_specification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,24 +138,16 @@ def materialize_for_installation
source.local!

if use_exact_resolved_specifications?
materialize(self) do |matching_specs|
choose_compatible(matching_specs)
end
else
materialize([name, version]) do |matching_specs|
target_platform = source.is_a?(Source::Path) ? platform : Bundler.local_platform

installable_candidates = MatchPlatform.select_best_platform_match(matching_specs, target_platform)

specification = choose_compatible(installable_candidates, fallback_to_non_installable: false)
return specification unless specification.nil?
spec = materialize(self) {|specs| choose_compatible(specs, fallback_to_non_installable: false) }
return spec if spec

if target_platform != platform
installable_candidates = MatchPlatform.select_best_platform_match(matching_specs, platform)
end

choose_compatible(installable_candidates)
# Exact spec is incompatible; in frozen mode, try to find a compatible platform variant
# In non-frozen mode, return nil to trigger re-resolution and lockfile update
if Bundler.frozen_bundle?
materialize([name, version]) {|specs| resolve_best_platform(specs) }
end
else
materialize([name, version]) {|specs| resolve_best_platform(specs) }
end
end

Expand Down Expand Up @@ -190,6 +182,39 @@ def use_exact_resolved_specifications?
!source.is_a?(Source::Path) && ruby_platform_materializes_to_ruby_platform?
end

# Try platforms in order of preference until finding a compatible spec.
# Used for legacy lockfiles and as a fallback when the exact locked spec
# is incompatible. Falls back to frozen bundle behavior if none match.
def resolve_best_platform(specs)
find_compatible_platform_spec(specs) || frozen_bundle_fallback(specs)
end

def find_compatible_platform_spec(specs)
candidate_platforms.each do |plat|
candidates = MatchPlatform.select_best_platform_match(specs, plat)
spec = choose_compatible(candidates, fallback_to_non_installable: false)
return spec if spec
end
nil
end

# Platforms to try in order of preference. Ruby platform is last since it
# requires compilation, but works when precompiled gems are incompatible.
def candidate_platforms
target = source.is_a?(Source::Path) ? platform : Bundler.local_platform
[target, platform, Gem::Platform::RUBY].uniq
end

# In frozen mode, accept any candidate. Will error at install time.
# When target differs from locked platform, prefer locked platform's candidates
# to preserve lockfile integrity.
def frozen_bundle_fallback(specs)
target = source.is_a?(Source::Path) ? platform : Bundler.local_platform
fallback_platform = target == platform ? target : platform
candidates = MatchPlatform.select_best_platform_match(specs, fallback_platform)
choose_compatible(candidates)
end

def ruby_platform_materializes_to_ruby_platform?
generic_platform = Bundler.generic_local_platform == Gem::Platform::JAVA ? Gem::Platform::JAVA : Gem::Platform::RUBY

Expand Down
2 changes: 1 addition & 1 deletion lib/bundler/version.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: false

module Bundler
VERSION = "4.0.2".freeze
VERSION = "4.0.3".freeze

def self.bundler_major_version
@bundler_major_version ||= gem_version.segments.first
Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
require "rbconfig"

module Gem
VERSION = "4.0.2"
VERSION = "4.0.3"
end

require_relative "rubygems/defaults"
Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems/psych_tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def visit_Hash(o)
def register(target, obj)
end

# This is ported over from the yaml_tree in 1.9.3
# This is ported over from the YAMLTree implementation in Ruby 1.9.3
def format_time(time)
if time.utc?
time.strftime("%Y-%m-%d %H:%M:%S.%9N Z")
Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems/request_set/lockfile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def initialize(message, column, line, path)
end

##
# Creates a new Lockfile for the given +request_set+ and +gem_deps_file+
# Creates a new Lockfile for the given Gem::RequestSet and +gem_deps_file+
# location.

def self.build(request_set, gem_deps_file, dependencies = nil)
Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems/security/policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def check_root(chain, time)
end

##
# Ensures the root of +chain+ has a trusted certificate in +trust_dir+ and
# Ensures the root of +chain+ has a trusted certificate in Gem::Security.trust_dir and
# the digests of the two certificates match according to +digester+

def check_trust(chain, digester, trust_dir)
Expand Down
2 changes: 1 addition & 1 deletion lib/rubygems/source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def update_cache?
end

##
# Fetches a specification for the given +name_tuple+.
# Fetches a specification for the given Gem::NameTuple.

def fetch_spec(name_tuple)
fetcher = Gem::RemoteFetcher.fetcher
Expand Down
12 changes: 11 additions & 1 deletion spec/bundler/bundler/plugin/events_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

RSpec.describe Bundler::Plugin::Events do
context "plugin events" do
before { Bundler::Plugin::Events.send :reset }
before do
@old_constants = Bundler::Plugin::Events.constants.map {|name| [name, Bundler::Plugin::Events.const_get(name)] }
Bundler::Plugin::Events.send :reset
end

after do
Bundler::Plugin::Events.send(:reset)
Hash[@old_constants].each do |name, value|
Bundler::Plugin::Events.send(:define, name, value)
end
end

describe "#define" do
it "raises when redefining a constant" do
Expand Down
8 changes: 8 additions & 0 deletions spec/bundler/bundler/plugin_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@
s.write "plugins.rb", code
end

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

after do
Bundler::Plugin::Events.send(:reset)
Hash[@old_constants].each do |name, value|
Bundler::Plugin::Events.send(:define, name, value)
end
end

let(:code) { <<-RUBY }
Bundler::Plugin::API.hook("event-1") { puts "hook for event 1" }
RUBY
Expand Down
25 changes: 25 additions & 0 deletions spec/bundler/bundler/uri_normalizer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

RSpec.describe Bundler::URINormalizer do
describe ".normalize_suffix" do
context "when trailing_slash is true" do
it "adds a trailing slash when missing" do
expect(described_class.normalize_suffix("https://example.com", trailing_slash: true)).to eq("https://example.com/")
end

it "keeps the trailing slash when present" do
expect(described_class.normalize_suffix("https://example.com/", trailing_slash: true)).to eq("https://example.com/")
end
end

context "when trailing_slash is false" do
it "removes a trailing slash when present" do
expect(described_class.normalize_suffix("https://example.com/", trailing_slash: false)).to eq("https://example.com")
end

it "keeps the value unchanged when no trailing slash exists" do
expect(described_class.normalize_suffix("https://example.com", trailing_slash: false)).to eq("https://example.com")
end
end
end
end
69 changes: 65 additions & 4 deletions spec/bundler/install/gemfile/specific_platform_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@
end

context "when running on a legacy lockfile locked only to ruby" do
# Exercises the legacy lockfile path (use_exact_resolved_specifications? = false)
# because most_specific_locked_platform is ruby, matching the generic platform.
# Key insight: when target (arm64-darwin-22) != platform (ruby), the code tries
# both platforms before falling back, preserving lockfile integrity.

around do |example|
build_repo4 do
build_gem "nokogiri", "1.3.10"
Expand Down Expand Up @@ -192,13 +197,69 @@
end

it "still installs the generic ruby variant if necessary" do
bundle "install --verbose"
expect(out).to include("Installing nokogiri 1.3.10")
bundle "install"
expect(the_bundle).to include_gem("nokogiri 1.3.10")
expect(the_bundle).not_to include_gem("nokogiri 1.3.10 arm64-darwin")
end

it "still installs the generic ruby variant if necessary, even in frozen mode" do
bundle "install --verbose", env: { "BUNDLE_FROZEN" => "true" }
expect(out).to include("Installing nokogiri 1.3.10")
bundle "install", env: { "BUNDLE_FROZEN" => "true" }
expect(the_bundle).to include_gem("nokogiri 1.3.10")
expect(the_bundle).not_to include_gem("nokogiri 1.3.10 arm64-darwin")
end
end

context "when platform-specific gem has incompatible required_ruby_version" do
# Key insight: candidate_platforms tries [target, platform, ruby] in order.
# Ruby platform is last since it requires compilation, but works when
# precompiled gems are incompatible with the current Ruby version.
#
# Note: This fix requires the lockfile to include both ruby and platform-
# specific variants (typical after `bundle lock --add-platform`). If the
# lockfile only has platform-specific gems, frozen mode cannot help because
# Bundler.setup would still expect the locked (incompatible) gem.

# Exercises the exact spec path (use_exact_resolved_specifications? = true)
# because lockfile has platform-specific entry as most_specific_locked_platform
it "falls back to ruby platform in frozen mode when lockfile includes both variants" do
build_repo4 do
build_gem "nokogiri", "1.18.10"
build_gem "nokogiri", "1.18.10" do |s|
s.platform = "x86_64-linux"
s.required_ruby_version = "< #{Gem.ruby_version}"
end
end

gemfile <<~G
source "https://gem.repo4"

gem "nokogiri"
G

# Lockfile has both ruby and platform-specific gem (typical after `bundle lock --add-platform`)
lockfile <<-L
GEM
remote: https://gem.repo4/
specs:
nokogiri (1.18.10)
nokogiri (1.18.10-x86_64-linux)

PLATFORMS
ruby
x86_64-linux

DEPENDENCIES
nokogiri

BUNDLED WITH
#{Bundler::VERSION}
L

simulate_platform "x86_64-linux" do
bundle "install", env: { "BUNDLE_FROZEN" => "true" }
expect(the_bundle).to include_gem("nokogiri 1.18.10")
expect(the_bundle).not_to include_gem("nokogiri 1.18.10 x86_64-linux")
end
end
end

Expand Down
2 changes: 1 addition & 1 deletion spec/bundler/realworld/fixtures/tapioca/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ DEPENDENCIES
tapioca

BUNDLED WITH
4.0.2
4.0.3
2 changes: 1 addition & 1 deletion spec/bundler/realworld/fixtures/warbler/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ DEPENDENCIES
warbler!

BUNDLED WITH
4.0.2
4.0.3
1 change: 1 addition & 0 deletions spec/bundler/support/windows_tag_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ module WindowsTagGroup
"spec/bundler/resolver/candidate_spec.rb",
"spec/bundler/digest_spec.rb",
"spec/bundler/fetcher/gem_remote_fetcher_spec.rb",
"spec/bundler/uri_normalizer_spec.rb",
],
}.freeze
end
Expand Down
2 changes: 1 addition & 1 deletion tool/bundler/dev_gems.rb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,4 @@ CHECKSUMS
turbo_tests (2.2.5) sha256=3fa31497d12976d11ccc298add29107b92bda94a90d8a0a5783f06f05102509f

BUNDLED WITH
4.0.2
4.0.3
2 changes: 1 addition & 1 deletion tool/bundler/rubocop_gems.rb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,4 @@ CHECKSUMS
unicode-emoji (4.1.0) sha256=4997d2d5df1ed4252f4830a9b6e86f932e2013fbff2182a9ce9ccabda4f325a5

BUNDLED WITH
4.0.2
4.0.3
2 changes: 1 addition & 1 deletion tool/bundler/standard_gems.rb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,4 @@ CHECKSUMS
unicode-emoji (4.1.0) sha256=4997d2d5df1ed4252f4830a9b6e86f932e2013fbff2182a9ce9ccabda4f325a5

BUNDLED WITH
4.0.2
4.0.3
2 changes: 1 addition & 1 deletion tool/bundler/test_gems.rb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,4 @@ CHECKSUMS
tilt (2.6.1) sha256=35a99bba2adf7c1e362f5b48f9b581cce4edfba98117e34696dde6d308d84770

BUNDLED WITH
4.0.2
4.0.3
4 changes: 0 additions & 4 deletions tool/rbs_skip_tests
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,3 @@ test_linear_time?(RegexpSingletonTest)
test_new(RegexpSingletonTest)

## Failed tests caused by unreleased version of Ruby

# BigDecimal v4.0.0 changed behavior
test_divmod(BigDecimalTest)
test_precs(BigDecimalTest)