Repository: python/cpython
Pull request #131313: gh-90548: Make musl test skips smarter (fixes Alpine errors)
Author: login: bitdancer
Labels: name: skip news
Created: 2025-03-15T22:31:22Z | Merged: 2025-03-19T17:05:09Z

Make musl test skips smarter (fixes Alpine errors)

A relatively small number of tests fail when the underlying c library is
provided by musl.  This was originally reported in bpo-46390 by
Christian Heimes.  Among other changes, these tests were marked for
skipping in gh-31947/ef1327e3 as part of bpo-40280 (emscripten support),
but the skips were conditioned on the *platform* being emscripten (or
wasi, skips for which ere added in 9b50585e02).

In gh-131071 Victor Stinner added a linked_to_musl function to enable
skipping a test in test_math that fails under musl, like it does on a
number of other platforms.  This check can successfully detect that
python is running under musl on Alpine, which was the original problem
report in bpo-46390.

This PR replaces Victor's solution with an enhancement to
platform.libc_ver that does the check more cheaply, and also gets the
version number.  The latter is important because the math test being
skipped is due to a bug in musl that has been fixed, but as of this
checkin date has not yet been released.  When it is, the test skip can
be fixed to check for the minimum needed version.

The enhanced version of linked_to_musl is also used to do the skips of
the other tests that generically fail under musl, as opposed to
emscripten or wasi only failures.  This will allow these tests to be
skipped automatically on Alpine.

This PR does *not* enhance libc_ver to support emscripten and wasi, as
I'm not familiar with those platforms; instead it returns a version
triple of (0, 0, 0) for those platforms.  This means the musl tests will
be skipped regardless of musl version, so ideally someone will add
support to libc_ver for these platforms.


<!-- readthedocs-preview cpython-previews start -->
----
📚 Documentation preview 📚: https://cpython-previews--131313.org.readthedocs.build/

<!-- readthedocs-preview cpython-previews end -->

<!-- gh-issue-number: gh-90548 -->
* Issue: gh-90548
<!-- /gh-issue-number -->



Diff:
diff --git a/Doc/library/test.rst b/Doc/library/test.rst
index def22f8bb8ab2d..14b843077ed211 100644
--- a/Doc/library/test.rst
+++ b/Doc/library/test.rst
@@ -246,7 +246,27 @@ The :mod:`test.support` module defines the following constants:
 
 .. data:: is_android
 
-   ``True`` if the system is Android.
+   ``True`` if ``sys.platform`` is ``android``.
+
+
+.. data:: is_emscripten
+
+   ``True`` if ``sys.platform`` is ``emscripten``.
+
+
+.. data:: is_wasi
+
+   ``True`` if ``sys.platform`` is ``wasi``.
+
+
+.. data:: is_apple_mobile
+
+   ``True`` if ``sys.platform`` is ``ios``, ``tvos``, or ``watchos``.
+
+
+.. data:: is_apple
+
+   ``True`` if ``sys.platform`` is ``darwin`` or ``is_apple_mobile`` is ``True``.
 
 
 .. data:: unix_shell
@@ -831,6 +851,15 @@ The :mod:`test.support` module defines the following functions:
    Decorator for tests that fill the address space.
 
 
+.. function:: linked_with_musl()
+
+   Return ``False`` if there is no evidence the interperter was compiled with
+   ``musl``, otherwise return a version triple, either ``(0, 0, 0)`` if the
+   version is unknown, or the actual version if it is known.  Intended for use
+   in ``skip`` decorators.  ``emscripten`` and ``wasi`` are assumed to be
+   compiled with ``musl``; otherwise ``platform.libc_ver`` is checked.
+
+
 .. function:: check_syntax_error(testcase, statement, errtext='', *, lineno=None, offset=None)
 
    Test for syntax errors in *statement* by attempting to compile *statement*.
diff --git a/Lib/platform.py b/Lib/platform.py
index 1f6baed66d3df9..a62192589af8ff 100644
--- a/Lib/platform.py
+++ b/Lib/platform.py
@@ -189,22 +189,25 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
             # sys.executable is not set.
             return lib, version
 
-    libc_search = re.compile(b'(__libc_init)'
-                          b'|'
-                          b'(GLIBC_([0-9.]+))'
-                          b'|'
-                          br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)', re.ASCII)
+    libc_search = re.compile(br"""
+          (__libc_init)
+        | (GLIBC_([0-9.]+))
+        | (libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)
+        | (musl-([0-9.]+))
+        """,
+        re.ASCII | re.VERBOSE)
 
     V = _comparable_version
     # We use os.path.realpath()
     # here to work around problems with Cygwin not being
     # able to open symlinks for reading
     executable = os.path.realpath(executable)
+    ver = None
     with open(executable, 'rb') as f:
         binary = f.read(chunksize)
         pos = 0
         while pos < len(binary):
-            if b'libc' in binary or b'GLIBC' in binary:
+            if b'libc' in binary or b'GLIBC' in binary or b'musl' in binary:
                 m = libc_search.search(binary, pos)
             else:
                 m = None
@@ -216,7 +219,7 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
                     continue
                 if not m:
                     break
-            libcinit, glibc, glibcversion, so, threads, soversion = [
+            libcinit, glibc, glibcversion, so, threads, soversion, musl, muslversion = [
                 s.decode('latin1') if s is not None else s
                 for s in m.groups()]
             if libcinit and not lib:
@@ -224,18 +227,22 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
             elif glibc:
                 if lib != 'glibc':
                     lib = 'glibc'
-                    version = glibcversion
-                elif V(glibcversion) > V(version):
-                    version = glibcversion
+                    ver = glibcversion
+                elif V(glibcversion) > V(ver):
+                    ver = glibcversion
             elif so:
                 if lib != 'glibc':
                     lib = 'libc'
-                    if soversion and (not version or V(soversion) > V(version)):
-                        version = soversion
-                    if threads and version[-len(threads):] != threads:
-                        version = version + threads
+                    if soversion and (not ver or V(soversion) > V(ver)):
+                        ver = soversion
+                    if threads and ver[-len(threads):] != threads:
+                        ver = ver + threads
+            elif musl:
+                lib = 'musl'
+                if not ver or V(muslversion) > V(ver):
+                    ver = muslversion
             pos = m.end()
-    return lib, version
+    return lib, version if ver is None else ver
 
 def _norm_version(version, build=''):
 
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index b9ccf7bb4c67de..94a0e267b35828 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -3017,20 +3017,35 @@ def is_libssl_fips_mode():
     return get_fips_mode() != 0
 
 
+_linked_to_musl = None
 def linked_to_musl():
     """
-    Test if the Python executable is linked to the musl C library.
+    Report if the Python executable is linked to the musl C library.
+
+    Return False if we don't think it is, or a version triple otherwise.
     """
+    # This is can be a relatively expensive check, so we use a cache.
+    global _linked_to_musl
+    if _linked_to_musl is not None:
+        return _linked_to_musl
+
+    # emscripten (at least as far as we're concerned) and wasi use musl,
+    # but platform doesn't know how to get the version, so set it to zero.
+    if is_emscripten or is_wasi:
+        _linked_to_musl = (0, 0, 0)
+        return _linked_to_musl
+
+    # On all other non-linux platforms assume no musl.
     if sys.platform != 'linux':
-        return False
+        _linked_to_musl = False
+        return _linked_to_musl
 
-    import subprocess
-    exe = getattr(sys, '_base_executable', sys.executable)
-    cmd = ['ldd', exe]
-    try:
-        stdout = subprocess.check_output(cmd,
-                                         text=True,
-                                         stderr=subprocess.STDOUT)
-    except (OSError, subprocess.CalledProcessError):
-        return False
-    return ('musl' in stdout)
+    # On linux, we'll depend on the platform module to do the check, so new
+    # musl platforms should add support in that module if possible.
+    import platform
+    lib, version = platform.libc_ver()
+    if lib != 'musl':
+        _linked_to_musl = False
+        return _linked_to_musl
+    _linked_to_musl = tuple(map(int, version.split('.')))
+    return _linked_to_musl
diff --git a/Lib/test/test__locale.py b/Lib/test/test__locale.py
index cef84fd9580c37..11b2c9545a1b43 100644
--- a/Lib/test/test__locale.py
+++ b/Lib/test/test__locale.py
@@ -137,10 +137,7 @@ def numeric_tester(self, calc_type, calc_value, data_type, used_locale):
             return True
 
     @unittest.skipUnless(nl_langinfo, "nl_langinfo is not available")
-    @unittest.skipIf(
-        support.is_emscripten or support.is_wasi,
-        "musl libc issue on Emscripten, bpo-46390"
-    )
+    @unittest.skipIf(support.linked_to_musl(), "musl libc issue, bpo-46390")
     def test_lc_numeric_nl_langinfo(self):
         # Test nl_langinfo against known values
         tested = False
@@ -158,10 +155,7 @@ def test_lc_numeric_nl_langinfo(self):
         if not tested:
             self.skipTest('no suitable locales')
 
-    @unittest.skipIf(
-        support.is_emscripten or support.is_wasi,
-        "musl libc issue on Emscripten, bpo-46390"
-    )
+    @unittest.skipIf(support.linked_to_musl(), "musl libc issue, bpo-46390")
     def test_lc_numeric_localeconv(self):
         # Test localeconv against known values
         tested = False
@@ -210,10 +204,7 @@ def test_lc_numeric_basic(self):
 
     @unittest.skipUnless(nl_langinfo, "nl_langinfo is not available")
     @unittest.skipUnless(hasattr(locale, 'ALT_DIGITS'), "requires locale.ALT_DIGITS")
-    @unittest.skipIf(
-        support.is_emscripten or support.is_wasi,
-        "musl libc issue on Emscripten, bpo-46390"
-    )
+    @unittest.skipIf(support.linked_to_musl(), "musl libc issue, bpo-46390")
     def test_alt_digits_nl_langinfo(self):
         # Test nl_langinfo(ALT_DIGITS)
         tested = False
@@ -245,10 +236,7 @@ def test_alt_digits_nl_langinfo(self):
 
     @unittest.skipUnless(nl_langinfo, "nl_langinfo is not available")
     @unittest.skipUnless(hasattr(locale, 'ERA'), "requires locale.ERA")
-    @unittest.skipIf(
-        support.is_emscripten or support.is_wasi,
-        "musl libc issue on Emscripten, bpo-46390"
-    )
+    @unittest.skipIf(support.linked_to_musl(), "musl libc issue, bpo-46390")
     def test_era_nl_langinfo(self):
         # Test nl_langinfo(ERA)
         tested = False
diff --git a/Lib/test/test_locale.py b/Lib/test/test_locale.py
index dc1ba258e0b81b..73c02849cca3c0 100644
--- a/Lib/test/test_locale.py
+++ b/Lib/test/test_locale.py
@@ -1,5 +1,5 @@
 from decimal import Decimal
-from test.support import verbose, is_android, is_emscripten, is_wasi, os_helper
+from test.support import verbose, is_android, linked_to_musl, os_helper
 from test.support.warnings_helper import check_warnings
 from test.support.import_helper import import_fresh_module
 from unittest import mock
@@ -351,10 +351,7 @@ def setUp(self):
 
     @unittest.skipIf(sys.platform.startswith('aix'),
                      'bpo-29972: broken test on AIX')
-    @unittest.skipIf(
-        is_emscripten or is_wasi,
-        "musl libc issue on Emscripten/WASI, bpo-46390"
-    )
+    @unittest.skipIf(linked_to_musl(), "musl libc issue, bpo-46390")
     @unittest.skipIf(sys.platform.startswith("netbsd"),
                      "gh-124108: NetBSD doesn't support UTF-8 for LC_COLLATE")
     def test_strcoll_with_diacritic(self):
@@ -362,10 +359,7 @@ def test_strcoll_with_diacritic(self):
 
     @unittest.skipIf(sys.platform.startswith('aix'),
                      'bpo-29972: broken test on AIX')
-    @unittest.skipIf(
-        is_emscripten or is_wasi,
-        "musl libc issue on Emscripten/WASI, bpo-46390"
-    )
+    @unittest.skipIf(linked_to_musl(), "musl libc issue, bpo-46390")
     @unittest.skipIf(sys.platform.startswith("netbsd"),
                      "gh-124108: NetBSD doesn't support UTF-8 for LC_COLLATE")
     def test_strxfrm_with_diacritic(self):
diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py
index 2649be86e5086e..b4f5dd80f55f86 100644
--- a/Lib/test/test_math.py
+++ b/Lib/test/test_math.py
@@ -2772,6 +2772,9 @@ def test_fma_infinities(self):
         or (sys.platform == "android" and platform.machine() == "x86_64")
         or support.linked_to_musl(),  # gh-131032
         f"this platform doesn't implement IEE 754-2008 properly")
+    # gh-131032: musl is fixed but the fix is not yet released; when the fixed
+    # version is known change this to:
+    #   or support.linked_to_musl() < (1, <m>, <p>)
     def test_fma_zero_result(self):
         nonnegative_finites = [0.0, 1e-300, 2.3, 1e300]
 
diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index 6e40cb4f58bfee..22610adf92df94 100644
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -2555,15 +2555,18 @@ def test_fchown(self):
     @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()')
     def test_fpathconf(self):
         self.assertIn("PC_NAME_MAX", os.pathconf_names)
-        if not (support.is_emscripten or support.is_wasi):
-            # musl libc pathconf ignores the file descriptor and always returns
-            # a constant, so the assertion that it should notice a bad file
-            # descriptor and return EBADF fails.
-            self.check(os.pathconf, "PC_NAME_MAX")
-            self.check(os.fpathconf, "PC_NAME_MAX")
         self.check_bool(os.pathconf, "PC_NAME_MAX")
         self.check_bool(os.fpathconf, "PC_NAME_MAX")
 
+    @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()')
+    @unittest.skipIf(
+        support.linked_to_musl(),
+        'musl pathconf ignores the file descriptor and returns a constant',
+        )
+    def test_fpathconf_bad_fd(self):
+        self.check(os.pathconf, "PC_NAME_MAX")
+        self.check(os.fpathconf, "PC_NAME_MAX")
+
     @unittest.skipUnless(hasattr(os, 'ftruncate'), 'test needs os.ftruncate()')
     def test_ftruncate(self):
         self.check(os.truncate, 0)
diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py
index e04ad142061ad3..ceb0eb9bc657dd 100644
--- a/Lib/test/test_platform.py
+++ b/Lib/test/test_platform.py
@@ -552,6 +552,10 @@ def test_libc_ver(self):
                 (b'GLIBC_2.9', ('glibc', '2.9')),
                 (b'libc.so.1.2.5', ('libc', '1.2.5')),
                 (b'libc_pthread.so.1.2.5', ('libc', '1.2.5_pthread')),
+                (b'/aports/main/musl/src/musl-1.2.5', ('musl', '1.2.5')),
+                # musl uses semver, but we accept some variations anyway:
+                (b'/aports/main/musl/src/musl-12.5', ('musl', '12.5')),
+                (b'/aports/main/musl/src/musl-1.2.5.7', ('musl', '1.2.5.7')),
                 (b'', ('', '')),
             ):
                 with open(filename, 'wb') as fp:
@@ -563,14 +567,29 @@ def test_libc_ver(self):
                                  expected)
 
         # binary containing multiple versions: get the most recent,
-        # make sure that 1.9 is seen as older than 1.23.4
-        chunksize = 16384
-        with open(filename, 'wb') as f:
-            # test match at chunk boundary
-            f.write(b'x'*(chunksize - 10))
-            f.write(b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0')
-        self.assertEqual(platform.libc_ver(filename, chunksize=chunksize),
-                         ('glibc', '1.23.4'))
+        # make sure that eg 1.9 is seen as older than 1.23.4, and that
+        # the arguments don't count even if they are set.
+        chunksize = 200
+        for data, expected in (
+                (b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0', ('glibc', '1.23.4')),
+                (b'libc.so.2.4\0libc.so.9\0libc.so.23.1\0', ('libc', '23.1')),
+                (b'musl-1.4.1\0musl-2.1.1\0musl-2.0.1\0', ('musl', '2.1.1')),
+                (b'no match here, so defaults are used', ('test', '100.1.0')),
+            ):
+            with open(filename, 'wb') as f:
+                # test match at chunk boundary
+                f.write(b'x'*(chunksize - 10))
+                f.write(data)
+            self.assertEqual(
+                expected,
+                platform.libc_ver(
+                    filename,
+                    lib='test',
+                    version='100.1.0',
+                    chunksize=chunksize,
+                    ),
+                )
+
 
     def test_android_ver(self):
         res = platform.android_ver()
diff --git a/Lib/test/test_re.py b/Lib/test/test_re.py
index 5538de60b2a03a..f65b4076aee2c6 100644
--- a/Lib/test/test_re.py
+++ b/Lib/test/test_re.py
@@ -1,6 +1,6 @@
 from test.support import (gc_collect, bigmemtest, _2G,
                           cpython_only, captured_stdout,
-                          check_disallow_instantiation, is_emscripten, is_wasi,
+                          check_disallow_instantiation, linked_to_musl,
                           warnings_helper, SHORT_TIMEOUT, CPUStopwatch, requires_resource)
 import locale
 import re
@@ -2172,10 +2172,7 @@ def test_bug_20998(self):
         # with ignore case.
         self.assertEqual(re.fullmatch('[a-c]+', 'ABC', re.I).span(), (0, 3))
 
-    @unittest.skipIf(
-        is_emscripten or is_wasi,
-        "musl libc issue on Emscripten/WASI, bpo-46390"
-    )
+    @unittest.skipIf(linked_to_musl(), "musl libc issue, bpo-46390")
     def test_locale_caching(self):
         # Issue #22410
         oldlocale = locale.setlocale(locale.LC_CTYPE)
@@ -2212,10 +2209,7 @@ def check_en_US_utf8(self):
         self.assertIsNone(re.match(b'(?Li)\xc5', b'\xe5'))
         self.assertIsNone(re.match(b'(?Li)\xe5', b'\xc5'))
 
-    @unittest.skipIf(
-        is_emscripten or is_wasi,
-        "musl libc issue on Emscripten/WASI, bpo-46390"
-    )
+    @unittest.skipIf(linked_to_musl(), "musl libc issue, bpo-46390")
     def test_locale_compiled(self):
         oldlocale = locale.setlocale(locale.LC_CTYPE)
         self.addCleanup(locale.setlocale, locale.LC_CTYPE, oldlocale)
diff --git a/Lib/test/test_strptime.py b/Lib/test/test_strptime.py
index 0d30a63ab0c140..fbc43829e22a96 100644
--- a/Lib/test/test_strptime.py
+++ b/Lib/test/test_strptime.py
@@ -544,10 +544,7 @@ def test_date_locale(self):
         self.roundtrip('%x', slice(0, 3), time.localtime(now - 366*24*3600))
 
     # NB: Dates before 1969 do not roundtrip on many locales, including C.
-    @unittest.skipIf(
-        support.is_emscripten or support.is_wasi,
-        "musl libc issue on Emscripten, bpo-46390"
-    )
+    @unittest.skipIf(support.linked_to_musl(), "musl libc issue, bpo-46390")
     @run_with_locales('LC_TIME', 'en_US', 'fr_FR', 'de_DE', 'ja_JP',
                       'eu_ES', 'ar_AE', 'my_MM', 'shn_MM')
     def test_date_locale2(self):
diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py
index 46d796379fa212..8d5b3440d3bd30 100644
--- a/Lib/test/test_support.py
+++ b/Lib/test/test_support.py
@@ -746,7 +746,18 @@ def test_get_signal_name(self):
 
     def test_linked_to_musl(self):
         linked = support.linked_to_musl()
-        self.assertIsInstance(linked, bool)
+        self.assertIsNotNone(linked)
+        if support.is_wasi or support.is_emscripten:
+            self.assertTrue(linked)
+        # The value is cached, so make sure it returns the same value again.
+        self.assertIs(linked, support.linked_to_musl())
+        # The unlike libc, the musl version is a triple.
+        if linked:
+            self.assertIsInstance(linked, tuple)
+            self.assertEqual(3, len(linked))
+            for v in linked:
+                self.assertIsInstance(v, int)
+
 
     # XXX -follows a list of untested API
     # make_legacy_pyc
diff --git a/Misc/NEWS.d/next/Library/2025-03-17-17-11-41.gh-issue-90548.xSPf_L.rst b/Misc/NEWS.d/next/Library/2025-03-17-17-11-41.gh-issue-90548.xSPf_L.rst
new file mode 100644
index 00000000000000..88746c1866f14e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-03-17-17-11-41.gh-issue-90548.xSPf_L.rst
@@ -0,0 +1,2 @@
+:func:`platform.libc_ver` can now detect and report the version of ``musl``
+on Alpine Linux.


Reviews:
user: login: vstinner
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T17:40:32Z
user: login: picnixz
body: For my suggestions with dedents, please check them manually as I'm not entirely sure that I didn't mix tabs and spaces.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T17:52:08Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T18:34:01Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T18:34:10Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T18:34:15Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T18:36:49Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T18:36:56Z
user: login: vstinner
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T18:40:16Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T18:43:36Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T18:44:21Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T18:48:25Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T19:20:44Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T19:56:30Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T19:57:21Z
user: login: vstinner
body: LGTM, but please fix the important typo `'musl' in binary`.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-18T10:02:29Z
user: login: picnixz
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T10:14:07Z
user: login: picnixz
body: As I said, I won't block the PR for cosmetics only though I would have aligned with the surrounding/existing code when possible.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-18T10:14:55Z
user: login: serhiy-storchaka
body: LGTM in general.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-18T11:24:30Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T18:50:17Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T18:54:59Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T18:57:00Z
user: login: serhiy-storchaka
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T19:01:50Z
user: login: bitdancer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T19:13:51Z
user: login: serhiy-storchaka
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-19T13:58:45Z

Review comments:
diff_hunk: @@ -831,6 +851,15 @@ The :mod:`test.support` module defines the following functions:
    Decorator for tests that fill the address space.
 
 
+.. function:: linked_with_musl()
+
+   Return ``False`` if there is no evidence the interperter was compiled with
+   ``musl``, otherwise return a version triple, either ``(0, 0, 0)`` if the
+   version is unknown, or the actual version if it is known.  Intended for use
+   in ``skip`` decorators.  ``emscripten`` and ``wasi`` are assumed to be
+   compiled with ``musl``; othewise ``platform.libc_ver`` is checked.
path: Doc/library/test.rst
user: login: vstinner
body: ```suggestion
   compiled with ``musl``; otherwise ``platform.libc_ver`` is checked.
```
created_at: 2025-03-17T17:33:05Z
author_association: MEMBER
diff_hunk: @@ -2772,6 +2772,9 @@ def test_fma_infinities(self):
         or (sys.platform == "android" and platform.machine() == "x86_64")
         or support.linked_to_musl(),  # gh-131032
         f"this platform doesn't implement IEE 754-2008 properly")
+    # XXX musl is fixed but the fix is not yet released; when the fixed version
path: Lib/test/test_math.py
user: login: vstinner
body: ```suggestion
    # gh-131032: musl is fixed but the fix is not yet released; when the fixed version
```
created_at: 2025-03-17T17:35:50Z
author_association: MEMBER
diff_hunk: @@ -2555,15 +2555,18 @@ def test_fchown(self):
     @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()')
     def test_fpathconf(self):
         self.assertIn("PC_NAME_MAX", os.pathconf_names)
-        if not (support.is_emscripten or support.is_wasi):
-            # musl libc pathconf ignores the file descriptor and always returns
-            # a constant, so the assertion that it should notice a bad file
-            # descriptor and return EBADF fails.
-            self.check(os.pathconf, "PC_NAME_MAX")
-            self.check(os.fpathconf, "PC_NAME_MAX")
         self.check_bool(os.pathconf, "PC_NAME_MAX")
         self.check_bool(os.fpathconf, "PC_NAME_MAX")
 
+    @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()')
+    @unittest.skipIf(
+        support.linked_to_musl(),
+        'musl pathconf ignores the file descriptor and returns a constant',
+        )
+    def test_fpathconf_with_bad_fd(self):
path: Lib/test/test_os.py
user: login: vstinner
body: ```suggestion
    def test_fpathconf_bad_fd(self):
```
created_at: 2025-03-17T17:36:32Z
author_association: MEMBER
diff_hunk: @@ -552,6 +552,7 @@ def test_libc_ver(self):
                 (b'GLIBC_2.9', ('glibc', '2.9')),
                 (b'libc.so.1.2.5', ('libc', '1.2.5')),
                 (b'libc_pthread.so.1.2.5', ('libc', '1.2.5_pthread')),
+                (b'/aports/main/musl/src/musl-1.2.5', ('musl', '1.2.5')),
path: Lib/test/test_platform.py
user: login: vstinner
body: Can you add tests on version with 2 numbers and 4 numbers? Like `/aports/main/musl/src/musl-1.2` and `/aports/main/musl/src/musl-1.2.5.1`.
created_at: 2025-03-17T17:37:47Z
author_association: MEMBER
diff_hunk: @@ -746,7 +746,18 @@ def test_get_signal_name(self):
 
     def test_linked_to_musl(self):
         linked = support.linked_to_musl()
-        self.assertIsInstance(linked, bool)
+        self.assertIsNotNone(linked)
+        if support.is_wasi:
+            self.assertTrue(linked)
+        # The value is cached, so make sure it returns the same value again.
+        self.assertEqual(linked, support.linked_to_musl())
path: Lib/test/test_support.py
user: login: vstinner
body: ```suggestion
        self.assertIs(linked, support.linked_to_musl())
```
created_at: 2025-03-17T17:38:57Z
author_association: MEMBER
diff_hunk: @@ -746,7 +746,18 @@ def test_get_signal_name(self):
 
     def test_linked_to_musl(self):
         linked = support.linked_to_musl()
-        self.assertIsInstance(linked, bool)
+        self.assertIsNotNone(linked)
+        if support.is_wasi:
path: Lib/test/test_support.py
user: login: vstinner
body: What about is_emscripten?
created_at: 2025-03-17T17:40:08Z
author_association: MEMBER
diff_hunk: @@ -746,7 +746,18 @@ def test_get_signal_name(self):
 
     def test_linked_to_musl(self):
         linked = support.linked_to_musl()
-        self.assertIsInstance(linked, bool)
+        self.assertIsNotNone(linked)
+        if support.is_wasi:
+            self.assertTrue(linked)
path: Lib/test/test_support.py
user: login: vstinner
body: Check maybe even that it's equal to (0, 0, 0)?
created_at: 2025-03-17T17:40:22Z
author_association: MEMBER
diff_hunk: @@ -189,22 +189,28 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
             # sys.executable is not set.
             return lib, version
 
-    libc_search = re.compile(b'(__libc_init)'
-                          b'|'
-                          b'(GLIBC_([0-9.]+))'
-                          b'|'
-                          br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)', re.ASCII)
+    libc_search = re.compile(
+        br'(__libc_init)'
+        br'|'
+        br'(GLIBC_([0-9.]+))'
+        br'|'
+        br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)'
+        br'|'
+        br'(musl-([0-9.]+))'
+        br'',
path: Lib/platform.py
user: login: picnixz
body: Is the empty string at the end intended?
created_at: 2025-03-17T17:46:21Z
author_association: MEMBER
diff_hunk: @@ -3017,20 +3017,31 @@ def is_libssl_fips_mode():
     return get_fips_mode() != 0
 
 
+_linked_to_musl = None
 def linked_to_musl():
     """
-    Test if the Python executable is linked to the musl C library.
+    Report if the Python executable is linked to the musl C library.
+
+    Return False if we don't think it is, or a version triple otherwise.
     """
+    # This is can be a relatively expensive check, so we use a cache.
+    global _linked_to_musl
+    if _linked_to_musl is not None:
+        return _linked_to_musl
+
+    # emscripten (at least as far as we're concerned) and wasi use musl,
+    # but platform doesn't know how to get the version, so set it to zero.
+    if is_emscripten or is_wasi:
+        return (_linked_to_musl := (0, 0, 0))
path: Lib/test/support/__init__.py
user: login: picnixz
body: TIL that we can also do this to save a few lines... Not sure if it's the most readable but I don't mind it.
created_at: 2025-03-17T17:47:27Z
author_association: MEMBER
diff_hunk: @@ -2555,15 +2555,18 @@ def test_fchown(self):
     @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()')
     def test_fpathconf(self):
         self.assertIn("PC_NAME_MAX", os.pathconf_names)
-        if not (support.is_emscripten or support.is_wasi):
-            # musl libc pathconf ignores the file descriptor and always returns
-            # a constant, so the assertion that it should notice a bad file
-            # descriptor and return EBADF fails.
-            self.check(os.pathconf, "PC_NAME_MAX")
-            self.check(os.fpathconf, "PC_NAME_MAX")
         self.check_bool(os.pathconf, "PC_NAME_MAX")
         self.check_bool(os.fpathconf, "PC_NAME_MAX")
 
+    @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()')
+    @unittest.skipIf(
+        support.linked_to_musl(),
+        'musl pathconf ignores the file descriptor and returns a constant',
+        )
path: Lib/test/test_os.py
user: login: picnixz
body: ```suggestion
    )
```
created_at: 2025-03-17T17:48:08Z
author_association: MEMBER
diff_hunk: @@ -563,14 +564,29 @@ def test_libc_ver(self):
                                  expected)
 
         # binary containing multiple versions: get the most recent,
-        # make sure that 1.9 is seen as older than 1.23.4
-        chunksize = 16384
-        with open(filename, 'wb') as f:
-            # test match at chunk boundary
-            f.write(b'x'*(chunksize - 10))
-            f.write(b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0')
-        self.assertEqual(platform.libc_ver(filename, chunksize=chunksize),
-                         ('glibc', '1.23.4'))
+        # make sure that eg 1.9 is seen as older than 1.23.4, and that
+        # the arguments don't count even if they are set.
+        chunksize = 200
+        for data, expected in (
+                (b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0', ('glibc', '1.23.4')),
+                (b'libc.so.2.4\0libc.so.9\0libc.so.23.1\0', ('libc', '23.1')),
+                (b'musl-1.4.1\0musl-2.1.1\0musl-2.0.1\0', ('musl', '2.1.1')),
+                (b'no match here, so defaults are used', ('test', '100.1.0')),
+            ):
+            with open(filename, 'wb') as f:
+                # test match at chunk boundary
+                f.write(b'x'*(chunksize - 10))
+                f.write(data)
+            self.assertEqual(
+                expected,
+                platform.libc_ver(
+                    filename,
+                    lib='test',
+                    version='100.1.0',
+                    chunksize=chunksize,
+                    ),
+                )
path: Lib/test/test_platform.py
user: login: picnixz
body: ```suggestion
            self.assertEqual(
                expected,
                platform.libc_ver(
                    filename,
                    lib='test',
                    version='100.1.0',
                    chunksize=chunksize,
                ),
            )
```
created_at: 2025-03-17T17:48:30Z
author_association: MEMBER
diff_hunk: @@ -746,7 +746,18 @@ def test_get_signal_name(self):
 
     def test_linked_to_musl(self):
         linked = support.linked_to_musl()
-        self.assertIsInstance(linked, bool)
+        self.assertIsNotNone(linked)
+        if support.is_wasi:
+            self.assertTrue(linked)
+        # The value is cached, so make sure it returns the same value again.
+        self.assertEqual(linked, support.linked_to_musl())
+        # The unlike libc, the musl version is a triple.
+        if linked:
+            self.assertIsInstance(linked, tuple)
+            self.assertEqual(3, len(linked))
path: Lib/test/test_support.py
user: login: picnixz
body: ```suggestion
            self.assertEqual(len(linked), 3)
```
created_at: 2025-03-17T17:49:23Z
author_association: MEMBER
diff_hunk: @@ -563,14 +564,29 @@ def test_libc_ver(self):
                                  expected)
 
         # binary containing multiple versions: get the most recent,
-        # make sure that 1.9 is seen as older than 1.23.4
-        chunksize = 16384
-        with open(filename, 'wb') as f:
-            # test match at chunk boundary
-            f.write(b'x'*(chunksize - 10))
-            f.write(b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0')
-        self.assertEqual(platform.libc_ver(filename, chunksize=chunksize),
-                         ('glibc', '1.23.4'))
+        # make sure that eg 1.9 is seen as older than 1.23.4, and that
+        # the arguments don't count even if they are set.
+        chunksize = 200
+        for data, expected in (
+                (b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0', ('glibc', '1.23.4')),
+                (b'libc.so.2.4\0libc.so.9\0libc.so.23.1\0', ('libc', '23.1')),
+                (b'musl-1.4.1\0musl-2.1.1\0musl-2.0.1\0', ('musl', '2.1.1')),
+                (b'no match here, so defaults are used', ('test', '100.1.0')),
+            ):
path: Lib/test/test_platform.py
user: login: picnixz
body: ```suggestion
        for data, expected in (
            (b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0', ('glibc', '1.23.4')),
            (b'libc.so.2.4\0libc.so.9\0libc.so.23.1\0', ('libc', '23.1')),
            (b'musl-1.4.1\0musl-2.1.1\0musl-2.0.1\0', ('musl', '2.1.1')),
            (b'no match here, so defaults are used', ('test', '100.1.0')),
        ):
```
created_at: 2025-03-17T17:50:00Z
author_association: MEMBER
diff_hunk: @@ -552,6 +552,7 @@ def test_libc_ver(self):
                 (b'GLIBC_2.9', ('glibc', '2.9')),
                 (b'libc.so.1.2.5', ('libc', '1.2.5')),
                 (b'libc_pthread.so.1.2.5', ('libc', '1.2.5_pthread')),
+                (b'/aports/main/musl/src/musl-1.2.5', ('musl', '1.2.5')),
path: Lib/test/test_platform.py
user: login: bitdancer
body: I could, but I don't believe musl will ever have such release numbers, so is it worth doing?
created_at: 2025-03-17T18:34:01Z
author_association: MEMBER
diff_hunk: @@ -746,7 +746,18 @@ def test_get_signal_name(self):
 
     def test_linked_to_musl(self):
         linked = support.linked_to_musl()
-        self.assertIsInstance(linked, bool)
+        self.assertIsNotNone(linked)
+        if support.is_wasi:
path: Lib/test/test_support.py
user: login: bitdancer
body: I used is_wasi as an example, but it doesn't hurt to add emscripten as well.
created_at: 2025-03-17T18:34:10Z
author_association: MEMBER
diff_hunk: @@ -746,7 +746,18 @@ def test_get_signal_name(self):
 
     def test_linked_to_musl(self):
         linked = support.linked_to_musl()
-        self.assertIsInstance(linked, bool)
+        self.assertIsNotNone(linked)
+        if support.is_wasi:
+            self.assertTrue(linked)
path: Lib/test/test_support.py
user: login: bitdancer
body: I'd rather not...eventually I hope it will be a version number.
created_at: 2025-03-17T18:34:15Z
author_association: MEMBER
diff_hunk: @@ -189,22 +189,28 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
             # sys.executable is not set.
             return lib, version
 
-    libc_search = re.compile(b'(__libc_init)'
-                          b'|'
-                          b'(GLIBC_([0-9.]+))'
-                          b'|'
-                          br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)', re.ASCII)
+    libc_search = re.compile(
+        br'(__libc_init)'
+        br'|'
+        br'(GLIBC_([0-9.]+))'
+        br'|'
+        br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)'
+        br'|'
+        br'(musl-([0-9.]+))'
+        br'',
path: Lib/platform.py
user: login: bitdancer
body: Yes.  It will produce cleaner diffs if more clauses are added in the future (equivalent to having an unneeded comma on the last line of a multiline list).
created_at: 2025-03-17T18:36:49Z
author_association: MEMBER
diff_hunk: @@ -3017,20 +3017,31 @@ def is_libssl_fips_mode():
     return get_fips_mode() != 0
 
 
+_linked_to_musl = None
 def linked_to_musl():
     """
-    Test if the Python executable is linked to the musl C library.
+    Report if the Python executable is linked to the musl C library.
+
+    Return False if we don't think it is, or a version triple otherwise.
     """
+    # This is can be a relatively expensive check, so we use a cache.
+    global _linked_to_musl
+    if _linked_to_musl is not None:
+        return _linked_to_musl
+
+    # emscripten (at least as far as we're concerned) and wasi use musl,
+    # but platform doesn't know how to get the version, so set it to zero.
+    if is_emscripten or is_wasi:
+        return (_linked_to_musl := (0, 0, 0))
path: Lib/test/support/__init__.py
user: login: bitdancer
body: Yeah, I wasn't quite sure about it, but it seemed like a reasonable use of the operator.  I'm fine with being more wordy if that is considered better style.
created_at: 2025-03-17T18:36:55Z
author_association: MEMBER
diff_hunk: @@ -552,6 +552,7 @@ def test_libc_ver(self):
                 (b'GLIBC_2.9', ('glibc', '2.9')),
                 (b'libc.so.1.2.5', ('libc', '1.2.5')),
                 (b'libc_pthread.so.1.2.5', ('libc', '1.2.5_pthread')),
+                (b'/aports/main/musl/src/musl-1.2.5', ('musl', '1.2.5')),
path: Lib/test/test_platform.py
user: login: vstinner
body: Parsing a binary file using a regular expression is a strange thing for me. So I prefer to have tests to make sure that the implementation is reliable. The question is more if the regex will match versions with 2 members (x.y) or 4 members (x.y.z.a), or if it only matchs version with 3 members (x.y.z).
created_at: 2025-03-17T18:40:16Z
author_association: MEMBER
diff_hunk: @@ -2555,15 +2555,18 @@ def test_fchown(self):
     @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()')
     def test_fpathconf(self):
         self.assertIn("PC_NAME_MAX", os.pathconf_names)
-        if not (support.is_emscripten or support.is_wasi):
-            # musl libc pathconf ignores the file descriptor and always returns
-            # a constant, so the assertion that it should notice a bad file
-            # descriptor and return EBADF fails.
-            self.check(os.pathconf, "PC_NAME_MAX")
-            self.check(os.fpathconf, "PC_NAME_MAX")
         self.check_bool(os.pathconf, "PC_NAME_MAX")
         self.check_bool(os.fpathconf, "PC_NAME_MAX")
 
+    @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()')
+    @unittest.skipIf(
+        support.linked_to_musl(),
+        'musl pathconf ignores the file descriptor and returns a constant',
+        )
path: Lib/test/test_os.py
user: login: bitdancer
body: I prefer the style I used because it lines up better with how python's statements work (first line only is outdented, lack of outdent signals return to outer logic block).  This is accepted by PEP 8, are there additional style constrains on the cpython code base these days?
created_at: 2025-03-17T18:43:36Z
author_association: MEMBER
diff_hunk: @@ -746,7 +746,18 @@ def test_get_signal_name(self):
 
     def test_linked_to_musl(self):
         linked = support.linked_to_musl()
-        self.assertIsInstance(linked, bool)
+        self.assertIsNotNone(linked)
+        if support.is_wasi:
+            self.assertTrue(linked)
+        # The value is cached, so make sure it returns the same value again.
+        self.assertEqual(linked, support.linked_to_musl())
+        # The unlike libc, the musl version is a triple.
+        if linked:
+            self.assertIsInstance(linked, tuple)
+            self.assertEqual(3, len(linked))
path: Lib/test/test_support.py
user: login: bitdancer
body: Expected should come first because it makes the assertEqual diffs clearer in the general case.  In this case it wouldn't matter, but I prefer to be consistent.
created_at: 2025-03-17T18:44:21Z
author_association: MEMBER
diff_hunk: @@ -563,14 +564,29 @@ def test_libc_ver(self):
                                  expected)
 
         # binary containing multiple versions: get the most recent,
-        # make sure that 1.9 is seen as older than 1.23.4
-        chunksize = 16384
-        with open(filename, 'wb') as f:
-            # test match at chunk boundary
-            f.write(b'x'*(chunksize - 10))
-            f.write(b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0')
-        self.assertEqual(platform.libc_ver(filename, chunksize=chunksize),
-                         ('glibc', '1.23.4'))
+        # make sure that eg 1.9 is seen as older than 1.23.4, and that
+        # the arguments don't count even if they are set.
+        chunksize = 200
+        for data, expected in (
+                (b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0', ('glibc', '1.23.4')),
+                (b'libc.so.2.4\0libc.so.9\0libc.so.23.1\0', ('libc', '23.1')),
+                (b'musl-1.4.1\0musl-2.1.1\0musl-2.0.1\0', ('musl', '2.1.1')),
+                (b'no match here, so defaults are used', ('test', '100.1.0')),
+            ):
path: Lib/test/test_platform.py
user: login: bitdancer
body: I believe my version is more consistent with pep8, though you could argue that the `):` should be indented four more spaces as well.
created_at: 2025-03-17T18:48:25Z
author_association: MEMBER
diff_hunk: @@ -552,6 +552,7 @@ def test_libc_ver(self):
                 (b'GLIBC_2.9', ('glibc', '2.9')),
                 (b'libc.so.1.2.5', ('libc', '1.2.5')),
                 (b'libc_pthread.so.1.2.5', ('libc', '1.2.5_pthread')),
+                (b'/aports/main/musl/src/musl-1.2.5', ('musl', '1.2.5')),
path: Lib/test/test_platform.py
user: login: bitdancer
body: Fair enough, though the existing tests don't do that, nor do they test the no-match cases.  I could add such tests, but at that point I'd want to rewrite this test method to split it up into multiple tests, and I don't think that's in scope for this PR :)  I'll add the couple of extra checks, though.
created_at: 2025-03-17T19:20:44Z
author_association: MEMBER
diff_hunk: @@ -2555,15 +2555,18 @@ def test_fchown(self):
     @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()')
     def test_fpathconf(self):
         self.assertIn("PC_NAME_MAX", os.pathconf_names)
-        if not (support.is_emscripten or support.is_wasi):
-            # musl libc pathconf ignores the file descriptor and always returns
-            # a constant, so the assertion that it should notice a bad file
-            # descriptor and return EBADF fails.
-            self.check(os.pathconf, "PC_NAME_MAX")
-            self.check(os.fpathconf, "PC_NAME_MAX")
         self.check_bool(os.pathconf, "PC_NAME_MAX")
         self.check_bool(os.fpathconf, "PC_NAME_MAX")
 
+    @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()')
+    @unittest.skipIf(
+        support.linked_to_musl(),
+        'musl pathconf ignores the file descriptor and returns a constant',
+        )
path: Lib/test/test_os.py
user: login: picnixz
body: I haven't seen lots of use of hanging indents in the lib. There is no additional constraints but usually we try to be consistent with the surrounding code when possible.
created_at: 2025-03-17T19:56:30Z
author_association: MEMBER
diff_hunk: @@ -746,7 +746,18 @@ def test_get_signal_name(self):
 
     def test_linked_to_musl(self):
         linked = support.linked_to_musl()
-        self.assertIsInstance(linked, bool)
+        self.assertIsNotNone(linked)
+        if support.is_wasi:
+            self.assertTrue(linked)
+        # The value is cached, so make sure it returns the same value again.
+        self.assertEqual(linked, support.linked_to_musl())
+        # The unlike libc, the musl version is a triple.
+        if linked:
+            self.assertIsInstance(linked, tuple)
+            self.assertEqual(3, len(linked))
path: Lib/test/test_support.py
user: login: picnixz
body: Ah, most of the tests I wrote and read were actually swapped. I think it's half-half so I don't mind.
created_at: 2025-03-17T19:57:21Z
author_association: MEMBER
diff_hunk: @@ -189,22 +189,28 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
             # sys.executable is not set.
             return lib, version
 
-    libc_search = re.compile(b'(__libc_init)'
-                          b'|'
-                          b'(GLIBC_([0-9.]+))'
-                          b'|'
-                          br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)', re.ASCII)
+    libc_search = re.compile(
+        br'(__libc_init)'
+        br'|'
+        br'(GLIBC_([0-9.]+))'
+        br'|'
+        br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)'
+        br'|'
+        br'(musl-([0-9.]+))'
+        br'',
+        re.ASCII)
 
     V = _comparable_version
     # We use os.path.realpath()
     # here to work around problems with Cygwin not being
     # able to open symlinks for reading
     executable = os.path.realpath(executable)
+    ver = None
     with open(executable, 'rb') as f:
         binary = f.read(chunksize)
         pos = 0
         while pos < len(binary):
-            if b'libc' in binary or b'GLIBC' in binary:
+            if b'libc' in binary or b'GLIBC' or 'musl' in binary:
path: Lib/platform.py
user: login: vstinner
body: ```suggestion
            if b'libc' in binary or b'GLIBC' or b'musl' in binary:
```
created_at: 2025-03-18T10:00:39Z
author_association: MEMBER
diff_hunk: @@ -563,14 +564,29 @@ def test_libc_ver(self):
                                  expected)
 
         # binary containing multiple versions: get the most recent,
-        # make sure that 1.9 is seen as older than 1.23.4
-        chunksize = 16384
-        with open(filename, 'wb') as f:
-            # test match at chunk boundary
-            f.write(b'x'*(chunksize - 10))
-            f.write(b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0')
-        self.assertEqual(platform.libc_ver(filename, chunksize=chunksize),
-                         ('glibc', '1.23.4'))
+        # make sure that eg 1.9 is seen as older than 1.23.4, and that
+        # the arguments don't count even if they are set.
+        chunksize = 200
+        for data, expected in (
+                (b'GLIBC_1.23.4\0GLIBC_1.9\0GLIBC_1.21\0', ('glibc', '1.23.4')),
+                (b'libc.so.2.4\0libc.so.9\0libc.so.23.1\0', ('libc', '23.1')),
+                (b'musl-1.4.1\0musl-2.1.1\0musl-2.0.1\0', ('musl', '2.1.1')),
+                (b'no match here, so defaults are used', ('test', '100.1.0')),
+            ):
path: Lib/test/test_platform.py
user: login: picnixz
body: Up to you. I won't block the PR for cosmetics only!
created_at: 2025-03-18T10:14:07Z
author_association: MEMBER
diff_hunk: @@ -189,22 +189,28 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
             # sys.executable is not set.
             return lib, version
 
-    libc_search = re.compile(b'(__libc_init)'
-                          b'|'
-                          b'(GLIBC_([0-9.]+))'
-                          b'|'
-                          br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)', re.ASCII)
+    libc_search = re.compile(
+        br'(__libc_init)'
+        br'|'
+        br'(GLIBC_([0-9.]+))'
+        br'|'
+        br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)'
+        br'|'
+        br'(musl-([0-9.]+))'
+        br'',
+        re.ASCII)
path: Lib/platform.py
user: login: serhiy-storchaka
body: BTW, we can switch to the VERBOSE mode, which would be slightly more clear.
```suggestion
    libc_search = re.compile(br'''
          (__libc_init)
        | (GLIBC_([0-9.]+))
        | (libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)
        | (musl-([0-9.]+))
        ''',
        re.ASCII | re.VERBOSE)
```
created_at: 2025-03-18T11:09:45Z
author_association: MEMBER
diff_hunk: @@ -3017,20 +3017,31 @@ def is_libssl_fips_mode():
     return get_fips_mode() != 0
 
 
+_linked_to_musl = None
 def linked_to_musl():
     """
-    Test if the Python executable is linked to the musl C library.
+    Report if the Python executable is linked to the musl C library.
+
+    Return False if we don't think it is, or a version triple otherwise.
     """
+    # This is can be a relatively expensive check, so we use a cache.
+    global _linked_to_musl
+    if _linked_to_musl is not None:
+        return _linked_to_musl
+
+    # emscripten (at least as far as we're concerned) and wasi use musl,
+    # but platform doesn't know how to get the version, so set it to zero.
+    if is_emscripten or is_wasi:
+        return (_linked_to_musl := (0, 0, 0))
path: Lib/test/support/__init__.py
user: login: serhiy-storchaka
body: I do not think that it is necessary to use the `:=` operator here.
created_at: 2025-03-18T11:15:25Z
author_association: MEMBER
diff_hunk: @@ -544,10 +544,7 @@ def test_date_locale(self):
         self.roundtrip('%x', slice(0, 3), time.localtime(now - 366*24*3600))
 
     # NB: Dates before 1969 do not roundtrip on many locales, including C.
-    @unittest.skipIf(
-        support.is_emscripten or support.is_wasi,
-        "musl libc issue on Emscripten, bpo-46390"
-    )
+    @unittest.skipIf(support.linked_to_musl(), "musl libc issue, bpo-46390")
     @run_with_locales('LC_TIME', 'en_US', 'fr_FR', 'de_DE', 'ja_JP',
path: Lib/test/test_strptime.py
user: login: serhiy-storchaka
body: We can add the `linked_to_musl()` check in `run_with_locales()` and `run_with_locale()`. This may be non-trivial, depending on arguments, a dummy run can be supported even if no suitable locale was found. For now, the proposed change is good as is.
created_at: 2025-03-18T11:23:09Z
author_association: MEMBER
diff_hunk: @@ -189,22 +189,28 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
             # sys.executable is not set.
             return lib, version
 
-    libc_search = re.compile(b'(__libc_init)'
-                          b'|'
-                          b'(GLIBC_([0-9.]+))'
-                          b'|'
-                          br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)', re.ASCII)
+    libc_search = re.compile(
+        br'(__libc_init)'
+        br'|'
+        br'(GLIBC_([0-9.]+))'
+        br'|'
+        br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)'
+        br'|'
+        br'(musl-([0-9.]+))'
+        br'',
+        re.ASCII)
 
     V = _comparable_version
     # We use os.path.realpath()
     # here to work around problems with Cygwin not being
     # able to open symlinks for reading
     executable = os.path.realpath(executable)
+    ver = None
     with open(executable, 'rb') as f:
         binary = f.read(chunksize)
         pos = 0
         while pos < len(binary):
-            if b'libc' in binary or b'GLIBC' in binary:
+            if b'libc' in binary or b'GLIBC' or 'musl' in binary:
path: Lib/platform.py
user: login: bitdancer
body: Oh, that's a nasty one.  The actual fix is `b'GLIBC` in binary or b'musl' in binary`.  That's why it passed without the b...it just ran slower ;)  (A lot slower, somewhat to my surprise.)
created_at: 2025-03-18T18:50:17Z
author_association: MEMBER
diff_hunk: @@ -189,22 +189,28 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384):
             # sys.executable is not set.
             return lib, version
 
-    libc_search = re.compile(b'(__libc_init)'
-                          b'|'
-                          b'(GLIBC_([0-9.]+))'
-                          b'|'
-                          br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)', re.ASCII)
+    libc_search = re.compile(
+        br'(__libc_init)'
+        br'|'
+        br'(GLIBC_([0-9.]+))'
+        br'|'
+        br'(libc(_\w+)?\.so(?:\.(\d[0-9.]*))?)'
+        br'|'
+        br'(musl-([0-9.]+))'
+        br'',
+        re.ASCII)
path: Lib/platform.py
user: login: bitdancer
body: I like that.
created_at: 2025-03-18T18:54:59Z
author_association: MEMBER
diff_hunk: @@ -3017,20 +3017,31 @@ def is_libssl_fips_mode():
     return get_fips_mode() != 0
 
 
+_linked_to_musl = None
 def linked_to_musl():
     """
-    Test if the Python executable is linked to the musl C library.
+    Report if the Python executable is linked to the musl C library.
+
+    Return False if we don't think it is, or a version triple otherwise.
     """
+    # This is can be a relatively expensive check, so we use a cache.
+    global _linked_to_musl
+    if _linked_to_musl is not None:
+        return _linked_to_musl
+
+    # emscripten (at least as far as we're concerned) and wasi use musl,
+    # but platform doesn't know how to get the version, so set it to zero.
+    if is_emscripten or is_wasi:
+        return (_linked_to_musl := (0, 0, 0))
path: Lib/test/support/__init__.py
user: login: bitdancer
body: It is never *necessary* to use the `:=` operator, so do you mean you prefer the more verbose style (which I'm fine with changing to), or am I missing something?
created_at: 2025-03-18T18:57:00Z
author_association: MEMBER
diff_hunk: @@ -3017,20 +3017,31 @@ def is_libssl_fips_mode():
     return get_fips_mode() != 0
 
 
+_linked_to_musl = None
 def linked_to_musl():
     """
-    Test if the Python executable is linked to the musl C library.
+    Report if the Python executable is linked to the musl C library.
+
+    Return False if we don't think it is, or a version triple otherwise.
     """
+    # This is can be a relatively expensive check, so we use a cache.
+    global _linked_to_musl
+    if _linked_to_musl is not None:
+        return _linked_to_musl
+
+    # emscripten (at least as far as we're concerned) and wasi use musl,
+    # but platform doesn't know how to get the version, so set it to zero.
+    if is_emscripten or is_wasi:
+        return (_linked_to_musl := (0, 0, 0))
path: Lib/test/support/__init__.py
user: login: serhiy-storchaka
body: I prefer the more verbose style, which is also more sequential and clear.
created_at: 2025-03-18T19:01:50Z
author_association: MEMBER
diff_hunk: @@ -3017,20 +3017,31 @@ def is_libssl_fips_mode():
     return get_fips_mode() != 0
 
 
+_linked_to_musl = None
 def linked_to_musl():
     """
-    Test if the Python executable is linked to the musl C library.
+    Report if the Python executable is linked to the musl C library.
+
+    Return False if we don't think it is, or a version triple otherwise.
     """
+    # This is can be a relatively expensive check, so we use a cache.
+    global _linked_to_musl
+    if _linked_to_musl is not None:
+        return _linked_to_musl
+
+    # emscripten (at least as far as we're concerned) and wasi use musl,
+    # but platform doesn't know how to get the version, so set it to zero.
+    if is_emscripten or is_wasi:
+        return (_linked_to_musl := (0, 0, 0))
path: Lib/test/support/__init__.py
user: login: bitdancer
body: Done.
created_at: 2025-03-18T19:13:51Z
author_association: MEMBER

PR comments:
user: login: vstinner
created_at: 2025-03-17T17:44:52Z
body: The code detecting the musl version works for me on Alpine Linux:

```
~/cpython # LD_LIBRARY_PATH=$PWD ./python 
Python 3.14.0a5+ (heads/main-dirty:fe186d76cfd, Mar 11 2025, 08:54:51) [GCC 14.2.0] on linux
>>> import test.support
>>> test.support.linked_to_musl()
(1, 2, 5)


~/cpython # LD_LIBRARY_PATH=$PWD ./python -m test.pythoninfo|grep libc
platform.libc_ver: musl 1.2.5


~/cpython # apk info musl
musl-1.2.5-r9 description:
the musl c library (libc) implementation

musl-1.2.5-r9 webpage:
https://musl.libc.org/

musl-1.2.5-r9 installed size:
646 KiB
```
author_association: MEMBER
user: login: picnixz
created_at: 2025-03-17T17:52:40Z
body: Btw, do we want to create an issue (maybe an umbrella one for musl issues) or do we make it a `skip-issue`?
author_association: MEMBER
user: login: bitdancer
created_at: 2025-03-17T18:11:23Z
body: There is an issue (gh-90548).  I forgot the gh issue existed when I mentioned the bpo issue :(  I'm not actually sure how one does  the link, as I'm still coming up to speed on the "modern" way of doing development ;)
author_association: MEMBER
user: login: picnixz
created_at: 2025-03-17T18:16:38Z
body: > I'm not actually sure how one does the link, as I'm still coming up to speed on the "modern" way of doing development ;)

No worries! the PR title should be `gh-XXXXXX: title` or `GH-XXXXXX: title` and the bot would link it.
author_association: MEMBER
user: login: bitdancer
created_at: 2025-03-19T17:05:59Z
body: I'm thinking this is not worth backporting?
author_association: MEMBER
user: login: vstinner
created_at: 2025-03-19T17:07:25Z
body: > I'm thinking this is not worth backporting?

I don't think that it's worth it to backport this change to stable branches. Supporting Alpine Linux can be seen as a new feature ;-)
author_association: MEMBER