From 511075496bb8e462ffec8cdbb4b207d50d7824b1 Mon Sep 17 00:00:00 2001 From: Samuel Giddins Date: Tue, 17 Jun 2025 08:59:15 -0700 Subject: [PATCH 1/3] Add comprehensive wheel platform support to RubyGems Implements wheel-type platforms (whl-{ruby}-{abi}-{platform}) for binary gem support. Key changes: - Add Gem::Platform::Wheel class for wheel platform parsing and matching - Add Gem::Platform::Specific class for structured environment representation - Implement comprehensive wheel tag generation with platform compatibility - Add Linux-specific platform detection (manylinux/musllinux) with ELF parsing - Update platform matching logic to handle wheel vs traditional platform resolution - Maintain full backwards compatibility with existing platform functionality All tests pass with enhanced platform resolution supporting both traditional and wheel platform formats for future binary gem distribution. Add Bundler integration tests for wheel platform support Add comprehensive tests to validate wheel platform gem building and resolution in Bundler: - Test wheel platform gem creation with correct platform naming - Test fallback to ruby gems when wheel platforms don't match - Test multi-tag wheel platform handling (currently skipped) - Test platform resolution priority (currently skipped) - Test lockfile wheel platform recording (currently skipped) Most tests are currently skipped as they require Bundler resolver updates to handle Gem::Platform::Wheel objects. The working test demonstrates that wheel platform gems can be built successfully with full platform names (e.g. wheel_native-1.0.0-whl-rb33-x86_64_linux.gem). --- Manifest.txt | 5 + lib/rubygems/basic_specification.rb | 2 +- lib/rubygems/commands/pristine_command.rb | 2 +- lib/rubygems/platform.rb | 209 +++-- lib/rubygems/platform/elffile.rb | 163 ++++ lib/rubygems/platform/manylinux.rb | 119 +++ lib/rubygems/platform/musllinux.rb | 139 +++ lib/rubygems/platform/specific.rb | 567 ++++++++++++ lib/rubygems/platform/wheel.rb | 375 ++++++++ lib/rubygems/resolver.rb | 8 +- lib/rubygems/specification.rb | 21 +- lib/rubygems/specification_policy.rb | 2 +- lib/rubygems/specification_record.rb | 2 +- test/rubygems/test_gem.rb | 9 +- .../test_gem_commands_build_command.rb | 70 ++ test/rubygems/test_gem_platform.rb | 132 ++- test/rubygems/test_gem_platform_elffile.rb | 162 ++++ test/rubygems/test_gem_platform_manylinux.rb | 86 ++ test/rubygems/test_gem_platform_musllinux.rb | 71 ++ test/rubygems/test_gem_platform_specific.rb | 732 +++++++++++++++ test/rubygems/test_gem_platform_wheel.rb | 834 ++++++++++++++++++ test/rubygems/test_gem_resolver.rb | 551 ++++++++++++ test/rubygems/test_gem_specification.rb | 109 +++ test/rubygems/test_require.rb | 145 +++ 24 files changed, 4397 insertions(+), 118 deletions(-) create mode 100644 lib/rubygems/platform/elffile.rb create mode 100644 lib/rubygems/platform/manylinux.rb create mode 100644 lib/rubygems/platform/musllinux.rb create mode 100644 lib/rubygems/platform/specific.rb create mode 100644 lib/rubygems/platform/wheel.rb create mode 100644 test/rubygems/test_gem_platform_elffile.rb create mode 100644 test/rubygems/test_gem_platform_manylinux.rb create mode 100644 test/rubygems/test_gem_platform_musllinux.rb create mode 100644 test/rubygems/test_gem_platform_specific.rb create mode 100644 test/rubygems/test_gem_platform_wheel.rb 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/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..0153852d6691 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 4.0+ for proper support + self.required_rubygems_version = ">= 4.0.0" 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 4.0+ for proper support + self.required_rubygems_version = ">= 4.0.0" 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..04d187db6f36 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(">= 4.0.0"), 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(">= 4.0.0"), 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) From 4f699bf51567760d959f3324eee1c4010ae66be6 Mon Sep 17 00:00:00 2001 From: Samuel Giddins Date: Wed, 20 Aug 2025 12:54:18 -0500 Subject: [PATCH 2/3] Add Bundler integration tests for wheel platform support Add comprehensive tests to validate wheel platform gem building and resolution in Bundler: - Test wheel platform gem creation with correct platform naming - Test fallback to ruby gems when wheel platforms don't match - Test multi-tag wheel platform handling (currently skipped) - Test platform resolution priority (currently skipped) - Test lockfile wheel platform recording (currently skipped) Most tests are currently skipped as they require Bundler resolver updates to handle Gem::Platform::Wheel objects. The working test demonstrates that wheel platform gems can be built successfully with full platform names (e.g. wheel_native-1.0.0-whl-rb33-x86_64_linux.gem). --- .../install/gemfile/wheel_platform_spec.rb | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 bundler/spec/install/gemfile/wheel_platform_spec.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 From 0b3a17256bbbe7e3c17ae04525a98d39edc39837 Mon Sep 17 00:00:00 2001 From: Samuel Giddins Date: Wed, 20 Aug 2025 17:03:55 -0500 Subject: [PATCH 3/3] Fix required_rubygems_version for wheel platforms Changes wheel platform gems to require RubyGems 3.8.0.dev instead of 4.0.0, matching the current development version. This allows wheel platform gems to be properly resolved by the dependency resolver. Fixes all wheel platform resolver test failures. --- lib/rubygems/specification.rb | 8 ++++---- test/rubygems/test_gem_specification.rb | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index 0153852d6691..b5d44c9193b1 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -476,8 +476,8 @@ def platform=(platform) when Gem::Platform::Wheel then @new_platform = platform - # Wheel platforms require RubyGems 4.0+ for proper support - self.required_rubygems_version = ">= 4.0.0" if required_rubygems_version == Gem::Requirement.default + # 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 @@ -489,8 +489,8 @@ def platform=(platform) else @new_platform = Gem::Platform.new platform if @new_platform.is_a?(Gem::Platform::Wheel) - # Wheel platforms require RubyGems 4.0+ for proper support - self.required_rubygems_version = ">= 4.0.0" if required_rubygems_version == Gem::Requirement.default + # 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 diff --git a/test/rubygems/test_gem_specification.rb b/test/rubygems/test_gem_specification.rb index 04d187db6f36..3db3e6660f49 100644 --- a/test/rubygems/test_gem_specification.rb +++ b/test/rubygems/test_gem_specification.rb @@ -4179,7 +4179,7 @@ def test_wheel_platform_sets_required_rubygems_version # Setting wheel platform string should set required_rubygems_version spec.platform = "whl-rb33-x86_64_linux" - assert_equal Gem::Requirement.new(">= 4.0.0"), spec.required_rubygems_version + assert_equal Gem::Requirement.new(">= 3.8.0.dev"), spec.required_rubygems_version assert_instance_of Gem::Platform::Wheel, spec.platform end @@ -4190,7 +4190,7 @@ def test_wheel_platform_object_sets_required_rubygems_version # Setting wheel platform object should set required_rubygems_version spec.platform = wheel_platform - assert_equal Gem::Requirement.new(">= 4.0.0"), spec.required_rubygems_version + assert_equal Gem::Requirement.new(">= 3.8.0.dev"), spec.required_rubygems_version assert_equal wheel_platform, spec.platform end