Skip to content

Commit e6bd3cb

Browse files
Merge pull request #7035 from rubygems/multiplatform-resolve-fix
Raise an error when adding a gem incompatible with some locked platform
2 parents 45d6818 + 25304f3 commit e6bd3cb

File tree

6 files changed

+168
-21
lines changed

6 files changed

+168
-21
lines changed

bundler/lib/bundler/definition.rb

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -452,8 +452,8 @@ def validate_platforms!
452452
return if current_platform_locked?
453453

454454
raise ProductionError, "Your bundle only supports platforms #{@platforms.map(&:to_s)} " \
455-
"but your local platform is #{Bundler.local_platform}. " \
456-
"Add the current platform to the lockfile with\n`bundle lock --add-platform #{Bundler.local_platform}` and try again."
455+
"but your local platform is #{local_platform}. " \
456+
"Add the current platform to the lockfile with\n`bundle lock --add-platform #{local_platform}` and try again."
457457
end
458458

459459
def add_platform(platform)
@@ -509,7 +509,7 @@ def dependencies_with_bundler
509509
def resolution_packages
510510
@resolution_packages ||= begin
511511
last_resolve = converge_locked_specs
512-
remove_ruby_from_platforms_if_necessary!(current_dependencies)
512+
remove_invalid_platforms!(current_dependencies)
513513
packages = Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, :locked_specs => @originally_locked_specs, :unlock => @unlock[:gems], :prerelease => gem_version_promoter.pre?)
514514
additional_base_requirements_for_resolve(packages, last_resolve)
515515
end
@@ -600,7 +600,7 @@ def current_ruby_platform_locked?
600600

601601
def current_platform_locked?
602602
@platforms.any? do |bundle_platform|
603-
MatchPlatform.platforms_match?(bundle_platform, Bundler.local_platform)
603+
MatchPlatform.platforms_match?(bundle_platform, local_platform)
604604
end
605605
end
606606

@@ -956,17 +956,19 @@ def additional_base_requirements_for_resolve(resolution_packages, last_resolve)
956956
resolution_packages
957957
end
958958

959-
def remove_ruby_from_platforms_if_necessary!(dependencies)
960-
return if Bundler.frozen_bundle? ||
961-
Bundler.local_platform == Gem::Platform::RUBY ||
962-
!platforms.include?(Gem::Platform::RUBY) ||
963-
(@new_platform && platforms.last == Gem::Platform::RUBY) ||
959+
def remove_invalid_platforms!(dependencies)
960+
return if Bundler.frozen_bundle?
961+
962+
platforms.each do |platform|
963+
next if local_platform == platform ||
964+
(@new_platform && platforms.last == platform) ||
964965
@path_changes ||
965966
@dependency_changes ||
966-
!@originally_locked_specs.incomplete_ruby_specs?(dependencies)
967+
!@originally_locked_specs.incomplete_for_platform?(dependencies, platform)
967968

968-
remove_platform(Gem::Platform::RUBY)
969-
add_current_platform
969+
remove_platform(platform)
970+
add_current_platform if platform == Gem::Platform::RUBY
971+
end
970972
end
971973

972974
def source_map

bundler/lib/bundler/resolver.rb

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,22 @@ def all_versions_for(package)
248248
results = filter_matching_specs(results, locked_requirement) if locked_requirement
249249

250250
versions = results.group_by(&:version).reduce([]) do |groups, (version, specs)|
251-
platform_specs = package.platforms.flat_map {|platform| select_best_platform_match(specs, platform) }
252-
next groups if platform_specs.empty?
251+
platform_specs = package.platforms.map {|platform| select_best_platform_match(specs, platform) }
252+
253+
# If package is a top-level dependency,
254+
# candidate is only valid if there are matching versions for all resolution platforms.
255+
#
256+
# If package is not a top-level deependency,
257+
# then it's not necessary that it has matching versions for all platforms, since it may have been introduced only as
258+
# a dependency for a platform specific variant, so it will only need to have a valid version for that platform.
259+
#
260+
if package.top_level?
261+
next groups if platform_specs.any?(&:empty?)
262+
else
263+
next groups if platform_specs.all?(&:empty?)
264+
end
265+
266+
platform_specs.flatten!
253267

254268
ruby_specs = select_best_platform_match(specs, Gem::Platform::RUBY)
255269
groups << Resolver::Candidate.new(version, :specs => ruby_specs) if ruby_specs.any?
@@ -295,15 +309,21 @@ def raise_not_found!(package)
295309
end
296310
specs_matching_requirement = filter_matching_specs(specs, package.dependency.requirement)
297311

298-
if specs_matching_requirement.any?
312+
not_found_message = if specs_matching_requirement.any?
299313
specs = specs_matching_requirement
300314
matching_part = requirement_label
301315
platforms = package.platforms
302-
platform_label = platforms.size == 1 ? "platform '#{platforms.first}" : "platforms '#{platforms.join("', '")}"
303-
requirement_label = "#{requirement_label}' with #{platform_label}"
316+
317+
if platforms.size == 1
318+
"Could not find gem '#{requirement_label}' with platform '#{platforms.first}'"
319+
else
320+
"Could not find gems matching '#{requirement_label}' valid for all resolution platforms (#{platforms.join(", ")})"
321+
end
322+
else
323+
"Could not find gem '#{requirement_label}'"
304324
end
305325

306-
message = String.new("Could not find gem '#{requirement_label}' in #{source}#{cache_message}.\n")
326+
message = String.new("#{not_found_message} in #{source}#{cache_message}.\n")
307327

308328
if specs.any?
309329
message << "\n#{other_specs_matching_message(specs, matching_part)}"

bundler/lib/bundler/resolver/package.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def initialize(name, platforms, locked_specs:, unlock:, prerelease: false, depen
2121
@locked_version = locked_specs[name].first&.version
2222
@unlock = unlock
2323
@dependency = dependency || Dependency.new(name, @locked_version)
24+
@top_level = !dependency.nil?
2425
@prerelease = @dependency.prerelease? || @locked_version&.prerelease? || prerelease ? :consider_first : :ignore
2526
end
2627

@@ -32,6 +33,10 @@ def root?
3233
false
3334
end
3435

36+
def top_level?
37+
@top_level
38+
end
39+
3540
def meta?
3641
@name.end_with?("\0")
3742
end

bundler/lib/bundler/spec_set.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ def materialized_for_all_platforms
100100
end
101101
end
102102

103-
def incomplete_ruby_specs?(deps)
103+
def incomplete_for_platform?(deps, platform)
104104
return false if @specs.empty?
105105

106106
@incomplete_specs = []
107107

108-
self.for(deps, true, [Gem::Platform::RUBY])
108+
self.for(deps, true, [platform])
109109

110110
@incomplete_specs.any?
111111
end

bundler/spec/install/gemfile/specific_platform_spec.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,70 @@
685685
L
686686
end
687687

688+
it "automatically fixes the lockfile if multiple platforms locked, but no valid versions of direct dependencies for all of them" do
689+
simulate_platform "x86_64-linux" do
690+
build_repo4 do
691+
build_gem "nokogiri", "1.14.0" do |s|
692+
s.platform = "x86_64-linux"
693+
end
694+
build_gem "nokogiri", "1.14.0" do |s|
695+
s.platform = "arm-linux"
696+
end
697+
698+
build_gem "sorbet-static", "0.5.10696" do |s|
699+
s.platform = "x86_64-linux"
700+
end
701+
end
702+
703+
gemfile <<~G
704+
source "#{file_uri_for(gem_repo4)}"
705+
706+
gem "nokogiri"
707+
gem "sorbet-static"
708+
G
709+
710+
lockfile <<~L
711+
GEM
712+
remote: #{file_uri_for(gem_repo4)}/
713+
specs:
714+
nokogiri (1.14.0-arm-linux)
715+
nokogiri (1.14.0-x86_64-linux)
716+
sorbet-static (0.5.10696-x86_64-linux)
717+
718+
PLATFORMS
719+
arm-linux
720+
x86_64-linux
721+
722+
DEPENDENCIES
723+
nokogiri
724+
sorbet-static
725+
726+
BUNDLED WITH
727+
#{Bundler::VERSION}
728+
L
729+
730+
bundle "update"
731+
732+
expect(lockfile).to eq <<~L
733+
GEM
734+
remote: #{file_uri_for(gem_repo4)}/
735+
specs:
736+
nokogiri (1.14.0-x86_64-linux)
737+
sorbet-static (0.5.10696-x86_64-linux)
738+
739+
PLATFORMS
740+
x86_64-linux
741+
742+
DEPENDENCIES
743+
nokogiri
744+
sorbet-static
745+
746+
BUNDLED WITH
747+
#{Bundler::VERSION}
748+
L
749+
end
750+
end
751+
688752
it "automatically fixes the lockfile without removing other variants if it's missing platform gems, but they are installed locally" do
689753
simulate_platform "x86_64-darwin-21" do
690754
build_repo4 do

bundler/spec/install/gems/resolving_spec.rb

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@
416416
end
417417

418418
nice_error = <<~E.strip
419-
Could not find gem 'sorbet-static (= 0.5.10554)' with platforms 'arm64-darwin-21', 'aarch64-linux' in rubygems repository #{file_uri_for(gem_repo4)}/ or installed locally.
419+
Could not find gems matching 'sorbet-static (= 0.5.10554)' valid for all resolution platforms (arm64-darwin-21, aarch64-linux) in rubygems repository #{file_uri_for(gem_repo4)}/ or installed locally.
420420
421421
The source contains the following gems matching 'sorbet-static (= 0.5.10554)':
422422
* sorbet-static-0.5.10554-universal-darwin-21
@@ -425,6 +425,62 @@
425425
end
426426
end
427427

428+
context "when adding a new gem that does not resolve under all locked platforms" do
429+
before do
430+
simulate_platform "x86_64-linux" do
431+
build_repo4 do
432+
build_gem "nokogiri", "1.14.0" do |s|
433+
s.platform = "x86_64-linux"
434+
end
435+
build_gem "nokogiri", "1.14.0" do |s|
436+
s.platform = "arm-linux"
437+
end
438+
439+
build_gem "sorbet-static", "0.5.10696" do |s|
440+
s.platform = "x86_64-linux"
441+
end
442+
end
443+
444+
lockfile <<~L
445+
GEM
446+
remote: #{file_uri_for(gem_repo4)}/
447+
specs:
448+
nokogiri (1.14.0-arm-linux)
449+
nokogiri (1.14.0-x86_64-linux)
450+
451+
PLATFORMS
452+
arm-linux
453+
x86_64-linux
454+
455+
DEPENDENCIES
456+
nokogiri
457+
458+
BUNDLED WITH
459+
#{Bundler::VERSION}
460+
L
461+
462+
gemfile <<~G
463+
source "#{file_uri_for(gem_repo4)}"
464+
465+
gem "nokogiri"
466+
gem "sorbet-static"
467+
G
468+
469+
bundle "lock", :raise_on_error => false
470+
end
471+
end
472+
473+
it "raises a proper error" do
474+
nice_error = <<~E.strip
475+
Could not find gems matching 'sorbet-static' valid for all resolution platforms (arm-linux, x86_64-linux) in rubygems repository #{file_uri_for(gem_repo4)}/ or installed locally.
476+
477+
The source contains the following gems matching 'sorbet-static':
478+
* sorbet-static-0.5.10696-x86_64-linux
479+
E
480+
expect(err).to end_with(nice_error)
481+
end
482+
end
483+
428484
it "gives a meaningful error on ruby version mismatches between dependencies" do
429485
build_repo4 do
430486
build_gem "requires-old-ruby" do |s|

0 commit comments

Comments
 (0)