Repository: ruby/rubygems (Ruby)

Issue:
Platform comparison mixing libc families
Hi! After #2918 and #2922, I've extended my own gem repository to contain glibc-linked gems also, and found out that in a machine with glibc, where the platform is `x86_64-linux`, bundler installs `x86_64-linux-musl` gems as if they were compatible.  I checked again and found out this on `rubygems/platform.rb`:

```ruby
  ##
  # Does +other+ match this platform?  Two platforms match if they have the
  # same CPU, or either has a CPU of 'universal', they have the same OS, and
  # they have the same version, or either has no version.
  #
  # Additionally, the platform will match if the local CPU is 'arm' and the
  # other CPU starts with "arm" (for generic ARM family support).

  def ===(other)
```

The "or either has no version" bit was the one that ends up comparing `x86_64-linux` to `x86_64-linux-musl` and finding them equal.  I patched the version check to `@version == other.version` and bundler installed the gem from the correct platform, but I'm wondering if it's ok to submit this patch as a PR since the documentation was so specific about it.  Could you clarify?  Thanks!


This issue is related to:

  - [ ] Network problems
  - [x] Installing a library
  - [ ] Publishing a library
  - [ ] The command line `gem`
  - [ ] Other

Here are my current environment details:

```
$ gem env version
3.0.3
```

I will abide by the [code of conduct](https://github.com/rubygems/rubygems/blob/master/CODE_OF_CONDUCT.md).



Discussion:
I report the same issue.

I produced [libv8-node](https://rubygems.org/gems/libv8-node) builds for the following platforms, published s such:

- `x86_64-darwin-19`
- `x86_64-darwin-20`
- `x86_64-linux` (IOW glibc-based)
- `x86_64-linux-musl`
- `x86_64-solaris-2.11`

On the gem production side, just to be able to properly produce the correct `x86_64-linux-musl`, I had to perform the following hack for ruby versions < 2.7 (I believe this is due to a rubygems change, which turns out to be bundled starting with ruby 2.7):

```
  def binary_gemspec(platform = Gem::Platform.local, str = RUBY_PLATFORM)
    platform.instance_eval { @version = 'musl' } if str =~ /-musl/ && platform.version.nil?

    gemspec = eval(File.read('libv8-node.gemspec'))
    gemspec.platform = platform
    gemspec
  end

  def binary_gem_name(platform = Gem::Platform.local)
    File.basename(binary_gemspec(platform).cache_file)
  end
```

On the gem consumption side, `gem install libv8-node` will install either the `-linux` one or the `-linux-musl` one, depending on whatever the order of search results happens to be.

This is a real problem for e.g `mini_racer` to depend on `libv8` and have the distinction made between musl and glibc, and part of why we had to fork it into `sq_mini_racer` which does not explicitly depends on `libv8`/`libv8-node` but has its own search and resolution implementation against the rubygems.org API.
> On the gem production side, just to be able to properly produce the correct `x86_64-linux-musl`, I had to perform the following hack for ruby versions < 2.7 (I believe this is due to a rubygems change, which turns out to be bundled starting with ruby 2.7):

It's due to a previous patch I submitted that was accepted and bundled with ruby 2.7, that made this one come up
Yup, the hack is only necessary for versions of rubygems before your fix, so thanks for that!
Regarding the "or either has no version" thing, I think this is e.g for darwin or solaris, so that maybe you can have a gem published as `x86_64-darwin` or `x86_64-solaris` and it can be installed regardless of the version, e.g on `x86_64-darwin-{17,18,19,20}`.

As for `linux`, while it's possible to build binaries that do run on both `glibc` and `musl`, it's fairly involved and requires extreme care (e.g at the very least strict conformance to POSIX and disabling any GNU extension when building it with GCC among other things) which which makes it impractical to assume that `linux, nil` should match as a wildcard in that case. Also there may be some other libc such as `uclibc`.

So, pragmatically today, it seems that `linux` is strictly equal to `linux-glibc` at large, a fact largely confirmed by Alpine's intentional behaviour of [dropping binary gems](https://gitlab.alpinelinux.org/alpine/aports/-/issues/10808) mentioned by @fauno.

Thus I'm currently attempting a patch, and ultimately a PR, implementing the following:

- preserve the version `nil` wildcard for non-`linux` platforms, just in case
- ensure `linux` === `linux-glibc` and no other
- ensure `linux-foo` === `linux-foo` and no other
@fauno I opened the PR above, in hopes it would solve this issue, WDYT?
_I messed up something with the patch so I'm editing this comment out :)_
It works! +1
@deivid-rodriguez now that #4082 is reverted, can you reopen this issue for better tracking of [what we agreed upon](https://github.com/rubygems/rubygems/issues/4425#issuecomment-790420989)? (I'm going to start tackling this this week)
Hi! Please let me know if I can help with something :)
Sorry folks, it's been a bit of a hellhole for me, so just a ping that I have a lot of things to clear up but I'm not forgetting about this.
Just got notified Alpine 3.15 is going to reenable the musl platform, but not sure which bundler version is going to include: https://gitlab.alpinelinux.org/alpine/aports/-/issues/10808
It seems like the Ruby 3.x Docker images (which have a recent rubygems + bundler) behave correctly (or at least more correctly than before).

Got some reasonable backport for people stuck with the official Docker images that have rubygems 3.0 + bundler 1.17 and can't update them due to e.g rails 4.x-5.1 `bundler < 2.0` constraint.

https://gist.github.com/lloeki/12dcf61324f64a2fa1e8a8b2109c1f00
> at least more correctly than before

Well, there are apparently still some situations where recent rubygems+bundler trip up and decide to fetch `linux-musl` on `linux`.

I was starting to wonder if this issue was still relevant, but I guess it definitely is.
Yes, this still happens: https://github.com/rubygems/rubygems/issues/5508
Incorrectly using `linux-muslc` on glibc systems (the current situation) seems worse than not using `-linux` gems on Alpine which uses musl (#4434).

Maybe we can do a quick fix that just always prevents `-linux-musl` to be used on a `-linux` system? That's always wrong.
I'm experiencing this on the latest ruby alpine image. Are there any workarounds to get this working properly? I've followed all the linked cases and it appears this is still an issue. 
This will be fixed by #5852, sorry it took so long!



Patch:
diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb
index fd1c0a62ac7a..e01d0494d6b2 100644
--- a/lib/rubygems/platform.rb
+++ b/lib/rubygems/platform.rb
@@ -66,7 +66,7 @@ def initialize(arch)
     when String then
       arch = arch.split '-'
 
-      if arch.length > 2 and arch.last !~ /\d/ # reassemble x86-linux-gnu
+      if arch.length > 2 and arch.last !~ /\d+(\.\d+)?$/ # reassemble x86-linux-{libc}
         extra = arch.pop
         arch.last << "-#{extra}"
       end
@@ -146,7 +146,8 @@ def hash # :nodoc:
   ##
   # Does +other+ match this platform?  Two platforms match if they have the
   # same CPU, or either has a CPU of 'universal', they have the same OS, and
-  # they have the same version, or either has no version.
+  # they have the same version, or either has no version (except for 'linux'
+  # where the version is the libc name, with no version standing for 'gnu')
   #
   # Additionally, the platform will match if the local CPU is 'arm' and the
   # other CPU starts with "arm" (for generic ARM family support).
@@ -162,7 +163,10 @@ def ===(other)
     @os == other.os and
 
     # version
-    (@version.nil? or other.version.nil? or @version == other.version)
+    (
+      (@os != 'linux' and (@version.nil? or other.version.nil?)) or
+      @version == other.version
+    )
   end
 
   ##


Test patch:
diff --git a/test/rubygems/test_gem_platform.rb b/test/rubygems/test_gem_platform.rb
index 84754402ad8d..ad7285c08270 100644
--- a/test/rubygems/test_gem_platform.rb
+++ b/test/rubygems/test_gem_platform.rb
@@ -134,7 +134,9 @@ def test_initialize
       'i386-solaris2.8'        => ['x86',       'solaris',   '2.8'],
       'mswin32'                => ['x86',       'mswin32',   nil],
       'x86_64-linux'           => ['x86_64',    'linux',     nil],
+      'x86_64-linux-gnu'       => ['x86_64',    'linux',     nil],
       'x86_64-linux-musl'      => ['x86_64',    'linux',     'musl'],
+      'x86_64-linux-uclibc'    => ['x86_64',    'linux',     'uclibc'],
       'x86_64-openbsd3.9'      => ['x86_64',    'openbsd',   '3.9'],
       'x86_64-openbsd4.0'      => ['x86_64',    'openbsd',   '4.0'],
       'x86_64-openbsd'         => ['x86_64',    'openbsd',   nil],
@@ -143,6 +145,7 @@ 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, Gem::Platform.new(platform.to_s).to_a, arch.inspect
     end
   end
 
@@ -261,6 +264,32 @@ def test_nil_cpu_arch_is_treated_as_universal
     assert((with_x86_arch === with_nil_arch), 'x86 =~ nil')
   end
 
+  def test_nil_version_is_treated_as_any_version
+    x86_darwin_8 = Gem::Platform.new 'i686-darwin8.0'
+    x86_darwin_nil = Gem::Platform.new 'i686-darwin'
+
+    assert((x86_darwin_8 === x86_darwin_nil), '8.0 =~ nil')
+    assert((x86_darwin_nil === x86_darwin_8), 'nil =~ 8.0')
+  end
+
+  def test_nil_version_is_stricter_for_linux_os
+    x86_linux = Gem::Platform.new 'i686-linux'
+    x86_linux_gnu = Gem::Platform.new 'i686-linux-gnu'
+    x86_linux_musl = Gem::Platform.new 'i686-linux-musl'
+    x86_linux_uclibc = Gem::Platform.new 'i686-linux-uclibc'
+
+    assert((x86_linux === x86_linux_gnu), 'linux =~ linux-gnu')
+    assert((x86_linux_gnu === x86_linux), 'linux-gnu =~ linux')
+    assert(!(x86_linux_gnu === x86_linux_musl), 'linux-gnu =~ linux-musl')
+    assert(!(x86_linux_musl === x86_linux_gnu), 'linux-musl =~ linux-gnu')
+    assert(!(x86_linux_uclibc === x86_linux_musl), 'linux-uclibc =~ linux-musl')
+    assert(!(x86_linux_musl === x86_linux_uclibc), 'linux-musl =~ linux-uclibc')
+    assert(!(x86_linux === x86_linux_musl), 'linux =~ linux-musl')
+    assert(!(x86_linux_musl === x86_linux), 'linux-musl =~ linux')
+    assert(!(x86_linux === x86_linux_uclibc), 'linux =~ linux-uclibc')
+    assert(!(x86_linux_uclibc === x86_linux), 'linux-uclibc =~ linux')
+  end
+
   def test_equals3_cpu_arm
     arm   = Gem::Platform.new 'arm-linux'
     armv5 = Gem::Platform.new 'armv5-linux'