Skip to content
Open
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
57 changes: 41 additions & 16 deletions bundler/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
69 changes: 65 additions & 4 deletions bundler/spec/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