diff --git a/Manifest.txt b/Manifest.txt index e5b0a0e6d542..46f05d6589f2 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -456,6 +456,11 @@ lib/rubygems/package/tar_writer.rb lib/rubygems/package_task.rb lib/rubygems/path_support.rb lib/rubygems/platform.rb +lib/rubygems/platform/elffile.rb +lib/rubygems/platform/manylinux.rb +lib/rubygems/platform/musllinux.rb +lib/rubygems/platform/specific.rb +lib/rubygems/platform/wheel.rb lib/rubygems/psych_tree.rb lib/rubygems/query_utils.rb lib/rubygems/rdoc.rb diff --git a/bundler/spec/install/gemfile/wheel_platform_spec.rb b/bundler/spec/install/gemfile/wheel_platform_spec.rb new file mode 100644 index 000000000000..ddbc58d57c23 --- /dev/null +++ b/bundler/spec/install/gemfile/wheel_platform_spec.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install with wheel platform gems" do + before do + build_repo4 do + # Build wheel platform specific gems + build_gem "wheel_native", "1.0.0" do |s| + s.platform = "whl-rb33-x86_64_linux" + s.write "lib/wheel_native.rb", "WHEEL_NATIVE = '1.0.0 whl-rb33-x86_64_linux'" + end + + build_gem "wheel_native", "1.0.0" do |s| + s.platform = "whl-rb32-x86_64_linux" + s.write "lib/wheel_native.rb", "WHEEL_NATIVE = '1.0.0 whl-rb32-x86_64_linux'" + end + + build_gem "wheel_native", "1.0.0" do |s| + s.platform = "whl-rb33-x86_64_darwin" + s.write "lib/wheel_native.rb", "WHEEL_NATIVE = '1.0.0 whl-rb33-x86_64_darwin'" + end + + # Fallback ruby gem + build_gem "wheel_native", "1.0.0" do |s| + s.platform = "ruby" + s.write "lib/wheel_native.rb", "WHEEL_NATIVE = '1.0.0 ruby'" + end + + # Multi-tag wheel gem + build_gem "multi_wheel", "2.0.0" do |s| + s.platform = "whl-rb33.rb32-x86_64_linux" + s.write "lib/multi_wheel.rb", "MULTI_WHEEL_VERSION = '2.0.0-whl-rb33.rb32-x86_64_linux'" + end + + build_gem "multi_wheel", "2.0.0" do |s| + s.platform = "ruby" + s.write "lib/multi_wheel.rb", "MULTI_WHEEL_VERSION = '2.0.0-ruby'" + end + end + end + + context "when wheel platform gem is available for current platform" do + it "installs the wheel platform specific gem" do + skip "Wheel platform detection not fully implemented in resolver yet" + + install_gemfile <<-G + source "https://gem.repo4" + gem "wheel_native" + G + + expect(the_bundle).to include_gems "wheel_native 1.0.0" + # Would need to check that the correct platform version was installed + end + end + + context "when no wheel platform gem matches" do + it "falls back to ruby platform gem" do + install_gemfile <<-G + source "https://gem.repo4" + gem "wheel_native" + G + + expect(the_bundle).to include_gems "wheel_native 1.0.0" + + ruby <<-R + require 'wheel_native' + puts WHEEL_NATIVE + R + + expect(out).to include("1.0.0 ruby") + end + end + + context "with multi-tag wheel gems" do + it "matches compatible multi-tag wheel gems" do + skip "Multi-tag wheel matching not fully implemented yet" + + install_gemfile <<-G + source "https://gem.repo4" + gem "multi_wheel" + G + + expect(the_bundle).to include_gems "multi_wheel 2.0.0" + end + end + + context "platform resolution priority" do + it "prefers wheel platform over traditional platform" do + skip "Platform priority not fully implemented in bundler yet" + + build_repo4 do + # Traditional platform gem + build_gem "priority_test", "1.0.0" do |s| + s.platform = "x86_64-linux" + s.write "lib/priority_test.rb", "PRIORITY_VERSION = '1.0.0-x86_64-linux'" + end + + # Wheel platform gem (should have higher priority) + build_gem "priority_test", "1.0.0" do |s| + s.platform = "whl-rb33-x86_64_linux" + s.write "lib/priority_test.rb", "PRIORITY_VERSION = '1.0.0-whl-rb33-x86_64_linux'" + end + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "priority_test" + G + + ruby <<-R + require 'priority_test' + puts PRIORITY_VERSION + R + + # Should prefer wheel platform over traditional platform + expect(out).to include("whl-rb33-x86_64_linux") + end + end + + context "lockfile generation with wheel platforms" do + it "records wheel platforms in the lockfile" do + skip "Lockfile wheel platform recording not implemented yet" + + install_gemfile <<-G + source "https://gem.repo4" + gem "wheel_native" + G + + lockfile_should_be <<-L + GEM + remote: https://gem.repo4/ + specs: + wheel_native (1.0.0-whl-rb33-x86_64_linux) + + PLATFORMS + whl-rb33-x86_64_linux + + DEPENDENCIES + wheel_native + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end +end diff --git a/lib/rubygems/basic_specification.rb b/lib/rubygems/basic_specification.rb index 591b5557250b..7585eba6431d 100644 --- a/lib/rubygems/basic_specification.rb +++ b/lib/rubygems/basic_specification.rb @@ -72,7 +72,7 @@ def base_dir def contains_requirable_file?(file) if ignored? - if platform == Gem::Platform::RUBY || Gem::Platform.local === platform + if platform == Gem::Platform::RUBY || Gem::Platform::Specific.local === platform warn "Ignoring #{full_name} because its extensions are not built. " \ "Try: gem pristine #{name} --version #{version}" end diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb index 93503d2b6991..8d8a4a80a65b 100644 --- a/lib/rubygems/commands/pristine_command.rb +++ b/lib/rubygems/commands/pristine_command.rb @@ -125,7 +125,7 @@ def execute end end - specs = specs.select {|spec| spec.platform == RUBY_ENGINE || Gem::Platform.local === spec.platform || spec.platform == Gem::Platform::RUBY } + specs = specs.select {|spec| spec.platform == RUBY_ENGINE || Gem::Platform::Specific.local === spec.platform || spec.platform == Gem::Platform::RUBY } if specs.to_a.empty? raise Gem::Exception, diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index e30c266fab1c..906630039e72 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -8,6 +8,12 @@ # See `gem help platform` for information on platform matching. class Gem::Platform + require_relative "platform/elffile" + require_relative "platform/manylinux" + require_relative "platform/musllinux" + require_relative "platform/wheel" + require_relative "platform/specific" + @local = nil attr_accessor :cpu, :os, :version @@ -49,19 +55,23 @@ def self.match_gem?(platform, gem_name) raise "Not a string: #{gem_name.inspect}" unless String === gem_name if REUSE_AS_BINARY_ON_TRUFFLERUBY.include?(gem_name) - match_platforms?(platform, [Gem::Platform::RUBY, Gem::Platform.local]) + match_platforms?(platform, [Gem::Platform::RUBY, Gem::Platform::Specific.local]) else - match_platforms?(platform, Gem.platforms) + match_platforms?(platform, Gem.platforms.map {|pl| Specific.local(pl) }) end end else def self.match_gem?(platform, gem_name) - match_platforms?(platform, Gem.platforms) + match_platforms?(platform, Gem.platforms.map {|pl| Specific.local(pl) }) end end def self.sort_priority(platform) - platform == Gem::Platform::RUBY ? -1 : 1 + case platform + when Gem::Platform::RUBY then -1 + when Gem::Platform::Wheel then 2 # Higher priority than traditional platforms + else 1 + end end def self.installable?(spec) @@ -78,6 +88,14 @@ def self.new(arch) # :nodoc: Gem::Platform.local when Gem::Platform::RUBY, nil, "" then Gem::Platform::RUBY + when /^whl-/ then + Gem::Platform::Wheel.new(arch) + when Wheel then + Wheel.new(arch) + when Specific then + Specific.new(arch) + when / v:\d+/ + Gem::Platform::Specific.parse(arch) else super end @@ -85,57 +103,64 @@ def self.new(arch) # :nodoc: def initialize(arch) case arch + when String then when Array then + raise "Array #{arch.inspect} is not a valid platform" unless arch.size <= 3 @cpu, @os, @version = arch - when String then - cpu, os = arch.sub(/-+$/, "").split("-", 2) - - @cpu = if cpu&.match?(/i\d86/) - "x86" - else - cpu - end - - if os.nil? - @cpu = nil - os = cpu - end # legacy jruby - - @os, @version = case os - when /aix-?(\d+)?/ then ["aix", $1] - when /cygwin/ then ["cygwin", nil] - when /darwin-?(\d+)?/ then ["darwin", $1] - when "macruby" then ["macruby", nil] - when /^macruby-?(\d+(?:\.\d+)*)?/ then ["macruby", $1] - when /freebsd-?(\d+)?/ then ["freebsd", $1] - when "java", "jruby" then ["java", nil] - when /^java-?(\d+(?:\.\d+)*)?/ then ["java", $1] - when /^dalvik-?(\d+)?$/ then ["dalvik", $1] - when /^dotnet$/ then ["dotnet", nil] - when /^dotnet-?(\d+(?:\.\d+)*)?/ then ["dotnet", $1] - when /linux-?(\w+)?/ then ["linux", $1] - when /mingw32/ then ["mingw32", nil] - when /mingw-?(\w+)?/ then ["mingw", $1] - when /(mswin\d+)(?:[_-](\d+))?/ then - os = $1 - version = $2 - @cpu = "x86" if @cpu.nil? && os.end_with?("32") - [os, version] - when /netbsdelf/ then ["netbsdelf", nil] - when /openbsd-?(\d+\.\d+)?/ then ["openbsd", $1] - when /solaris-?(\d+\.\d+)?/ then ["solaris", $1] - when /wasi/ then ["wasi", nil] - # test - when /^(\w+_platform)-?(\d+)?/ then [$1, $2] - else ["unknown", nil] - end - when Gem::Platform then + return + when Gem::Platform @cpu = arch.cpu @os = arch.os @version = arch.version + return else raise ArgumentError, "invalid argument #{arch.inspect}" end + + cpu, os = arch.sub(/-+$/, "").split("-", 2) + + @cpu = if cpu&.match?(/i\d86/) + "x86" + elsif cpu == "dotnet" + os = "dotnet-#{os}" + nil + else + cpu + end + + if os.nil? + @cpu = nil + os = cpu + end # legacy jruby + + @os, @version = case os + when /aix-?(\d+)?/ then ["aix", $1] + when /cygwin/ then ["cygwin", nil] + when /darwin-?(\d+)?/ then ["darwin", $1] + when "macruby" then ["macruby", nil] + when /^macruby-?(\d+(?:\.\d+)*)?/ then ["macruby", $1] + when /freebsd-?(\d+)?/ then ["freebsd", $1] + when "java", "jruby" then ["java", nil] + when /^java-?(\d+(?:\.\d+)*)?/ then ["java", $1] + when /^dalvik-?(\d+)?$/ then ["dalvik", $1] + when "dotnet" then ["dotnet", nil] + when /^dotnet-?(\d+(?:\.\d+)*)?/ then ["dotnet", $1] + when /linux-?(\w+)?/ then ["linux", $1] + when /mingw32/ then ["mingw32", nil] + when /mingw-?(\w+)?/ then ["mingw", $1] + when /(mswin\d+)(?:[_-](\d+))?/ then + os = $1 + version = $2 + @cpu = "x86" if @cpu.nil? && os.end_with?("32") + [os, version] + when /netbsdelf/ then ["netbsdelf", nil] + when /openbsd-?(\d+\.\d+)?/ then ["openbsd", $1] + when /solaris-?(\d+\.\d+)?/ then ["solaris", $1] + when /wasi/ then ["wasi", nil] + # test + when /^(\w+_platform)-?(\d+)?/ then [$1, $2] + else ["unknown", nil] + end end def to_a @@ -218,25 +243,9 @@ def normalized_linux_version def =~(other) case other - when Gem::Platform then # nop - when String then - # This data is from http://gems.rubyforge.org/gems/yaml on 19 Aug 2007 - other = case other - when /^i686-darwin(\d)/ then ["x86", "darwin", $1] - when /^i\d86-linux/ then ["x86", "linux", nil] - when "java", "jruby" then [nil, "java", nil] - when /^dalvik(\d+)?$/ then [nil, "dalvik", $1] - when /dotnet(\-(\d+\.\d+))?/ then ["universal","dotnet", $2] - when /mswin32(\_(\d+))?/ then ["x86", "mswin32", $2] - when /mswin64(\_(\d+))?/ then ["x64", "mswin64", $2] - when "powerpc-darwin" then ["powerpc", "darwin", nil] - when /powerpc-darwin(\d)/ then ["powerpc", "darwin", $1] - when /sparc-solaris2.8/ then ["sparc", "solaris", "2.8"] - when /universal-darwin(\d)/ then ["universal", "darwin", $1] - else other - end - - other = Gem::Platform.new other + when Gem::Platform, Gem::Platform::Wheel + when Gem::Platform::Specific then other = other.platform + when String then other = Gem::Platform.new(other) else return nil end @@ -278,7 +287,15 @@ class << self # Returns the generic platform for the given platform. def generic(platform) - return Gem::Platform::RUBY if platform.nil? || platform == Gem::Platform::RUBY + case platform + when NilClass, Gem::Platform::RUBY + return Gem::Platform::RUBY + when Gem::Platform::Wheel + return platform + when Gem::Platform + else + raise ArgumentError, "invalid argument #{platform.inspect}" + end GENERIC_CACHE[platform] ||= begin found = GENERICS.find do |match| @@ -295,6 +312,48 @@ 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 + # Handle Specific user platforms + if user_platform.is_a?(Gem::Platform::Specific) + case spec_platform + when Gem::Platform::Wheel + # Use each_possible_match to find the best match for wheels + # Return negative values to indicate better matches than traditional platforms + index = user_platform.each_possible_match.to_a.index do |abi_tag, platform_tag| + # Check if the wheel matches this generated tag pair + spec_platform.ruby_abi_tag.split(".").include?(abi_tag) && spec_platform.platform_tags.split(".").include?(platform_tag) + end + return(if index == 0 + -10 + elsif index + index + else + 1_000_000 + end) + when Gem::Platform + # For traditional platforms with Specific user platforms, use original scoring + user_platform = user_platform.platform + return -1 if spec_platform == user_platform # Better than non-matching wheels but worse than matching wheels + else + raise ArgumentError, "spec_platform must be Gem::Platform or Gem::Platform::Wheel, given #{spec_platform.inspect}" + end + end + + # Handle traditional Platform user platforms + case user_platform + when Gem::Platform + # For wheel spec platforms with traditional user platforms, create a Specific user platform + if spec_platform.is_a?(Gem::Platform::Wheel) + specific_user = Gem::Platform::Specific.local(user_platform) + return platform_specificity_match(spec_platform, specific_user) + end + when Gem::Platform::Specific + # TODO: also match on ruby ABI tags! + user_platform = user_platform.platform + return -1 if spec_platform == user_platform + else + raise ArgumentError, "user_platform must be Gem::Platform or Gem::Platform::Specific, given #{user_platform.inspect}" + end + os_match(spec_platform, user_platform) + cpu_match(spec_platform, user_platform) * 10 + version_match(spec_platform, user_platform) * 100 @@ -303,25 +362,25 @@ def platform_specificity_match(spec_platform, user_platform) ## # Sorts and filters the best platform match for the given matching specs and platform. - def sort_and_filter_best_platform_match(matching, platform) + def sort_and_filter_best_platform_match(matching, user_platform) return matching if matching.one? - exact = matching.select {|spec| spec.platform == platform } + exact = matching.select {|spec| spec.platform == user_platform } return exact if exact.any? - sorted_matching = sort_best_platform_match(matching, platform) + sorted_matching = sort_best_platform_match(matching, user_platform) exemplary_spec = sorted_matching.first - sorted_matching.take_while {|spec| same_specificity?(platform, spec, exemplary_spec) && same_deps?(spec, exemplary_spec) } + sorted_matching.take_while {|spec| same_specificity?(user_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) + def sort_best_platform_match(matching, user_platform) matching.sort_by.with_index do |spec, i| [ - platform_specificity_match(spec.platform, platform), + platform_specificity_match(spec.platform, user_platform), i, # for stable sort ] end @@ -329,8 +388,8 @@ def sort_best_platform_match(matching, platform) private - def same_specificity?(platform, spec, exemplary_spec) - platform_specificity_match(spec.platform, platform) == platform_specificity_match(exemplary_spec.platform, platform) + def same_specificity?(user_platform, spec, exemplary_spec) + platform_specificity_match(spec.platform, user_platform) == platform_specificity_match(exemplary_spec.platform, user_platform) end def same_deps?(spec, exemplary_spec) diff --git a/lib/rubygems/platform/elffile.rb b/lib/rubygems/platform/elffile.rb new file mode 100644 index 000000000000..fd1dd35b3aac --- /dev/null +++ b/lib/rubygems/platform/elffile.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +## +# Minimal ELF file parser for Ruby platform detection, inspired by Python's packaging._elffile +# This module implements just enough ELF parsing to extract the dynamic interpreter path +# needed for musl/glibc detection. +# +# Based on Python's packaging._elffile +# https://github.com/pypa/packaging/blob/0055d4b56ae868bbcc7825c9ad68f49cdcb9f8b9/src/packaging/_elffile.py + +module Gem::Platform::ELFFile + # ELF file format constants + EI_MAG0 = 0 + EI_MAG1 = 1 + EI_MAG2 = 2 + EI_MAG3 = 3 + EI_CLASS = 4 + EI_DATA = 5 + + ELFMAG0 = 0x7f + ELFMAG1 = 0x45 # 'E' + ELFMAG2 = 0x4c # 'L' + ELFMAG3 = 0x46 # 'F' + + ELFCLASS32 = 1 + ELFCLASS64 = 2 + + ELFDATA2LSB = 1 # Little endian + ELFDATA2MSB = 2 # Big endian + + PT_INTERP = 3 # Program header type for interpreter + + # Minimal ELF file reader to extract interpreter path + class Reader + attr_reader :interpreter + + def initialize(file_path) + @file_path = file_path + @interpreter = nil + @file_size = File.size(file_path) + + File.open(file_path, "rb") do |file| + parse_elf_header(file) + extract_interpreter(file) if valid_elf? + end + end + + private + + def parse_elf_header(file) + # Read ELF identification (16 bytes) + @e_ident = file.read(16)&.unpack("C*") + return unless @e_ident&.size == 16 + + # Verify ELF magic number + return unless @e_ident[EI_MAG0] == ELFMAG0 && + @e_ident[EI_MAG1] == ELFMAG1 && + @e_ident[EI_MAG2] == ELFMAG2 && + @e_ident[EI_MAG3] == ELFMAG3 + + @ei_class = @e_ident[EI_CLASS] + @ei_data = @e_ident[EI_DATA] + + # Determine if 32-bit or 64-bit and endianness + @is_64bit = @ei_class == ELFCLASS64 + @is_little_endian = @ei_data == ELFDATA2LSB + + # Read rest of ELF header based on architecture + read_elf_header_fields(file) + end + + def read_elf_header_fields(file) + if @is_64bit + # 64-bit ELF header (remaining fields after e_ident) + header_data = file.read(48) + return unless header_data&.size == 48 + + # ELF64 header: e_type(2) e_machine(2) e_version(4) e_entry(8) e_phoff(8) e_shoff(8) e_flags(4) e_ehsize(2) e_phentsize(2) e_phnum(2) e_shentsize(2) e_shnum(2) e_shstrndx(2) + if @is_little_endian + @e_type, @e_machine, @e_version, @e_entry, @e_phoff, @e_shoff, @e_flags, @e_ehsize, @e_phentsize, @e_phnum, @e_shentsize, @e_shnum, @e_shstrndx = header_data.unpack("vvVQQ>Q>Nnnnnnn") + end + else + # 32-bit ELF header + header_data = file.read(36) + return unless header_data&.size == 36 + + # ELF32 header: e_type(2) e_machine(2) e_version(4) e_entry(4) e_phoff(4) e_shoff(4) e_flags(4) e_ehsize(2) e_phentsize(2) e_phnum(2) e_shentsize(2) e_shnum(2) e_shstrndx(2) + if @is_little_endian + @e_type, @e_machine, @e_version, @e_entry, @e_phoff, @e_shoff, @e_flags, @e_ehsize, @e_phentsize, @e_phnum, @e_shentsize, @e_shnum, @e_shstrndx = header_data.unpack("vvVVVVVvvvvvv") + else + @e_type, @e_machine, @e_version, @e_entry, @e_phoff, @e_shoff, @e_flags, @e_ehsize, @e_phentsize, @e_phnum, @e_shentsize, @e_shnum, @e_shstrndx = header_data.unpack("nnNNNNNnnnnnnn") + end + end + end + + def extract_interpreter(file) + return unless @e_phoff && @e_phnum && @e_phentsize + + # Read program headers to find PT_INTERP + @e_phnum.times do |idx| + ph_offset = @e_phoff + @e_phentsize * idx + + file.seek(ph_offset) + + if @is_64bit + # 64-bit program header: p_type(4) p_flags(4) p_offset(8) p_vaddr(8) p_paddr(8) p_filesz(8) p_memsz(8) p_align(8) + ph_data = file.read(56) + next unless ph_data&.size == 56 + + if @is_little_endian + p_type, _p_flags, p_offset, _p_vaddr, _p_paddr, p_filesz, _p_memsz, _p_align = ph_data.unpack("VVQQ>Q>Q>Q>Q>") + end + else + # 32-bit program header: p_type(4) p_offset(4) p_vaddr(4) p_paddr(4) p_filesz(4) p_memsz(4) p_flags(4) p_align(4) + ph_data = file.read(32) + next unless ph_data&.size == 32 + + if @is_little_endian + p_type, p_offset, _p_vaddr, _p_paddr, p_filesz, _p_memsz, _p_flags, _p_align = ph_data.unpack("VVVVVVVV") + else + p_type, p_offset, _p_vaddr, _p_paddr, p_filesz, _p_memsz, _p_flags, _p_align = ph_data.unpack("NNNNNNNN") + end + end + + next unless p_type == PT_INTERP && p_filesz > 0 && p_offset < @file_size + # Found interpreter segment, read the path + file.seek(p_offset) + interp_data = file.read([p_filesz, 256].min) # Limit read size + @interpreter = interp_data&.unpack("Z*")&.first # Null-terminated string + break + end + end + + def valid_elf? + return false unless @e_ident && + @e_ident[EI_MAG0] == ELFMAG0 && + @e_ident[EI_MAG1] == ELFMAG1 && + @e_ident[EI_MAG2] == ELFMAG2 && + @e_ident[EI_MAG3] == ELFMAG3 && + (@ei_class == ELFCLASS32 || @ei_class == ELFCLASS64) + + # Check if we have enough data for a complete ELF header + min_size = @is_64bit ? 64 : 52 # e_ident(16) + header(48 for 64-bit, 36 for 32-bit) + @file_size >= min_size + end + end + + module_function + + # Extract interpreter path from ELF executable + # Based on Python's packaging._elffile.ELFFile + # https://github.com/pypa/packaging/blob/0055d4b56ae868bbcc7825c9ad68f49cdcb9f8b9/src/packaging/_elffile.py#L44-L74 + def interpreter(file_path) + reader = Reader.new(file_path) + reader.interpreter + rescue Errno::ENOENT + nil + end +end diff --git a/lib/rubygems/platform/manylinux.rb b/lib/rubygems/platform/manylinux.rb new file mode 100644 index 000000000000..d85cb28b9e5a --- /dev/null +++ b/lib/rubygems/platform/manylinux.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +## +# Manylinux support for Ruby platform detection, inspired by Python's packaging system. +# This module implements logic to detect glibc version for generating compatible manylinux platform tags. +# +# Based on Python's packaging._manylinux +# https://github.com/pypa/packaging/blob/0055d4b56ae868bbcc7825c9ad68f49cdcb9f8b9/src/packaging/_manylinux.py + +module Gem::Platform::Manylinux + module_function + + # glibc version detection for manylinux support + # Based on Python's packaging._manylinux._get_glibc_version + # https://github.com/pypa/packaging/blob/0055d4b56ae868bbcc7825c9ad68f49cdcb9f8b9/src/packaging/_manylinux.py#L172-L177 + def glibc_version + return @glibc_version if defined?(@glibc_version) + + # Try confstr method first (faster and more reliable) + version_str = glibc_version_string_confstr + version_str ||= glibc_version_string_ctypes + + @glibc_version = version_str ? parse_glibc_version(version_str) : nil + end + + def glibc_version_string_confstr + # Ruby equivalent of Python's os.confstr approach + # Based on Python's packaging._manylinux._glibc_version_string_confstr + # https://github.com/pypa/packaging/blob/0055d4b56ae868bbcc7825c9ad68f49cdcb9f8b9/src/packaging/_manylinux.py#L85-L101 + begin + # CS_GNU_LIBC_VERSION might not be defined on all systems + return nil unless defined?(Etc::CS_GNU_LIBC_VERSION) + + version_string = Etc.confstr(Etc::CS_GNU_LIBC_VERSION) + + # Should return something like "glibc 2.17" + return version_string if version_string&.include?("glibc") + rescue LoadError, SystemCallError, ArgumentError + # Etc not available, confstr not supported, CS_GNU_LIBC_VERSION not supported + end + + nil + end + + def glibc_version_string_ctypes + # Ruby equivalent of Python's ctypes approach to get glibc version + # Based on Python's packaging._manylinux._glibc_version_string_ctypes + # https://github.com/pypa/packaging/blob/0055d4b56ae868bbcc7825c9ad68f49cdcb9f8b9/src/packaging/_manylinux.py#L104-L149 + + # Try to get version from ldd --version (most reliable) + begin + output = Gem::Util.popen("ldd", "--version", { err: [:child, :out] }) + if output.match?(/glibc|GNU libc/i) + # Look for version pattern like "ldd (GNU libc) 2.17" or "glibc 2.17" + if match = output.match(/(?:glibc|GNU libc|libc).*?(\d+\.\d+)/i) + return match[1] + end + end + rescue StandardError + # Ignore errors and try alternative method + end + + # Try to get version from GNU libc shared library directly + begin + # Common libc.so.6 locations + libc_paths = [ + "/lib/libc.so.6", + "/lib64/libc.so.6", + "/lib/x86_64-linux-gnu/libc.so.6", + "/lib/aarch64-linux-gnu/libc.so.6", + ] + + libc_paths.each do |path| + next unless File.exist?(path) + + output = Gem::Util.popen(path, { err: [:child, :out] }) + if match = output.match(/GNU C Library.*?version (\d+\.\d+)/i) + return match[1] + end + end + rescue StandardError + # Ignore errors + end + + nil + end + + # Based on Python's packaging._manylinux._parse_glibc_version + # https://github.com/pypa/packaging/blob/0055d4b56ae868bbcc7825c9ad68f49cdcb9f8b9/src/packaging/_manylinux.py#L153-L169 + def parse_glibc_version(version_str) + if match = version_str.match(/^(\d+)\.(\d+)/) + [match[1].to_i, match[2].to_i] + end + end + + # Generate manylinux tags for given architectures + # Based on Python's packaging._manylinux.platform_tags + # https://github.com/pypa/packaging/blob/0055d4b56ae868bbcc7825c9ad68f49cdcb9f8b9/src/packaging/_manylinux.py#L217-L261 + def platform_tags(archs, glibc_ver) + return enum_for(__method__, archs, glibc_ver) unless block_given? + + major, minor = glibc_ver + return if major < 2 # glibc must be at least 2.x + + archs.each do |arch| + # Generate compatible glibc versions from current down to minimum + min_minor = arch.match?(/^(x86_64|i686)$/) ? 5 : 17 # x86/i686 supports older glibc + + major.downto(2) do |maj| + max_min = maj == major ? minor : 50 # Assume max minor version + start_minor = maj == 2 && min_minor > 0 ? [max_min, min_minor].max : max_min + + start_minor.downto(maj == 2 ? [min_minor, 0].max : 0) do |min| + yield "manylinux_#{maj}_#{min}_#{arch}" + end + end + end + end +end diff --git a/lib/rubygems/platform/musllinux.rb b/lib/rubygems/platform/musllinux.rb new file mode 100644 index 000000000000..87d472fd145c --- /dev/null +++ b/lib/rubygems/platform/musllinux.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +## +# Musllinux support for Ruby platform detection, inspired by Python's packaging system. +# This module implements logic to detect musl version for generating compatible musllinux platform tags. +# +# Based on Python's packaging._musllinux +# https://github.com/pypa/packaging/blob/0055d4b56ae868bbcc7825c9ad68f49cdcb9f8b9/src/packaging/_musllinux.py + +require_relative "elffile" + +module Gem::Platform::Musllinux + module_function + + # musl version detection for musllinux support + # Based on Python's packaging._musllinux._get_musl_version + # https://github.com/pypa/packaging/blob/0055d4b56ae868bbcc7825c9ad68f49cdcb9f8b9/src/packaging/_musllinux.py#L33-L53 + def musl_version + return @musl_version if defined?(@musl_version) + + @musl_version = detect_musl_version + end + + # Detect musl version using ELF parsing approach like Python + # Based on Python's packaging._musllinux._get_musl_version + # https://github.com/pypa/packaging/blob/0055d4b56ae868bbcc7825c9ad68f49cdcb9f8b9/src/packaging/_musllinux.py#L33-L53 + def detect_musl_version + # Get current Ruby executable path + executable = RbConfig.ruby + + # Extract ELF interpreter path + interpreter = Gem::Platform::ELFFile.interpreter(executable) + return nil unless interpreter&.include?("musl") + + # Execute the interpreter to get version info + begin + # Run the musl interpreter which prints version to stderr + result = Gem::Util.popen(interpreter, { err: [:child, :out] }) + parse_musl_version(result) + rescue StandardError + # Fallback to ldd-based detection if ELF parsing fails + fallback_musl_detection + end + end + + # Parse musl version from interpreter output + # Based on Python's packaging._musllinux._parse_musl_version + # https://github.com/pypa/packaging/blob/0055d4b56ae868bbcc7825c9ad68f49cdcb9f8b9/src/packaging/_musllinux.py#L23-L30 + def parse_musl_version(output) + lines = output.strip.split("\n") + return nil if lines.empty? + + # First line should start with "musl libc" + first_line = lines[0] + return nil unless first_line&.start_with?("musl") + + # Look for version in format "Version X.Y" in any line + lines.each do |line| + if match = line.match(/Version (\d+)\.(\d+)/i) + return [match[1].to_i, match[2].to_i] + end + end + + nil + end + + # Fallback musl detection when ELF parsing fails + def fallback_musl_detection + # Try to get musl version from ldd output + begin + output = Gem::Util.popen("ldd", "--version", { err: [:child, :out] }) + if output.match?(/musl/i) + # Look for version pattern like "musl libc (x86_64) Version 1.2.2" + if match = output.match(/Version (\d+)\.(\d+)/i) + return [match[1].to_i, match[2].to_i] + end + end + rescue StandardError + # Ignore errors + end + + # Try alternative detection methods + begin + # Check if we can find musl ld.so and execute it + musl_loaders = Dir.glob("/lib/ld-musl-*.so.1") + Dir.glob("/usr/lib/ld-musl-*.so.1") + + musl_loaders.each do |loader| + next unless File.executable?(loader) + + output = Gem::Util.popen(loader, { err: [:child, :out] }) + if match = output.match(/Version (\d+)\.(\d+)/i) + return [match[1].to_i, match[2].to_i] + end + end + rescue StandardError + # Ignore errors + end + + nil + end + + def musl_system? + # Use ELF parsing approach like Python to detect musl + executable = RbConfig.ruby + interpreter = Gem::Platform::ELFFile.interpreter(executable) + + return true if interpreter&.include?("musl") + + # Fallback to traditional detection methods + return true if Dir.glob("/lib/ld-musl-*.so.1").any? + return true if Dir.glob("/usr/lib/ld-musl-*.so.1").any? + + # Check ldd version for musl signature + begin + output = Gem::Util.popen("ldd", "--version", { err: [:child, :out] }) + return true if output.match?(/musl/i) + rescue StandardError + # Ignore errors + end + + false + end + + # Generate musllinux tags for given architectures + # Based on Python's packaging._musllinux.platform_tags + # https://github.com/pypa/packaging/blob/0055d4b56ae868bbcc7825c9ad68f49cdcb9f8b9/src/packaging/_musllinux.py#L56-L72 + def platform_tags(archs, musl_ver) + return enum_for(__method__, archs, musl_ver) unless block_given? + + major, minor = musl_ver + + archs.each do |arch| + # Generate compatible musl versions from current down to 0 + minor.downto(0) do |min| + yield "musllinux_#{major}_#{min}_#{arch}" + end + end + end +end diff --git a/lib/rubygems/platform/specific.rb b/lib/rubygems/platform/specific.rb new file mode 100644 index 000000000000..9aba4faeb031 --- /dev/null +++ b/lib/rubygems/platform/specific.rb @@ -0,0 +1,567 @@ +# frozen_string_literal: true + +## +# Platform-specific gem matching for Ruby platform tags. +# +# The Gem::Platform::Specific class extends traditional platform matching with +# detailed Ruby environment information, enabling precise gem selection based on +# interpreter type, ABI version, and platform details. This is particularly +# useful for gems with native extensions or platform-specific behavior. +# +# == When to Use Gem::Platform::Specific vs Gem::Platform +# +# Use Gem::Platform::Specific when you need: +# - Precise Ruby interpreter and ABI version matching +# - Linux libc version detection (glibc vs musl) +# - Wheel-format compatibility matching +# - Advanced platform tag generation for gem publishing +# +# Use traditional Gem::Platform for: +# - Simple platform string matching ("x86_64-linux") +# - Legacy compatibility requirements +# - Basic platform detection without Ruby environment details +# +# == Basic Usage +# +# # Create from current environment +# specific = Gem::Platform::Specific.local +# specific.platform #=> # +# specific.ruby_engine #=> "ruby" +# specific.ruby_version #=> "3.3.1" +# specific.libc_type #=> "glibc" +# +# # Create for specific platform and Ruby version +# specific = Gem::Platform::Specific.new( +# "x86_64-linux", +# ruby_engine: "ruby", +# ruby_version: "3.2.0", +# abi_version: "3.2.0" +# ) +# +# == Wheel Compatibility +# +# Generate wheel-compatible tags for gem publishing: +# +# specific = Gem::Platform::Specific.local +# specific.each_possible_match do |abi_tag, platform_tag| +# puts "#{abi_tag}-#{platform_tag}" +# end +# # Output: +# # cr33-x86_64_linux +# # rb33-x86_64_linux +# # rb3-x86_64_linux +# # any-x86_64_linux +# # any-any +# +# == Linux libc Detection +# +# On Linux systems, automatically detects libc implementation: +# +# # On glibc system +# specific = Gem::Platform::Specific.local +# specific.libc_type #=> "glibc" +# specific.libc_version #=> [2, 31] +# +# # On musl system +# specific = Gem::Platform::Specific.local +# specific.libc_type #=> "musl" +# specific.libc_version #=> [1, 2] +# +# == Performance Characteristics +# +# - Platform tag generation is cached after first computation +# - Linux libc detection executes shell commands once per process +# - Thread-safe for read operations after initialization +# - Memory usage scales with number of generated platform tags +# +# == Migration from Gem::Platform +# +# # Before +# platform = Gem::Platform.local +# compatible = platform === other_platform +# +# # After +# specific = Gem::Platform::Specific.local +# compatible = specific === other_platform +# # Provides same compatibility but with enhanced matching +# +# This class provides detailed platform and Ruby environment information +# to enable precise gem matching based on interpreter, ABI, and platform details. + +class Gem::Platform::Specific + attr_reader :platform, :ruby_engine, :ruby_engine_version, :ruby_version, :abi_version, :libc_type, :libc_version, + :ruby_abi_tag, :platform_tags, :rb_version_range, :normalized_platform_tags + + ## + # Creates a new Gem::Platform::Specific instance. + # + # [+platform+] Platform string or Gem::Platform object (e.g., "x86_64-linux") + # [+ruby_engine+] Ruby engine name ("ruby", "jruby", "truffleruby") + # [+ruby_engine_version+] Engine version (e.g., "3.3.1") + # [+ruby_version+] Ruby language version (e.g., "3.3.1") + # [+abi_version+] ABI version for binary compatibility (e.g., "3.3.0") + # [+libc_type+] Linux libc implementation ("glibc" or "musl") + # [+libc_version+] libc version as [major, minor] array + # + # If ruby environment parameters are omitted, some features (like ABI tag + # generation) will not be available. Use .local for current environment. + def initialize(platform, ruby_engine: nil, ruby_engine_version: nil, ruby_version: nil, abi_version: nil, libc_type: nil, libc_version: nil) + @platform = platform.is_a?(Gem::Platform) ? platform : Gem::Platform.new(platform) + @ruby_engine = ruby_engine + @ruby_engine_version = ruby_engine_version + @ruby_version = ruby_version + @abi_version = abi_version + @libc_type = libc_type + @libc_version = libc_version + @ruby_abi_tag = Gem::Platform::Specific.generate_ruby_abi_tag(ruby_engine, ruby_engine_version, ruby_version, abi_version) + + # Precompute expensive arrays + @platform_tags = _platform_tags.freeze + @rb_version_range = _rb_version_range.freeze + @normalized_platform_tags = @platform_tags.map {|platform_str| Gem::Platform::Wheel.normalize_tag_set(platform_str) }.freeze + end + + def to_s + components = [@platform.to_s] + # Always include version as the first attribute for format tracking + components << "v:1" + components << "engine:#{@ruby_engine}" if @ruby_engine + components << "engine_version:#{@ruby_engine_version}" if @ruby_engine_version + components << "ruby_version:#{@ruby_version}" if @ruby_version + components << "abi_version:#{@abi_version}" if @abi_version + components << "libc_type:#{@libc_type}" if @libc_type + if @libc_version + # Serialize libc_version array as dot-joined string to avoid parsing issues + components << "libc_version:#{@libc_version.join(".")}" + end + components.join(" ") + end + + def ==(other) + other.is_a?(self.class) && + @platform == other.platform && + @ruby_engine == other.ruby_engine && + @ruby_engine_version == other.ruby_engine_version && + @ruby_version == other.ruby_version && + @abi_version == other.abi_version && + @libc_type == other.libc_type && + @libc_version == other.libc_version + end + alias_method :eql?, :== + + def hash + [@platform, @ruby_engine, @ruby_engine_version, @ruby_version, @abi_version, @libc_type, @libc_version].hash + end + + ## + # Generates wheel-compatible platform tags in priority order. + # + # Yields [abi_tag, platform_tag] pairs in descending compatibility order, + # from most specific (exact interpreter + platform match) to most general + # (any-any fallback). + # + # Tag generation follows this priority order: + # 1. Current interpreter + specific ABI + platform variations (e.g., cr34_static-arm64_darwin) + # 2. Generic Ruby versions + platform variations (e.g., rb34-arm64_darwin, rb3-arm64_darwin) + # 3. Any ABI + platform variations (e.g., any-arm64_darwin) + # 4. Universal fallback (any-any) + # + # This is designed for wheel format compatibility but can be used for any + # tag-based platform matching system. + # + # specific = Gem::Platform::Specific.local + # tags = specific.each_possible_match.take(5) + # tags.each { |abi, platform| puts "#{abi}-#{platform}" } + # # cr33-x86_64_linux + # # rb33-x86_64_linux + # # rb3-x86_64_linux + # # any-x86_64_linux + # # any-any + # + # Returns an Enumerator if no block is given. + def each_possible_match(&) + return enum_for(__method__) unless block_given? + + # For ruby platform, the `platform` tag should only ever be `any`, but the ruby tag should still take into account the interpreter/ruby version + if platform == Gem::Platform::RUBY + yield ["any", "any"] + return + end + + # Use precomputed normalized platform tags + platform_tags = normalized_platform_tags + + # 1. Most specific: exact interpreter ABI with all platform variations + # Only generates tags for the current Ruby version (no stable ABI like Python) + if ruby_engine && ruby_engine_version && ruby_version && abi_version + if ruby_abi_tag + platform_tags.each do |platform_tag| + yield [ruby_abi_tag, platform_tag] + end + end + end + + # 2. Generic Ruby version tags with platform variations (backward compatibility) + # Generate rb* tags for backward compatibility, but only the versions that weren't already covered + rb_version_range.each do |version| + platform_tags.each do |platform_tag| + yield [version, platform_tag] + end + end + + # Also generate "any" platform versions for Ruby versions + rb_version_range.each do |version| + yield [version, "any"] + end + + # 3. Any ABI with platform variations (broad compatibility) + platform_tags.each do |platform_tag| + yield ["any", platform_tag] + end + + # 4. Universal fallback (maximum compatibility) + yield ["any", "any"] + end + + # Generate platform tags specific to this environment, including manylinux/musllinux tags + def _platform_tags + if platform.nil? || platform == Gem::Platform::RUBY + return [] + end + + tags = [] + + # Generate base platform tags first + if platform.os == "darwin" && platform.version + _darwin_platform_tags(tags) + else + # Non-Darwin platforms: use existing logic + tags << platform.to_s + if platform.cpu != "universal" + tags << ["universal", platform.os, platform.version].compact.join("-") + end + + # For Linux platforms with glibc suffix, also generate version without suffix for broader compatibility + if platform.os == "linux" && platform.version == "gnu" + tags << [platform.cpu, platform.os].compact.join("-") + if platform.cpu != "universal" + tags << ["universal", platform.os].compact.join("-") + end + end + + # Generate manylinux/musllinux tags if we have libc information + if platform.os == "linux" && libc_type && libc_version + case libc_type + when "glibc" + tags.concat(Gem::Platform::Manylinux.platform_tags([platform.cpu], libc_version).to_a) + when "musl" + tags.concat(Gem::Platform::Musllinux.platform_tags([platform.cpu], libc_version).to_a) + end + end + + if platform.version && platform.os != "linux" + tags << [platform.cpu, platform.os].compact.join("-") + end + + tags << platform.os if (platform.cpu || platform.version) && platform.os != "linux" + end + + tags + end + + # Generate Ruby version range tags (rb33, rb3, rb32, etc.) + def _rb_version_range + return [] unless ruby_version + + tags = [] + parts = ruby_version.split(".").map!(&:to_i) + tags << "rb#{parts[0, 2].join}" if parts.size > 1 + tags << "rb#{parts[0]}" + + if parts.size > 1 + parts[1].pred.downto(0) do |minor| + tags << "rb#{parts[0]}#{minor}" + end + end + + tags + end + + # Generate Darwin platform tags that can actually match via === operator + def _darwin_platform_tags(tags) + # Generate Darwin platform tags that can actually match via === operator + # Only generate exact version and generic tags since platform matching requires exact version matches + current_version = platform.version.to_i + cpu_arch = platform.cpu + + # Generate tags for current version only + formats = _darwin_binary_formats(cpu_arch, current_version) + formats.each do |format| + tags << [format, platform.os, current_version].compact.join("-") + end + + # Generic OS tags without version (broadest compatibility) + _darwin_binary_formats(cpu_arch, current_version).each do |format| + tags << [format, platform.os].compact.join("-") + end + tags << platform.os + end + + # Generate binary format combinations for Ruby-supported architectures + def _darwin_binary_formats(cpu_arch, darwin_version) + # Generate binary format combinations for Ruby-supported architectures + # Simplified from Python's _mac_binary_formats for RubyGems needs + + case cpu_arch + when "x86_64" + # x86_64 supported from Darwin 8+ (Mac OS X 10.4+) + if darwin_version >= 8 + ["x86_64", "universal"] + else + [] + end + when "x86" + # x86 (i386) supported from Darwin 8+ (Mac OS X 10.4+) + if darwin_version >= 8 + ["x86", "universal"] + else + [] + end + when "arm64" + # arm64 supported from Darwin 20+ (macOS 11+) + if darwin_version >= 20 + ["arm64", "universal"] + else + [] + end + when "universal" + # universal always works for any Darwin version + ["universal"] + else + [] + end + end + + # Generate compatible tags for this specific environment + def compatible_tags + return enum_for(__method__) unless block_given? + + rb_version_range.each do |version| + normalized_platform_tags.each do |platform_tag| + yield version, platform_tag + end + end + # yield engine, "any" if engine + rb_version_range.each do |version| + yield version, "any" + end + end + + def =~(other) + case other + when Gem::Platform, Gem::Platform::Wheel + when Gem::Platform::Specific then other = other.platform + when String then other = Gem::Platform.new(other) + else + return + end + platform === other + end + + def ===(other) + case other + when Gem::Platform::Specific then + # Compare both platform and Ruby environment specifics + @platform === other.platform && + (@ruby_engine.nil? || other.ruby_engine.nil? || @ruby_engine == other.ruby_engine) && + (@ruby_engine_version.nil? || other.ruby_engine_version.nil? || @ruby_engine_version == other.ruby_engine_version) && + (@ruby_version.nil? || other.ruby_version.nil? || @ruby_version == other.ruby_version) && + (@abi_version.nil? || other.abi_version.nil? || @abi_version == other.abi_version) + when Gem::Platform::Wheel then + # Use wheel matching logic with this Specific object + other.send(:match?, self) + when Gem::Platform then + # Delegate to underlying platform matching + @platform === other + else + false + end + end + + private + + # Get the current Ruby ABI tag for the local environment + def self.current_ruby_abi_tag + local.ruby_abi_tag + end + + ENGINE_MAP = { + "truffleruby" => :tr, + "ruby" => :cr, + "jruby" => :jr, + }.freeze + private_constant :ENGINE_MAP + + # Generate ruby ABI tag from specific Ruby environment details + def self.generate_ruby_abi_tag(ruby_engine, ruby_engine_version, ruby_version, abi_version) + return nil if !ruby_engine || !ruby_engine_version || !ruby_version + + engine_prefix = ENGINE_MAP[ruby_engine] || ruby_engine + version_segments = ruby_engine_version.split(".") + version_part = "#{version_segments[0]}#{version_segments[1]}" + + abi_suffix = extract_abi_suffix(abi_version || ruby_version, ruby_version) + + abi_suffix.empty? ? "#{engine_prefix}#{version_part}" : "#{engine_prefix}#{version_part}_#{abi_suffix}" + end + + # Extract ABI suffix from version strings with consistent string manipulation + def self.extract_abi_suffix(abi_version_to_use, ruby_version) + ruby_version_segments = ruby_version.split(".") + major_minor_zero = "#{ruby_version_segments[0]}.#{ruby_version_segments[1]}.0" + + suffix = if abi_version_to_use.start_with?(major_minor_zero) + abi_version_to_use.sub(/^#{Regexp.escape(major_minor_zero)}/, "") + elsif abi_version_to_use =~ /^#{ruby_version_segments[0]}\.#{ruby_version_segments[1]}\.(\d+)(.*)$/ + patch_version = $1 + extra_suffix = $2 + + # For engines like TruffleRuby, if the abi_version starts with the exact ruby_version + # followed by engine-specific versioning, ignore the engine-specific part entirely + if ruby_version_segments[2] && patch_version == ruby_version_segments[2] && + !extra_suffix.empty? && abi_version_to_use.start_with?(ruby_version + ".") + "" + else + patch_and_suffix = patch_version + extra_suffix + patch_and_suffix == "0" ? "" : patch_and_suffix + end + else + abi_version_to_use.tr(".", "") + end + + normalize_abi_suffix(suffix) + end + + # Normalize ABI suffix by converting separators and removing leading chars + def self.normalize_abi_suffix(suffix) + suffix.tr("-", "_").tr(".", "_").sub(/^[._]/, "") + end + + private_class_method :extract_abi_suffix, :normalize_abi_suffix + + ## + # Parses a Gem::Platform::Specific string representation back into an object. + # + # [+specific_string+] String output from Gem::Platform::Specific#to_s + # + # Parses the space-separated key:value format produced by #to_s back into + # a Specific object with all original attributes restored. This is essential + # for Bundler lockfile parsing and other serialization needs. + # + # The format expected is: + # "platform_string v:version engine:value engine_version:value ruby_version:value abi_version:value libc_type:value libc_version:value" + # + # The version (v:) attribute is mandatory and tracks the format version (currently 1). + # All other attributes except the platform string are optional and will be nil if not present. + # The libc_version is expected as dot-separated values (e.g., "2.31") which are parsed to [2, 31]. + # + # # Parse a full specification + # str = "x86_64-linux v:1 engine:ruby engine_version:3.3.1 ruby_version:3.3.1 abi_version:3.3.0 libc_type:glibc libc_version:2.31" + # specific = Gem::Platform::Specific.parse(str) + # specific.platform.to_s #=> "x86_64-linux" + # specific.ruby_engine #=> "ruby" + # specific.libc_version #=> [2, 31] + # + # # Parse minimal specification (platform only) + # minimal = Gem::Platform::Specific.parse("x86_64-linux v:1") + # minimal.platform.to_s #=> "x86_64-linux" + # minimal.ruby_engine #=> nil + # + # Raises ArgumentError if the string format is invalid or if required + # platform information cannot be parsed. + def self.parse(specific_string) + return nil if specific_string.nil? || specific_string.empty? + + parts = specific_string.strip.split(/\s+/) + return nil if parts.empty? + + # First part is always the platform string + platform_str = parts.shift + platform = Gem::Platform.new(platform_str) + + # Parse remaining key:value pairs and validate version + attributes = {} + format_version = nil + + parts.each do |part| + key, value = part.split(":", 2) + next unless key && value + + case key + when "v" + format_version = value.to_i + when "engine" + attributes[:ruby_engine] = value + when "engine_version" + attributes[:ruby_engine_version] = value + when "ruby_version" + attributes[:ruby_version] = value + when "abi_version" + attributes[:abi_version] = value + when "libc_type" + attributes[:libc_type] = value + when "libc_version" + # Parse dot-separated format like "2.31" back to [2, 31] + if value.include?(".") + attributes[:libc_version] = value.split(".").map(&:to_i) + else + # Single value or malformed - for manylinux/musllinux compatibility, + # we need at least 2 elements [major, minor], so set to nil for single values + attributes[:libc_version] = nil + end + end + end + + # Validate format version - version is mandatory + case format_version + when 1 + # Current format version - proceed normally + when nil + raise ArgumentError, "missing required version field (v:1)" + else + raise ArgumentError, "unsupported specific format version: #{format_version} (supported: 1)" + end + + new(platform, **attributes) + rescue StandardError => e + raise ArgumentError, "invalid specific string format: #{specific_string.inspect} (#{e.message})" + end + + # Create a Specific instance representing the local Ruby environment + def self.local(platform = Gem::Platform.local) + return platform if platform == Gem::Platform::RUBY + + # For Linux platforms, detect libc type and version + libc_type = nil + libc_version = nil + if platform.os == "linux" + if platform.version&.include?("musl") + libc_type = "musl" + libc_version = Gem::Platform::Musllinux.musl_version + else + libc_type = "glibc" + libc_version = Gem::Platform::Manylinux.glibc_version + end + end + + new( + platform, + ruby_engine: RUBY_ENGINE, + ruby_engine_version: RUBY_ENGINE_VERSION, + ruby_version: RUBY_VERSION, + abi_version: Gem.extension_api_version, + libc_type: libc_type, + libc_version: libc_version + ) + end +end diff --git a/lib/rubygems/platform/wheel.rb b/lib/rubygems/platform/wheel.rb new file mode 100644 index 000000000000..237b4a26f44c --- /dev/null +++ b/lib/rubygems/platform/wheel.rb @@ -0,0 +1,375 @@ +# frozen_string_literal: true + +## +# Wheel platform matching for Ruby wheel formats. +# +# The Gem::Platform::Wheel class provides wheel-format platform tag parsing +# and matching against Ruby platform specifications. This enables compatibility +# with Python's wheel format conventions while maintaining Ruby-specific +# platform semantics. +# +# Wheel format follows the pattern: whl-{abi_tag}-{platform_tag} +# where tags can be combined with dots (e.g., "whl-rb33.rb32-x86_64_linux.any") +# +# == When to Use Gem::Platform::Wheel +# +# Use Gem::Platform::Wheel when you need: +# - Parsing wheel-format platform strings from gem specifications +# - Cross-language compatibility with Python wheel conventions +# - Multi-platform gem distribution with precise ABI targeting +# - Checking compatibility between wheel specs and Ruby environments +# +# Use Gem::Platform::Specific for: +# - Generating wheel-compatible tags from Ruby environments +# - Ruby-centric platform detection and matching +# - Local environment analysis and tag generation +# +# == Basic Usage +# +# # Parse wheel format string +# wheel = Gem::Platform::Wheel.new("whl-rb33-x86_64_linux") +# wheel.ruby_abi_tag #=> "rb33" +# wheel.platform_tags #=> "x86_64_linux" +# +# # Check compatibility with current environment +# current_platform = Gem::Platform::Specific.local +# compatible = wheel === current_platform #=> true/false +# +# # Multi-tag wheel support +# multi_wheel = Gem::Platform::Wheel.new("whl-rb33.rb32.any-x86_64_linux.any") +# multi_wheel.expand +# #=> [["rb33", "x86_64_linux"], ["rb33", "any"], +# # ["rb32", "x86_64_linux"], ["rb32", "any"], +# # ["any", "x86_64_linux"], ["any", "any"]] +# +# == Tag Normalization +# +# Platform and ABI tags are automatically normalized: +# - Dots and hyphens become underscores: "x86-64" -> "x86_64" +# - Tags are sorted and deduplicated: "rb32.rb33.rb32" -> "rb32.rb33" +# +# wheel = Gem::Platform::Wheel.new("whl-rb33-x86-64.darwin") +# wheel.platform_tags #=> "darwin.x86_64" +# +# == Compatibility Matching +# +# Wheel compatibility follows these rules: +# 1. "any" tags match everything +# 2. Specific tags must match exactly +# 3. Multi-tag wheels match if ANY tag combination is compatible +# +# # Universal wheel matches everything +# universal = Gem::Platform::Wheel.new("whl-any-any") +# universal === any_platform #=> true +# +# # Multi-tag wheel has fallback compatibility +# fallback = Gem::Platform::Wheel.new("whl-rb33.any-x86_64_linux.any") +# fallback === old_ruby_env #=> true (via "any" ABI tag) +# fallback === different_arch #=> true (via "any" platform tag) +# +# == Error Handling +# +# Invalid wheel format strings raise ArgumentError: +# +# Gem::Platform::Wheel.new("invalid-format") #=> ArgumentError +# Gem::Platform::Wheel.new("whl-INVALID-tag") #=> ArgumentError +# Gem::Platform::Wheel.new("whl-rb33") #=> ArgumentError (missing platform) +# +# == Performance Characteristics +# +# - Tag parsing and normalization happen at initialization +# - Compatibility checks are O(nxm) where n,m are tag counts +# - Memory usage scales with number of tags in wheel specification +# - Thread-safe for read operations after initialization +# +# == Integration with Gem::Platform::Specific +# +# Wheel objects work seamlessly with Specific objects for environment matching: +# +# specific = Gem::Platform::Specific.local +# wheel = Gem::Platform::Wheel.new("whl-rb33-x86_64_linux") +# +# compatible = wheel === specific +# # Uses specific.each_possible_match internally for comprehensive checking +# +# This class bridges Python wheel conventions with Ruby's platform system, +# enabling cross-ecosystem compatibility while maintaining Ruby semantics. + +class Gem::Platform::Wheel + attr_reader :ruby_abi_tag, :platform_tags + + ## + # Normalizes wheel tag sets by converting separators and sorting. + # + # [+tags+] Tag string with dot-separated values, or nil/empty + # + # Performs normalization by: + # - Converting dots and hyphens to underscores + # - Splitting on dots, deduplicating, and sorting + # - Rejoining with dots for consistent representation + # + # Returns "any" for nil or empty input, preserving wheel format conventions. + # + # normalize_tag_set("rb33.rb32.rb33") #=> "rb32.rb33" + # normalize_tag_set("x86-64.darwin") #=> "darwin.x86_64" + # normalize_tag_set(nil) #=> "any" + # normalize_tag_set("") #=> "any" + def self.normalize_tag_set(tags) + return "any" if tags.nil? || tags.empty? + tags.split(".").map {|tag| tag.gsub(/[.-]/, "_") }.uniq.sort.join(".") + end + + ## + # Creates a new Gem::Platform::Wheel instance. + # + # [+wheel_string+] Wheel format string or existing Wheel object + # + # Parses wheel format strings following the pattern "whl-{abi_tag}-{platform_tag}". + # Both abi_tag and platform_tag can contain multiple dot-separated values for + # compatibility with multiple targets. + # + # If passed an existing Wheel object, creates a copy with the same tags. + # + # Raises ArgumentError for: + # - Invalid wheel format (missing "whl-" prefix or wrong part count) + # - Invalid tag characters (must follow platform naming conventions) + # - Non-string, non-Wheel arguments + # + # # Basic wheel specification + # wheel = Gem::Platform::Wheel.new("whl-rb33-x86_64_linux") + # + # # Multi-target wheel + # wheel = Gem::Platform::Wheel.new("whl-rb33.rb32.any-x86_64_linux.any") + # + # # Copy constructor + # copy = Gem::Platform::Wheel.new(existing_wheel) + def initialize(wheel_string) + case wheel_string + when Gem::Platform::Wheel + @ruby_abi_tag = wheel_string.ruby_abi_tag + @platform_tags = wheel_string.platform_tags + return + when String + else + raise ArgumentError + end + + parts = wheel_string.split("-", 3) + unless parts.size == 3 && parts[0] == "whl" + raise ArgumentError, "invalid wheel string format: #{wheel_string.inspect}" + end + + @ruby_abi_tag = self.class.normalize_tag_set(parts[1]) + @platform_tags = self.class.normalize_tag_set(parts[2]) + + validate_tags! + end + + ## + # Returns the canonical wheel format string representation. + # + # Reconstructs the wheel string from parsed components in the format + # "whl-{abi_tag}-{platform_tag}". Tags remain normalized as stored. + # + # wheel = Gem::Platform::Wheel.new("whl-rb33.rb32-x86_64_linux.any") + # wheel.to_s #=> "whl-rb32.rb33-any.x86_64_linux" + def to_s + to_a.join("-") + end + + ## + # Returns the wheel components as an array. + # + # Provides access to the wheel's three components in order: + # [prefix, abi_tag, platform_tag] where prefix is always "whl". + # + # wheel = Gem::Platform::Wheel.new("whl-rb33-x86_64_linux") + # wheel.to_a #=> ["whl", "rb33", "x86_64_linux"] + def to_a + ["whl", @ruby_abi_tag, @platform_tags] + end + + ## + # Expands multi-tag wheel into all possible tag combinations. + # + # For wheels with dot-separated multiple tags, generates the Cartesian + # product of all ABI tags and platform tags. This is useful for checking + # compatibility against all possible combinations the wheel supports. + # + # Returns an array of [abi_tag, platform_tag] pairs. + # + # wheel = Gem::Platform::Wheel.new("whl-rb33.any-x86_64_linux.any") + # wheel.expand + # #=> [["rb33", "x86_64_linux"], ["rb33", "any"], + # # ["any", "x86_64_linux"], ["any", "any"]] + # + # # Single-tag wheels return single combination + # simple = Gem::Platform::Wheel.new("whl-rb33-x86_64_linux") + # simple.expand #=> [["rb33", "x86_64_linux"]] + def expand + ruby_abi_tags = @ruby_abi_tag == "any" ? ["any"] : @ruby_abi_tag.split(".") + platform_tags = @platform_tags == "any" ? ["any"] : @platform_tags.split(".") + + ruby_abi_tags.product(platform_tags) + end + + ## + # Tests wheel equality based on tag content. + # + # [+other+] Another Wheel object to compare against + # + # Two wheels are equal if they have identical normalized ABI and platform tags. + # The order of tags doesn't matter since normalization sorts them. + # + # wheel1 = Gem::Platform::Wheel.new("whl-rb33.rb32-x86_64_linux") + # wheel2 = Gem::Platform::Wheel.new("whl-rb32.rb33-x86_64_linux") + # wheel1 == wheel2 #=> true (tags are normalized and sorted) + # + # wheel3 = Gem::Platform::Wheel.new("whl-rb33-arm64_darwin") + # wheel1 == wheel3 #=> false + def ==(other) + return false unless self.class === other + to_a == other.to_a + end + + alias_method :eql?, :== + + ## + # Generates hash code for use in Hash collections. + # + # Hash is computed from the wheel's normalized components, ensuring + # equal wheels produce the same hash code for proper Hash behavior. + # + # wheel = Gem::Platform::Wheel.new("whl-rb33-x86_64_linux") + # hash = { wheel => "cached_gem" } + # hash[wheel] #=> "cached_gem" + def hash + to_a.hash + end + + ## + # Pattern matching alias for compatibility checking. + # + # [+other+] Platform object, string, or Specific object to check against + # + # Provides =~ operator support for pattern-like matching. Delegates to + # the === operator for actual compatibility logic. + # + # wheel = Gem::Platform::Wheel.new("whl-rb33-x86_64_linux") + # wheel =~ "x86_64-linux" #=> same as wheel === "x86_64-linux" + def =~(other) + case other + when Gem::Platform, Gem::Platform::Wheel then + when Gem::Platform::Specific then other = other.platform + when String then other = Gem::Platform.new(other) + else + return + end + self === other + end + + ## + # Checks wheel compatibility with Ruby platforms and environments. + # + # [+other+] Platform, Wheel, Specific object, or string to check against + # + # Performs comprehensive compatibility checking based on the type of object: + # + # - Gem::Platform::Wheel: Direct wheel-to-wheel equality comparison + # - Gem::Platform::Specific: Advanced matching using environment tag generation + # - Gem::Platform: Legacy platform matching with current Ruby ABI + # - String: Converts to Platform and matches + # + # Returns true if this wheel is compatible with the target environment. + # + # == Matching Rules + # + # 1. "any" tags always match + # 2. Specific tags must have exact matches + # 3. Multi-tag wheels match if ANY combination is compatible + # 4. ABI and platform tags are checked independently + # + # # Universal wheel matches everything + # universal = Gem::Platform::Wheel.new("whl-any-any") + # universal === anything #=> true + # + # # Specific wheel requires compatible environment + # specific = Gem::Platform::Wheel.new("whl-rb33-x86_64_linux") + # specific === Gem::Platform::Specific.local # true if Ruby 3.3 on x86_64 Linux + # + # # Multi-tag provides fallback compatibility + # fallback = Gem::Platform::Wheel.new("whl-rb33.any-x86_64_linux.any") + # fallback === old_ruby_platform # true via "any" ABI tag + def ===(other) + case other + when Gem::Platform::Wheel then + # Handle wheel-to-wheel comparison + self == other + when Gem::Platform::Specific then + # Use wheel matching logic with the Specific object + send(:match?, other) + when Gem::Platform then + # Use the new tuple-based matching approach + send(:match?, ruby_abi_tag: Gem::Platform::Specific.current_ruby_abi_tag, platform: other) + when Gem::Platform::RUBY, nil, "" + true + else + raise ArgumentError, "invalid argument #{other.inspect}" + end + end + + private + + def match?(specific = nil, ruby_abi_tag: nil, platform: nil) + # Handle both new Specific-based API and legacy parameter-based API + if specific + raise ArgumentError, "specific must be a Gem::Platform::Specific" unless specific.is_a?(Gem::Platform::Specific) + raise ArgumentError, "cannot specify both specific and keyword arguments" if ruby_abi_tag || platform + + # Use each_possible_match to check if this wheel matches any of the possible tags + wheel_abi_tags = @ruby_abi_tag.split(".") + wheel_platform_tags = @platform_tags.split(".") + + specific.each_possible_match do |abi_tag, platform_tag| + if wheel_abi_tags.include?(abi_tag) && wheel_platform_tags.include?(platform_tag) + return true + end + end + + return false + else + raise ArgumentError, "must provide either specific or both ruby_abi_tag and platform" unless ruby_abi_tag && platform + end + + # Legacy matching for non-Specific objects + # Check ruby/ABI compatibility + return false unless @ruby_abi_tag == "any" || @ruby_abi_tag.split(".").include?(ruby_abi_tag) + + # Check platform compatibility + platform_tag = Gem::Platform::Wheel.normalize_tag_set(platform.to_s) + @platform_tags == "any" || @platform_tags.split(".").include?(platform_tag) + end + + def validate_tags! + validate_ruby_abi_tag! + validate_platform_tags! + end + + def validate_ruby_abi_tag! + return if @ruby_abi_tag == "any" + @ruby_abi_tag.split(".").each do |tag| + unless /^[a-z][a-z0-9_]*$/.match?(tag) + raise ArgumentError, "invalid ruby/ABI tag: #{tag.inspect}" + end + end + end + + def validate_platform_tags! + return if @platform_tags == "any" + @platform_tags.split(".").each do |tag| + unless /^[a-z0-9_][a-z0-9_-]*$/.match?(tag) + raise ArgumentError, "invalid platform tag: #{tag.inspect}" + end + end + end +end diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index ed4cbde3bab1..7df719c4e39c 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -186,6 +186,11 @@ def resolve Gem::Molinillo::Resolver.new(self, self).resolve(@needed.map {|d| DependencyRequest.new d, nil }).tsort.filter_map(&:payload) rescue Gem::Molinillo::VersionConflict => e conflict = e.conflicts.values.first + if conflict.existing.nil? + exc = Gem::UnsatisfiableDependencyError.new conflict.requirement, [] + exc.errors = @set.errors + raise exc + end raise Gem::DependencyResolutionError, Conflict.new(conflict.requirement_trees.first.first, conflict.existing, conflict.requirement) ensure @output.close if defined?(@output) && !debug? @@ -223,6 +228,7 @@ def select_local_platforms(specs) # :nodoc: def search_for(dependency) possibles, all = find_possible(dependency) if !@soft_missing && possibles.empty? + return [] if all.empty? exc = Gem::UnsatisfiableDependencyError.new dependency, all exc.errors = @set.errors raise exc @@ -241,7 +247,7 @@ def search_for(dependency) sources.each do |source| groups[source]. - sort_by {|spec| [spec.version, -Gem::Platform.platform_specificity_match(spec.platform, Gem::Platform.local)] }. + sort_by {|spec| [spec.version, -Gem::Platform.platform_specificity_match(spec.platform, Gem::Platform::Specific.local)] }. map {|spec| ActivationRequest.new spec, dependency }. each {|activation_request| activation_requests << activation_request } end diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index 1d351f8aff97..b5d44c9193b1 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -474,7 +474,10 @@ def platform=(platform) when Gem::Platform then @new_platform = platform - # legacy constants + when Gem::Platform::Wheel then + @new_platform = platform + # Wheel platforms require RubyGems 3.8+ for proper support + self.required_rubygems_version = ">= 3.8.0.dev" if required_rubygems_version == Gem::Requirement.default when nil, Gem::Platform::RUBY then @new_platform = Gem::Platform::RUBY when "mswin32" then # was Gem::Platform::WIN32 @@ -485,6 +488,10 @@ def platform=(platform) @new_platform = Gem::Platform.new "ppc-darwin" else @new_platform = Gem::Platform.new platform + if @new_platform.is_a?(Gem::Platform::Wheel) + # Wheel platforms require RubyGems 3.8+ for proper support + self.required_rubygems_version = ">= 3.8.0.dev" if required_rubygems_version == Gem::Requirement.default + end end @platform = @new_platform.to_s @@ -1365,7 +1372,7 @@ def _dump(limit) @description, @homepage, true, # has_rdoc - @new_platform, + @new_platform.to_s, @licenses, @metadata, ] @@ -1661,6 +1668,7 @@ def conflicts_when_loaded_with?(list_of_specs) # :nodoc: def has_conflicts? return true unless Gem.env_requirement(name).satisfied_by?(version) + return true unless Gem::Platform.match_spec?(self) runtime_dependencies.any? do |dep| spec = Gem.loaded_specs[dep.name] spec && !spec.satisfies_requirement?(dep) @@ -2181,7 +2189,14 @@ def original_platform # :nodoc: # The platform this gem runs on. See Gem::Platform for details. def platform - @new_platform ||= Gem::Platform::RUBY # rubocop:disable Naming/MemoizedInstanceVariableName + @new_platform ||= Gem::Platform::RUBY + + # Handle wheel platforms stored as strings + if @new_platform.is_a?(String) && @new_platform.start_with?("whl-") + @new_platform = Gem::Platform.new(@new_platform) + end + + @new_platform end def pretty_print(q) # :nodoc: diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb index d79ee7df9252..4aaa06e5422a 100644 --- a/lib/rubygems/specification_policy.rb +++ b/lib/rubygems/specification_policy.rb @@ -345,7 +345,7 @@ def validate_platform platform = @specification.platform case platform - when Gem::Platform, Gem::Platform::RUBY # ok + when Gem::Platform, Gem::Platform::RUBY, Gem::Platform::Wheel # ok else error "invalid platform #{platform.inspect}, see Gem::Platform" end diff --git a/lib/rubygems/specification_record.rb b/lib/rubygems/specification_record.rb index 195a35549670..445ed129abf4 100644 --- a/lib/rubygems/specification_record.rb +++ b/lib/rubygems/specification_record.rb @@ -148,7 +148,7 @@ def find_all_by_name(name, *requirements) def find_by_path(path) path = path.dup.freeze spec = @spec_with_requirable_file[path] ||= stubs.find do |s| - s.contains_requirable_file? path + s.contains_requirable_file?(path) && Gem::Platform.match_spec?(s) end || NOT_FOUND spec.to_spec diff --git a/test/rubygems/test_gem.rb b/test/rubygems/test_gem.rb index 49e81fcedb24..7fe929e2fc13 100644 --- a/test/rubygems/test_gem.rb +++ b/test/rubygems/test_gem.rb @@ -328,13 +328,10 @@ def test_activate_bin_path_raises_a_meaningful_error_if_a_gem_thats_finally_acti install_specs c1, b1, b2, a1 - # c2 is missing, and b2 which has it as a dependency will be activated, so we should get an error about the orphaned dependency + # c2 is missing, and so we should resolve b1 which has a satisfiable dependency on c + load Gem.activate_bin_path("a", "exec", ">= 0") - e = assert_raise Gem::UnsatisfiableDependencyError do - load Gem.activate_bin_path("a", "exec", ">= 0") - end - - assert_equal "Unable to resolve dependency: 'b (>= 0)' requires 'c (= 2)'", e.message + assert_equal %w[a-1 b-1 c-1], loaded_spec_names end def test_activate_bin_path_in_debug_mode diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb index d44126d2046a..1658670e4033 100644 --- a/test/rubygems/test_gem_commands_build_command.rb +++ b/test/rubygems/test_gem_commands_build_command.rb @@ -120,6 +120,76 @@ def test_execute_platform assert_match spec.platform, "java" end + def test_execute_platform_wheel + gemspec_file = File.join(@tempdir, @gem.spec_name) + + File.open gemspec_file, "w" do |gs| + gs.write @gem.to_ruby + end + + # Test building with wheel platform using --platform option + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + current_platform = Gem::Platform.local.to_s.tr("-", "_") + wheel_platform_string = "whl-#{current_abi}-#{current_platform}" + + @cmd.handle_options ["--platform", wheel_platform_string, gemspec_file] + + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + + # Verify the gem was built with wheel platform + expected_gem_file = "some_gem-2-#{wheel_platform_string}.gem" + gem_file = File.join(@tempdir, expected_gem_file) + assert File.exist?(gem_file), "Expected gem file #{expected_gem_file} to exist" + + # Verify the spec has the correct platform + spec = Gem::Package.new(gem_file).spec + assert_equal wheel_platform_string, spec.platform.to_s, + "Spec platform should match the wheel platform specified" + + # Verify output shows correct platform + output = @ui.output.split "\n" + assert_equal " Successfully built RubyGem", output.shift + assert_equal " Name: some_gem", output.shift + assert_equal " Version: 2", output.shift + assert_equal " File: #{expected_gem_file}", output.shift + assert_equal [], output + end + + def test_execute_platform_wheel_universal + gemspec_file = File.join(@tempdir, @gem.spec_name) + + File.open gemspec_file, "w" do |gs| + gs.write @gem.to_ruby + end + + # Test building with universal wheel platform + wheel_platform_string = "whl-any-any" + + @cmd.handle_options ["--platform", wheel_platform_string, gemspec_file] + + use_ui @ui do + Dir.chdir @tempdir do + @cmd.execute + end + end + + # Verify the gem was built with universal wheel platform + expected_gem_file = "some_gem-2-#{wheel_platform_string}.gem" + gem_file = File.join(@tempdir, expected_gem_file) + assert File.exist?(gem_file), "Expected gem file #{expected_gem_file} to exist" + + # Verify the spec has the correct platform + spec = Gem::Package.new(gem_file).spec + assert_equal wheel_platform_string, spec.platform.to_s, + "Spec platform should match the universal wheel platform specified" + assert spec.platform.is_a?(Gem::Platform::Wheel), + "Platform should be a Gem::Platform::Wheel instance" + end + def test_execute_bad_name [".", "-", "_"].each do |special_char| gem = util_spec "some_gem_with_bad_name" do |s| diff --git a/test/rubygems/test_gem_platform.rb b/test/rubygems/test_gem_platform.rb index a3ae919809d3..1d2aefac6310 100644 --- a/test/rubygems/test_gem_platform.rb +++ b/test/rubygems/test_gem_platform.rb @@ -84,6 +84,81 @@ def test_self_new assert_equal Gem::Platform::RUBY, Gem::Platform.new("") end + def test_self_new_with_specific_string + # Test that Platform.new can parse Specific string format + specific_str = "x86_64-linux v:1 engine:ruby engine_version:3.3.1 ruby_version:3.3.1 abi_version:3.3.0" + result = Gem::Platform.new(specific_str) + + assert_instance_of Gem::Platform::Specific, result + assert_equal Gem::Platform.new("x86_64-linux"), result.platform + assert_equal "ruby", result.ruby_engine + assert_equal "3.3.1", result.ruby_engine_version + assert_equal "3.3.1", result.ruby_version + assert_equal "3.3.0", result.abi_version + end + + def test_self_new_with_specific_minimal_string + # Test with just platform, no key:value pairs - should create normal Platform + result = Gem::Platform.new("x86_64-linux") + assert_instance_of Gem::Platform, result + refute_instance_of Gem::Platform::Specific, result + assert_equal "x86_64", result.cpu + assert_equal "linux", result.os + end + + def test_self_new_with_specific_string_libc + # Test parsing with libc information + specific_str = "x86_64-linux v:1 libc_type:glibc libc_version:2.31" + result = Gem::Platform.new(specific_str) + + assert_instance_of Gem::Platform::Specific, result + assert_equal "glibc", result.libc_type + assert_equal [2, 31], result.libc_version + end + + def test_self_new_with_wheel_string + # Ensure wheel strings still work + wheel_str = "whl-rb33-x86_64_linux" + result = Gem::Platform.new(wheel_str) + + assert_instance_of Gem::Platform::Wheel, result + assert_equal "rb33", result.ruby_abi_tag + assert_equal "x86_64_linux", result.platform_tags + end + + def test_self_new_handles_specific_strings + # Test parsing of version-specific platform strings + specific_platform = Gem::Platform.new("x86_64-linux v:1") + assert_instance_of Gem::Platform::Specific, specific_platform + assert_equal "x86_64", specific_platform.platform.cpu + assert_equal "linux", specific_platform.platform.os + end + + def test_self_sort_priority_ordering + # Test that wheel platforms get higher priority than traditional platforms + ruby_priority = Gem::Platform.sort_priority(Gem::Platform::RUBY) + wheel_priority = Gem::Platform.sort_priority(Gem::Platform::Wheel.new("whl-rb33-x86_64_linux")) + traditional_priority = Gem::Platform.sort_priority(Gem::Platform.new("x86_64-linux")) + + assert_equal(-1, ruby_priority, "Ruby platform should have highest priority") + assert_equal(2, wheel_priority, "Wheel platforms should have higher priority than traditional") + assert_equal(1, traditional_priority, "Traditional platforms should have lowest priority") + + # Verify correct sorting order (lower numbers sort first) + assert ruby_priority < traditional_priority, "Ruby should sort before traditional" + assert traditional_priority < wheel_priority, "Traditional should sort before wheel" + end + + def test_self_new_preserves_backward_compatibility + # Regular platform strings should still work normally + result = Gem::Platform.new("x86_64-darwin20") + assert_instance_of Gem::Platform, result + refute_instance_of Gem::Platform::Specific, result + assert_equal "x86_64", result.cpu + assert_equal "darwin", result.os + assert_equal "20", result.version + end + def test_initialize test_cases = { "amd64-freebsd6" => ["amd64", "freebsd", "6"], @@ -91,6 +166,7 @@ def test_initialize "jruby" => [nil, "java", nil], "universal-dotnet" => ["universal", "dotnet", nil], "universal-dotnet2.0" => ["universal", "dotnet", "2.0"], + "dotnet-2.0" => [nil, "dotnet", "2.0"], "universal-dotnet4.0" => ["universal", "dotnet", "4.0"], "powerpc-aix5.3.0.0" => ["powerpc", "aix", "5"], "powerpc-darwin7" => ["powerpc", "darwin", "7"], @@ -168,9 +244,9 @@ def test_initialize test_cases.each do |arch, expected| platform = Gem::Platform.new arch - assert_equal expected, platform.to_a, arch.inspect + assert_equal expected, platform.to_a, "Expected #{expected.inspect} for Gem::Platform.new(#{arch.inspect}).to_a, got #{platform.to_a.inspect}" platform2 = Gem::Platform.new platform.to_s - assert_equal expected, platform2.to_a, "#{arch.inspect} => #{platform2.inspect}" + assert_equal expected, platform2.to_a, "Expected #{expected.inspect} for #{arch.inspect}, got #{platform2.to_a.inspect}" end end @@ -276,6 +352,31 @@ def test_equals3_cpu assert((ppc_darwin8 === Gem::Platform.local), "universal =~ ppc") assert((uni_darwin8 === Gem::Platform.local), "universal =~ universal") assert((x86_darwin8 === Gem::Platform.local), "universal =~ x86") + + arm = Gem::Platform.new "arm-linux" + armv5 = Gem::Platform.new "armv5-linux" + armv7 = Gem::Platform.new "armv7-linux" + arm64 = Gem::Platform.new "arm64-linux" + + util_set_arch "armv5-linux" + assert((arm === Gem::Platform.local), "arm === armv5") + assert((armv5 === Gem::Platform.local), "armv5 === armv5") + refute((armv7 === Gem::Platform.local), "armv7 === armv5") + refute((arm64 === Gem::Platform.local), "arm64 === armv5") + refute((Gem::Platform.local === arm), "armv5 === arm") + + util_set_arch "armv7-linux" + assert((arm === Gem::Platform.local), "arm === armv7") + refute((armv5 === Gem::Platform.local), "armv5 === armv7") + assert((armv7 === Gem::Platform.local), "armv7 === armv7") + refute((arm64 === Gem::Platform.local), "arm64 === armv7") + refute((Gem::Platform.local === arm), "armv7 === arm") + + util_set_arch "arm64-linux" + refute((arm === Gem::Platform.local), "arm === arm64") + refute((armv5 === Gem::Platform.local), "armv5 === arm64") + refute((armv7 === Gem::Platform.local), "armv7 === arm64") + assert((arm64 === Gem::Platform.local), "arm64 === arm64") end def test_nil_cpu_arch_is_treated_as_universal @@ -381,33 +482,6 @@ def test_eabi_and_nil_version_combination_strictness refute(arm_linux === arm_linux_uclibceabihf, "arm-linux =~ arm-linux-uclibceabihf") end - def test_equals3_cpu_arm - arm = Gem::Platform.new "arm-linux" - armv5 = Gem::Platform.new "armv5-linux" - armv7 = Gem::Platform.new "armv7-linux" - arm64 = Gem::Platform.new "arm64-linux" - - util_set_arch "armv5-linux" - assert((arm === Gem::Platform.local), "arm === armv5") - assert((armv5 === Gem::Platform.local), "armv5 === armv5") - refute((armv7 === Gem::Platform.local), "armv7 === armv5") - refute((arm64 === Gem::Platform.local), "arm64 === armv5") - refute((Gem::Platform.local === arm), "armv5 === arm") - - util_set_arch "armv7-linux" - assert((arm === Gem::Platform.local), "arm === armv7") - refute((armv5 === Gem::Platform.local), "armv5 === armv7") - assert((armv7 === Gem::Platform.local), "armv7 === armv7") - refute((arm64 === Gem::Platform.local), "arm64 === armv7") - refute((Gem::Platform.local === arm), "armv7 === arm") - - util_set_arch "arm64-linux" - refute((arm === Gem::Platform.local), "arm === arm64") - refute((armv5 === Gem::Platform.local), "armv5 === arm64") - refute((armv7 === Gem::Platform.local), "armv7 === arm64") - assert((arm64 === Gem::Platform.local), "arm64 === arm64") - end - def test_equals3_universal_mingw uni_mingw = Gem::Platform.new "universal-mingw" mingw_ucrt = Gem::Platform.new "x64-mingw-ucrt" diff --git a/test/rubygems/test_gem_platform_elffile.rb b/test/rubygems/test_gem_platform_elffile.rb new file mode 100644 index 000000000000..ce515d0e606e --- /dev/null +++ b/test/rubygems/test_gem_platform_elffile.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require_relative "helper" +require "rubygems/platform/elffile" + +class TestGemPlatformELFFile < Gem::TestCase + def setup + super + + # Paths to test ELF files from Python's packaging repository + @packaging_tests_dir = "/Users/segiddins/Development/github.com/pypa/packaging/tests" + @manylinux_dir = File.join(@packaging_tests_dir, "manylinux") + @musllinux_dir = File.join(@packaging_tests_dir, "musllinux") + + pend "Python packaging test files not available" unless File.directory?(@packaging_tests_dir) + end + + def test_elffile_glibc_files + test_cases = [ + ["hello-world-x86_64-i386", 32, :little_endian], + ["hello-world-x86_64-amd64", 64, :little_endian], + ["hello-world-armv7l-armel", 32, :little_endian], + ["hello-world-armv7l-armhf", 32, :little_endian], + ["hello-world-s390x-s390x", 64, :big_endian], + ] + + test_cases.each do |name, expected_bits, expected_endian| + path = File.join(@manylinux_dir, name) + + reader = Gem::Platform::ELFFile::Reader.new(path) + + # Test that we can read the file without errors + refute_nil reader, "Should be able to create reader for #{name}" + + # Verify bit width detection + if expected_bits == 64 + assert reader.instance_variable_get(:@is_64bit), "#{name} should be detected as 64-bit" + else + refute reader.instance_variable_get(:@is_64bit), "#{name} should be detected as 32-bit" + end + + # Verify endianness detection + if expected_endian == :little_endian + assert reader.instance_variable_get(:@is_little_endian), "#{name} should be little endian" + else + refute reader.instance_variable_get(:@is_little_endian), "#{name} should be big endian" + end + + # Verify it's a valid ELF file + assert reader.send(:valid_elf?), "#{name} should be valid ELF" + end + end + + def test_elffile_musl_files + test_cases = [ + ["musl-aarch64", 64, :little_endian, "/lib/ld-musl-aarch64.so.1"], + ["musl-i386", 32, :little_endian, "/lib/ld-musl-i386.so.1"], + ["musl-x86_64", 64, :little_endian, "/lib/ld-musl-x86_64.so.1"], + ] + + test_cases.each do |name, expected_bits, expected_endian, expected_interpreter| + path = File.join(@musllinux_dir, name) + + reader = Gem::Platform::ELFFile::Reader.new(path) + + # Test interpreter extraction + assert_equal expected_interpreter, reader.interpreter, "#{name} should have correct interpreter" + + # Test bit width + if expected_bits == 64 + assert reader.instance_variable_get(:@is_64bit), "#{name} should be 64-bit" + else + refute reader.instance_variable_get(:@is_64bit), "#{name} should be 32-bit" + end + + # Test endianness + if expected_endian == :little_endian + assert reader.instance_variable_get(:@is_little_endian), "#{name} should be little endian" + else + refute reader.instance_variable_get(:@is_little_endian), "#{name} should be big endian" + end + end + end + + def test_elffile_module_function + # Test the module-level interpreter function + musl_x86_64_path = File.join(@musllinux_dir, "musl-x86_64") + + interpreter = Gem::Platform::ELFFile.interpreter(musl_x86_64_path) + assert_equal "/lib/ld-musl-x86_64.so.1", interpreter + end + + def test_elffile_bad_magic + # Test with various invalid ELF files + invalid_files = [ + "hello-world-invalid-magic", + "hello-world-too-short", + ] + + invalid_files.each do |name| + path = File.join(@manylinux_dir, name) + + # Should not raise an exception, but should return nil interpreter + reader = Gem::Platform::ELFFile::Reader.new(path) + assert_nil reader.interpreter, "#{name} should have nil interpreter" + refute reader.send(:valid_elf?), "#{name} should not be valid ELF" + end + end + + def test_elffile_nonexistent_file + # Test with non-existent file + interpreter = Gem::Platform::ELFFile.interpreter("/nonexistent/file") + assert_nil interpreter, "Non-existent file should return nil" + end + + def test_elffile_truncated_file + # Test with truncated ELF file (simulating incomplete read) + musl_x86_64_path = File.join(@musllinux_dir, "musl-x86_64") + + # Create a truncated version (just the header) + original_data = File.read(musl_x86_64_path, mode: "rb") + truncated_data = original_data[0, 58] # Just enough for header, not sections + + Tempfile.create(["truncated", ".elf"]) do |tmpfile| + tmpfile.write(truncated_data) + tmpfile.close + + reader = Gem::Platform::ELFFile::Reader.new(tmpfile.path) + # Should handle gracefully and return nil interpreter + assert_nil reader.interpreter, "Truncated file should have nil interpreter" + end + end + + def test_elffile_current_ruby_executable + pend "current ruby will only have an interpreter on linux" unless RUBY_PLATFORM.include?("linux") + + # Test with the current Ruby executable + ruby_path = RbConfig.ruby + + # This should work without raising an exception + interpreter = Gem::Platform::ELFFile.interpreter(ruby_path) + + # On Linux, we expect some interpreter (either glibc or musl) + assert_match(%r{^/lib}, interpreter) + end + + def test_elffile_constants + # Test that constants are defined correctly + assert_equal 0x7f, Gem::Platform::ELFFile::ELFMAG0 + assert_equal 0x45, Gem::Platform::ELFFile::ELFMAG1 # 'E' + assert_equal 0x4c, Gem::Platform::ELFFile::ELFMAG2 # 'L' + assert_equal 0x46, Gem::Platform::ELFFile::ELFMAG3 # 'F' + + assert_equal 1, Gem::Platform::ELFFile::ELFCLASS32 + assert_equal 2, Gem::Platform::ELFFile::ELFCLASS64 + + assert_equal 1, Gem::Platform::ELFFile::ELFDATA2LSB # Little endian + assert_equal 2, Gem::Platform::ELFFile::ELFDATA2MSB # Big endian + + assert_equal 3, Gem::Platform::ELFFile::PT_INTERP + end +end diff --git a/test/rubygems/test_gem_platform_manylinux.rb b/test/rubygems/test_gem_platform_manylinux.rb new file mode 100644 index 000000000000..60935f197d9d --- /dev/null +++ b/test/rubygems/test_gem_platform_manylinux.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require_relative "helper" +require "rubygems/platform/manylinux" + +class TestGemPlatformManylinux < Gem::TestCase + def test_parse_glibc_version + # Test glibc version parsing + assert_equal [2, 17], Gem::Platform::Manylinux.parse_glibc_version("2.17") + assert_equal [2, 31], Gem::Platform::Manylinux.parse_glibc_version("2.31-ubuntu1") + assert_nil Gem::Platform::Manylinux.parse_glibc_version("invalid") + assert_nil Gem::Platform::Manylinux.parse_glibc_version("") + end + + def test_platform_tags + glibc_version = [2, 17] + + expected_tags = [ + "manylinux_2_17_x86_64", "manylinux_2_16_x86_64", "manylinux_2_15_x86_64", + "manylinux_2_14_x86_64", "manylinux_2_13_x86_64", "manylinux_2_12_x86_64", + "manylinux_2_11_x86_64", "manylinux_2_10_x86_64", "manylinux_2_9_x86_64", + "manylinux_2_8_x86_64", "manylinux_2_7_x86_64", "manylinux_2_6_x86_64", + "manylinux_2_5_x86_64" + ] + + actual_tags = Gem::Platform::Manylinux.platform_tags(["x86_64"], glibc_version).to_a + assert_equal expected_tags, actual_tags + end + + def test_architecture_support + glibc_version = [2, 17] + + # Test x86_64 architecture (supports back to glibc 2.5) + x86_64_tags = Gem::Platform::Manylinux.platform_tags(["x86_64"], glibc_version).to_a + assert x86_64_tags.include?("manylinux_2_5_x86_64") + assert x86_64_tags.include?("manylinux_2_12_x86_64") + assert x86_64_tags.include?("manylinux_2_17_x86_64") + + # Test aarch64 architecture (supports back to glibc 2.17 only) + aarch64_tags = Gem::Platform::Manylinux.platform_tags(["aarch64"], glibc_version).to_a + refute aarch64_tags.include?("manylinux_2_5_aarch64") + refute aarch64_tags.include?("manylinux_2_12_aarch64") + assert aarch64_tags.include?("manylinux_2_17_aarch64") + end + + def test_no_glibc_returns_empty + # Mock glibc version detection to return nil (no glibc) + Gem::Platform::Manylinux.instance_variable_set(:@glibc_version, nil) + + glibc_ver = Gem::Platform::Manylinux.glibc_version + tags = [] + Gem::Platform::Manylinux.platform_tags(["x86_64"], glibc_ver) {|tag| tags << tag } if glibc_ver + assert_empty tags + ensure + Gem::Platform::Manylinux.remove_instance_variable(:@glibc_version) + end + + def test_standard_tag_format + glibc_version = [2, 17] + + tags = Gem::Platform::Manylinux.platform_tags(["x86_64"], glibc_version).to_a + + # Check that only standard manylinux_maj_min_arch format is used + assert_includes tags, "manylinux_2_5_x86_64" + assert_includes tags, "manylinux_2_12_x86_64" + assert_includes tags, "manylinux_2_17_x86_64" + + # Verify no legacy tags are present + refute tags.any? {|tag| tag.match?(/^manylinux[0-9]+_/) } + refute tags.any? {|tag| tag.match?(/^manylinux[0-9]{4}_/) } + end + + def test_version_compatibility_ordering + glibc_version = [2, 17] + + tags = Gem::Platform::Manylinux.platform_tags(["x86_64"], glibc_version).to_a + + # Verify that newer versions come first (more specific) + manylinux_2_17_index = tags.index("manylinux_2_17_x86_64") + manylinux_2_16_index = tags.index("manylinux_2_16_x86_64") + manylinux_2_5_index = tags.index("manylinux_2_5_x86_64") + + assert manylinux_2_17_index < manylinux_2_16_index + assert manylinux_2_16_index < manylinux_2_5_index + end +end diff --git a/test/rubygems/test_gem_platform_musllinux.rb b/test/rubygems/test_gem_platform_musllinux.rb new file mode 100644 index 000000000000..1f9d7f4056e5 --- /dev/null +++ b/test/rubygems/test_gem_platform_musllinux.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require_relative "helper" +require "rubygems/platform/musllinux" + +class TestGemPlatformMusllinux < Gem::TestCase + def teardown + Gem::Platform::Musllinux.remove_instance_variable(:@musl_version) if Gem::Platform::Musllinux.instance_variable_defined?(:@musl_version) + end + + def test_platform_tags + musl_version = [1, 2] + + expected_tags = [ + "musllinux_1_2_x86_64", "musllinux_1_1_x86_64", "musllinux_1_0_x86_64" + ] + + actual_tags = Gem::Platform::Musllinux.platform_tags(["x86_64"], musl_version).to_a + assert_equal expected_tags, actual_tags + end + + def test_no_musl_returns_empty + # Mock musl version detection to return nil (no musl) + Gem::Platform::Musllinux.instance_variable_set(:@musl_version, nil) + + musl_ver = Gem::Platform::Musllinux.musl_version + tags = [] + Gem::Platform::Musllinux.platform_tags(["x86_64"], musl_ver) {|tag| tags << tag } if musl_ver + assert_empty tags + end + + def test_multiple_architectures + musl_version = [1, 2] + + expected_tags = [ + "musllinux_1_2_x86_64", "musllinux_1_1_x86_64", "musllinux_1_0_x86_64", + "musllinux_1_2_aarch64", "musllinux_1_1_aarch64", "musllinux_1_0_aarch64" + ] + + actual_tags = Gem::Platform::Musllinux.platform_tags(["x86_64", "aarch64"], musl_version).to_a + assert_equal expected_tags, actual_tags + end + + def test_version_compatibility_ordering + musl_version = [1, 2] + + tags = Gem::Platform::Musllinux.platform_tags(["x86_64"], musl_version).to_a + + # Verify that newer versions come first (more specific) + musllinux_1_2_index = tags.index("musllinux_1_2_x86_64") + musllinux_1_1_index = tags.index("musllinux_1_1_x86_64") + musllinux_1_0_index = tags.index("musllinux_1_0_x86_64") + + assert musllinux_1_2_index < musllinux_1_1_index + assert musllinux_1_1_index < musllinux_1_0_index + end + + def test_detect_musl_version_system_checks + # Test that detect_musl_version returns nil when not on musl system + Gem::Platform::Musllinux.define_singleton_method(:musl_system?) { false } + + assert_nil Gem::Platform::Musllinux.detect_musl_version + ensure + # Remove mock + begin + Gem::Platform::Musllinux.singleton_class.remove_method(:musl_system?) + rescue StandardError + nil + end + end +end diff --git a/test/rubygems/test_gem_platform_specific.rb b/test/rubygems/test_gem_platform_specific.rb new file mode 100644 index 000000000000..edf473621e54 --- /dev/null +++ b/test/rubygems/test_gem_platform_specific.rb @@ -0,0 +1,732 @@ +# frozen_string_literal: true + +require_relative "helper" +require "rubygems/platform" + +class TestGemPlatformSpecific < Gem::TestCase + def test_initialize_with_platform_object + platform = Gem::Platform.new("x86_64-linux") + specific = Gem::Platform::Specific.new(platform, ruby_engine: "ruby", ruby_engine_version: "3.3.1", ruby_version: "3.3.1", abi_version: "3.3.0") + + assert_equal platform, specific.platform + assert_equal "ruby", specific.ruby_engine + assert_equal "3.3.1", specific.ruby_engine_version + assert_equal "3.3.1", specific.ruby_version + assert_equal "3.3.0", specific.abi_version + end + + def test_initialize_with_platform_string + specific = Gem::Platform::Specific.new("x86_64-linux", ruby_engine: "ruby", ruby_engine_version: "3.3.1", ruby_version: "3.3.1", abi_version: "3.3.0") + + assert_equal Gem::Platform.new("x86_64-linux"), specific.platform + assert_equal "ruby", specific.ruby_engine + assert_equal "3.3.1", specific.ruby_engine_version + assert_equal "3.3.1", specific.ruby_version + assert_equal "3.3.0", specific.abi_version + end + + def test_initialize_with_minimal_parameters + specific = Gem::Platform::Specific.new("x86_64-linux") + + assert_equal Gem::Platform.new("x86_64-linux"), specific.platform + assert_nil specific.ruby_engine + assert_nil specific.ruby_engine_version + assert_nil specific.ruby_version + assert_nil specific.abi_version + end + + def test_to_s_full_specification + specific = Gem::Platform::Specific.new("x86_64-linux", ruby_engine: "ruby", ruby_engine_version: "3.3.1", ruby_version: "3.3.1", abi_version: "3.3.0") + expected = "x86_64-linux v:1 engine:ruby engine_version:3.3.1 ruby_version:3.3.1 abi_version:3.3.0" + assert_equal expected, specific.to_s + end + + def test_to_s_partial_specification + specific = Gem::Platform::Specific.new("x86_64-linux", ruby_engine: "ruby", ruby_version: "3.3.1", abi_version: "3.3.0") + expected = "x86_64-linux v:1 engine:ruby ruby_version:3.3.1 abi_version:3.3.0" + assert_equal expected, specific.to_s + end + + def test_to_s_platform_only + specific = Gem::Platform::Specific.new("x86_64-linux") + expected = "x86_64-linux v:1" + assert_equal expected, specific.to_s + end + + def test_inspect + specific = Gem::Platform::Specific.new("x86_64-linux", ruby_engine: "ruby", ruby_engine_version: "3.3.1", ruby_version: "3.3.1", abi_version: "3.3.0") + result = specific.inspect + + assert_match(/^# "value1", specific3 => "value3" } + + assert_equal "value1", hash[specific1] + assert_equal "value1", hash[specific2] # Should find same key due to equality + assert_equal "value3", hash[specific3] + assert_nil hash[Gem::Platform::Specific.new("x86_64-darwin")] + end + + # Tests moved from test_gem_platform.rb + + def test_self_current_ruby_abi_tag_includes_extension_api_version + # Test that current_ruby_abi_tag returns the same result as generate_ruby_abi_tag with current environment + current_abi_tag = Gem::Platform::Specific.local.ruby_abi_tag + expected_abi_tag = Gem::Platform::Specific.generate_ruby_abi_tag( + RUBY_ENGINE, + RUBY_ENGINE_VERSION, + RUBY_VERSION, + Gem.extension_api_version + ) + + assert_equal expected_abi_tag, current_abi_tag, + "current_ruby_abi_tag should match generate_ruby_abi_tag for current environment" + end + + def test_generate_ruby_abi_tag_integration + # Test the new ruby ABI tag generation method with abi_version + # With standard abi_version (3.3.0), no suffix is added since it matches major.minor.0 + tag = Gem::Platform::Specific.generate_ruby_abi_tag("ruby", "3.3.1", "3.3.1", "3.3.0") + assert_equal "cr33", tag + + # With abi_version that has suffix (like static build) + tag = Gem::Platform::Specific.generate_ruby_abi_tag("ruby", "3.3.1", "3.3.1", "3.3.0-static") + assert_equal "cr33_static", tag + + # JRuby with standard abi_version + tag = Gem::Platform::Specific.generate_ruby_abi_tag("jruby", "9.4.0", "3.1.0", "3.1.0") + assert_equal "jr94", tag + + # Test fallback to ruby_version when abi_version is nil - extracts suffix after major.minor.0 + tag = Gem::Platform::Specific.generate_ruby_abi_tag("ruby", "3.3.1", "3.3.1", nil) + assert_equal "cr33_1", tag + + # Test fallback to current when missing info + tag = Gem::Platform::Specific.generate_ruby_abi_tag(nil, nil, nil, nil) + assert_nil tag + + tag = Gem::Platform::Specific.generate_ruby_abi_tag("ruby", nil, "3.3.1", "3.3.0") + assert_nil tag + end + + def test_platform_specificity_match_integration + [ + ["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_platform_specificity_match_traditional_vs_specific + # Test traditional vs Specific - should extract platform for standard matching + traditional = Gem::Platform.new("x86_64-linux") + specific_same = Gem::Platform::Specific.new("x86_64-linux", ruby_engine: "ruby", ruby_engine_version: "3.3.1", ruby_version: "3.3.1", abi_version: "3.3.0") + specific_different = Gem::Platform::Specific.new("aarch64-linux", ruby_engine: "ruby", ruby_engine_version: "3.3.1", ruby_version: "3.3.1", abi_version: "3.3.0") + + # Same platform should be perfect match + specificity_same = Gem::Platform.platform_specificity_match(traditional, specific_same) + assert_equal(-1, specificity_same, "Traditional vs same Specific platform should be perfect match (-1)") + + # Different platform should use standard platform matching + specificity_different = Gem::Platform.platform_specificity_match(traditional, specific_different) + assert specificity_different > 0, "Traditional vs different Specific platform should have positive specificity" + end + + def test_platform_specificity_match_edge_cases + # Test edge cases and special platforms + ruby_platform = Gem::Platform::RUBY + specific = Gem::Platform::Specific.new("x86_64-linux", ruby_engine: "ruby", ruby_engine_version: "3.3.1", ruby_version: "3.3.1", abi_version: "3.3.0") + + # Ruby platform should return 1_000_000 for anything + specificity_ruby_specific = Gem::Platform.platform_specificity_match(ruby_platform, specific) + assert_equal 1_000_000, specificity_ruby_specific, "Ruby platform should return 1_000_000" + + specificity_specific_ruby = Gem::Platform.platform_specificity_match(specific, ruby_platform) + assert_equal 1_000_000, specificity_specific_ruby, "Anything vs Ruby platform should return 1_000_000" + + # Nil platform should return 1_000_000 + specificity_nil = Gem::Platform.platform_specificity_match(nil, specific) + assert_equal 1_000_000, specificity_nil, "Nil platform should return 1_000_000" + end + + def test_tags_rb_version_range + # Test with different ruby versions + specific_331 = Gem::Platform::Specific.new("ruby", ruby_version: "3.3.1") + assert_equal ["rb33", "rb3", "rb32", "rb31", "rb30"], specific_331.send(:_rb_version_range).to_a + + specific_33 = Gem::Platform::Specific.new("ruby", ruby_version: "3.3") + assert_equal ["rb33", "rb3", "rb32", "rb31", "rb30"], specific_33.send(:_rb_version_range).to_a + + specific_3 = Gem::Platform::Specific.new("ruby", ruby_version: "3") + assert_equal ["rb3"], specific_3.send(:_rb_version_range).to_a + + specific_40 = Gem::Platform::Specific.new("ruby", ruby_version: "4.0") + assert_equal ["rb40", "rb4"], specific_40.send(:_rb_version_range).to_a + end + + def test_tags_compatible_tags + # Create a Specific instance with ruby version 3.3.1 + specific = Gem::Platform::Specific.new("ruby", ruby_version: "3.3.1") + assert_equal [ + ["rb33", "any"], + ["rb3", "any"], + ["rb32", "any"], + ["rb31", "any"], + ["rb30", "any"], + ], + specific.compatible_tags.to_a + end + + def test_tags_platform_tags + do_test = ->(platform, expected) { + platform = Gem::Platform.new(platform) + specific = Gem::Platform::Specific.new(platform) + expected.each do |a| + pl = Gem::Platform.new(a) + assert_equal a, pl.to_s, "#{pl.inspect}.to_s" + assert platform === pl, "#{platform.inspect} === #{pl.inspect}" + end + actual = specific._platform_tags.to_a + assert_equal(expected, actual, "_platform_tags(#{platform.inspect})") + } + + do_test.call("ruby", []) + do_test.call("arm64-darwin-23",["arm64-darwin-23", + "universal-darwin-23", + "arm64-darwin", + "universal-darwin", + "darwin"]) + do_test.call("aarch64-darwin", %w[aarch64-darwin universal-darwin darwin]) + do_test.call("universal-darwin-23", %w[universal-darwin-23 universal-darwin darwin]) + do_test.call("universal-darwin", %w[universal-darwin darwin]) + do_test.call("java", %w[java universal-java]) + do_test.call("universal-java", %w[universal-java java]) + do_test.call("universal-java-1.6", %w[universal-java-1.6 universal-java java]) + do_test.call("arm-java", %w[arm-java universal-java java]) + + do_test.call("x86-linux", ["x86-linux", "universal-linux"]) + do_test.call("x86_64-linux-musl", ["x86_64-linux-musl", "universal-linux-musl"]) + + do_test.call("x86-mingw32", ["x86-mingw32", "universal-mingw32", "mingw32"]) + + do_test.call("x86_64-linux-android", ["x86_64-linux-android", "universal-linux-android"]) + end + + def test_specific_all_tags + do_test = ->(platform, expected:, **kwargs) { + platform = Gem::Platform.new(platform) + specific = Gem::Platform::Specific.new(platform, **kwargs) + + expected.each do |rb, pl| + whl = Gem::Platform.new("whl-#{rb}-#{pl}") + assert whl === specific, "#{whl} === #{specific}" + end + actual = specific.each_possible_match.to_a + + assert_empty(actual.tally.select {|_, v| v > 1 }, "no duplicate tags should be generated") + + assert_equal expected, specific.each_possible_match.to_a + } + + do_test.call("ruby", expected: [%w[any any]]) + do_test.call("arm64-darwin-24", abi_version: "3.4.0-static", ruby_engine: "ruby", ruby_engine_version: "3.4.4", ruby_version: "3.4.4", expected: [ + %w[cr34_static arm64_darwin_24], %w[cr34_static universal_darwin_24], %w[cr34_static arm64_darwin], %w[cr34_static universal_darwin], %w[cr34_static darwin], + %w[rb34 arm64_darwin_24], %w[rb34 universal_darwin_24], %w[rb34 arm64_darwin], %w[rb34 universal_darwin], %w[rb34 darwin], + %w[rb3 arm64_darwin_24], %w[rb3 universal_darwin_24], %w[rb3 arm64_darwin], %w[rb3 universal_darwin], %w[rb3 darwin], + %w[rb33 arm64_darwin_24], %w[rb33 universal_darwin_24], %w[rb33 arm64_darwin], %w[rb33 universal_darwin], %w[rb33 darwin], + %w[rb32 arm64_darwin_24], %w[rb32 universal_darwin_24], %w[rb32 arm64_darwin], %w[rb32 universal_darwin], %w[rb32 darwin], + %w[rb31 arm64_darwin_24], %w[rb31 universal_darwin_24], %w[rb31 arm64_darwin], %w[rb31 universal_darwin], %w[rb31 darwin], + %w[rb30 arm64_darwin_24], %w[rb30 universal_darwin_24], %w[rb30 arm64_darwin], %w[rb30 universal_darwin], %w[rb30 darwin], + %w[rb34 any], %w[rb3 any], %w[rb33 any], %w[rb32 any], %w[rb31 any], %w[rb30 any], + %w[any arm64_darwin_24], %w[any universal_darwin_24], %w[any arm64_darwin], %w[any universal_darwin], %w[any darwin], + %w[any any] + ]) + end + + def test_tags_extract_abi_suffix + assert_equal "static", Gem::Platform::Specific.send(:extract_abi_suffix, "3.4.0-static", "3.4.0") + end + + def test_tags_generate_ruby_abi_tag + assert_equal "cr34", Gem::Platform::Specific.generate_ruby_abi_tag("ruby", "3.4.1", "3.4.1", "3.4.0") + assert_equal "cr34_static", Gem::Platform::Specific.generate_ruby_abi_tag("ruby", "3.4.1", "3.4.1", "3.4.0-static") + assert_equal "cr34____", Gem::Platform::Specific.generate_ruby_abi_tag("ruby", "3.4.1", "3.4.1", "3.4.0-...") + assert_equal "jr94", Gem::Platform::Specific.generate_ruby_abi_tag("jruby", "9.4.9.0", "3.1.4", "3.1.0") + assert_equal "tr240", Gem::Platform::Specific.generate_ruby_abi_tag("truffleruby", "24.0.2", "3.2.2", "3.2.2.24.0.0.2") + end + + def test_self_local_linux_libc_detection + # Test that Platform.local.to_a remains clean (no version modification) + util_set_arch "x86_64-linux-gnu" do + assert_equal ["x86_64", "linux", "gnu"], Gem::Platform.local.to_a + end + + # Test that Specific.local properly detects glibc on standard Linux systems + util_set_arch "x86_64-linux-gnu" do + Gem::Platform::Manylinux.stub :glibc_version, [2, 17] do + specific = Gem::Platform::Specific.local + assert_equal "glibc", specific.libc_type + assert_equal [2, 17], specific.libc_version + assert_equal ["x86_64", "linux", "gnu"], specific.platform.to_a + end + end + + # Test glibc detection with ARM EABI variants + util_set_arch "arm-linux-gnueabi" do + Gem::Platform::Manylinux.stub :glibc_version, [2, 31] do + specific = Gem::Platform::Specific.local + assert_equal "glibc", specific.libc_type + assert_equal [2, 31], specific.libc_version + assert_equal ["arm", "linux", "gnueabi"], specific.platform.to_a + end + end + + util_set_arch "arm-linux-gnueabihf" do + Gem::Platform::Manylinux.stub :glibc_version, [2, 28] do + specific = Gem::Platform::Specific.local + assert_equal "glibc", specific.libc_type + assert_equal [2, 28], specific.libc_version + assert_equal ["arm", "linux", "gnueabihf"], specific.platform.to_a + end + end + + # Test musl detection on musl-based systems + util_set_arch "x86_64-linux-musl" do + Gem::Platform::Musllinux.stub :musl_version, [1, 2] do + specific = Gem::Platform::Specific.local + assert_equal "musl", specific.libc_type + assert_equal [1, 2], specific.libc_version + assert_equal ["x86_64", "linux", "musl"], specific.platform.to_a + end + end + + # Test musl detection with ARM EABI variants + util_set_arch "arm-linux-musleabi" do + Gem::Platform::Musllinux.stub :musl_version, [1, 1] do + specific = Gem::Platform::Specific.local + assert_equal "musl", specific.libc_type + assert_equal [1, 1], specific.libc_version + assert_equal ["arm", "linux", "musleabi"], specific.platform.to_a + end + end + + util_set_arch "arm-linux-musleabihf" do + Gem::Platform::Musllinux.stub :musl_version, [1, 3] do + specific = Gem::Platform::Specific.local + assert_equal "musl", specific.libc_type + assert_equal [1, 3], specific.libc_version + assert_equal ["arm", "linux", "musleabihf"], specific.platform.to_a + end + end + + # Test detection failure scenarios - should have nil libc_version when detection fails + util_set_arch "x86_64-linux-gnu" do + Gem::Platform::Manylinux.stub :glibc_version, nil do + specific = Gem::Platform::Specific.local + assert_equal "glibc", specific.libc_type + assert_nil specific.libc_version + assert_equal ["x86_64", "linux", "gnu"], specific.platform.to_a + end + end + + util_set_arch "x86_64-linux-musl" do + Gem::Platform::Musllinux.stub :musl_version, nil do + specific = Gem::Platform::Specific.local + assert_equal "musl", specific.libc_type + assert_nil specific.libc_version + assert_equal ["x86_64", "linux", "musl"], specific.platform.to_a + end + end + + # Test that non-glibc/musl Linux systems have glibc detection (default case) + util_set_arch "arm-linux-uclibceabi" do + Gem::Platform::Manylinux.stub :glibc_version, [2, 24] do + specific = Gem::Platform::Specific.local + assert_equal "glibc", specific.libc_type # defaults to glibc for non-musl Linux + assert_equal [2, 24], specific.libc_version + assert_equal ["arm", "linux", "uclibceabi"], specific.platform.to_a + end + end + + util_set_arch "arm-linux-uclibceabihf" do + Gem::Platform::Manylinux.stub :glibc_version, [2, 19] do + specific = Gem::Platform::Specific.local + assert_equal "glibc", specific.libc_type # defaults to glibc for non-musl Linux + assert_equal [2, 19], specific.libc_version + assert_equal ["arm", "linux", "uclibceabihf"], specific.platform.to_a + end + end + + # Test generic Linux systems without libc suffix (defaults to glibc) + util_set_arch "x86_64-linux" do + Gem::Platform::Manylinux.stub :glibc_version, [2, 35] do + specific = Gem::Platform::Specific.local + assert_equal "glibc", specific.libc_type + assert_equal [2, 35], specific.libc_version + assert_equal ["x86_64", "linux", nil], specific.platform.to_a + end + end + + # Test basic EABI systems (defaults to glibc) + util_set_arch "arm-linux-eabi" do + Gem::Platform::Manylinux.stub :glibc_version, [2, 24] do + specific = Gem::Platform::Specific.local + assert_equal "glibc", specific.libc_type + assert_equal [2, 24], specific.libc_version + assert_equal ["arm", "linux", "eabi"], specific.platform.to_a + end + end + + util_set_arch "arm-linux-eabihf" do + Gem::Platform::Manylinux.stub :glibc_version, [2, 27] do + specific = Gem::Platform::Specific.local + assert_equal "glibc", specific.libc_type + assert_equal [2, 27], specific.libc_version + assert_equal ["arm", "linux", "eabihf"], specific.platform.to_a + end + end + + # Test non-Linux platforms have no libc detection + util_set_arch "x86_64-darwin20" do + specific = Gem::Platform::Specific.local + assert_nil specific.libc_type + assert_nil specific.libc_version + assert_equal ["x86_64", "darwin", "20"], specific.platform.to_a + end + + util_set_arch "x64-mingw-ucrt" do + specific = Gem::Platform::Specific.local + assert_nil specific.libc_type + assert_nil specific.libc_version + assert_equal ["x64", "mingw", "ucrt"], specific.platform.to_a + end + end + + # Tests for parsing Specific string representations + + def test_parse_full_specific_string + str = "x86_64-linux v:1 engine:ruby engine_version:3.3.1 ruby_version:3.3.1 abi_version:3.3.0 libc_type:glibc libc_version:2.31" + specific = Gem::Platform::Specific.parse(str) + + assert_equal Gem::Platform.new("x86_64-linux"), specific.platform + assert_equal "ruby", specific.ruby_engine + assert_equal "3.3.1", specific.ruby_engine_version + assert_equal "3.3.1", specific.ruby_version + assert_equal "3.3.0", specific.abi_version + assert_equal "glibc", specific.libc_type + assert_equal [2, 31], specific.libc_version + end + + def test_parse_minimal_specific_string + str = "x86_64-linux v:1" + specific = Gem::Platform::Specific.parse(str) + + assert_equal Gem::Platform.new("x86_64-linux"), specific.platform + assert_nil specific.ruby_engine + assert_nil specific.ruby_engine_version + assert_nil specific.ruby_version + assert_nil specific.abi_version + assert_nil specific.libc_type + assert_nil specific.libc_version + end + + def test_parse_partial_specific_string + str = "arm64-darwin v:1 engine:ruby ruby_version:3.2.0 abi_version:3.2.0" + specific = Gem::Platform::Specific.parse(str) + + assert_equal Gem::Platform.new("arm64-darwin"), specific.platform + assert_equal "ruby", specific.ruby_engine + assert_nil specific.ruby_engine_version + assert_equal "3.2.0", specific.ruby_version + assert_equal "3.2.0", specific.abi_version + assert_nil specific.libc_type + assert_nil specific.libc_version + end + + def test_parse_with_jruby + str = "java v:1 engine:jruby engine_version:9.4.0 ruby_version:3.1.0 abi_version:3.1.0" + specific = Gem::Platform::Specific.parse(str) + + assert_equal Gem::Platform.new("java"), specific.platform + assert_equal "jruby", specific.ruby_engine + assert_equal "9.4.0", specific.ruby_engine_version + assert_equal "3.1.0", specific.ruby_version + assert_equal "3.1.0", specific.abi_version + end + + def test_parse_with_musl + str = "x86_64-linux-musl v:1 libc_type:musl libc_version:1.2" + specific = Gem::Platform::Specific.parse(str) + + assert_equal Gem::Platform.new("x86_64-linux-musl"), specific.platform + assert_equal "musl", specific.libc_type + assert_equal [1, 2], specific.libc_version + end + + def test_parse_roundtrip_consistency + # Test that parse(to_s) produces equivalent objects + original = Gem::Platform::Specific.new( + "x86_64-linux", + ruby_engine: "ruby", + ruby_engine_version: "3.3.1", + ruby_version: "3.3.1", + abi_version: "3.3.0", + libc_type: "glibc", + libc_version: [2, 31] + ) + + str = original.to_s + parsed = Gem::Platform::Specific.parse(str) + + assert_equal original, parsed + assert_equal original.platform, parsed.platform + assert_equal original.ruby_engine, parsed.ruby_engine + assert_equal original.ruby_engine_version, parsed.ruby_engine_version + assert_equal original.ruby_version, parsed.ruby_version + assert_equal original.abi_version, parsed.abi_version + assert_equal original.libc_type, parsed.libc_type + assert_equal original.libc_version, parsed.libc_version + end + + def test_parse_handles_empty_and_nil + assert_nil Gem::Platform::Specific.parse(nil) + assert_nil Gem::Platform::Specific.parse("") + assert_nil Gem::Platform::Specific.parse(" ") + end + + def test_parse_handles_various_formats + # Empty key:value pair should be handled gracefully + result = Gem::Platform::Specific.parse("x86_64-linux v:1 invalid:") + assert_equal Gem::Platform.new("x86_64-linux"), result.platform + + # Invalid platform strings create "unknown" platforms but don't error + result = Gem::Platform::Specific.parse("invalid:platform:format v:1 engine:ruby") + assert_equal "ruby", result.ruby_engine + assert_equal "unknown", result.platform.os + end + + def test_parse_ignores_unknown_attributes + str = "x86_64-linux v:1 engine:ruby unknown_attr:value another:attr" + specific = Gem::Platform::Specific.parse(str) + + assert_equal Gem::Platform.new("x86_64-linux"), specific.platform + assert_equal "ruby", specific.ruby_engine + # Unknown attributes should be ignored + assert_nil specific.ruby_version + end + + def test_parse_handles_single_libc_version + # Single numeric value should be set to nil (manylinux needs major.minor) + str = "x86_64-linux v:1 libc_type:glibc libc_version:2" + specific = Gem::Platform::Specific.parse(str) + + assert_equal "glibc", specific.libc_type + assert_nil specific.libc_version # Single values are not valid for libc_version + end + + def test_parse_handles_malformed_libc_version + # Non-dot format should be set to nil + str = "x86_64-linux v:1 libc_type:glibc libc_version:invalid" + specific = Gem::Platform::Specific.parse(str) + + assert_equal "glibc", specific.libc_type + assert_nil specific.libc_version # Malformed libc_version becomes nil + end + + def test_to_s_uses_dot_format_and_includes_version + # Test that to_s now uses the dot format and includes version + specific = Gem::Platform::Specific.new( + "x86_64-linux", + libc_type: "glibc", + libc_version: [2, 31] + ) + + str = specific.to_s + assert_includes str, "v:1" + assert_includes str, "libc_version:2.31" + refute_includes str, "[2, 31]" + end + + def test_parse_requires_version + # Missing version should raise error + error = assert_raise(ArgumentError) do + Gem::Platform::Specific.parse("x86_64-linux engine:ruby") + end + assert_match(/missing required version field/, error.message) + end + + def test_parse_rejects_unsupported_version + # Unsupported version should raise error + error = assert_raise(ArgumentError) do + Gem::Platform::Specific.parse("x86_64-linux v:2 engine:ruby") + end + assert_match(/unsupported specific format version: 2/, error.message) + + error = assert_raise(ArgumentError) do + Gem::Platform::Specific.parse("x86_64-linux v:0 engine:ruby") + end + assert_match(/unsupported specific format version: 0/, error.message) + end + + def test_specific_linux_libc_tag_generation + # Test that Linux Specific platforms generate appropriate manylinux/musllinux tags + + # Test glibc platform generates manylinux tags + util_set_arch "x86_64-linux-gnu" do + Gem::Platform::Manylinux.stub :glibc_version, [2, 17] do + specific = Gem::Platform::Specific.local + platform_tags = specific.each_possible_match.to_a + + # Should include manylinux tags + manylinux_tags = platform_tags.select {|_, tag| tag.start_with?("manylinux") } + refute_empty manylinux_tags, "glibc platform should generate manylinux tags" + + # Should include at least manylinux_2_17_x86_64 + assert platform_tags.any? {|_, tag| tag == "manylinux_2_17_x86_64" }, + "Should include manylinux_2_17_x86_64 tag for glibc 2.17" + end + end + + # Test musl platform generates musllinux tags + util_set_arch "x86_64-linux-musl" do + Gem::Platform::Musllinux.stub :musl_version, [1, 2] do + specific = Gem::Platform::Specific.local + platform_tags = specific.each_possible_match.to_a + + # Should include musllinux tags + musllinux_tags = platform_tags.select {|_, tag| tag.start_with?("musllinux") } + refute_empty musllinux_tags, "musl platform should generate musllinux tags" + + # Should include at least musllinux_1_2_x86_64 + assert platform_tags.any? {|_, tag| tag == "musllinux_1_2_x86_64" }, + "Should include musllinux_1_2_x86_64 tag for musl 1.2" + end + end + + # Test ARM glibc platform generates appropriate manylinux tags + util_set_arch "arm-linux-gnueabihf" do + Gem::Platform::Manylinux.stub :glibc_version, [2, 28] do + specific = Gem::Platform::Specific.local + platform_tags = specific.each_possible_match.to_a + + # Should include manylinux tags for ARM + manylinux_tags = platform_tags.select {|_, tag| tag.start_with?("manylinux") && tag.include?("arm") } + refute_empty manylinux_tags, "ARM glibc platform should generate arm manylinux tags" + + # Should include at least manylinux_2_28_arm + assert platform_tags.any? {|_, tag| tag == "manylinux_2_28_arm" }, + "Should include manylinux_2_28_arm tag for ARM glibc 2.28" + end + end + + # Test that platforms without libc versions don't generate specific tags + util_set_arch "x86_64-linux-gnu" do + Gem::Platform::Manylinux.stub :glibc_version, nil do + specific = Gem::Platform::Specific.local + platform_tags = specific.each_possible_match.to_a + + # Should still include generic linux tags but no versioned manylinux tags + assert platform_tags.any? {|_, tag| tag == "x86_64_linux" }, + "Should include generic x86_64_linux tag" + + # Should not include versioned manylinux tags when libc version is nil + versioned_manylinux_tags = platform_tags.select {|_, tag| tag.match?(/manylinux_\d+_\d+/) } + assert_empty versioned_manylinux_tags, "Should not generate versioned manylinux tags without libc version" + end + end + end +end diff --git a/test/rubygems/test_gem_platform_wheel.rb b/test/rubygems/test_gem_platform_wheel.rb new file mode 100644 index 000000000000..2e351b472b9b --- /dev/null +++ b/test/rubygems/test_gem_platform_wheel.rb @@ -0,0 +1,834 @@ +# frozen_string_literal: true + +require_relative "helper" +require "rubygems/platform" + +class TestGemPlatformWheel < Gem::TestCase + def test_initialize_valid_wheel_string + wheel = Gem::Platform::Wheel.new("whl-rb3_cr33-musllinux_1_2_x86_64") + assert_equal "rb3_cr33", wheel.ruby_abi_tag + assert_equal "musllinux_1_2_x86_64", wheel.platform_tags + end + + def test_initialize_invalid_wheel_string_wrong_prefix + assert_raise(ArgumentError) do + Gem::Platform::Wheel.new("invalid-rb3_cr33-musllinux_1_2_x86_64") + end + end + + def test_initialize_invalid_wheel_string_missing_parts + assert_raise(ArgumentError) do + Gem::Platform::Wheel.new("whl-rb3_cr33") + end + end + + def test_initialize_invalid_wheel_string_too_many_parts + # The split with limit 3 means extra hyphens become part of platform_tags + wheel = Gem::Platform::Wheel.new("whl-rb3_cr33-linux-extra-part") + assert_equal "rb3_cr33", wheel.ruby_abi_tag + assert_equal "linux_extra_part", wheel.platform_tags + end + + def test_to_s + wheel = Gem::Platform::Wheel.new("whl-rb3_cr33-musllinux_1_2_x86_64") + assert_equal "whl-rb3_cr33-musllinux_1_2_x86_64", wheel.to_s + end + + def test_to_a + wheel = Gem::Platform::Wheel.new("whl-rb3_cr33-musllinux_1_2_x86_64") + assert_equal ["whl", "rb3_cr33", "musllinux_1_2_x86_64"], wheel.to_a + end + + def test_expand_single_tags + wheel = Gem::Platform::Wheel.new("whl-rb3_cr33-linux_x86_64") + expected = [["rb3_cr33", "linux_x86_64"]] + assert_equal expected, wheel.expand + end + + def test_expand_multiple_ruby_abi_tags + wheel = Gem::Platform::Wheel.new("whl-rb3_cr33.rb3_cr34-linux_x86_64") + expected = [ + ["rb3_cr33", "linux_x86_64"], + ["rb3_cr34", "linux_x86_64"], + ] + assert_equal expected, wheel.expand + end + + def test_expand_multiple_platform_tags + wheel = Gem::Platform::Wheel.new("whl-rb3_cr33-linux_x86_64.linux_aarch64") + expected = [ + ["rb3_cr33", "linux_aarch64"], + ["rb3_cr33", "linux_x86_64"], + ] + assert_equal expected, wheel.expand + end + + def test_expand_any_ruby_abi_tag + wheel = Gem::Platform::Wheel.new("whl-any-linux_x86_64") + expected = [["any", "linux_x86_64"]] + assert_equal expected, wheel.expand + end + + def test_expand_any_platform_tag + wheel = Gem::Platform::Wheel.new("whl-rb3_cr33-any") + expected = [["rb3_cr33", "any"]] + assert_equal expected, wheel.expand + end + + def test_equality_same_tags + wheel1 = Gem::Platform::Wheel.new("whl-rb3_cr33-linux_x86_64") + wheel2 = Gem::Platform::Wheel.new("whl-rb3_cr33-linux_x86_64") + assert_equal wheel1, wheel2 + assert wheel1.eql?(wheel2) + assert_equal wheel1.hash, wheel2.hash + end + + def test_equality_different_tags + wheel1 = Gem::Platform::Wheel.new("whl-rb3_cr33-linux_x86_64") + wheel2 = Gem::Platform::Wheel.new("whl-rb3_cr34-linux_x86_64") + refute_equal wheel1, wheel2 + refute wheel1.eql?(wheel2) + end + + def test_equality_different_order_same_content + wheel1 = Gem::Platform::Wheel.new("whl-rb3_cr33.rb3_cr34-linux_x86_64.linux_aarch64") + wheel2 = Gem::Platform::Wheel.new("whl-rb3_cr34.rb3_cr33-linux_aarch64.linux_x86_64") + assert_equal wheel1, wheel2 + assert_equal wheel1.hash, wheel2.hash + end + + def test_match_operator_with_platform + # Use current ruby_abi_tag for proper matching + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + normalized_platform = Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + wheel = Gem::Platform::Wheel.new("whl-#{current_abi}-#{normalized_platform}") + platform = Gem::Platform.new("x86_64-linux") + assert wheel =~ platform + end + + def test_match_operator_with_string + # Use current ruby_abi_tag for proper matching + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + normalized_platform = Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + wheel = Gem::Platform::Wheel.new("whl-#{current_abi}-#{normalized_platform}") + assert wheel =~ "x86_64-linux" + end + + def test_case_equality_with_platform + # Use current ruby_abi_tag for proper matching + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + normalized_platform = Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + wheel = Gem::Platform::Wheel.new("whl-#{current_abi}-#{normalized_platform}") + platform = Gem::Platform.new("x86_64-linux") + assert wheel === platform + end + + def test_case_equality_any_platform_tag + # Use "any" ruby_abi_tag to match any Ruby environment + wheel = Gem::Platform::Wheel.new("whl-any-any") + platform = Gem::Platform.new("x86_64-linux") + assert wheel === platform + end + + def test_case_equality_specific_platform_tag + # Use current ruby_abi_tag but incompatible platform + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + wheel = Gem::Platform::Wheel.new("whl-#{current_abi}-x86_64_linux") + platform = Gem::Platform.new("aarch64-linux") + refute wheel === platform + end + + def test_case_equality_multiple_platform_tags + # Use current ruby_abi_tag with multiple platform tags + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + normalized_x86_linux = Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + normalized_aarch64_linux = Gem::Platform::Wheel.normalize_tag_set("aarch64-linux") + + wheel = Gem::Platform::Wheel.new("whl-#{current_abi}-#{normalized_aarch64_linux}.#{normalized_x86_linux}") + platform1 = Gem::Platform.new("x86_64-linux") + platform2 = Gem::Platform.new("aarch64-linux") + platform3 = Gem::Platform.new("x86_64-darwin") + + assert wheel === platform1 + assert wheel === platform2 + refute wheel === platform3 + end + + def test_ruby_abi_tag_validation_valid_tags + valid_tags = %w[rb3_cr33 jr91_1800 tr234_240 any] + valid_tags.each do |tag| + wheel = Gem::Platform::Wheel.new("whl-#{tag}-linux_x86_64") + assert_equal tag, wheel.ruby_abi_tag + end + end + + def test_ruby_abi_tag_validation_invalid_tags + invalid_tags = ["3rb_cr33", "RB3_CR33"] + invalid_tags.each do |tag| + assert_raise(ArgumentError) do + Gem::Platform::Wheel.new("whl-#{tag}-linux_x86_64") + end + end + end + + def test_platform_tag_validation_valid_tags + valid_tags = %w[linux_x86_64 darwin21_arm64 win32 any musllinux_1_2_x86_64] + valid_tags.each do |tag| + wheel = Gem::Platform::Wheel.new("whl-rb3_cr33-#{tag}") + assert_equal tag, wheel.platform_tags + end + end + + def test_platform_tag_validation_invalid_tags + invalid_tags = ["LINUX"] + invalid_tags.each do |tag| + assert_raise(ArgumentError) do + Gem::Platform::Wheel.new("whl-rb3_cr33-#{tag}") + end + end + end + + def test_tag_normalization_dots_to_underscores + wheel = Gem::Platform::Wheel.new("whl-rb3.cr33-linux.x86.64") + assert_equal "cr33.rb3", wheel.ruby_abi_tag + assert_equal "64.linux.x86", wheel.platform_tags + end + + def test_tag_normalization_hyphens_to_underscores + wheel = Gem::Platform::Wheel.new("whl-rb3-cr33-linux-x86-64") + assert_equal "rb3", wheel.ruby_abi_tag + assert_equal "cr33_linux_x86_64", wheel.platform_tags + end + + def test_tag_normalization_sorting + wheel = Gem::Platform::Wheel.new("whl-rb3_cr34.rb3_cr33-linux_aarch64.linux_x86_64") + assert_equal "rb3_cr33.rb3_cr34", wheel.ruby_abi_tag + assert_equal "linux_aarch64.linux_x86_64", wheel.platform_tags + end + + def test_tag_normalization_deduplication + wheel = Gem::Platform::Wheel.new("whl-rb3_cr33.rb3_cr33-linux_x86_64.linux_x86_64") + assert_equal "rb3_cr33", wheel.ruby_abi_tag + assert_equal "linux_x86_64", wheel.platform_tags + end + + def test_normalize_platform_tags_empty + assert_equal "any", Gem::Platform::Wheel.normalize_tag_set("") + assert_equal "any", Gem::Platform::Wheel.normalize_tag_set(nil) + end + + def test_normalize_platform_tags_single + assert_equal "64.linux_x86", Gem::Platform::Wheel.normalize_tag_set("linux-x86.64") + end + + def test_normalize_platform_tags_multiple + result = Gem::Platform::Wheel.normalize_tag_set("linux-x86.64.darwin.arm64") + assert_equal "64.arm64.darwin.linux_x86", result + end + + # Test interactions between wheel and traditional platforms + def test_wheel_vs_traditional_platform_equality + wheel = Gem::Platform::Wheel.new("whl-rb3_cr33-x86_64_linux") + traditional = Gem::Platform.new("x86_64-linux") + + # Wheels and traditional platforms should never be equal + refute_equal wheel, traditional + refute_equal traditional, wheel + refute wheel.eql?(traditional) + refute traditional.eql?(wheel) + end + + def test_wheel_matches_traditional_platform_via_case_equality + # Use current ruby_abi_tag for proper matching + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + normalized_platform = Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + wheel = Gem::Platform::Wheel.new("whl-#{current_abi}-#{normalized_platform}") + traditional = Gem::Platform.new("x86_64-linux") + + # Wheel should match traditional platform (wheel can run on traditional) + assert wheel === traditional + + # Traditional platform should NOT match wheel (traditional gem can't run on wheel platform) + refute traditional === wheel + end + + def test_wheel_any_tag_matches_all_traditional_platforms + # Use "any" for both ruby_abi_tag and platform_tags + wheel = Gem::Platform::Wheel.new("whl-any-any") + platforms = [ + Gem::Platform.new("x86_64-linux"), + Gem::Platform.new("aarch64-linux"), + Gem::Platform.new("x86_64-darwin"), + Gem::Platform.new("arm64-darwin"), + Gem::Platform.new("x64-mingw-ucrt"), + ] + + platforms.each do |platform| + assert wheel === platform, "Wheel with 'any' tags should match #{platform}" + end + end + + def test_wheel_specific_platform_only_matches_compatible + # Use current ruby_abi_tag for proper matching + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + normalized_x86_linux = Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + wheel = Gem::Platform::Wheel.new("whl-#{current_abi}-#{normalized_x86_linux}") + + # Should match + assert wheel === Gem::Platform.new("x86_64-linux") + + # Should not match + refute wheel === Gem::Platform.new("aarch64-linux") + refute wheel === Gem::Platform.new("x86_64-darwin") + refute wheel === Gem::Platform.new("x64-mingw-ucrt") + end + + def test_wheel_platform_normalization_matches_traditional + # Test that platform tag normalization allows matching + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + wheel = Gem::Platform::Wheel.new("whl-#{current_abi}-x86_64_linux") + traditional = Gem::Platform.new("x86_64-linux") + + # Should match with normalized platform tags + assert wheel === traditional + end + + def test_multiple_ruby_abi_tags_irrelevant_for_traditional_matching + # Test multiple ruby_abi_tags where one matches current environment + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + normalized_platform = Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + wheel = Gem::Platform::Wheel.new("whl-rb3_cr33.#{current_abi}-#{normalized_platform}") + traditional = Gem::Platform.new("x86_64-linux") + + # Should match because one of the ruby_abi_tags matches + assert wheel === traditional + end + + def test_wheel_design_ruby_abi_format + # Test the design decision for Ruby/ABI tag format: {engine}{version}_{abi} + valid_formats = [ + "cr33_220", # CRuby 3.3, ABI 220 + "jr91_1800", # JRuby 9.1, Java 18.0.0 + "tr234_240", # TruffleRuby 23.4, ABI 240 + "rb3_cr33", # Alternative format + "any", # Universal wildcard + ] + + valid_formats.each do |format| + wheel = Gem::Platform::Wheel.new("whl-#{format}-x86_64_linux") + assert_equal format, wheel.ruby_abi_tag + end + end + + def test_wheel_design_platform_tag_normalization + # Test the design decision for tag normalization + test_cases = { + "linux-x86.64" => "64.linux_x86", + "linux_x86_64" => "linux_x86_64", # No change needed + "x86-64.linux" => "linux.x86_64", + "x86_64.linux" => "linux.x86_64", + } + + test_cases.each do |input, expected| + wheel = Gem::Platform::Wheel.new("whl-rb3_cr33-#{input}") + assert_equal expected, wheel.platform_tags, + "Input '#{input}' should normalize to '#{expected}'" + end + end + + def test_wheel_design_backward_compatibility + # Ensure wheel platforms don't interfere with existing platform functionality + traditional_strings = [ + "ruby", + "x86_64-linux", + "universal-darwin", + "java", + "x64-mingw-ucrt", + ] + + traditional_strings.each do |platform_string| + platform = Gem::Platform.new(platform_string) + refute platform.is_a?(Gem::Platform::Wheel), + "Traditional platform '#{platform_string}' should not create Wheel instance" + end + end + + # Tests moved from test_gem_platform.rb + + def test_initialize_wheel + platform = Gem::Platform.new("whl-rb3.cr33-musllinux_1_2_x86_64") + assert_equal [["cr33", "musllinux_1_2_x86_64"], ["rb3", "musllinux_1_2_x86_64"]], platform.expand + assert_equal "whl-cr33.rb3-musllinux_1_2_x86_64", platform.to_s + end + + def test_platform_new_with_wheel_instance + wheel = Gem::Platform::Wheel.new("whl-rb3.cr33-linux_x86_64") + platform = Gem::Platform.new(wheel) + refute_same wheel, platform + assert_equal wheel, platform + end + + def test_wheel_platform_matching + # Use current ruby_abi_tag for proper matching + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + normalized_platform = Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + wheel = Gem::Platform.new("whl-#{current_abi}-#{normalized_platform}") + traditional = Gem::Platform.new("x86_64-linux") + + # Wheel should match traditional platform + assert wheel === traditional + + # Traditional platform should not match wheel + refute traditional === wheel + end + + def test_wheel_platform_sorting + wheel1 = Gem::Platform.new("whl-rb3.cr33-x86_64_linux") + wheel2 = Gem::Platform.new("whl-rb3.cr34-x86_64_linux") + traditional = Gem::Platform.new("x86_64-linux") + + specs = [ + util_spec("test", "1.0") {|s| s.platform = traditional }, + util_spec("test", "1.0") {|s| s.platform = wheel2 }, + util_spec("test", "1.0") {|s| s.platform = wheel1 }, + ] + + # Test with Ruby 3.3 environment - wheel1 (cr33) should match best + target_platform = Gem::Platform::Specific.new(traditional, ruby_engine: "ruby", ruby_engine_version: "3.3.1", ruby_version: "3.3.1", abi_version: "3.3.0") + sorted_specs = Gem::Platform.sort_best_platform_match(specs, target_platform) + assert_equal [wheel1, traditional, wheel2], sorted_specs.map(&:platform) + + # Test with Ruby 3.4 environment - wheel2 (cr34) should match best + target_platform = Gem::Platform::Specific.new(traditional, ruby_engine: "ruby", ruby_engine_version: "3.4.0", ruby_version: "3.4.0", abi_version: "3.4.0") + sorted_specs = Gem::Platform.sort_best_platform_match(specs, target_platform) + assert_equal [wheel2, traditional, wheel1], sorted_specs.map(&:platform) + end + + def test_match_platforms_wheel_vs_traditional + # Use current ruby_abi_tag for proper matching + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + normalized_platform = Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + wheel_platform = Gem::Platform.new("whl-#{current_abi}-#{normalized_platform}") + traditional_platform = Gem::Platform.new("x86_64-linux") + ruby_platform = Gem::Platform::RUBY + + # Test wheel platform against various user platforms + user_platforms = [traditional_platform, ruby_platform] + + # Wheel should match traditional and ruby platforms + assert Gem::Platform.send(:match_platforms?, wheel_platform, user_platforms) + + # Traditional should NOT match wheel platform + refute Gem::Platform.send(:match_platforms?, traditional_platform, [wheel_platform]) + + # But traditional should match itself and ruby + assert Gem::Platform.send(:match_platforms?, traditional_platform, user_platforms) + end + + def test_match_spec_with_wheel_platforms + # Use current ruby_abi_tag for proper matching + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + normalized_platform = Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + + wheel_spec = util_spec "wheel-gem", "1.0" do |s| + s.platform = Gem::Platform.new("whl-#{current_abi}-#{normalized_platform}") + end + + traditional_spec = util_spec "traditional-gem", "1.0" do |s| + s.platform = Gem::Platform.new("x86_64-linux") + end + + ruby_spec = util_spec "ruby-gem", "1.0" do |s| + s.platform = Gem::Platform::RUBY + end + + # Set current platforms to traditional + platforms = Gem.platforms + Gem.platforms = [Gem::Platform.new("x86_64-linux"), Gem::Platform::RUBY] + + begin + # Wheel should match current platforms (wheel can run on traditional) + assert Gem::Platform.match_spec?(wheel_spec) + + # Traditional should match + assert Gem::Platform.match_spec?(traditional_spec) + + # Ruby should match + assert Gem::Platform.match_spec?(ruby_spec) + ensure + Gem.platforms = platforms + end + end + + def test_match_gem_with_wheel_platforms + # Test wheel gems vs traditional user platforms + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + normalized_platform = Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + + platforms = Gem.platforms + Gem.platforms = [Gem::Platform.new("x86_64-linux"), Gem::Platform::RUBY] + + begin + assert Gem::Platform.match_gem?("whl-#{current_abi}-#{normalized_platform}", "some-gem") + assert Gem::Platform.match_gem?("x86_64-linux", "some-gem") + assert Gem::Platform.match_gem?(Gem::Platform::RUBY, "some-gem") + ensure + Gem.platforms = platforms + end + end + + def test_installable_with_wheel_platforms + # Use current ruby_abi_tag for proper matching + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + normalized_platform = Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + + wheel_spec = util_spec "wheel-gem", "1.0" do |s| + s.platform = Gem::Platform.new("whl-#{current_abi}-#{normalized_platform}") + end + + traditional_spec = util_spec "traditional-gem", "1.0" do |s| + s.platform = Gem::Platform.new("x86_64-linux") + end + + platforms = Gem.platforms + Gem.platforms = [Gem::Platform.new("x86_64-linux"), Gem::Platform::RUBY] + + begin + # Both should be installable on traditional platforms + assert Gem::Platform.installable?(wheel_spec) + assert Gem::Platform.installable?(traditional_spec) + ensure + Gem.platforms = platforms + end + end + + def test_sort_priority_wheel_vs_traditional + wheel_platform = Gem::Platform.new("whl-rb3.cr33-x86_64_linux") + traditional_platform = Gem::Platform.new("x86_64-linux") + ruby_platform = Gem::Platform::RUBY + + # Ruby should have lowest priority (most preferred) + assert_equal(-1, Gem::Platform.sort_priority(ruby_platform)) + + # Wheel platforms should have higher priority than traditional platforms + assert_equal 2, Gem::Platform.sort_priority(wheel_platform) + assert_equal 1, Gem::Platform.sort_priority(traditional_platform) + end + + def test_platform_specificity_cross_platform_types + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + current_platform = Gem::Platform.local + normalized_platform = Gem::Platform::Wheel.normalize_tag_set(current_platform.to_s) + + # Use a different ruby abi to ensure wheel doesn't match perfectly + test_abi = current_abi == "cr33" ? "cr34" : "cr33" + wheel_platform = Gem::Platform.new("whl-#{test_abi}-#{normalized_platform}") + traditional_platform = current_platform + user_platform = current_platform + + wheel_specificity = Gem::Platform.platform_specificity_match(wheel_platform, user_platform) + traditional_specificity = Gem::Platform.platform_specificity_match(traditional_platform, user_platform) + + # Traditional platform should be more specific for traditional user platform when wheel doesn't match Ruby ABI + assert traditional_specificity < wheel_specificity, + "Traditional platform should be more specific than wheel for traditional user platform when wheel Ruby ABI doesn't match" + end + + def test_sort_and_filter_best_platform_match_mixed_types + wheel1 = Gem::Platform.new("whl-rb3.cr33-x86_64_linux") + wheel2 = Gem::Platform.new("whl-rb3_cr34-x86_64_linux") + traditional = Gem::Platform.new("x86_64-linux") + ruby = Gem::Platform::RUBY + + specs = [ + util_spec("gem", "1.0") {|s| s.platform = wheel1 }, + util_spec("gem", "1.0") {|s| s.platform = wheel2 }, + util_spec("gem", "1.0") {|s| s.platform = traditional }, + util_spec("gem", "1.0") {|s| s.platform = ruby }, + ] + + user_platform = Gem::Platform.new("x86_64-linux") + filtered = Gem::Platform.sort_and_filter_best_platform_match(specs, user_platform) + + # Should prioritize traditional platform for traditional user + assert_equal traditional, filtered.first.platform + + # Should include ruby platform as well (same specificity) + platform_types = filtered.map {|s| s.platform.class }.uniq + assert_includes platform_types, Gem::Platform + end + + def test_match_wheel_with_current_environment + # Create a wheel that matches current environment + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + current_platform = Gem::Platform.local + platform_tag = Gem::Platform::Wheel.normalize_tag_set(current_platform.to_s) + + matching_wheel = Gem::Platform.new("whl-#{current_abi}-#{platform_tag}") + + # Should match when explicitly specifying current environment + assert matching_wheel.send(:match?, ruby_abi_tag: current_abi, platform: current_platform), + "Wheel matching current environment should match" + end + + def test_match_wheel_with_different_ruby_abi + current_platform = Gem::Platform.local + platform_tag = Gem::Platform::Wheel.normalize_tag_set(current_platform.to_s) + + # Create wheel with different Ruby ABI + different_abi_wheel = Gem::Platform.new("whl-jr91_1800-#{platform_tag}") + + # Should not match current environment + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + refute different_abi_wheel.send(:match?, ruby_abi_tag: current_abi, platform: current_platform), + "Wheel with different Ruby ABI should not match current environment" + end + + def test_match_wheel_with_any_tags + # Test wheel with "any" ruby_abi_tag + any_abi_wheel = Gem::Platform.new("whl-any-x86_64_linux") + linux_platform = Gem::Platform.new("x86_64-linux") + + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + assert any_abi_wheel.send(:match?, ruby_abi_tag: current_abi, platform: linux_platform), + "Wheel with 'any' ruby_abi_tag should match any Ruby environment" + + # Test wheel with "any" platform_tags + any_platform_wheel = Gem::Platform.new("whl-#{current_abi}-any") + + assert any_platform_wheel.send(:match?, ruby_abi_tag: current_abi, platform: linux_platform), + "Wheel with 'any' platform_tags should match any platform" + end + + def test_match_wheel_tuple_based_matching + # Test explicit ruby_abi_tag and platform specification + wheel = Gem::Platform.new("whl-cr33_220-x86_64_linux") + platform = Gem::Platform.new("x86_64-linux") + + # Should match when explicitly specified matching values + assert wheel.send(:match?, ruby_abi_tag: "cr33_220", platform: platform), + "Should match when ruby_abi_tag and platform are explicitly compatible" + + # Should not match when ruby_abi_tag differs + refute wheel.send(:match?, ruby_abi_tag: "jr91_1800", platform: platform), + "Should not match when ruby_abi_tag differs" + + # Should not match when platform differs + darwin_platform = Gem::Platform.new("x86_64-darwin") + refute wheel.send(:match?, ruby_abi_tag: "cr33_220", platform: darwin_platform), + "Should not match when platform differs" + end + + def test_match_wheel_with_specific_instance + # Test new Specific-based API + wheel = Gem::Platform.new("whl-cr33-x86_64_linux") + specific = Gem::Platform::Specific.new("x86_64-linux", ruby_engine: "ruby", ruby_engine_version: "3.3.1", ruby_version: "3.3.1", abi_version: "3.3.0") + + assert wheel.send(:match?, specific), + "Wheel should match compatible Specific instance" + + # Test with incompatible specific + incompatible_specific = Gem::Platform::Specific.new("aarch64-linux", ruby_engine: "ruby", ruby_engine_version: "3.3.1", ruby_version: "3.3.1", abi_version: "3.3.0") + refute wheel.send(:match?, incompatible_specific), + "Wheel should not match incompatible Specific instance" + end + + def test_match_wheel_with_current_specific + # Test using current environment + current_specific = Gem::Platform::Specific.local + # Generate ABI tag from the Specific object to ensure compatibility + generated_abi = Gem::Platform::Specific.generate_ruby_abi_tag( + current_specific.ruby_engine, + current_specific.ruby_engine_version, + current_specific.ruby_version, + current_specific.abi_version + ) + current_platform_tag = Gem::Platform::Wheel.normalize_tag_set(Gem::Platform.local.to_s) + + wheel = Gem::Platform.new("whl-#{generated_abi}-#{current_platform_tag}") + + assert wheel.send(:match?, current_specific), + "Wheel should match current environment via Specific" + end + + def test_match_wheel_specific_vs_keyword_arguments + # Test that both APIs produce same results + wheel = Gem::Platform.new("whl-cr33-x86_64_linux") + specific = Gem::Platform::Specific.new("x86_64-linux", ruby_engine: "ruby", ruby_engine_version: "3.3.1", ruby_version: "3.3.1", abi_version: "3.3.0") + + result_specific = wheel.send(:match?, specific) + result_keywords = wheel.send(:match?, ruby_abi_tag: "cr33", platform: Gem::Platform.new("x86_64-linux")) + + assert_equal result_specific, result_keywords, + "Both APIs should produce same matching result" + end + + def test_match_wheel_error_handling_with_specific + wheel = Gem::Platform.new("whl-cr33-x86_64_linux") + specific = Gem::Platform::Specific.new("x86_64-linux", ruby_engine: "ruby", ruby_engine_version: "3.3.1", ruby_version: "3.3.1", abi_version: "3.3.0") + + # Test error when providing both specific and keyword arguments + assert_raise(ArgumentError, "Should raise error when mixing specific and keywords") do + wheel.send(:match?, specific, ruby_abi_tag: "cr33") + end + + # Test error when providing wrong type for specific + assert_raise(ArgumentError, "Should raise error for wrong specific type") do + wheel.send(:match?, "not-a-specific") + end + + # Test error when providing neither + assert_raise(ArgumentError, "Should raise error when no parameters provided") do + wheel.send(:match?) + end + end + + def test_generate_ruby_abi_tag + # Test the new ruby ABI tag generation method with abi_version + # With standard abi_version (3.3.0), no suffix is added since it matches major.minor.0 + tag = Gem::Platform::Specific.generate_ruby_abi_tag("ruby", "3.3.1", "3.3.1", "3.3.0") + assert_equal "cr33", tag + + # With abi_version that has suffix (like static build) + tag = Gem::Platform::Specific.generate_ruby_abi_tag("ruby", "3.3.1", "3.3.1", "3.3.0-static") + assert_equal "cr33_static", tag + + # JRuby with standard abi_version + tag = Gem::Platform::Specific.generate_ruby_abi_tag("jruby", "9.4.0", "3.1.0", "3.1.0") + assert_equal "jr94", tag + + # Test fallback to ruby_version when abi_version is nil - extracts suffix after major.minor.0 + tag = Gem::Platform::Specific.generate_ruby_abi_tag("ruby", "3.3.1", "3.3.1", nil) + assert_equal "cr33_1", tag + + # Test fallback to current when missing info + tag = Gem::Platform::Specific.generate_ruby_abi_tag(nil, nil, nil, nil) + assert_nil tag + + tag = Gem::Platform::Specific.generate_ruby_abi_tag("ruby", nil, "3.3.1", "3.3.0") + assert_nil tag + end + + def test_wheel_case_equality_uses_tuple_matching + # Test that wheel === platform uses the new tuple matching + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + current_platform = Gem::Platform.local + platform_tag = Gem::Platform::Wheel.normalize_tag_set(current_platform.to_s) + + matching_wheel = Gem::Platform.new("whl-#{current_abi}-#{platform_tag}") + + # Should match current platform + assert matching_wheel === current_platform, + "Wheel should match compatible traditional platform via case equality" + + # Create incompatible wheel + incompatible_wheel = Gem::Platform.new("whl-jr91_1800-x86_64_darwin") + refute incompatible_wheel === current_platform, + "Incompatible wheel should not match traditional platform" + end + + def test_wheel_platform_tag_validation_integration + assert Gem::Platform.new("whl-rb3.cr33-linux_x86_64") + assert Gem::Platform.new("whl-rb3.cr33-mingw_x86_64") + assert Gem::Platform.new("whl-rb3.cr33-darwin_x86_64") + assert Gem::Platform.new("whl-rb3.cr33-linux_x86_64_musl") + end + + def test_wheel_platform_string_variations_integration + # Test various wheel platform string formats + assert_equal "whl-cr33.rb3-x86_64_linux", Gem::Platform.new("whl-rb3.cr33-x86_64_linux").to_s + assert_equal "whl-cr33.rb3-x86_64_linux_musl", Gem::Platform.new("whl-rb3.cr33-x86_64_linux_musl").to_s + assert_equal "whl-cr33.rb3-x86_64_darwin", Gem::Platform.new("whl-rb3.cr33-x86_64_darwin").to_s + assert_equal "whl-cr33.rb3-arm64_darwin", Gem::Platform.new("whl-rb3.cr33-arm64_darwin").to_s + assert_equal "whl-any-any", Gem::Platform.new("whl-any-any").to_s + assert_equal "whl-rb3-any", Gem::Platform.new("whl-rb3-any").to_s + assert_equal "whl-any-x86_64_linux", Gem::Platform.new("whl-any-x86_64_linux").to_s + end + + def test_wheel_platform_equality_integration + # Test == operator + p1 = Gem::Platform.new("whl-rb3.cr33-x86_64_linux") + p2 = Gem::Platform.new("whl-rb3.cr33-x86_64_linux") + p3 = Gem::Platform.new("whl-rb3.cr33-x86_64_linux_musl") + p4 = Gem::Platform.new("x86_64-linux") + + assert_equal p1, p1 + assert_equal p1, p2 + refute_equal p1, p3 + refute_equal p1, p4 + + # Test hash method + assert_equal p1.hash, p2.hash + refute_equal p1.hash, p3.hash + refute_equal p1.hash, p4.hash + + # Test to_a method + assert_equal ["whl", "cr33.rb3", "x86_64_linux"], p1.to_a + assert_equal ["whl", "cr33.rb3", "x86_64_linux_musl"], p3.to_a + assert_equal ["x86_64", "linux", nil], p4.to_a + + # Test with mixed platform formats + p5 = Gem::Platform.new("whl-rb3.cr33-x86_64_linux.x86_64_linux") + p6 = Gem::Platform.new("whl-rb3.cr33-x86_64_linux.x86_64_darwin") + + assert_equal p1, p5 + refute_equal p1, p6 + assert_equal p1.hash, p5.hash + refute_equal p1.hash, p6.hash + end + + def test_wheel_basics_integration + linux = Gem::Platform.new("whl-any-x86_64_linux") + + assert Gem::Platform.send(:match_platforms?, linux, [Gem::Platform.new("x86_64-linux")]), + "expected #{linux} to match [x86_64-linux]" + # assert Gem::Platform.send(:match_platforms?, linux, [Gem::Platform.new("x86_64-linux-20")]), + # "expected #{linux} to match [x86_64-linux-20]" + refute Gem::Platform.send(:match_platforms?, linux, [Gem::Platform.new("x86_64-darwin")]), + "expected #{linux} to not match [x86_64-darwin]" + end + + def test_normalize_platform_tags_integration + # Test legacy platform tags + assert_equal "x86_64_linux", Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + assert_equal "x86_64_linux_musl", Gem::Platform::Wheel.normalize_tag_set("x86_64-linux-musl") + assert_equal "x86_64_darwin", Gem::Platform::Wheel.normalize_tag_set("x86_64-darwin") + assert_equal "arm64_darwin", Gem::Platform::Wheel.normalize_tag_set("arm64-darwin") + + # Test wheel platform tags + assert_equal "x86_64_linux", Gem::Platform::Wheel.normalize_tag_set("x86_64-linux") + assert_equal "x86_64_linux_musl", Gem::Platform::Wheel.normalize_tag_set("x86_64-linux-musl") + assert_equal "x86_64_darwin", Gem::Platform::Wheel.normalize_tag_set("x86_64-darwin") + assert_equal "arm64_darwin", Gem::Platform::Wheel.normalize_tag_set("arm64-darwin") + + # Test mixed platform formats + assert_equal "x86_64_linux", Gem::Platform::Wheel.normalize_tag_set("x86_64-linux.x86_64-linux") + assert_equal "x86_64_darwin.x86_64_linux", Gem::Platform::Wheel.normalize_tag_set("x86_64-linux.x86_64-darwin") + end + + def test_platform_specificity_match_wheel_vs_specific_integration + # Test wheel vs Specific object matching - should use full Specific environment details + wheel = Gem::Platform.new("whl-cr33-x86_64_linux") + specific_compatible = Gem::Platform::Specific.new("x86_64-linux", ruby_engine: "ruby", ruby_engine_version: "3.3.1", ruby_version: "3.3.1", abi_version: "3.3.0") + specific_incompatible = Gem::Platform::Specific.new("x86_64-linux", ruby_engine: "ruby", ruby_engine_version: "3.2.0", ruby_version: "3.2.0", abi_version: "3.2.0") + + # Compatible Specific should match with wheel specificity (-5 to -3 range) + specificity_match = Gem::Platform.platform_specificity_match(wheel, specific_compatible) + assert_equal(-10, specificity_match, "#{wheel} to #{specific_compatible}") + + # Incompatible Specific should not match + specificity_no_match = Gem::Platform.platform_specificity_match(wheel, specific_incompatible) + assert_equal 1_000_000, specificity_no_match, "Incompatible Specific should not match wheel" + end + + def test_platform_specificity_match_wheel_vs_traditional_integration + # Test wheel vs traditional platform - should use current environment fallback + + wheel = Gem::Platform.new("whl-#{Gem::Platform::Specific.current_ruby_abi_tag}-x86_64_linux") + traditional = Gem::Platform.new("x86_64-linux") + + # Should use wheel matching logic with current environment, returning -10 for exact match + specificity = Gem::Platform.platform_specificity_match(wheel, traditional) + assert_equal(-10, specificity, "Gem::Platform.platform_specificity_match(#{wheel}, #{traditional})") + end +end diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index 4990d5d2dd13..2beb3ebcdccf 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -850,4 +850,555 @@ def test_raises_and_explains_when_platform_prevents_install assert_match "No match for 'a (= 1)' on this platform. Found: c-p-1", e.message end + + def test_wheel_platform_resolution_best_match_installation + # Test that wheel platforms with precise ruby/ABI matching get chosen over traditional platforms + is = Gem::Resolver::IndexSpecification + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + current_platform = Gem::Platform.local.to_s.tr("-", "_") + + spec_fetcher do |fetcher| + # Traditional platform variant + fetcher.spec "native-gem", "1.0" do |s| + s.platform = Gem::Platform.local + end + + # Wheel variant for current environment - should be preferred + fetcher.spec "native-gem", "1.0" do |s| + s.platform = "whl-#{current_abi}-#{current_platform}" + end + + # Wheel variant for different ABI - should not match + fetcher.spec "native-gem", "1.0" do |s| + s.platform = "whl-cr32_320-#{current_platform}" + end + + # Universal wheel - should be fallback + fetcher.spec "native-gem", "1.0" do |s| + s.platform = "whl-any-any" + end + end + + source = Gem::Source.new @gem_repo + s = set + + # Create specifications for resolver + traditional = is.new s, "native-gem", v("1.0"), source, Gem::Platform.local.to_s + wheel_exact = is.new s, "native-gem", v("1.0"), source, "whl-#{current_abi}-#{current_platform}" + wheel_different = is.new s, "native-gem", v("1.0"), source, "whl-cr32_320-#{current_platform}" + wheel_universal = is.new s, "native-gem", v("1.0"), source, "whl-any-any" + + s.add traditional + s.add wheel_exact + s.add wheel_different + s.add wheel_universal + + dep = make_dep "native-gem" + resolver = Gem::Resolver.new([dep], s) + + # Should resolve to the wheel with exact ABI match + result = resolver.resolve + resolved_spec = result.first.spec + + assert_equal "whl-#{current_abi}-#{current_platform}", resolved_spec.platform.to_s, + "Should choose wheel with exact ABI match over traditional platform" + end + + def test_wheel_platform_resolution_traditional_fallback + # Test that traditional platforms are chosen when no compatible wheel exists + is = Gem::Resolver::IndexSpecification + current_platform = Gem::Platform.local.to_s.tr("-", "_") + + spec_fetcher do |fetcher| + # Traditional platform variant + fetcher.spec "native-gem", "1.0" do |s| + s.platform = Gem::Platform.local + end + + # Wheel variant for incompatible ABI + fetcher.spec "native-gem", "1.0" do |s| + s.platform = "whl-cr32_320-#{current_platform}" + end + + # Wheel variant for incompatible platform + fetcher.spec "native-gem", "1.0" do |s| + s.platform = "whl-cr33-aarch64_linux" + end + end + + source = Gem::Source.new @gem_repo + s = set + + traditional = is.new s, "native-gem", v("1.0"), source, Gem::Platform.local.to_s + wheel_wrong_abi = is.new s, "native-gem", v("1.0"), source, "whl-cr32_320-#{current_platform}" + wheel_wrong_platform = is.new s, "native-gem", v("1.0"), source, "whl-cr33-aarch64_linux" + + s.add traditional + s.add wheel_wrong_abi + s.add wheel_wrong_platform + + dep = make_dep "native-gem" + resolver = Gem::Resolver.new([dep], s) + + # Should resolve to traditional platform when wheels don't match + result = resolver.resolve + resolved_spec = result.first.spec + + assert_equal Gem::Platform.local.to_s, resolved_spec.platform.to_s, + "Should fall back to traditional platform when no wheel matches" + end + + def test_wheel_platform_resolution_universal_wheel_fallback + # Test that universal wheels are chosen when no platform-specific wheel matches + is = Gem::Resolver::IndexSpecification + current_platform = Gem::Platform.local.to_s.tr("-", "_") + + spec_fetcher do |fetcher| + # No traditional platform variant + + # Wheel variant for incompatible ABI + fetcher.spec "native-gem", "1.0" do |s| + s.platform = "whl-cr32_320-#{current_platform}" + end + + # Universal wheel - should be chosen + fetcher.spec "native-gem", "1.0" do |s| + s.platform = "whl-any-any" + end + + # Platform-specific universal wheel - should be preferred over full universal + fetcher.spec "native-gem", "1.0" do |s| + s.platform = "whl-any-#{current_platform}" + end + end + + source = Gem::Source.new @gem_repo + s = set + + wheel_wrong_abi = is.new s, "native-gem", v("1.0"), source, "whl-cr32_320-#{current_platform}" + wheel_universal = is.new s, "native-gem", v("1.0"), source, "whl-any-any" + wheel_platform_universal = is.new s, "native-gem", v("1.0"), source, "whl-any-#{current_platform}" + + s.add wheel_wrong_abi + s.add wheel_universal + s.add wheel_platform_universal + + dep = make_dep "native-gem" + resolver = Gem::Resolver.new([dep], s) + + # Should resolve to platform-specific universal wheel + result = resolver.resolve + resolved_spec = result.first.spec + + assert_equal "whl-any-#{current_platform}", resolved_spec.platform.to_s, + "Should choose platform-specific universal wheel over full universal" + end + + def test_wheel_platform_resolution_mixed_versions + # Test wheel platform resolution with different versions + is = Gem::Resolver::IndexSpecification + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + current_platform = Gem::Platform.local.to_s.tr("-", "_") + + spec_fetcher do |fetcher| + # Older version with traditional platform + fetcher.spec "native-gem", "1.0" do |s| + s.platform = Gem::Platform.local + end + + # Newer version with wheel platform for current environment + fetcher.spec "native-gem", "2.0" do |s| + s.platform = "whl-#{current_abi}-#{current_platform}" + end + + # Even newer version but incompatible wheel + fetcher.spec "native-gem", "3.0" do |s| + s.platform = "whl-cr32_320-#{current_platform}" + end + end + + source = Gem::Source.new @gem_repo + s = set + + traditional_old = is.new s, "native-gem", v("1.0"), source, Gem::Platform.local.to_s + wheel_newer = is.new s, "native-gem", v("2.0"), source, "whl-#{current_abi}-#{current_platform}" + wheel_newest_incompatible = is.new s, "native-gem", v("3.0"), source, "whl-cr32_320-#{current_platform}" + + s.add traditional_old + s.add wheel_newer + s.add wheel_newest_incompatible + + dep = make_dep "native-gem" + resolver = Gem::Resolver.new([dep], s) + + # Should resolve to compatible wheel version even if older than incompatible wheel + result = resolver.resolve + resolved_spec = result.first.spec + + assert_equal "2.0", resolved_spec.version.to_s, + "Should choose compatible wheel version over incompatible newer version" + assert_equal "whl-#{current_abi}-#{current_platform}", resolved_spec.platform.to_s, + "Should choose wheel platform over traditional for same compatibility" + end + + def test_wheel_platform_specific_dependencies + # Test wheel platforms with platform-specific dependency requirements + is = Gem::Resolver::IndexSpecification + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + current_platform = Gem::Platform.local.to_s.tr("-", "_") + + spec_fetcher do |fetcher| + # Traditional platform gem requires older version of dependency + fetcher.spec "native-gem", "1.0" do |s| + s.platform = Gem::Platform.local + s.add_dependency "shared-dep", "~> 1.0" + end + + # Wheel platform gem requires newer version of dependency + fetcher.spec "native-gem", "1.0" do |s| + s.platform = "whl-#{current_abi}-#{current_platform}" + s.add_dependency "shared-dep", "~> 2.0" + end + + # Dependencies available + fetcher.spec "shared-dep", "1.5" + fetcher.spec "shared-dep", "2.1" + end + + source = Gem::Source.new @gem_repo + s = set + + traditional = is.new s, "native-gem", v("1.0"), source, Gem::Platform.local.to_s + wheel = is.new s, "native-gem", v("1.0"), source, "whl-#{current_abi}-#{current_platform}" + dep_old = is.new s, "shared-dep", v("1.5"), source, Gem::Platform::RUBY.to_s + dep_new = is.new s, "shared-dep", v("2.1"), source, Gem::Platform::RUBY.to_s + + s.add traditional + s.add wheel + s.add dep_old + s.add dep_new + + dep = make_dep "native-gem" + resolver = Gem::Resolver.new([dep], s) + + # Should resolve to wheel platform and its newer dependency + result = resolver.resolve.sort_by {|spec| spec.spec.name } + main_spec = result.find {|spec| spec.spec.name == "native-gem" }.spec + dep_spec = result.find {|spec| spec.spec.name == "shared-dep" }.spec + + assert_equal "whl-#{current_abi}-#{current_platform}", main_spec.platform.to_s, + "Should choose wheel platform with its specific dependencies" + assert_equal "2.1", dep_spec.version.to_s, + "Should resolve newer dependency required by wheel platform" + end + + def test_fallback_to_traditional_when_wheel_deps_missing + pend "Platform normalization adds x86- prefix on Windows" if Gem.win_platform? + + # Test fallback when wheel deps are missing from available gems + is = Gem::Resolver::IndexSpecification + + spec_fetcher do |fetcher| + # Traditional platform gem with satisfiable dependency + fetcher.spec "native-gem", "1.0" do |s| + s.platform = Gem::Platform.local.os + s.add_dependency "available-dep", "~> 1.0" + end + + # Wheel platform gem with unsatisfiable dependency + fetcher.spec "native-gem", "1.0" do |s| + s.platform = Gem::Platform.local + s.add_dependency "missing-dep", "~> 1.0" + end + + # Only one dependency is available + fetcher.spec "available-dep", "1.0" + # missing-dep is NOT available + end + + source = Gem::Source.new @gem_repo + s = set + + traditional = is.new s, "native-gem", v("1.0"), source, Gem::Platform.local.to_s + wheel = is.new s, "native-gem", v("1.0"), source, Gem::Platform.local.os.to_s + available = is.new s, "available-dep", v("1.0"), source, Gem::Platform::RUBY.to_s + + s.add traditional + s.add wheel + s.add available + + dep = make_dep "native-gem" + resolver = Gem::Resolver.new([dep], s) + + # Should fall back to traditional platform when wheel deps can't be satisfied + result = resolver.resolve.sort_by {|spec| spec.spec.name } + main_spec = result.find {|spec| spec.spec.name == "native-gem" }.spec + dep_spec = result.find {|spec| spec.spec.name == "available-dep" }.spec + + assert_equal Gem::Platform.local.os.to_s, main_spec.platform.to_s, + "Should fall back to traditional platform when wheel dependencies unsatisfiable" + assert_equal "1.0", dep_spec.version.to_s, + "Should resolve dependencies for traditional platform" + end + + def test_fallback_to_traditional_when_wheel_deps_unsatisfiable + # Test fallback when wheel deps cannot be satisfied + is = Gem::Resolver::IndexSpecification + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + current_platform = Gem::Platform.local.to_s.tr("-", "_") + + spec_fetcher do |fetcher| + # Traditional platform gem with satisfiable dependency + fetcher.spec "native-gem", "1.0" do |s| + s.platform = Gem::Platform.local + s.add_dependency "available-dep", "~> 1.0" + end + + # Wheel platform gem with unsatisfiable dependency + fetcher.spec "native-gem", "1.0" do |s| + s.platform = "whl-#{current_abi}-#{current_platform}" + s.add_dependency "missing-dep", "~> 1.0" + end + + # Only one dependency is available + fetcher.spec "available-dep", "1.0" + # missing-dep is NOT available + end + + source = Gem::Source.new @gem_repo + s = set + + traditional = is.new s, "native-gem", v("1.0"), source, Gem::Platform.local.to_s + wheel = is.new s, "native-gem", v("1.0"), source, "whl-#{current_abi}-#{current_platform}" + available = is.new s, "available-dep", v("1.0"), source, Gem::Platform::RUBY.to_s + + s.add traditional + s.add wheel + s.add available + + dep = make_dep "native-gem" + resolver = Gem::Resolver.new([dep], s) + + # Should fall back to traditional platform when wheel deps can't be satisfied + result = resolver.resolve.sort_by {|spec| spec.spec.name } + main_spec = result.find {|spec| spec.spec.name == "native-gem" }.spec + dep_spec = result.find {|spec| spec.spec.name == "available-dep" }.spec + + assert_equal Gem::Platform.local.to_s, main_spec.platform.to_s, + "Should fall back to traditional platform when wheel dependencies unsatisfiable" + assert_equal "1.0", dep_spec.version.to_s, + "Should resolve dependencies for traditional platform" + end + + def test_wheel_vs_traditional_dependency_conflicts + # Test resolution with conflicting dependency versions between wheel and traditional platforms + is = Gem::Resolver::IndexSpecification + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + current_platform = Gem::Platform.local.to_s.tr("-", "_") + + spec_fetcher do |fetcher| + # Traditional platform requires older version + fetcher.spec "native-gem", "1.0" do |s| + s.platform = Gem::Platform.local + s.add_dependency "conflict-dep", "~> 1.0" + end + + # Wheel platform requires newer version + fetcher.spec "native-gem", "1.0" do |s| + s.platform = "whl-#{current_abi}-#{current_platform}" + s.add_dependency "conflict-dep", "~> 2.0" + end + + # Another gem that requires the older version + fetcher.spec "legacy-gem", "1.0" do |s| + s.add_dependency "conflict-dep", "~> 1.0" + end + + # Conflicting dependency versions + fetcher.spec "conflict-dep", "1.5" + fetcher.spec "conflict-dep", "2.1" + end + + source = Gem::Source.new @gem_repo + s = set + + traditional = is.new s, "native-gem", v("1.0"), source, Gem::Platform.local.to_s + wheel = is.new s, "native-gem", v("1.0"), source, "whl-#{current_abi}-#{current_platform}" + legacy = is.new s, "legacy-gem", v("1.0"), source, Gem::Platform::RUBY.to_s + dep_old = is.new s, "conflict-dep", v("1.5"), source, Gem::Platform::RUBY.to_s + dep_new = is.new s, "conflict-dep", v("2.1"), source, Gem::Platform::RUBY.to_s + + s.add traditional + s.add wheel + s.add legacy + s.add dep_old + s.add dep_new + + # Request both gems - creates dependency conflict + deps = [make_dep("native-gem"), make_dep("legacy-gem")] + resolver = Gem::Resolver.new(deps, s) + + # Should resolve to traditional platform to satisfy both dependencies + result = resolver.resolve.sort_by {|spec| spec.spec.name } + native_spec = result.find {|spec| spec.spec.name == "native-gem" }.spec + legacy_spec = result.find {|spec| spec.spec.name == "legacy-gem" }.spec + dep_spec = result.find {|spec| spec.spec.name == "conflict-dep" }.spec + + assert_equal Gem::Platform.local.to_s, native_spec.platform.to_s, + "Should choose traditional platform to resolve dependency conflicts" + assert_equal "1.0", legacy_spec.version.to_s, + "Should include the legacy gem" + assert_equal "1.5", dep_spec.version.to_s, + "Should resolve to common dependency version" + end + + def test_wheel_platform_unique_dependencies + # Test wheel platforms with additional dependencies not in traditional variants + is = Gem::Resolver::IndexSpecification + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + current_platform = Gem::Platform.local.to_s.tr("-", "_") + + spec_fetcher do |fetcher| + # Traditional platform gem with minimal dependencies + fetcher.spec "native-gem", "1.0" do |s| + s.platform = Gem::Platform.local + s.add_dependency "basic-dep", "~> 1.0" + end + + # Wheel platform gem with additional platform-specific dependencies + fetcher.spec "native-gem", "1.0" do |s| + s.platform = "whl-#{current_abi}-#{current_platform}" + s.add_dependency "basic-dep", "~> 1.0" + s.add_dependency "wheel-specific-dep", "~> 1.0" + s.add_dependency "performance-dep", "~> 2.0" + end + + # All dependencies available + fetcher.spec "basic-dep", "1.0" + fetcher.spec "wheel-specific-dep", "1.2" + fetcher.spec "performance-dep", "2.1" + end + + source = Gem::Source.new @gem_repo + s = set + + traditional = is.new s, "native-gem", v("1.0"), source, Gem::Platform.local.to_s + wheel = is.new s, "native-gem", v("1.0"), source, "whl-#{current_abi}-#{current_platform}" + basic = is.new s, "basic-dep", v("1.0"), source, Gem::Platform::RUBY.to_s + wheel_specific = is.new s, "wheel-specific-dep", v("1.2"), source, Gem::Platform::RUBY.to_s + performance = is.new s, "performance-dep", v("2.1"), source, Gem::Platform::RUBY.to_s + + s.add traditional + s.add wheel + s.add basic + s.add wheel_specific + s.add performance + + dep = make_dep "native-gem" + resolver = Gem::Resolver.new([dep], s) + + # Should resolve to wheel platform and include all its unique dependencies + result = resolver.resolve.sort_by {|spec| spec.spec.name } + + resolved_names = result.map {|spec| spec.spec.name } + native_spec = result.find {|spec| spec.spec.name == "native-gem" }.spec + + assert_equal "whl-#{current_abi}-#{current_platform}", native_spec.platform.to_s, + "Should choose wheel platform with additional dependencies" + assert_includes resolved_names, "basic-dep", + "Should include basic dependency" + assert_includes resolved_names, "wheel-specific-dep", + "Should include wheel-specific dependency" + assert_includes resolved_names, "performance-dep", + "Should include performance dependency" + assert_equal 4, result.length, + "Should resolve all required dependencies" + end + + def test_wheel_platform_ignores_dev_dependencies + # Test that dev dependencies are ignored during platform resolution + is = Gem::Resolver::IndexSpecification + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + current_platform = Gem::Platform.local.to_s.tr("-", "_") + + spec_fetcher do |fetcher| + # Traditional platform gem + fetcher.spec "native-gem", "1.0" do |s| + s.platform = Gem::Platform.local + s.add_dependency "runtime-dep", "~> 1.0" + s.add_development_dependency "test-dep", "~> 1.0" + end + + # Wheel platform gem with different dev dependencies + fetcher.spec "native-gem", "1.0" do |s| + s.platform = "whl-#{current_abi}-#{current_platform}" + s.add_dependency "runtime-dep", "~> 1.0" + s.add_development_dependency "wheel-test-dep", "~> 2.0" + end + + # Runtime dependencies + fetcher.spec "runtime-dep", "1.0" + # Development dependencies (should not be resolved) + fetcher.spec "test-dep", "1.0" + fetcher.spec "wheel-test-dep", "2.0" + end + + source = Gem::Source.new @gem_repo + s = set + + traditional = is.new s, "native-gem", v("1.0"), source, Gem::Platform.local.to_s + wheel = is.new s, "native-gem", v("1.0"), source, "whl-#{current_abi}-#{current_platform}" + runtime = is.new s, "runtime-dep", v("1.0"), source, Gem::Platform::RUBY.to_s + test = is.new s, "test-dep", v("1.0"), source, Gem::Platform::RUBY.to_s + wheel_test = is.new s, "wheel-test-dep", v("2.0"), source, Gem::Platform::RUBY.to_s + + s.add traditional + s.add wheel + s.add runtime + s.add test + s.add wheel_test + + dep = make_dep "native-gem" + resolver = Gem::Resolver.new([dep], s) + + # Should resolve to wheel platform but only include runtime dependencies + result = resolver.resolve.sort_by {|spec| spec.spec.name } + + resolved_names = result.map {|spec| spec.spec.name } + native_spec = result.find {|spec| spec.spec.name == "native-gem" }.spec + + assert_equal "whl-#{current_abi}-#{current_platform}", native_spec.platform.to_s, + "Should choose wheel platform" + assert_includes resolved_names, "runtime-dep", + "Should include runtime dependency" + refute_includes resolved_names, "test-dep", + "Should not include traditional development dependency" + refute_includes resolved_names, "wheel-test-dep", + "Should not include wheel development dependency" + assert_equal 2, result.length, + "Should only resolve runtime dependencies" + end + + def test_resolver_handles_wheel_platform_objects + # Test that resolver can handle Gem::Platform::Wheel objects in specs + wheel_spec = util_spec "test_gem", "1.0.0" do |s| + s.platform = "whl-rb33-x86_64_linux" + end + + traditional_spec = util_spec "test_gem", "1.0.0" do |s| + s.platform = "x86_64-linux" + end + + ruby_spec = util_spec "test_gem", "1.0.0" do |s| + s.platform = "ruby" + end + + assert_equal "whl-rb33-x86_64_linux", wheel_spec.platform.to_s + assert_instance_of Gem::Platform::Wheel, wheel_spec.platform + + assert_equal "x86_64-linux", traditional_spec.platform.to_s + assert_instance_of Gem::Platform, traditional_spec.platform + + assert_equal Gem::Platform::RUBY, ruby_spec.platform + end end diff --git a/test/rubygems/test_gem_specification.rb b/test/rubygems/test_gem_specification.rb index af351f4d2e1f..3db3e6660f49 100644 --- a/test/rubygems/test_gem_specification.rb +++ b/test/rubygems/test_gem_specification.rb @@ -4107,4 +4107,113 @@ def util_setup_validate end end end + + def test_platform_assignment_with_wheel_strings + # Test that specifications can have wheel platforms assigned + spec = util_spec "test_gem", "1.0.0" + + # Test string assignment + spec.platform = "whl-rb33-x86_64_linux" + assert_instance_of Gem::Platform::Wheel, spec.platform + assert_equal "whl-rb33-x86_64_linux", spec.platform.to_s + + # Test object assignment + wheel_platform = Gem::Platform::Wheel.new("whl-rb32-arm64_darwin") + spec.platform = wheel_platform + assert_equal wheel_platform, spec.platform + assert_equal "whl-rb32-arm64_darwin", spec.platform.to_s + end + + def test_platform_comparison_with_mixed_types + # Test that wheel platforms can be compared with traditional platforms + wheel = Gem::Platform::Wheel.new("whl-rb33-x86_64_linux") + traditional = Gem::Platform.new("x86_64-linux") + ruby = Gem::Platform::RUBY + specific = Gem::Platform::Specific.local + + # Test === operator works across platform types + assert_respond_to wheel, :=== + assert_respond_to traditional, :=== + + # Test basic compatibility (specific tests in wheel test file) + refute_nil(wheel === ruby) + refute_nil(wheel === specific) + end + + def test_specification_with_wheel_platform_validation + Dir.mktmpdir do |tmpdir| + lib_dir = File.join(tmpdir, "lib") + Dir.mkdir(lib_dir) + File.write(File.join(lib_dir, "test.rb"), "# test file") + + gemspec_content = <<~GEMSPEC + Gem::Specification.new do |s| + s.name = "test_wheel_gem" + s.version = "1.0.0" + s.platform = "whl-rb33-x86_64_linux" + s.summary = "Test gem with wheel platform" + s.authors = ["Test Author"] + s.files = ["lib/test.rb"] + end + GEMSPEC + + gemspec_path = File.join(tmpdir, "test_wheel_gem.gemspec") + File.write(gemspec_path, gemspec_content) + + Dir.chdir(tmpdir) do + spec = Gem::Specification.load(gemspec_path) + assert_instance_of Gem::Platform::Wheel, spec.platform + assert_equal "whl-rb33-x86_64_linux", spec.platform.to_s + + spec.validate(:packaging) # Basic validation without file checks + end + end + end + + def test_wheel_platform_sets_required_rubygems_version + # Test that setting wheel platform automatically sets required_rubygems_version + spec = util_spec "test_gem", "1.0.0" + + # Initially should have default requirement + assert_equal Gem::Requirement.default, spec.required_rubygems_version + + # Setting wheel platform string should set required_rubygems_version + spec.platform = "whl-rb33-x86_64_linux" + assert_equal Gem::Requirement.new(">= 3.8.0.dev"), spec.required_rubygems_version + assert_instance_of Gem::Platform::Wheel, spec.platform + end + + def test_wheel_platform_object_sets_required_rubygems_version + # Test that setting wheel platform object automatically sets required_rubygems_version + spec = util_spec "test_gem", "1.0.0" + wheel_platform = Gem::Platform::Wheel.new("whl-rb33-arm64_darwin") + + # Setting wheel platform object should set required_rubygems_version + spec.platform = wheel_platform + assert_equal Gem::Requirement.new(">= 3.8.0.dev"), spec.required_rubygems_version + assert_equal wheel_platform, spec.platform + end + + def test_wheel_platform_respects_existing_required_rubygems_version + # Test that existing required_rubygems_version is not overridden + spec = util_spec "test_gem", "1.0.0" + spec.required_rubygems_version = ">= 4.0.0" + + # Setting wheel platform should not override existing requirement + spec.platform = "whl-rb33-x86_64_linux" + assert_equal Gem::Requirement.new(">= 4.0.0"), spec.required_rubygems_version + assert_instance_of Gem::Platform::Wheel, spec.platform + end + + def test_traditional_platform_does_not_set_required_rubygems_version + # Test that traditional platforms don't affect required_rubygems_version + spec = util_spec "test_gem", "1.0.0" + original_requirement = spec.required_rubygems_version + + # Setting traditional platform should not change required_rubygems_version + spec.platform = "x86_64-linux" + assert_equal original_requirement, spec.required_rubygems_version + assert_instance_of Gem::Platform, spec.platform + refute_instance_of Gem::Platform::Wheel, spec.platform + end end diff --git a/test/rubygems/test_require.rb b/test/rubygems/test_require.rb index f63c23c3159d..76aec3b7f05d 100644 --- a/test/rubygems/test_require.rb +++ b/test/rubygems/test_require.rb @@ -786,6 +786,151 @@ def test_require_does_not_crash_when_utilizing_bundler_version_finder assert_predicate $?, :success?, "Require failed due to #{out}" end + def test_require_best_wheel_platform_match + # Test that requiring chooses the best wheel platform match over traditional platforms + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + current_platform = Gem::Platform.local.to_s.tr("-", "_") + + # Create traditional platform gem + traditional_spec = util_spec "native-lib", "1.0", nil, "lib/native-lib.rb" + traditional_spec.platform = Gem::Platform.local + write_file File.join(traditional_spec.gem_dir, "lib", "native-lib.rb") do |io| + io.write "NATIVE_LIB_PLATFORM = 'traditional'" + end + install_specs traditional_spec + + # Create wheel platform gem for current environment + wheel_spec = util_spec "native-lib", "1.0", nil, "lib/native-lib.rb" + wheel_spec.platform = "whl-#{current_abi}-#{current_platform}" + write_file File.join(wheel_spec.gem_dir, "lib", "native-lib.rb") do |io| + io.write "NATIVE_LIB_PLATFORM = 'wheel_exact'" + end + install_specs wheel_spec + + # Require should choose the wheel platform gem + assert_require "native-lib" + assert_equal "wheel_exact", Object.const_get(:NATIVE_LIB_PLATFORM), + "Should require wheel platform gem over traditional platform" + + # Should load the wheel specification + loaded_spec = Gem.loaded_specs["native-lib"] + assert_equal "whl-#{current_abi}-#{current_platform}", loaded_spec.platform.to_s, + "Should load wheel platform specification" + ensure + Object.send :remove_const, :NATIVE_LIB_PLATFORM if Object.const_defined? :NATIVE_LIB_PLATFORM + end + + def test_require_traditional_platform_fallback_when_wheel_incompatible + # Test that requiring falls back to traditional platform when wheel doesn't match + current_platform = Gem::Platform.local.to_s.tr("-", "_") + + # Create traditional platform gem + traditional_spec = util_spec "native-lib", "1.0", nil, "lib/native-lib.rb" do |s| + s.platform = Gem::Platform.local + end + + # Create incompatible wheel platform gem + wheel_spec = util_spec "native-lib", "1.0", nil, "lib/native-lib.rb" do |s| + s.platform = "whl-jr31_310-#{current_platform}" # Incompatible JRuby version + end + + # Install both specs + install_specs traditional_spec, wheel_spec + + # Write files after installation + write_file File.join(traditional_spec.gem_dir, "lib", "native-lib.rb") do |io| + io.write "NATIVE_LIB_PLATFORM = 'traditional'" + end + write_file File.join(wheel_spec.gem_dir, "lib", "native-lib.rb") do |io| + io.write "NATIVE_LIB_PLATFORM = 'wheel_incompatible'" + end + + # Require should fall back to traditional platform gem + assert_require "native-lib" + assert_equal "traditional", Object.const_get(:NATIVE_LIB_PLATFORM), + "Should fall back to traditional platform when wheel incompatible" + + # Should load the traditional specification + loaded_spec = Gem.loaded_specs["native-lib"] + assert_equal Gem::Platform.local.to_s, loaded_spec.platform.to_s, + "Should load traditional platform specification" + ensure + Object.send :remove_const, :NATIVE_LIB_PLATFORM if Object.const_defined? :NATIVE_LIB_PLATFORM + end + + def test_require_universal_wheel_fallback + # Test that requiring chooses universal wheel when no platform-specific match + current_platform = Gem::Platform.local.to_s.tr("-", "_") + + # Create incompatible wheel platform gem + wheel_incompatible_spec = util_spec "native-lib", "1.0", nil, "lib/native-lib.rb" do |s| + s.platform = "whl-cr31_310-#{current_platform}" + end + + # Create universal wheel platform gem + wheel_universal_spec = util_spec "native-lib", "1.0", nil, "lib/native-lib.rb" do |s| + s.platform = "whl-any-any" + end + + install_specs wheel_incompatible_spec, wheel_universal_spec + + # Write files after installation + write_file File.join(wheel_incompatible_spec.gem_dir, "lib", "native-lib.rb") do |io| + io.write "NATIVE_LIB_PLATFORM = 'wheel_incompatible'" + end + write_file File.join(wheel_universal_spec.gem_dir, "lib", "native-lib.rb") do |io| + io.write "NATIVE_LIB_PLATFORM = 'wheel_universal'" + end + + # Require should choose the universal wheel + assert_require "native-lib" + assert_equal "wheel_universal", Object.const_get(:NATIVE_LIB_PLATFORM), + "Should choose universal wheel when no platform-specific match" + + # Should load the universal wheel specification + loaded_spec = Gem.loaded_specs["native-lib"] + assert_equal "whl-any-any", loaded_spec.platform.to_s, + "Should load universal wheel platform specification" + ensure + Object.send :remove_const, :NATIVE_LIB_PLATFORM if Object.const_defined? :NATIVE_LIB_PLATFORM + end + + def test_require_chooses_newer_compatible_wheel_over_older_exact_traditional + # Test version vs platform specificity prioritization in require + current_abi = Gem::Platform::Specific.current_ruby_abi_tag + current_platform = Gem::Platform.local.to_s.tr("-", "_") + + # Create older traditional platform gem + traditional_spec = util_spec "native-lib", "1.0", nil, "lib/native-lib.rb" + traditional_spec.platform = Gem::Platform.local + write_file File.join(traditional_spec.gem_dir, "lib", "native-lib.rb") do |io| + io.write "NATIVE_LIB_VERSION = '1.0-traditional'" + end + install_specs traditional_spec + + # Create newer wheel platform gem for current environment + wheel_spec = util_spec "native-lib", "2.0", nil, "lib/native-lib.rb" + wheel_spec.platform = "whl-#{current_abi}-#{current_platform}" + write_file File.join(wheel_spec.gem_dir, "lib", "native-lib.rb") do |io| + io.write "NATIVE_LIB_VERSION = '2.0-wheel'" + end + install_specs wheel_spec + + # Require should choose the newer wheel platform gem + assert_require "native-lib" + assert_equal "2.0-wheel", Object.const_get(:NATIVE_LIB_VERSION), + "Should choose newer wheel platform gem over older traditional" + + # Should load the wheel specification + loaded_spec = Gem.loaded_specs["native-lib"] + assert_equal "2.0", loaded_spec.version.to_s, + "Should load newer version" + assert_equal "whl-#{current_abi}-#{current_platform}", loaded_spec.platform.to_s, + "Should load wheel platform specification" + ensure + Object.send :remove_const, :NATIVE_LIB_VERSION if Object.const_defined? :NATIVE_LIB_VERSION + end + private def util_install_extension_file(name)