Move most of Bundler::GemHelpers to Gem::Platform

This will help centralize wheel platform selection logic eventually

Signed-off-by: Samuel Giddins <segiddins@segiddins.me>
This commit is contained in:
Samuel Giddins 2025-05-18 11:37:35 -04:00 committed by Hiroshi SHIBATA
parent 6a9af9f0b5
commit c0a1e877b3
No known key found for this signature in database
GPG Key ID: F9CF13417264FAC2
22 changed files with 490 additions and 249 deletions

View File

@ -53,7 +53,6 @@ module Bundler
autoload :FeatureFlag, File.expand_path("bundler/feature_flag", __dir__)
autoload :FREEBSD, File.expand_path("bundler/constants", __dir__)
autoload :GemHelper, File.expand_path("bundler/gem_helper", __dir__)
autoload :GemHelpers, File.expand_path("bundler/gem_helpers", __dir__)
autoload :GemVersionPromoter, File.expand_path("bundler/gem_version_promoter", __dir__)
autoload :Graph, File.expand_path("bundler/graph", __dir__)
autoload :Index, File.expand_path("bundler/index", __dir__)
@ -459,6 +458,10 @@ module Bundler
Gem::Platform.local
end
def generic_local_platform
Gem::Platform.generic(local_platform)
end
def default_gemfile
SharedHelpers.default_gemfile
end

View File

@ -155,7 +155,7 @@ module Bundler
return active_spec if strict
active_specs = active_spec.source.specs.search(current_spec.name).select {|spec| spec.match_platform(current_spec.platform) }.sort_by(&:version)
active_specs = active_spec.source.specs.search(current_spec.name).select {|spec| spec.installable_on_platform?(current_spec.platform) }.sort_by(&:version)
if !current_spec.version.prerelease? && !options[:pre] && active_specs.size > 1
active_specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? }
end

View File

@ -92,7 +92,7 @@ module Bundler
locked_spec = locked_info[:spec]
new_spec = Bundler.definition.specs[name].first
unless new_spec
unless locked_spec.match_platform(Bundler.local_platform)
unless locked_spec.installable_on_platform?(Bundler.local_platform)
Bundler.ui.warn "Bundler attempted to update #{name} but it was not considered because it is for a different platform from the current one"
end

View File

@ -32,7 +32,7 @@ module Bundler
end.freeze
def ruby?
return true if Bundler::GemHelpers.generic_local_platform_is_ruby?
return true if Bundler::MatchPlatform.generic_local_platform_is_ruby?
!windows? && (RUBY_ENGINE == "ruby" || RUBY_ENGINE == "rbx" || RUBY_ENGINE == "maglev" || RUBY_ENGINE == "truffleruby")
end

View File

@ -4,8 +4,6 @@ require_relative "lockfile_parser"
module Bundler
class Definition
include GemHelpers
class << self
# Do not create or modify a lockfile (Makes #lock a noop)
attr_accessor :no_lock
@ -282,7 +280,7 @@ module Bundler
end
def filter_relevant(dependencies)
platforms_array = [generic_local_platform].freeze
platforms_array = [Bundler.generic_local_platform].freeze
dependencies.select do |d|
d.should_include? && !d.gem_platforms(platforms_array).empty?
end
@ -456,8 +454,8 @@ module Bundler
return if current_platform_locked? || @platforms.include?(Gem::Platform::RUBY)
raise ProductionError, "Your bundle only supports platforms #{@platforms.map(&:to_s)} " \
"but your local platform is #{local_platform}. " \
"Add the current platform to the lockfile with\n`bundle lock --add-platform #{local_platform}` and try again."
"but your local platform is #{Bundler.local_platform}. " \
"Add the current platform to the lockfile with\n`bundle lock --add-platform #{Bundler.local_platform}` and try again."
end
def normalize_platforms
@ -568,7 +566,7 @@ module Bundler
end
def should_add_extra_platforms?
!lockfile_exists? && generic_local_platform_is_ruby? && !Bundler.settings[:force_ruby_platform]
!lockfile_exists? && Bundler::MatchPlatform.generic_local_platform_is_ruby? && !Bundler.settings[:force_ruby_platform]
end
def lockfile_exists?
@ -632,7 +630,7 @@ module Bundler
@resolution_base ||= begin
last_resolve = converge_locked_specs
remove_invalid_platforms!
new_resolution_platforms = @current_platform_missing ? @new_platforms + [local_platform] : @new_platforms
new_resolution_platforms = @current_platform_missing ? @new_platforms + [Bundler.local_platform] : @new_platforms
base = Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: @unlocking_all || @gems_to_unlock, prerelease: gem_version_promoter.pre?, prefer_local: @prefer_local, new_platforms: new_resolution_platforms)
base = additional_base_requirements_to_prevent_downgrades(base)
base = additional_base_requirements_to_force_updates(base)
@ -738,8 +736,8 @@ module Bundler
end
def start_resolution
local_platform_needed_for_resolvability = @most_specific_non_local_locked_platform && !@platforms.include?(local_platform)
@platforms << local_platform if local_platform_needed_for_resolvability
local_platform_needed_for_resolvability = @most_specific_non_local_locked_platform && !@platforms.include?(Bundler.local_platform)
@platforms << Bundler.local_platform if local_platform_needed_for_resolvability
add_platform(Gem::Platform::RUBY) if RUBY_ENGINE == "truffleruby"
result = SpecSet.new(resolver.start)
@ -758,7 +756,7 @@ module Bundler
if result.incomplete_for_platform?(current_dependencies, @most_specific_non_local_locked_platform)
@platforms.delete(@most_specific_non_local_locked_platform)
elsif local_platform_needed_for_resolvability
@platforms.delete(local_platform)
@platforms.delete(Bundler.local_platform)
end
end
@ -777,17 +775,17 @@ module Bundler
def current_platform_locked?
@platforms.any? do |bundle_platform|
generic_local_platform == bundle_platform || local_platform === bundle_platform
Bundler.generic_local_platform == bundle_platform || Bundler.local_platform === bundle_platform
end
end
def add_current_platform
return if @platforms.include?(local_platform)
return if @platforms.include?(Bundler.local_platform)
@most_specific_non_local_locked_platform = find_most_specific_locked_platform
return if @most_specific_non_local_locked_platform
@platforms << local_platform
@platforms << Bundler.local_platform
true
end
@ -1167,7 +1165,7 @@ module Bundler
def remove_invalid_platforms!
return if Bundler.frozen_bundle?
skips = (@new_platforms + [local_platform]).uniq
skips = (@new_platforms + [Bundler.local_platform]).uniq
# We should probably avoid removing non-ruby platforms, since that means
# lockfile will no longer install on those platforms, so a error to give

View File

@ -99,7 +99,7 @@ module Bundler
return RUBY_PLATFORM_ARRAY if force_ruby_platform
return valid_platforms if platforms.empty?
valid_platforms.select {|p| expanded_platforms.include?(GemHelpers.generic(p)) }
valid_platforms.select {|p| expanded_platforms.include?(Gem::Platform.generic(p)) }
end
def expanded_platforms

View File

@ -73,7 +73,7 @@ module Bundler
case specs_by_name_and_version.size
when 1
specs = specs_by_name_and_version.values.first
spec = specs.find {|s| s.match_platform(Bundler.local_platform) } || specs.first
spec = specs.find {|s| s.installable_on_platform?(Bundler.local_platform) } || specs.first
@gemspecs << spec

View File

@ -1,144 +0,0 @@
# frozen_string_literal: true
module Bundler
module GemHelpers
GENERIC_CACHE = { Gem::Platform::RUBY => Gem::Platform::RUBY } # rubocop:disable Style/MutableConstant
GENERICS = [
Gem::Platform::JAVA,
*Gem::Platform::WINDOWS,
].freeze
def generic(p)
GENERIC_CACHE[p] ||= begin
found = GENERICS.find do |match|
p === match
end
found || Gem::Platform::RUBY
end
end
module_function :generic
def generic_local_platform
generic(local_platform)
end
module_function :generic_local_platform
def local_platform
Bundler.local_platform
end
module_function :local_platform
def generic_local_platform_is_ruby?
generic_local_platform == Gem::Platform::RUBY
end
module_function :generic_local_platform_is_ruby?
def platform_specificity_match(spec_platform, user_platform)
spec_platform = Gem::Platform.new(spec_platform)
PlatformMatch.specificity_score(spec_platform, user_platform)
end
module_function :platform_specificity_match
def select_all_platform_match(specs, platform, force_ruby: false, prefer_locked: false)
matching = if force_ruby
specs.select {|spec| spec.match_platform(Gem::Platform::RUBY) && spec.force_ruby_platform! }
else
specs.select {|spec| spec.match_platform(platform) }
end
if prefer_locked
locked_originally = matching.select {|spec| spec.is_a?(LazySpecification) }
return locked_originally if locked_originally.any?
end
matching
end
module_function :select_all_platform_match
def select_best_platform_match(specs, platform, force_ruby: false, prefer_locked: false)
matching = select_all_platform_match(specs, platform, force_ruby: force_ruby, prefer_locked: prefer_locked)
sort_and_filter_best_platform_match(matching, platform)
end
module_function :select_best_platform_match
def select_best_local_platform_match(specs, force_ruby: false)
matching = select_all_platform_match(specs, local_platform, force_ruby: force_ruby).filter_map(&:materialized_for_installation)
sort_best_platform_match(matching, local_platform)
end
module_function :select_best_local_platform_match
def sort_and_filter_best_platform_match(matching, platform)
return matching if matching.one?
exact = matching.select {|spec| spec.platform == platform }
return exact if exact.any?
sorted_matching = sort_best_platform_match(matching, platform)
exemplary_spec = sorted_matching.first
sorted_matching.take_while {|spec| same_specificity(platform, spec, exemplary_spec) && same_deps(spec, exemplary_spec) }
end
module_function :sort_and_filter_best_platform_match
def sort_best_platform_match(matching, platform)
matching.sort_by {|spec| platform_specificity_match(spec.platform, platform) }
end
module_function :sort_best_platform_match
class PlatformMatch
def self.specificity_score(spec_platform, user_platform)
return -1 if spec_platform == user_platform
return 1_000_000 if spec_platform.nil? || spec_platform == Gem::Platform::RUBY || user_platform == Gem::Platform::RUBY
os_match(spec_platform, user_platform) +
cpu_match(spec_platform, user_platform) * 10 +
platform_version_match(spec_platform, user_platform) * 100
end
def self.os_match(spec_platform, user_platform)
if spec_platform.os == user_platform.os
0
else
1
end
end
def self.cpu_match(spec_platform, user_platform)
if spec_platform.cpu == user_platform.cpu
0
elsif spec_platform.cpu == "arm" && user_platform.cpu.to_s.start_with?("arm")
0
elsif spec_platform.cpu.nil? || spec_platform.cpu == "universal"
1
else
2
end
end
def self.platform_version_match(spec_platform, user_platform)
if spec_platform.version == user_platform.version
0
elsif spec_platform.version.nil?
1
else
2
end
end
end
def same_specificity(platform, spec, exemplary_spec)
platform_specificity_match(spec.platform, platform) == platform_specificity_match(exemplary_spec.platform, platform)
end
module_function :same_specificity
def same_deps(spec, exemplary_spec)
same_runtime_deps = spec.dependencies.sort == exemplary_spec.dependencies.sort
same_metadata_deps = spec.required_ruby_version == exemplary_spec.required_ruby_version && spec.required_rubygems_version == exemplary_spec.required_rubygems_version
same_runtime_deps && same_metadata_deps
end
module_function :same_deps
end
end

View File

@ -142,15 +142,15 @@ module Bundler
end
else
materialize([name, version]) do |matching_specs|
target_platform = source.is_a?(Source::Path) ? platform : local_platform
target_platform = source.is_a?(Source::Path) ? platform : Bundler.local_platform
installable_candidates = GemHelpers.select_best_platform_match(matching_specs, target_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?
if target_platform != platform
installable_candidates = GemHelpers.select_best_platform_match(matching_specs, platform)
installable_candidates = MatchPlatform.select_best_platform_match(matching_specs, platform)
end
choose_compatible(installable_candidates)
@ -190,7 +190,7 @@ module Bundler
end
def ruby_platform_materializes_to_ruby_platform?
generic_platform = generic_local_platform == Gem::Platform::JAVA ? Gem::Platform::JAVA : Gem::Platform::RUBY
generic_platform = Bundler.generic_local_platform == Gem::Platform::JAVA ? Gem::Platform::JAVA : Gem::Platform::RUBY
(most_specific_locked_platform != generic_platform) || force_ruby_platform || Bundler.settings[:force_ruby_platform]
end

View File

@ -4,8 +4,6 @@ require_relative "shared_helpers"
module Bundler
class LockfileParser
include GemHelpers
class Position
attr_reader :line, :column
def initialize(line, column)
@ -157,7 +155,7 @@ module Bundler
end
@most_specific_locked_platform = @platforms.min_by do |bundle_platform|
platform_specificity_match(bundle_platform, local_platform)
Gem::Platform.platform_specificity_match(bundle_platform, Bundler.local_platform)
end
@specs = @specs.values.sort_by!(&:full_name).each do |spec|
spec.most_specific_locked_platform = @most_specific_locked_platform

View File

@ -1,23 +1,42 @@
# frozen_string_literal: true
require_relative "gem_helpers"
module Bundler
module MatchPlatform
include GemHelpers
def match_platform(p)
MatchPlatform.platforms_match?(platform, p)
end
def self.platforms_match?(gemspec_platform, local_platform)
return true if gemspec_platform.nil?
return true if gemspec_platform == Gem::Platform::RUBY
return true if local_platform == gemspec_platform
gemspec_platform = Gem::Platform.new(gemspec_platform)
return true if gemspec_platform === local_platform
def installable_on_platform?(target_platform) # :nodoc:
return true if [Gem::Platform::RUBY, nil, target_platform].include?(platform)
return true if Gem::Platform.new(platform) === target_platform
false
end
def self.select_best_platform_match(specs, platform, force_ruby: false, prefer_locked: false)
matching = select_all_platform_match(specs, platform, force_ruby: force_ruby, prefer_locked: prefer_locked)
Gem::Platform.sort_and_filter_best_platform_match(matching, platform)
end
def self.select_best_local_platform_match(specs, force_ruby: false)
local = Bundler.local_platform
matching = select_all_platform_match(specs, local, force_ruby: force_ruby).filter_map(&:materialized_for_installation)
Gem::Platform.sort_best_platform_match(matching, local)
end
def self.select_all_platform_match(specs, platform, force_ruby: false, prefer_locked: false)
matching = specs.select {|spec| spec.installable_on_platform?(force_ruby ? Gem::Platform::RUBY : platform) }
specs.each(&:force_ruby_platform!) if force_ruby
if prefer_locked
locked_originally = matching.select {|spec| spec.is_a?(::Bundler::LazySpecification) }
return locked_originally if locked_originally.any?
end
matching
end
def self.generic_local_platform_is_ruby?
Bundler.generic_local_platform == Gem::Platform::RUBY
end
end
end

View File

@ -22,9 +22,9 @@ module Bundler
@specs ||= if @candidates.nil?
[]
elsif platform
GemHelpers.select_best_platform_match(@candidates, platform, force_ruby: dep.force_ruby_platform)
MatchPlatform.select_best_platform_match(@candidates, platform, force_ruby: dep.force_ruby_platform)
else
GemHelpers.select_best_local_platform_match(@candidates, force_ruby: dep.force_ruby_platform || dep.default_force_ruby_platform)
MatchPlatform.select_best_local_platform_match(@candidates, force_ruby: dep.force_ruby_platform || dep.default_force_ruby_platform)
end
end

View File

@ -14,8 +14,6 @@ module Bundler
require_relative "resolver/root"
require_relative "resolver/strategy"
include GemHelpers
def initialize(base, gem_version_promoter, most_specific_locked_platform = nil)
@source_requirements = base.source_requirements
@base = base
@ -273,7 +271,7 @@ module Bundler
next groups if platform_specs.all?(&:empty?)
end
ruby_specs = select_best_platform_match(specs, Gem::Platform::RUBY)
ruby_specs = MatchPlatform.select_best_platform_match(specs, Gem::Platform::RUBY)
ruby_group = Resolver::SpecGroup.new(ruby_specs)
unless ruby_group.empty?

View File

@ -30,7 +30,7 @@ module Bundler
def platform_specs(specs)
platforms.map do |platform|
prefer_locked = @new_platforms.include?(platform) ? false : !unlock?
GemHelpers.select_best_platform_match(specs, platform, prefer_locked: prefer_locked)
MatchPlatform.select_best_platform_match(specs, platform, prefer_locked: prefer_locked)
end
end

View File

@ -52,16 +52,123 @@ module Gem
require "rubygems/platform"
class Platform
JAVA = Gem::Platform.new("java")
MSWIN = Gem::Platform.new("mswin32")
MSWIN64 = Gem::Platform.new("mswin64")
MINGW = Gem::Platform.new("x86-mingw32")
X64_MINGW_LEGACY = Gem::Platform.new("x64-mingw32")
X64_MINGW = Gem::Platform.new("x64-mingw-ucrt")
UNIVERSAL_MINGW = Gem::Platform.new("universal-mingw")
WINDOWS = [MSWIN, MSWIN64, UNIVERSAL_MINGW].flatten.freeze
X64_LINUX = Gem::Platform.new("x86_64-linux")
X64_LINUX_MUSL = Gem::Platform.new("x86_64-linux-musl")
# Can be removed once RubyGems 3.6.9 support is dropped
unless respond_to?(:generic)
JAVA = Gem::Platform.new("java") # :nodoc:
MSWIN = Gem::Platform.new("mswin32") # :nodoc:
MSWIN64 = Gem::Platform.new("mswin64") # :nodoc:
MINGW = Gem::Platform.new("x86-mingw32") # :nodoc:
X64_MINGW_LEGACY = Gem::Platform.new("x64-mingw32") # :nodoc:
X64_MINGW = Gem::Platform.new("x64-mingw-ucrt") # :nodoc:
UNIVERSAL_MINGW = Gem::Platform.new("universal-mingw") # :nodoc:
WINDOWS = [MSWIN, MSWIN64, UNIVERSAL_MINGW].freeze # :nodoc:
X64_LINUX = Gem::Platform.new("x86_64-linux") # :nodoc:
X64_LINUX_MUSL = Gem::Platform.new("x86_64-linux-musl") # :nodoc:
GENERICS = [JAVA, *WINDOWS].freeze # :nodoc:
private_constant :GENERICS
GENERIC_CACHE = GENERICS.each_with_object({}) {|g, h| h[g] = g } # :nodoc:
private_constant :GENERIC_CACHE
class << self
##
# Returns the generic platform for the given platform.
def generic(platform)
return Gem::Platform::RUBY if platform.nil? || platform == Gem::Platform::RUBY
GENERIC_CACHE[platform] ||= begin
found = GENERICS.find do |match|
platform === match
end
found || Gem::Platform::RUBY
end
end
##
# Returns the platform specificity match for the given spec platform and user platform.
def platform_specificity_match(spec_platform, user_platform)
return -1 if spec_platform == user_platform
return 1_000_000 if spec_platform.nil? || spec_platform == Gem::Platform::RUBY || user_platform == Gem::Platform::RUBY
os_match(spec_platform, user_platform) +
cpu_match(spec_platform, user_platform) * 10 +
version_match(spec_platform, user_platform) * 100
end
##
# Sorts and filters the best platform match for the given matching specs and platform.
def sort_and_filter_best_platform_match(matching, platform)
return matching if matching.one?
exact = matching.select {|spec| spec.platform == platform }
return exact if exact.any?
sorted_matching = sort_best_platform_match(matching, platform)
exemplary_spec = sorted_matching.first
sorted_matching.take_while {|spec| same_specificity?(platform, spec, exemplary_spec) && same_deps?(spec, exemplary_spec) }
end
##
# Sorts the best platform match for the given matching specs and platform.
def sort_best_platform_match(matching, platform)
matching.sort_by.with_index do |spec, i|
[
platform_specificity_match(spec.platform, platform),
i, # for stable sort
]
end
end
private
def same_specificity?(platform, spec, exemplary_spec)
platform_specificity_match(spec.platform, platform) == platform_specificity_match(exemplary_spec.platform, platform)
end
def same_deps?(spec, exemplary_spec)
spec.required_ruby_version == exemplary_spec.required_ruby_version &&
spec.required_rubygems_version == exemplary_spec.required_rubygems_version &&
spec.dependencies.sort == exemplary_spec.dependencies.sort
end
def os_match(spec_platform, user_platform)
if spec_platform.os == user_platform.os
0
else
1
end
end
def cpu_match(spec_platform, user_platform)
if spec_platform.cpu == user_platform.cpu
0
elsif spec_platform.cpu == "arm" && user_platform.cpu.to_s.start_with?("arm")
0
elsif spec_platform.cpu.nil? || spec_platform.cpu == "universal"
1
else
2
end
end
def version_match(spec_platform, user_platform)
if spec_platform.version == user_platform.version
0
elsif spec_platform.version.nil?
1
else
2
end
end
end
end
end
require "rubygems/specification"
@ -80,7 +187,6 @@ module Gem
require_relative "match_platform"
include ::Bundler::MatchMetadata
include ::Bundler::MatchPlatform
attr_accessor :remote, :relative_loaded_from
@ -285,6 +391,11 @@ module Gem
@ignored = missing_extensions?
end
end
# Can be removed once RubyGems 3.6.9 support is dropped
unless new.respond_to?(:installable_on_platform?)
include(::Bundler::MatchPlatform)
end
end
require "rubygems/name_tuple"

View File

@ -76,7 +76,7 @@ module Bundler
new_platforms = all_platforms.select do |platform|
next if platforms.include?(platform)
next unless GemHelpers.generic(platform) == Gem::Platform::RUBY
next unless Gem::Platform.generic(platform) == Gem::Platform::RUBY
complete_platform(platform)
end
@ -183,7 +183,7 @@ module Bundler
end
def find_by_name_and_platform(name, platform)
@specs.detect {|spec| spec.name == name && spec.match_platform(platform) }
@specs.detect {|spec| spec.name == name && spec.installable_on_platform?(platform) }
end
def specs_with_additional_variants_from(other)
@ -280,7 +280,7 @@ module Bundler
valid_platform = lookup.all? do |_, specs|
spec = specs.first
matching_specs = spec.source.specs.search([spec.name, spec.version])
platform_spec = GemHelpers.select_best_platform_match(matching_specs, platform).find do |s|
platform_spec = MatchPlatform.select_best_platform_match(matching_specs, platform).find do |s|
valid?(s)
end

View File

@ -256,6 +256,13 @@ class Gem::BasicSpecification
raise NotImplementedError
end
def installable_on_platform?(target_platform) # :nodoc:
return true if [Gem::Platform::RUBY, nil, target_platform].include?(platform)
return true if Gem::Platform.new(platform) === target_platform
false
end
def raw_require_paths # :nodoc:
raise NotImplementedError
end

View File

@ -255,4 +255,118 @@ class Gem::Platform
# This will be replaced with Gem::Platform::local.
CURRENT = "current"
JAVA = Gem::Platform.new("java") # :nodoc:
MSWIN = Gem::Platform.new("mswin32") # :nodoc:
MSWIN64 = Gem::Platform.new("mswin64") # :nodoc:
MINGW = Gem::Platform.new("x86-mingw32") # :nodoc:
X64_MINGW_LEGACY = Gem::Platform.new("x64-mingw32") # :nodoc:
X64_MINGW = Gem::Platform.new("x64-mingw-ucrt") # :nodoc:
UNIVERSAL_MINGW = Gem::Platform.new("universal-mingw") # :nodoc:
WINDOWS = [MSWIN, MSWIN64, UNIVERSAL_MINGW].freeze # :nodoc:
X64_LINUX = Gem::Platform.new("x86_64-linux") # :nodoc:
X64_LINUX_MUSL = Gem::Platform.new("x86_64-linux-musl") # :nodoc:
GENERICS = [JAVA, *WINDOWS].freeze # :nodoc:
private_constant :GENERICS
GENERIC_CACHE = GENERICS.each_with_object({}) {|g, h| h[g] = g } # :nodoc:
private_constant :GENERIC_CACHE
class << self
##
# Returns the generic platform for the given platform.
def generic(platform)
return Gem::Platform::RUBY if platform.nil? || platform == Gem::Platform::RUBY
GENERIC_CACHE[platform] ||= begin
found = GENERICS.find do |match|
platform === match
end
found || Gem::Platform::RUBY
end
end
##
# Returns the platform specificity match for the given spec platform and user platform.
def platform_specificity_match(spec_platform, user_platform)
return -1 if spec_platform == user_platform
return 1_000_000 if spec_platform.nil? || spec_platform == Gem::Platform::RUBY || user_platform == Gem::Platform::RUBY
os_match(spec_platform, user_platform) +
cpu_match(spec_platform, user_platform) * 10 +
version_match(spec_platform, user_platform) * 100
end
##
# Sorts and filters the best platform match for the given matching specs and platform.
def sort_and_filter_best_platform_match(matching, platform)
return matching if matching.one?
exact = matching.select {|spec| spec.platform == platform }
return exact if exact.any?
sorted_matching = sort_best_platform_match(matching, platform)
exemplary_spec = sorted_matching.first
sorted_matching.take_while {|spec| same_specificity?(platform, spec, exemplary_spec) && same_deps?(spec, exemplary_spec) }
end
##
# Sorts the best platform match for the given matching specs and platform.
def sort_best_platform_match(matching, platform)
matching.sort_by.with_index do |spec, i|
[
platform_specificity_match(spec.platform, platform),
i, # for stable sort
]
end
end
private
def same_specificity?(platform, spec, exemplary_spec)
platform_specificity_match(spec.platform, platform) == platform_specificity_match(exemplary_spec.platform, platform)
end
def same_deps?(spec, exemplary_spec)
spec.required_ruby_version == exemplary_spec.required_ruby_version &&
spec.required_rubygems_version == exemplary_spec.required_rubygems_version &&
spec.dependencies.sort == exemplary_spec.dependencies.sort
end
def os_match(spec_platform, user_platform)
if spec_platform.os == user_platform.os
0
else
1
end
end
def cpu_match(spec_platform, user_platform)
if spec_platform.cpu == user_platform.cpu
0
elsif spec_platform.cpu == "arm" && user_platform.cpu.to_s.start_with?("arm")
0
elsif spec_platform.cpu.nil? || spec_platform.cpu == "universal"
1
else
2
end
end
def version_match(spec_platform, user_platform)
if spec_platform.version == user_platform.version
0
elsif spec_platform.version.nil?
1
else
2
end
end
end
end

View File

@ -1,57 +1,20 @@
# frozen_string_literal: true
RSpec.describe "Gem::Specification#match_platform" do
RSpec.describe "Gem::Specification#installable_on_platform?" do
it "does not match platforms other than the gem platform" do
darwin = gem "lol", "1.0", "platform_specific-1.0-x86-darwin-10"
expect(darwin.match_platform(pl("java"))).to eq(false)
expect(darwin.installable_on_platform?(pl("java"))).to eq(false)
end
context "when platform is a string" do
it "matches when platform is a string" do
lazy_spec = Bundler::LazySpecification.new("lol", "1.0", "universal-mingw32")
expect(lazy_spec.match_platform(pl("x86-mingw32"))).to eq(true)
expect(lazy_spec.match_platform(pl("x64-mingw32"))).to eq(true)
expect(lazy_spec.installable_on_platform?(pl("x86-mingw32"))).to eq(true)
expect(lazy_spec.installable_on_platform?(pl("x64-mingw32"))).to eq(true)
end
end
end
RSpec.describe "Bundler::GemHelpers#generic" do
include Bundler::GemHelpers
it "converts non-windows platforms into ruby" do
expect(generic(pl("x86-darwin-10"))).to eq(pl("ruby"))
expect(generic(pl("ruby"))).to eq(pl("ruby"))
end
it "converts java platform variants into java" do
expect(generic(pl("universal-java-17"))).to eq(pl("java"))
expect(generic(pl("java"))).to eq(pl("java"))
end
it "converts mswin platform variants into x86-mswin32" do
expect(generic(pl("mswin32"))).to eq(pl("x86-mswin32"))
expect(generic(pl("i386-mswin32"))).to eq(pl("x86-mswin32"))
expect(generic(pl("x86-mswin32"))).to eq(pl("x86-mswin32"))
end
it "converts 32-bit mingw platform variants into universal-mingw" do
expect(generic(pl("i386-mingw32"))).to eq(pl("universal-mingw"))
expect(generic(pl("x86-mingw32"))).to eq(pl("universal-mingw"))
end
it "converts 64-bit mingw platform variants into universal-mingw" do
expect(generic(pl("x64-mingw32"))).to eq(pl("universal-mingw"))
end
it "converts x64 mingw UCRT platform variants into universal-mingw" do
expect(generic(pl("x64-mingw-ucrt"))).to eq(pl("universal-mingw"))
end
it "converts aarch64 mingw UCRT platform variants into universal-mingw" do
expect(generic(pl("aarch64-mingw-ucrt"))).to eq(pl("universal-mingw"))
end
end
RSpec.describe "Gem::SourceIndex#refresh!" do
before do
install_gemfile <<-G

View File

@ -2,12 +2,18 @@
module Spec
module Platforms
include Bundler::GemHelpers
def not_local
generic_local_platform == Gem::Platform::RUBY ? "java" : Gem::Platform::RUBY
end
def local_platform
Bundler.local_platform
end
def generic_local_platform
Gem::Platform.generic(local_platform)
end
def local_tag
if Gem.java_platform?
:jruby
@ -61,7 +67,7 @@ module Spec
end
def generic_default_locked_platform
return unless generic_local_platform_is_ruby?
return unless Bundler::MatchPlatform.generic_local_platform_is_ruby?
Gem::Platform::RUBY
end

View File

@ -418,6 +418,9 @@ class Gem::TestCase < Test::Unit::TestCase
@orig_hooks[name] = Gem.send(name).dup
end
Gem::Platform.const_get(:GENERIC_CACHE).clear
Gem::Platform.const_get(:GENERICS).each {|g| Gem::Platform.const_get(:GENERIC_CACHE)[g] = g }
@marshal_version = "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}"
@orig_loaded_features = $LOADED_FEATURES.dup
end

View File

@ -509,6 +509,171 @@ class TestGemPlatform < Gem::TestCase
end
end
def test_constants
assert_equal [nil, "java", nil], Gem::Platform::JAVA.to_a
assert_equal ["x86", "mswin32", nil], Gem::Platform::MSWIN.to_a
assert_equal [nil, "mswin64", nil], Gem::Platform::MSWIN64.to_a
assert_equal ["x86", "mingw32", nil], Gem::Platform::MINGW.to_a
assert_equal ["x64", "mingw", "ucrt"], Gem::Platform::X64_MINGW.to_a
assert_equal ["universal", "mingw", nil], Gem::Platform::UNIVERSAL_MINGW.to_a
assert_equal [["x86", "mswin32", nil], [nil, "mswin64", nil], ["universal", "mingw", nil]], Gem::Platform::WINDOWS.map(&:to_a)
assert_equal ["x86_64", "linux", nil], Gem::Platform::X64_LINUX.to_a
assert_equal ["x86_64", "linux", "musl"], Gem::Platform::X64_LINUX_MUSL.to_a
end
def test_generic
# converts non-windows platforms into ruby
assert_equal Gem::Platform::RUBY, Gem::Platform.generic(Gem::Platform.new("x86-darwin-10"))
assert_equal Gem::Platform::RUBY, Gem::Platform.generic(Gem::Platform::RUBY)
# converts java platform variants into java
assert_equal Gem::Platform::JAVA, Gem::Platform.generic(Gem::Platform.new("java"))
assert_equal Gem::Platform::JAVA, Gem::Platform.generic(Gem::Platform.new("universal-java-17"))
# converts mswin platform variants into x86-mswin32
assert_equal Gem::Platform::MSWIN, Gem::Platform.generic(Gem::Platform.new("mswin32"))
assert_equal Gem::Platform::MSWIN, Gem::Platform.generic(Gem::Platform.new("i386-mswin32"))
assert_equal Gem::Platform::MSWIN, Gem::Platform.generic(Gem::Platform.new("x86-mswin32"))
# converts 32-bit mingw platform variants into universal-mingw
assert_equal Gem::Platform::UNIVERSAL_MINGW, Gem::Platform.generic(Gem::Platform.new("i386-mingw32"))
assert_equal Gem::Platform::UNIVERSAL_MINGW, Gem::Platform.generic(Gem::Platform.new("x86-mingw32"))
# converts 64-bit mingw platform variants into universal-mingw
assert_equal Gem::Platform::UNIVERSAL_MINGW, Gem::Platform.generic(Gem::Platform.new("x64-mingw32"))
# converts x64 mingw UCRT platform variants into universal-mingw
assert_equal Gem::Platform::UNIVERSAL_MINGW, Gem::Platform.generic(Gem::Platform.new("x64-mingw-ucrt"))
# converts aarch64 mingw UCRT platform variants into universal-mingw
assert_equal Gem::Platform::UNIVERSAL_MINGW, Gem::Platform.generic(Gem::Platform.new("aarch64-mingw-ucrt"))
assert_equal Gem::Platform::RUBY, Gem::Platform.generic(Gem::Platform.new("unknown"))
assert_equal Gem::Platform::RUBY, Gem::Platform.generic(nil)
assert_equal Gem::Platform::MSWIN64, Gem::Platform.generic(Gem::Platform.new("mswin64"))
end
def test_platform_specificity_match
[
["ruby", "ruby", -1, -1],
["x86_64-linux-musl", "x86_64-linux-musl", -1, -1],
["x86_64-linux", "x86_64-linux-musl", 100, 200],
["universal-darwin", "x86-darwin", 10, 20],
["universal-darwin-19", "x86-darwin", 210, 120],
["universal-darwin-19", "universal-darwin-20", 200, 200],
["arm-darwin-19", "arm64-darwin-19", 0, 20],
].each do |spec_platform, user_platform, s1, s2|
spec_platform = Gem::Platform.new(spec_platform)
user_platform = Gem::Platform.new(user_platform)
assert_equal s1, Gem::Platform.platform_specificity_match(spec_platform, user_platform),
"Gem::Platform.platform_specificity_match(#{spec_platform.to_s.inspect}, #{user_platform.to_s.inspect})"
assert_equal s2, Gem::Platform.platform_specificity_match(user_platform, spec_platform),
"Gem::Platform.platform_specificity_match(#{user_platform.to_s.inspect}, #{spec_platform.to_s.inspect})"
end
end
def test_sort_and_filter_best_platform_match
a_1 = util_spec "a", "1"
a_1_java = util_spec "a", "1" do |s|
s.platform = Gem::Platform::JAVA
end
a_1_universal_darwin = util_spec "a", "1" do |s|
s.platform = Gem::Platform.new("universal-darwin")
end
a_1_universal_darwin_19 = util_spec "a", "1" do |s|
s.platform = Gem::Platform.new("universal-darwin-19")
end
a_1_universal_darwin_20 = util_spec "a", "1" do |s|
s.platform = Gem::Platform.new("universal-darwin-20")
end
a_1_arm_darwin_19 = util_spec "a", "1" do |s|
s.platform = Gem::Platform.new("arm64-darwin-19")
end
a_1_x86_darwin = util_spec "a", "1" do |s|
s.platform = Gem::Platform.new("x86-darwin")
end
specs = [a_1, a_1_java, a_1_universal_darwin, a_1_universal_darwin_19, a_1_universal_darwin_20, a_1_arm_darwin_19, a_1_x86_darwin]
assert_equal [a_1], Gem::Platform.sort_and_filter_best_platform_match(specs, "ruby")
assert_equal [a_1_java], Gem::Platform.sort_and_filter_best_platform_match(specs, Gem::Platform::JAVA)
assert_equal [a_1_arm_darwin_19], Gem::Platform.sort_and_filter_best_platform_match(specs, Gem::Platform.new("arm64-darwin-19"))
assert_equal [a_1_universal_darwin_20], Gem::Platform.sort_and_filter_best_platform_match(specs, Gem::Platform.new("arm64-darwin-20"))
assert_equal [a_1_universal_darwin_19], Gem::Platform.sort_and_filter_best_platform_match(specs, Gem::Platform.new("x86-darwin-19"))
assert_equal [a_1_universal_darwin_20], Gem::Platform.sort_and_filter_best_platform_match(specs, Gem::Platform.new("x86-darwin-20"))
assert_equal [a_1_x86_darwin], Gem::Platform.sort_and_filter_best_platform_match(specs, Gem::Platform.new("x86-darwin-21"))
end
def test_sort_best_platform_match
a_1 = util_spec "a", "1"
a_1_java = util_spec "a", "1" do |s|
s.platform = Gem::Platform::JAVA
end
a_1_universal_darwin = util_spec "a", "1" do |s|
s.platform = Gem::Platform.new("universal-darwin")
end
a_1_universal_darwin_19 = util_spec "a", "1" do |s|
s.platform = Gem::Platform.new("universal-darwin-19")
end
a_1_universal_darwin_20 = util_spec "a", "1" do |s|
s.platform = Gem::Platform.new("universal-darwin-20")
end
a_1_arm_darwin_19 = util_spec "a", "1" do |s|
s.platform = Gem::Platform.new("arm64-darwin-19")
end
a_1_x86_darwin = util_spec "a", "1" do |s|
s.platform = Gem::Platform.new("x86-darwin")
end
specs = [a_1, a_1_java, a_1_universal_darwin, a_1_universal_darwin_19, a_1_universal_darwin_20, a_1_arm_darwin_19, a_1_x86_darwin]
assert_equal ["ruby",
"java",
"universal-darwin",
"universal-darwin-19",
"universal-darwin-20",
"arm64-darwin-19",
"x86-darwin"], Gem::Platform.sort_best_platform_match(specs, "ruby").map {|s| s.platform.to_s }
assert_equal ["java",
"universal-darwin",
"x86-darwin",
"universal-darwin-19",
"universal-darwin-20",
"arm64-darwin-19",
"ruby"], Gem::Platform.sort_best_platform_match(specs, Gem::Platform::JAVA).map {|s| s.platform.to_s }
assert_equal ["arm64-darwin-19",
"universal-darwin-19",
"universal-darwin",
"java",
"x86-darwin",
"universal-darwin-20",
"ruby"], Gem::Platform.sort_best_platform_match(specs, Gem::Platform.new("arm64-darwin-19")).map {|s| s.platform.to_s }
assert_equal ["universal-darwin-20",
"universal-darwin",
"java",
"x86-darwin",
"arm64-darwin-19",
"universal-darwin-19",
"ruby"], Gem::Platform.sort_best_platform_match(specs, Gem::Platform.new("arm64-darwin-20")).map {|s| s.platform.to_s }
assert_equal ["universal-darwin-19",
"arm64-darwin-19",
"x86-darwin",
"universal-darwin",
"java",
"universal-darwin-20",
"ruby"], Gem::Platform.sort_best_platform_match(specs, Gem::Platform.new("x86-darwin-19")).map {|s| s.platform.to_s }
assert_equal ["universal-darwin-20",
"x86-darwin",
"universal-darwin",
"java",
"universal-darwin-19",
"arm64-darwin-19",
"ruby"], Gem::Platform.sort_best_platform_match(specs, Gem::Platform.new("x86-darwin-20")).map {|s| s.platform.to_s }
assert_equal ["x86-darwin",
"universal-darwin",
"java",
"universal-darwin-19",
"universal-darwin-20",
"arm64-darwin-19",
"ruby"], Gem::Platform.sort_best_platform_match(specs, Gem::Platform.new("x86-darwin-21")).map {|s| s.platform.to_s }
end
def assert_local_match(name)
assert_match Gem::Platform.local, name
end