From d56a32ddcd1accbb68a74210c029cc76a6e3fcd3 Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Sat, 30 Jul 2022 20:35:22 -0400 Subject: [PATCH 01/13] ENH: Don't add system library directories to rpath Last of the patches from #73 Might close pypa/setuptools#3257 Dual purposes here: - On platforms like Cygwin that don't have `rpath`, try to avoid adding things to `rpath` - Some distribution binary package makers require that no shared library list a system library directory (`/lib`, `/lib64`, `/usr/lib`, `/usr/lib64`) in its `rpath`; this patch simplifies the code to ensure the shared library can find its dependencies at runtime. --- distutils/unixccompiler.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py index 7e68596b..cf49e359 100644 --- a/distutils/unixccompiler.py +++ b/distutils/unixccompiler.py @@ -148,6 +148,15 @@ class UnixCCompiler(CCompiler): dylib_lib_extension = ".dll" dylib_lib_format = "cyg%s%s" + def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs): + """Remove standard library path from rpath""" + libraries, library_dirs, runtime_library_dirs = super()._fix_lib_args( + libraries, library_dirs, runtime_library_dirs) + libdir = sysconfig.get_config_var('LIBDIR') + if runtime_library_dirs and (libdir in runtime_library_dirs): + runtime_library_dirs.remove(libdir) + return libraries, library_dirs, runtime_library_dirs + def preprocess( self, source, From 18203a23695ebb91b915b9e6465c473b5acd8f0b Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Sat, 20 Aug 2022 12:05:48 -0400 Subject: [PATCH 02/13] ENH: Drop LIBDIR from RPATH only if starting with /usr/lib. Avoids problems with odd LIBDIR Package managers will be putting things in LIBDIR anyway, so this should catch all use-cases I know of. --- distutils/tests/test_build_ext.py | 10 ++++++++++ distutils/unixccompiler.py | 6 +++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 6c4c4ba8..ec8a818d 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -8,6 +8,7 @@ import sys import tempfile import textwrap +import time from distutils import sysconfig from distutils.command.build_ext import build_ext from distutils.core import Distribution @@ -55,6 +56,9 @@ def user_site_dir(request): site.USER_BASE = orig_user_base build_ext.USER_BASE = orig_user_base + if sys.platform == 'cygwin': + time.sleep(1) + @contextlib.contextmanager def safe_extension_import(name, path): @@ -95,6 +99,12 @@ def test_build_ext(self): copy_xxmodule_c(self.tmp_dir) xx_c = os.path.join(self.tmp_dir, 'xxmodule.c') xx_ext = Extension('xx', [xx_c]) + if sys.platform != "win32": + xx_ext = Extension( + 'xx', [xx_c], + library_dirs=['/usr/lib'], libraries=['z'], + runtime_library_dirs=['/usr/lib'] + ) dist = Distribution({'name': 'xx', 'ext_modules': [xx_ext]}) dist.package_dir = self.tmp_dir cmd = self.build_ext(dist) diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py index cf49e359..8a01bf3a 100644 --- a/distutils/unixccompiler.py +++ b/distutils/unixccompiler.py @@ -153,7 +153,11 @@ def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs): libraries, library_dirs, runtime_library_dirs = super()._fix_lib_args( libraries, library_dirs, runtime_library_dirs) libdir = sysconfig.get_config_var('LIBDIR') - if runtime_library_dirs and (libdir in runtime_library_dirs): + if ( + runtime_library_dirs + and libdir.startswith("/usr/lib") + and (libdir in runtime_library_dirs) + ): runtime_library_dirs.remove(libdir) return libraries, library_dirs, runtime_library_dirs From 22a78ae832f35a75ac764a4880e99e4a1d7d11e9 Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Sat, 20 Aug 2022 12:07:40 -0400 Subject: [PATCH 03/13] TST: Get all tests passing on Cygwin. The check reference caused docutils problems with no ldesc. The cygwinccompiler change produced a DeprecationWarning. --- distutils/command/check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/command/check.py b/distutils/command/check.py index 58b3f949..2eb72b45 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -144,7 +144,7 @@ def _check_rst_data(self, data): document.note_source(source_path, -1) try: parser.parse(data, document) - except AttributeError as e: + except (AttributeError, TypeError) as e: reporter.messages.append(( -1, f'Could not finish the parsing: {e}.', From 0a479d338b8caeae0f28b8560a13d58c04278b14 Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Sat, 20 Aug 2022 19:57:10 -0400 Subject: [PATCH 04/13] STY: Apply suggestions from CI running black --- distutils/tests/test_build_ext.py | 8 +++++--- distutils/unixccompiler.py | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index ec8a818d..d47a4e8a 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -101,9 +101,11 @@ def test_build_ext(self): xx_ext = Extension('xx', [xx_c]) if sys.platform != "win32": xx_ext = Extension( - 'xx', [xx_c], - library_dirs=['/usr/lib'], libraries=['z'], - runtime_library_dirs=['/usr/lib'] + 'xx', + [xx_c], + library_dirs=['/usr/lib'], + libraries=['z'], + runtime_library_dirs=['/usr/lib'], ) dist = Distribution({'name': 'xx', 'ext_modules': [xx_ext]}) dist.package_dir = self.tmp_dir diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py index 8a01bf3a..b04359a5 100644 --- a/distutils/unixccompiler.py +++ b/distutils/unixccompiler.py @@ -151,7 +151,8 @@ class UnixCCompiler(CCompiler): def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs): """Remove standard library path from rpath""" libraries, library_dirs, runtime_library_dirs = super()._fix_lib_args( - libraries, library_dirs, runtime_library_dirs) + libraries, library_dirs, runtime_library_dirs + ) libdir = sysconfig.get_config_var('LIBDIR') if ( runtime_library_dirs @@ -160,7 +161,7 @@ def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs): ): runtime_library_dirs.remove(libdir) return libraries, library_dirs, runtime_library_dirs - + def preprocess( self, source, From 7b693ab556bfc6dd2da1978189e82d1c87652136 Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Sun, 21 Aug 2022 06:57:52 -0400 Subject: [PATCH 05/13] CI: Install a library to link to on Cygwin Cygwin separates import libraries from dynamic libraries (needed link time vs run time). --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b6b757db..5f104963 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -114,6 +114,7 @@ jobs: python${{ matrix.python }}-tox, gcc-core, gcc-g++, + zlib-devel, ncompress git - name: Run tests From 19ea50b30f0113981bb5272069a056645f72acbb Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Thu, 1 Sep 2022 18:05:55 -0400 Subject: [PATCH 06/13] TST: Try testing rpath with non-FHS library dir Let's see if I got the syntax right. Next step will be hooking the temporary /tmp/libxx_z.so file handling into pytest's tempfile handling. --- distutils/tests/test_build_ext.py | 47 ++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index d47a4e8a..ba494fef 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -1,4 +1,5 @@ import contextlib +import glob import importlib import os import platform @@ -94,19 +95,32 @@ class TestBuildExt(TempdirManager): def build_ext(self, *args, **kwargs): return build_ext(*args, **kwargs) - def test_build_ext(self): + @pytest.mark.parametrize("copy_so", [False, True]) + def test_build_ext(self, copy_so): missing_compiler_executable() copy_xxmodule_c(self.tmp_dir) xx_c = os.path.join(self.tmp_dir, 'xxmodule.c') xx_ext = Extension('xx', [xx_c]) if sys.platform != "win32": - xx_ext = Extension( - 'xx', - [xx_c], - library_dirs=['/usr/lib'], - libraries=['z'], - runtime_library_dirs=['/usr/lib'], - ) + if not copy_so: + xx_ext = Extension( + 'xx', + [xx_c], + library_dirs=['/usr/lib'], + libraries=['z'], + runtime_library_dirs=['/usr/lib'], + ) + elif sys.platform == 'linux': + libz_so = glob.glob('/usr/lib*/libz.so*') + shutil.copyfile(libz_so[0], '/tmp/libxx_z.so') + + xx_ext = Extension( + 'xx', + [xx_c], + library_dirs=['/tmp'], + libraries=['xx_z'], + runtime_library_dirs=['/tmp'], + ) dist = Distribution({'name': 'xx', 'ext_modules': [xx_ext]}) dist.package_dir = self.tmp_dir cmd = self.build_ext(dist) @@ -125,10 +139,13 @@ def test_build_ext(self): sys.stdout = old_stdout with safe_extension_import('xx', self.tmp_dir): - self._test_xx() + self._test_xx(copy_so) + + if sys.platform == 'linux' and copy_so: + os.unlink('/tmp/libxx_z.so') @staticmethod - def _test_xx(): + def _test_xx(copy_so): import xx for attr in ('error', 'foo', 'new', 'roj'): @@ -142,6 +159,16 @@ def _test_xx(): assert xx.__doc__ == doc assert isinstance(xx.Null(), xx.Null) assert isinstance(xx.Str(), xx.Str) + + if sys.platform == 'linux': + so_headers = subprocess.check_output(["readelf", "-d", xx.__file__], universal_newlines=True) + if not copy_so: + # Linked against a library in /usr/lib{,64} + assert 'RPATH' not in so_headers and 'RUNPATH' not in so_headers + else: + # Linked against a library in /tmp + assert 'RPATH' in so_headers or 'RUNPATH' in so_headers + # The import is the real test here def test_solaris_enable_shared(self): dist = Distribution({'name': 'xx'}) From 9d45f137d8a462d86f996c9ac8ad59390b4c8001 Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Wed, 21 Sep 2022 12:21:31 -0400 Subject: [PATCH 07/13] TST: Use different library file for link test The old test seemed to pick up the 32-bit library, not the 64-bit one. The new test should pick up the 64-bit one consistently when relevant. --- distutils/tests/test_build_ext.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index ba494fef..a88554db 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -6,6 +6,7 @@ import re import shutil import site +import subprocess import sys import tempfile import textwrap @@ -112,8 +113,8 @@ def test_build_ext(self, copy_so): ) elif sys.platform == 'linux': libz_so = glob.glob('/usr/lib*/libz.so*') - shutil.copyfile(libz_so[0], '/tmp/libxx_z.so') - + shutil.copyfile(libz_so[-1], '/tmp/libxx_z.so') + xx_ext = Extension( 'xx', [xx_c], @@ -140,7 +141,7 @@ def test_build_ext(self, copy_so): with safe_extension_import('xx', self.tmp_dir): self._test_xx(copy_so) - + if sys.platform == 'linux' and copy_so: os.unlink('/tmp/libxx_z.so') @@ -159,9 +160,11 @@ def _test_xx(copy_so): assert xx.__doc__ == doc assert isinstance(xx.Null(), xx.Null) assert isinstance(xx.Str(), xx.Str) - + if sys.platform == 'linux': - so_headers = subprocess.check_output(["readelf", "-d", xx.__file__], universal_newlines=True) + so_headers = subprocess.check_output( + ["readelf", "-d", xx.__file__], universal_newlines=True + ) if not copy_so: # Linked against a library in /usr/lib{,64} assert 'RPATH' not in so_headers and 'RUNPATH' not in so_headers From 3fb68280f0721d1aa142e37ac193e7ef004eacde Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Wed, 3 Jul 2024 10:48:30 -0400 Subject: [PATCH 08/13] DBG: Print ELF headers of extension on Linux. --- distutils/tests/test_build_ext.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index a88554db..d2bf7f3f 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -166,6 +166,8 @@ def _test_xx(copy_so): ["readelf", "-d", xx.__file__], universal_newlines=True ) if not copy_so: + import pprint + pprint.pprint(so_headers) # Linked against a library in /usr/lib{,64} assert 'RPATH' not in so_headers and 'RUNPATH' not in so_headers else: From 9e873c2f24b44c9e3f9cfd4b5dd07e591929a80b Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Mon, 8 Jul 2024 07:02:59 -0400 Subject: [PATCH 09/13] TST: Clarify RPATH testing asserts. Still need to get the test compiling in the alternate location. Did I add -L/tmp? --- distutils/tests/test_build_ext.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index d2bf7f3f..d376404c 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -165,14 +165,20 @@ def _test_xx(copy_so): so_headers = subprocess.check_output( ["readelf", "-d", xx.__file__], universal_newlines=True ) + import pprint + pprint.pprint(so_headers) + rpaths = [ + rpath + for line in so_headers.split("\n") if "RPATH" in line or "RUNPATH" in line + for rpath in line.split()[2][1:-1].split(":") + ] if not copy_so: - import pprint - pprint.pprint(so_headers) + pprint.pprint(rpaths) # Linked against a library in /usr/lib{,64} - assert 'RPATH' not in so_headers and 'RUNPATH' not in so_headers + assert "/usr/lib" not in rpaths and "/usr/lib64" not in rpaths else: # Linked against a library in /tmp - assert 'RPATH' in so_headers or 'RUNPATH' in so_headers + assert "/tmp" in rpaths # The import is the real test here def test_solaris_enable_shared(self): From bd5b61400c501cab6fb5322dee113b91a91a0a9d Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Mon, 8 Jul 2024 07:06:24 -0400 Subject: [PATCH 10/13] FIX: Sort library names by length to avoid links I think /usr/lib/libz.so links to /usr/lib/libz.so.10, which in turn links to /usr/lib/libz.so.10.5.1 (numbers may differ), with the last being the actual library. Ensure that is the one I copy for linking. --- distutils/tests/test_build_ext.py | 1 + 1 file changed, 1 insertion(+) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index d376404c..b775a4dc 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -113,6 +113,7 @@ def test_build_ext(self, copy_so): ) elif sys.platform == 'linux': libz_so = glob.glob('/usr/lib*/libz.so*') + libz_so.sort(key=lambda lib_path: len(lib_path)) shutil.copyfile(libz_so[-1], '/tmp/libxx_z.so') xx_ext = Extension( From c9c550fc1c646b0b95d2f9973349a37d2b01c6f1 Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:58:09 -0400 Subject: [PATCH 11/13] BUG: Resolve links before sorting library paths. Maybe this will work better? I should look into whether ELF `so`s store their basename. --- distutils/tests/test_build_ext.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index b775a4dc..d7296f2c 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -1,7 +1,7 @@ import contextlib import glob import importlib -import os +import os.path import platform import re import shutil @@ -112,8 +112,8 @@ def test_build_ext(self, copy_so): runtime_library_dirs=['/usr/lib'], ) elif sys.platform == 'linux': - libz_so = glob.glob('/usr/lib*/libz.so*') - libz_so.sort(key=lambda lib_path: len(lib_path)) + libz_so = {os.path.realpath(name) for name in glob.iglob('/usr/lib*/libz.so*')} + libz_so = sorted(libz_so, key=lambda lib_path: len(lib_path)) shutil.copyfile(libz_so[-1], '/tmp/libxx_z.so') xx_ext = Extension( From 0a7fb2cde8f586b54b7da49cf3f093165232cd36 Mon Sep 17 00:00:00 2001 From: DWesl <22566757+DWesl@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:31:38 -0400 Subject: [PATCH 12/13] TST: Remove the part of the new test checking old behavior. Not the best solution, but I don't know how to check rpath in a manner that doesn't crash. --- distutils/tests/test_build_ext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index d7296f2c..27ccc7ab 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -96,7 +96,7 @@ class TestBuildExt(TempdirManager): def build_ext(self, *args, **kwargs): return build_ext(*args, **kwargs) - @pytest.mark.parametrize("copy_so", [False, True]) + @pytest.mark.parametrize("copy_so", [False]) def test_build_ext(self, copy_so): missing_compiler_executable() copy_xxmodule_c(self.tmp_dir) From c009de87ec226f9aee9fd1a1f9100874404d3871 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 17:05:14 -0400 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/compat/py38.py | 1 + distutils/tests/test_build_ext.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/distutils/compat/py38.py b/distutils/compat/py38.py index 2d442111..03ec73ef 100644 --- a/distutils/compat/py38.py +++ b/distutils/compat/py38.py @@ -14,6 +14,7 @@ def removeprefix(self, prefix): return self[len(prefix) :] else: return self[:] + else: def removesuffix(self, suffix): diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 27ccc7ab..f88d216c 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -112,7 +112,9 @@ def test_build_ext(self, copy_so): runtime_library_dirs=['/usr/lib'], ) elif sys.platform == 'linux': - libz_so = {os.path.realpath(name) for name in glob.iglob('/usr/lib*/libz.so*')} + libz_so = { + os.path.realpath(name) for name in glob.iglob('/usr/lib*/libz.so*') + } libz_so = sorted(libz_so, key=lambda lib_path: len(lib_path)) shutil.copyfile(libz_so[-1], '/tmp/libxx_z.so') @@ -167,10 +169,12 @@ def _test_xx(copy_so): ["readelf", "-d", xx.__file__], universal_newlines=True ) import pprint + pprint.pprint(so_headers) rpaths = [ rpath - for line in so_headers.split("\n") if "RPATH" in line or "RUNPATH" in line + for line in so_headers.split("\n") + if "RPATH" in line or "RUNPATH" in line for rpath in line.split()[2][1:-1].split(":") ] if not copy_so: