summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/bundler/lazy_specification.rb57
-rw-r--r--lib/bundler/version.rb2
-rw-r--r--lib/rubygems.rb2
-rw-r--r--lib/rubygems/psych_tree.rb2
-rw-r--r--lib/rubygems/request_set/lockfile.rb2
-rw-r--r--lib/rubygems/security/policy.rb2
-rw-r--r--lib/rubygems/source.rb2
-rw-r--r--spec/bundler/bundler/plugin/events_spec.rb12
-rw-r--r--spec/bundler/bundler/plugin_spec.rb8
-rw-r--r--spec/bundler/bundler/uri_normalizer_spec.rb25
-rw-r--r--spec/bundler/install/gemfile/specific_platform_spec.rb69
-rw-r--r--spec/bundler/realworld/fixtures/tapioca/Gemfile.lock2
-rw-r--r--spec/bundler/realworld/fixtures/warbler/Gemfile.lock2
-rw-r--r--spec/bundler/support/windows_tag_group.rb1
-rw-r--r--test/rubygems/test_gem_commands_install_command.rb2
-rw-r--r--tool/bundler/dev_gems.rb.lock2
-rw-r--r--tool/bundler/rubocop_gems.rb.lock2
-rw-r--r--tool/bundler/standard_gems.rb.lock2
-rw-r--r--tool/bundler/test_gems.rb.lock2
19 files changed, 164 insertions, 34 deletions
diff --git a/lib/bundler/lazy_specification.rb b/lib/bundler/lazy_specification.rb
index 81ded54797..786dbcae65 100644
--- a/lib/bundler/lazy_specification.rb
+++ b/lib/bundler/lazy_specification.rb
@@ -138,24 +138,16 @@ module Bundler
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
@@ -190,6 +182,39 @@ module Bundler
!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
diff --git a/lib/bundler/version.rb b/lib/bundler/version.rb
index ccb6ecab38..732b4a0563 100644
--- a/lib/bundler/version.rb
+++ b/lib/bundler/version.rb
@@ -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
diff --git a/lib/rubygems.rb b/lib/rubygems.rb
index a5d6c7fb66..e99176fec0 100644
--- a/lib/rubygems.rb
+++ b/lib/rubygems.rb
@@ -9,7 +9,7 @@
require "rbconfig"
module Gem
- VERSION = "4.0.2"
+ VERSION = "4.0.3"
end
require_relative "rubygems/defaults"
diff --git a/lib/rubygems/psych_tree.rb b/lib/rubygems/psych_tree.rb
index 24857adb9d..8b4c425a33 100644
--- a/lib/rubygems/psych_tree.rb
+++ b/lib/rubygems/psych_tree.rb
@@ -22,7 +22,7 @@ module Gem
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")
diff --git a/lib/rubygems/request_set/lockfile.rb b/lib/rubygems/request_set/lockfile.rb
index c446b3ae51..da6dd329bc 100644
--- a/lib/rubygems/request_set/lockfile.rb
+++ b/lib/rubygems/request_set/lockfile.rb
@@ -38,7 +38,7 @@ class Gem::RequestSet::Lockfile
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)
diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb
index 7b86ac5763..128958ab80 100644
--- a/lib/rubygems/security/policy.rb
+++ b/lib/rubygems/security/policy.rb
@@ -143,7 +143,7 @@ class Gem::Security::Policy
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)
diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb
index 8b031e27a8..f203b7312b 100644
--- a/lib/rubygems/source.rb
+++ b/lib/rubygems/source.rb
@@ -102,7 +102,7 @@ class Gem::Source
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
diff --git a/spec/bundler/bundler/plugin/events_spec.rb b/spec/bundler/bundler/plugin/events_spec.rb
index 28d70c6fdd..77e5fdb74c 100644
--- a/spec/bundler/bundler/plugin/events_spec.rb
+++ b/spec/bundler/bundler/plugin/events_spec.rb
@@ -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
diff --git a/spec/bundler/bundler/plugin_spec.rb b/spec/bundler/bundler/plugin_spec.rb
index fea3925000..e416772a36 100644
--- a/spec/bundler/bundler/plugin_spec.rb
+++ b/spec/bundler/bundler/plugin_spec.rb
@@ -279,6 +279,7 @@ RSpec.describe Bundler::Plugin do
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")
@@ -291,6 +292,13 @@ RSpec.describe Bundler::Plugin do
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
diff --git a/spec/bundler/bundler/uri_normalizer_spec.rb b/spec/bundler/bundler/uri_normalizer_spec.rb
new file mode 100644
index 0000000000..1308e86014
--- /dev/null
+++ b/spec/bundler/bundler/uri_normalizer_spec.rb
@@ -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
diff --git a/spec/bundler/install/gemfile/specific_platform_spec.rb b/spec/bundler/install/gemfile/specific_platform_spec.rb
index 39d2700474..448800d578 100644
--- a/spec/bundler/install/gemfile/specific_platform_spec.rb
+++ b/spec/bundler/install/gemfile/specific_platform_spec.rb
@@ -157,6 +157,11 @@ RSpec.describe "bundle install with specific platforms" do
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"
@@ -192,13 +197,69 @@ RSpec.describe "bundle install with specific platforms" do
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
diff --git a/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock
index d6734a6569..2db720a69f 100644
--- a/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock
+++ b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock
@@ -46,4 +46,4 @@ DEPENDENCIES
tapioca
BUNDLED WITH
- 4.0.2
+ 4.0.3
diff --git a/spec/bundler/realworld/fixtures/warbler/Gemfile.lock b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock
index 012f3fee97..c37fbbb7ee 100644
--- a/spec/bundler/realworld/fixtures/warbler/Gemfile.lock
+++ b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock
@@ -36,4 +36,4 @@ DEPENDENCIES
warbler!
BUNDLED WITH
- 4.0.2
+ 4.0.3
diff --git a/spec/bundler/support/windows_tag_group.rb b/spec/bundler/support/windows_tag_group.rb
index 8eb0a749da..c41c446462 100644
--- a/spec/bundler/support/windows_tag_group.rb
+++ b/spec/bundler/support/windows_tag_group.rb
@@ -184,6 +184,7 @@ module Spec
"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
diff --git a/test/rubygems/test_gem_commands_install_command.rb b/test/rubygems/test_gem_commands_install_command.rb
index d2ca933a63..72ca9d8262 100644
--- a/test/rubygems/test_gem_commands_install_command.rb
+++ b/test/rubygems/test_gem_commands_install_command.rb
@@ -1610,7 +1610,7 @@ ERROR: Possible alternatives: non_existent_with_hint
gem_make_out = File.read(File.join(gemspec.extension_dir, "gem_make.out"))
if vc_windows? && nmake_found?
- refute_includes(gem_make_out, " -j4")
+ refute_includes(gem_make_out, "-j4")
else
assert_includes(gem_make_out, "make -j4")
end
diff --git a/tool/bundler/dev_gems.rb.lock b/tool/bundler/dev_gems.rb.lock
index 30e2978270..fff9cfe70c 100644
--- a/tool/bundler/dev_gems.rb.lock
+++ b/tool/bundler/dev_gems.rb.lock
@@ -129,4 +129,4 @@ CHECKSUMS
turbo_tests (2.2.5) sha256=3fa31497d12976d11ccc298add29107b92bda94a90d8a0a5783f06f05102509f
BUNDLED WITH
- 4.0.2
+ 4.0.3
diff --git a/tool/bundler/rubocop_gems.rb.lock b/tool/bundler/rubocop_gems.rb.lock
index ca3f816b7f..70704bfc38 100644
--- a/tool/bundler/rubocop_gems.rb.lock
+++ b/tool/bundler/rubocop_gems.rb.lock
@@ -156,4 +156,4 @@ CHECKSUMS
unicode-emoji (4.1.0) sha256=4997d2d5df1ed4252f4830a9b6e86f932e2013fbff2182a9ce9ccabda4f325a5
BUNDLED WITH
- 4.0.2
+ 4.0.3
diff --git a/tool/bundler/standard_gems.rb.lock b/tool/bundler/standard_gems.rb.lock
index 1e31691b7f..669e5492a8 100644
--- a/tool/bundler/standard_gems.rb.lock
+++ b/tool/bundler/standard_gems.rb.lock
@@ -176,4 +176,4 @@ CHECKSUMS
unicode-emoji (4.1.0) sha256=4997d2d5df1ed4252f4830a9b6e86f932e2013fbff2182a9ce9ccabda4f325a5
BUNDLED WITH
- 4.0.2
+ 4.0.3
diff --git a/tool/bundler/test_gems.rb.lock b/tool/bundler/test_gems.rb.lock
index a257f8f8bc..d8f7d77122 100644
--- a/tool/bundler/test_gems.rb.lock
+++ b/tool/bundler/test_gems.rb.lock
@@ -103,4 +103,4 @@ CHECKSUMS
tilt (2.6.1) sha256=35a99bba2adf7c1e362f5b48f9b581cce4edfba98117e34696dde6d308d84770
BUNDLED WITH
- 4.0.2
+ 4.0.3