From 2f21f4dd26b9733fb7d7faf9a014c5155ae72973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Tue, 20 Jan 2026 14:29:54 +0100 Subject: [PATCH 1/5] add patch_ctypes_ld_library_path --- .../easyconfigs/p/Python/Python-3.13.5-GCCcore-14.3.0.eb | 6 ++++++ .../easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/easybuild/easyconfigs/p/Python/Python-3.13.5-GCCcore-14.3.0.eb b/easybuild/easyconfigs/p/Python/Python-3.13.5-GCCcore-14.3.0.eb index 42f0a87edf8c..c4a47aca6d1b 100644 --- a/easybuild/easyconfigs/p/Python/Python-3.13.5-GCCcore-14.3.0.eb +++ b/easybuild/easyconfigs/p/Python/Python-3.13.5-GCCcore-14.3.0.eb @@ -14,10 +14,16 @@ patches = [ 'Python-3.12.3_avoid-tkinter-build.patch', 'Python-3.13.1_runshared-ld-preload.patch', ] + +# Like patches, but these will only be applied if EasyBuild is configured to filter $LD_LIBRARY_PATH +# In that scenario, ctypes needs to be patched since it heavily relies on $LD_LIBRARY_PATH to find libraries +patch_ctypes_ld_library_path = 'Python-3.11.5-custom-ctypes.patch' + checksums = [ {'Python-3.13.5.tgz': 'e6190f52699b534ee203d9f417bdbca05a92f23e35c19c691a50ed2942835385'}, {'Python-3.12.3_avoid-tkinter-build.patch': '34fa44ca67fc08d41c58db2e289317f12f32777a352a982dca2e63459fc089e3'}, {'Python-3.13.1_runshared-ld-preload.patch': 'ca9ec56c71aafa881e7ddf6fba23fbecc016be48c2d912e5ccd92962ddd38edf'}, + {'Python-3.11.5-custom-ctypes.patch': 'b29c22f47587460149e05296ff09b29bf790a83e2b3b13fb2f42f5f236ad8ea7'}, ] builddependencies = [ diff --git a/easybuild/easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb b/easybuild/easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb index 713c546fdd66..f9090c12c330 100644 --- a/easybuild/easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb +++ b/easybuild/easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb @@ -15,12 +15,18 @@ patches = [ 'Python-3.13.1_runshared-ld-preload.patch', 'Python-3.14.2_skip-inf-recursion-tests.patch', ] + +# Like patches, but these will only be applied if EasyBuild is configured to filter $LD_LIBRARY_PATH +# In that scenario, ctypes needs to be patched since it heavily relies on $LD_LIBRARY_PATH to find libraries +patch_ctypes_ld_library_path = 'Python-3.11.5-custom-ctypes.patch' + checksums = [ {'Python-3.14.2.tgz': 'c609e078adab90e2c6bacb6afafacd5eaf60cd94cf670f1e159565725fcd448d'}, {'Python-3.12.3_avoid-tkinter-build.patch': '34fa44ca67fc08d41c58db2e289317f12f32777a352a982dca2e63459fc089e3'}, {'Python-3.13.1_runshared-ld-preload.patch': 'ca9ec56c71aafa881e7ddf6fba23fbecc016be48c2d912e5ccd92962ddd38edf'}, {'Python-3.14.2_skip-inf-recursion-tests.patch': 'ff9d0951f169fa5c34f883093d7688b1823693e2950b6c4cfb21682ece33646a'}, + {'Python-3.11.5-custom-ctypes.patch': 'b29c22f47587460149e05296ff09b29bf790a83e2b3b13fb2f42f5f236ad8ea7'}, ] builddependencies = [ From c920be960638e1bba235257917ad6a133860ec4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Tue, 20 Jan 2026 16:44:29 +0100 Subject: [PATCH 2/5] modify ctypes patch for Python 3.14.2 --- .../Python/Python-3.14.2-custom-ctypes.patch | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 easybuild/easyconfigs/p/Python/Python-3.14.2-custom-ctypes.patch diff --git a/easybuild/easyconfigs/p/Python/Python-3.14.2-custom-ctypes.patch b/easybuild/easyconfigs/p/Python/Python-3.14.2-custom-ctypes.patch new file mode 100644 index 000000000000..8ff350fa3d2a --- /dev/null +++ b/easybuild/easyconfigs/p/Python/Python-3.14.2-custom-ctypes.patch @@ -0,0 +1,163 @@ +Ctypes heavily relies on LD_LIBRARY_PATH in it's find_library, ctypes.CDLL, ctypes.cdll.LoadLibrary functions. +This patch is meant for systems where LD_LIBRARY_PATH is filtered. It will rely on LIBRARY_PATH instead. +It makes the following essential changes: +- Whereever find_library searched LD_LIBRARY_PATH, LIBRARY_PATH will be searched instead +- find_library is adapted so that it returns the full library path, not just the library name, which replaces + https://github.com/easybuilders/easybuild-easyblocks/pull/3352 +- The internal function _findLib_gcc, one of the functions called by find_library to locate libraries, is adapted + so that it also works when called by full library name (e.g. libfoo.so.1) instead of the short name (foo) only + This is necessary since CDLL is typically called by full name, and needs to be able to call find_library 9see below) +- The initialization of CDLL is adapted so that it calls find_library. Then, it overwrites the name + with the full library path. This defers all the library localization issues to the (patched) find_library +Authors: Danilo Gonzalez (DoItNow group), Caspar van Leeuwen (SURF) and Alan O'cais (CECAM) +Updated for Python 3.14.2 by Bob Dröge (University of Groningen) and Caspar van Leeuwen (SURF) +diff -Nru Python-3.14.2.orig/Lib/ctypes/__init__.py Python-3.14.2/Lib/ctypes/__init__.py +--- Python-3.14.2.orig/Lib/ctypes/__init__.py 2025-12-05 17:49:16.000000000 +0100 ++++ Python-3.14.2/Lib/ctypes/__init__.py 2026-01-20 16:41:39.177935966 +0100 +@@ -18,6 +18,9 @@ + + from struct import calcsize as _calcsize + ++# Add util to use find_library capabilities ++from ctypes import util ++ + if __version__ != _ctypes_version: + raise Exception("Version number mismatch", __version__, _ctypes_version) + +@@ -469,6 +472,12 @@ + """ + if name and name.endswith(")") and ".a(" in name: + mode |= _os.RTLD_MEMBER | _os.RTLD_NOW ++ # define CDLL instance name as fullpath ++ if _os.name == "posix": ++ if name: ++ fullpath = util.find_library(name) ++ if fullpath is not None: ++ name = fullpath + self._name = name + return _dlopen(name, mode) + +diff -Nru Python-3.11.5.orig/Lib/ctypes/util.py Python-3.11.5/Lib/ctypes/util.py +--- Python-3.11.5.orig/Lib/ctypes/util.py 2025-10-13 15:58:38.306949000 +0200 ++++ Python-3.11.5/Lib/ctypes/util.py 2025-10-13 17:17:07.589997890 +0200 +@@ -99,12 +99,19 @@ + with open(filename, 'br') as thefile: + return thefile.read(4) == elf_header + +- def _findLib_gcc(name): ++ def _findLib_gcc(name, name_is_fullname=False): + # Run GCC's linker with the -t (aka --trace) option and examine the + # library name it prints out. The GCC command will fail because we + # haven't supplied a proper program with main(), but that does not + # matter. +- expr = os.fsencode(r'[^\(\)\s]*lib%s\.[^\(\)\s]*' % re.escape(name)) ++ # If the name is a full library name (e.g. libfoo.so), this function calls gcc with -l: instead of ++ # -l and uses a slightly more strict regular expression to avoid matching the error ++ # '/path/to/ld: cannot find -l:libfoo.so: No such file or directory' ++ # since we only want a regex match if the library exists ++ if name_is_fullname: ++ expr = os.fsencode(r'^[^\(\)\s]*%s[^\(\)\s]*' % re.escape(name)) ++ else: ++ expr = os.fsencode(r'[^\(\)\s]*lib%s\.[^\(\)\s]*' % re.escape(name)) + + c_compiler = shutil.which('gcc') + if not c_compiler: +@@ -115,7 +122,10 @@ + + temp = tempfile.NamedTemporaryFile() + try: +- args = [c_compiler, '-Wl,-t', '-o', temp.name, '-l' + name] ++ if name_is_fullname: ++ args = [c_compiler, '-Wl,-t', '-o', temp.name, '-l:' + name] ++ else: ++ args = [c_compiler, '-Wl,-t', '-o', temp.name, '-l' + name] + + env = dict(os.environ) + env['LC_ALL'] = 'C' +@@ -136,7 +146,12 @@ + # Raised if the file was already removed, which is the normal + # behaviour of GCC if linking fails + pass +- res = re.findall(expr, trace) ++ # If name_is_fullname, the regex in expr starts wity ^. ++ # Use re.MULTILINE, to make surethat ^ matches the start of EACH line in trace ++ if name_is_fullname: ++ res = re.findall(expr, trace, re.MULTILINE) ++ else: ++ res = re.findall(expr, trace) + if not res: + return None + +@@ -146,8 +161,11 @@ + # shared objects. See bpo-41976 for more details + if not _is_elf(file): + continue +- return os.fsdecode(file) +- ++ file = os.fsdecode(file) ++ # Avoid returning CUDA stubs libraries, as those are not intended for runtime use ++ if re.search(r'cuda.*stubs', file, re.IGNORECASE): ++ continue ++ return file + + if sys.platform == "sunos5": + # use /usr/ccs/bin/dump on solaris +@@ -283,7 +301,8 @@ + abi_type = mach_map.get(machine, 'libc6') + + # XXX assuming GLIBC's ldconfig (with option -p) +- regex = r'\s+(lib%s\.[^\s]+)\s+\(%s' ++ # Regular expresion that captures complete line of ldconfig -p output that matches with library name. ++ regex = r'\s+(lib%s\.[^\s]+)\s+\(%s\)\s+=>\s+(\S+)' + regex = os.fsencode(regex % (re.escape(name), abi_type)) + try: + with subprocess.Popen(['/sbin/ldconfig', '-p'], +@@ -293,7 +312,8 @@ + env={'LC_ALL': 'C', 'LANG': 'C'}) as p: + res = re.search(regex, p.stdout.read()) + if res: +- return os.fsdecode(res.group(1)) ++ # return the regex second group, that is the full path to the library ++ return os.fsdecode(res.group(2)) + except OSError: + pass + +@@ -301,10 +321,17 @@ + # See issue #9998 for why this is needed + expr = r'[^\(\)\s]*lib%s\.[^\(\)\s]*' % re.escape(name) + cmd = ['ld', '-t'] +- libpath = os.environ.get('LD_LIBRARY_PATH') ++ # use LIBRARY_PATH instead of LD_LIBRARY_PATH to use EB provided shared libraries ++ libpath = os.environ.get('LIBRARY_PATH') + if libpath: + for d in libpath.split(':'): +- cmd.extend(['-L', d]) ++ # Avoid picking up CUDA stubs libraries, as those are not intended for runtime use ++ parts = d.split('/') ++ # We want to add 'd' in all cases EXCEPT if the path contains both CUDA (as partial ++ # directory name) and stubs (as full directory name). I.e. /my/bar/stubs and /my/cudafoo ++ # will be added to cmd, but /my/cudafoo/bar/stubs won't be ++ if not (any('cuda' in p.lower() for p in parts) and 'stubs' in parts): ++ cmd.extend(['-L', d]) + cmd.extend(['-o', os.devnull, '-l%s' % name]) + result = None + try: +@@ -325,9 +352,15 @@ + return result + + def find_library(name): +- # See issue #9998 +- return _findSoname_ldconfig(name) or \ +- _get_soname(_findLib_gcc(name)) or _get_soname(_findLib_ld(name)) ++ # Redefine find_library function, it will return the provided name if ++ # path exist, else it will use the set of functions to find the full library path. ++ # it will return the one that has a match. ++ if os.path.isabs(name) and os.path.exists(name): ++ return name ++ else: ++ return _findSoname_ldconfig(name) or \ ++ _findLib_gcc(name) or _findLib_ld(name) or \ ++ _findLib_gcc(name, name_is_fullname=True) + + ################################################################ + # test code From b738bc536a02cd77124f1eb154bb99b8d3936ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Tue, 20 Jan 2026 16:44:37 +0100 Subject: [PATCH 3/5] use modified ctypes patch --- .../easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/easybuild/easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb b/easybuild/easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb index f9090c12c330..4cd0c9b3bfe3 100644 --- a/easybuild/easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb +++ b/easybuild/easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb @@ -18,7 +18,7 @@ patches = [ # Like patches, but these will only be applied if EasyBuild is configured to filter $LD_LIBRARY_PATH # In that scenario, ctypes needs to be patched since it heavily relies on $LD_LIBRARY_PATH to find libraries -patch_ctypes_ld_library_path = 'Python-3.11.5-custom-ctypes.patch' +patch_ctypes_ld_library_path = 'Python-3.14.2-custom-ctypes.patch' checksums = [ {'Python-3.14.2.tgz': 'c609e078adab90e2c6bacb6afafacd5eaf60cd94cf670f1e159565725fcd448d'}, @@ -26,7 +26,7 @@ checksums = [ {'Python-3.13.1_runshared-ld-preload.patch': 'ca9ec56c71aafa881e7ddf6fba23fbecc016be48c2d912e5ccd92962ddd38edf'}, {'Python-3.14.2_skip-inf-recursion-tests.patch': 'ff9d0951f169fa5c34f883093d7688b1823693e2950b6c4cfb21682ece33646a'}, - {'Python-3.11.5-custom-ctypes.patch': 'b29c22f47587460149e05296ff09b29bf790a83e2b3b13fb2f42f5f236ad8ea7'}, + {'Python-3.14.2-custom-ctypes.patch': '05191402bbe43f287d5fa64ee019da286f98f7fa80753e7abeff05a57e7ce800'}, ] builddependencies = [ From 9cd5dc48ecebc158c9475b724e4154d8a33ea9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Wed, 21 Jan 2026 10:42:25 +0100 Subject: [PATCH 4/5] fix patch, make import lazy to prevent circular import --- .../Python/Python-3.14.2-custom-ctypes.patch | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/easybuild/easyconfigs/p/Python/Python-3.14.2-custom-ctypes.patch b/easybuild/easyconfigs/p/Python/Python-3.14.2-custom-ctypes.patch index 8ff350fa3d2a..a7006f664ce3 100644 --- a/easybuild/easyconfigs/p/Python/Python-3.14.2-custom-ctypes.patch +++ b/easybuild/easyconfigs/p/Python/Python-3.14.2-custom-ctypes.patch @@ -13,36 +13,27 @@ Authors: Danilo Gonzalez (DoItNow group), Caspar van Leeuwen (SURF) and Alan O'c Updated for Python 3.14.2 by Bob Dröge (University of Groningen) and Caspar van Leeuwen (SURF) diff -Nru Python-3.14.2.orig/Lib/ctypes/__init__.py Python-3.14.2/Lib/ctypes/__init__.py --- Python-3.14.2.orig/Lib/ctypes/__init__.py 2025-12-05 17:49:16.000000000 +0100 -+++ Python-3.14.2/Lib/ctypes/__init__.py 2026-01-20 16:41:39.177935966 +0100 -@@ -18,6 +18,9 @@ - - from struct import calcsize as _calcsize - -+# Add util to use find_library capabilities -+from ctypes import util -+ - if __version__ != _ctypes_version: - raise Exception("Version number mismatch", __version__, _ctypes_version) - -@@ -469,6 +472,12 @@ ++++ Python-3.14.2/Lib/ctypes/__init__.py 2026-01-21 09:44:04.003467039 +0100 +@@ -469,6 +469,13 @@ """ if name and name.endswith(")") and ".a(" in name: mode |= _os.RTLD_MEMBER | _os.RTLD_NOW + # define CDLL instance name as fullpath + if _os.name == "posix": + if name: ++ from ctypes import util + fullpath = util.find_library(name) + if fullpath is not None: + name = fullpath self._name = name return _dlopen(name, mode) - -diff -Nru Python-3.11.5.orig/Lib/ctypes/util.py Python-3.11.5/Lib/ctypes/util.py ---- Python-3.11.5.orig/Lib/ctypes/util.py 2025-10-13 15:58:38.306949000 +0200 -+++ Python-3.11.5/Lib/ctypes/util.py 2025-10-13 17:17:07.589997890 +0200 -@@ -99,12 +99,19 @@ - with open(filename, 'br') as thefile: - return thefile.read(4) == elf_header + +diff -Nru Python-3.14.2.orig/Lib/ctypes/util.py Python-3.14.2/Lib/ctypes/util.py +--- Python-3.14.2.orig/Lib/ctypes/util.py 2025-12-05 17:49:16.000000000 +0100 ++++ Python-3.14.2/Lib/ctypes/util.py 2026-01-21 09:47:05.102745349 +0100 +@@ -205,12 +205,19 @@ + except FileNotFoundError: + return False - def _findLib_gcc(name): + def _findLib_gcc(name, name_is_fullname=False): @@ -62,7 +53,7 @@ diff -Nru Python-3.11.5.orig/Lib/ctypes/util.py Python-3.11.5/Lib/ctypes/util.py c_compiler = shutil.which('gcc') if not c_compiler: -@@ -115,7 +122,10 @@ +@@ -221,7 +228,10 @@ temp = tempfile.NamedTemporaryFile() try: @@ -74,7 +65,7 @@ diff -Nru Python-3.11.5.orig/Lib/ctypes/util.py Python-3.11.5/Lib/ctypes/util.py env = dict(os.environ) env['LC_ALL'] = 'C' -@@ -136,7 +146,12 @@ +@@ -242,7 +252,12 @@ # Raised if the file was already removed, which is the normal # behaviour of GCC if linking fails pass @@ -88,7 +79,7 @@ diff -Nru Python-3.11.5.orig/Lib/ctypes/util.py Python-3.11.5/Lib/ctypes/util.py if not res: return None -@@ -146,8 +161,11 @@ +@@ -252,8 +267,11 @@ # shared objects. See bpo-41976 for more details if not _is_elf(file): continue @@ -102,7 +93,7 @@ diff -Nru Python-3.11.5.orig/Lib/ctypes/util.py Python-3.11.5/Lib/ctypes/util.py if sys.platform == "sunos5": # use /usr/ccs/bin/dump on solaris -@@ -283,7 +301,8 @@ +@@ -389,7 +407,8 @@ abi_type = mach_map.get(machine, 'libc6') # XXX assuming GLIBC's ldconfig (with option -p) @@ -112,7 +103,7 @@ diff -Nru Python-3.11.5.orig/Lib/ctypes/util.py Python-3.11.5/Lib/ctypes/util.py regex = os.fsencode(regex % (re.escape(name), abi_type)) try: with subprocess.Popen(['/sbin/ldconfig', '-p'], -@@ -293,7 +312,8 @@ +@@ -399,7 +418,8 @@ env={'LC_ALL': 'C', 'LANG': 'C'}) as p: res = re.search(regex, p.stdout.read()) if res: @@ -122,7 +113,7 @@ diff -Nru Python-3.11.5.orig/Lib/ctypes/util.py Python-3.11.5/Lib/ctypes/util.py except OSError: pass -@@ -301,10 +321,17 @@ +@@ -407,10 +427,17 @@ # See issue #9998 for why this is needed expr = r'[^\(\)\s]*lib%s\.[^\(\)\s]*' % re.escape(name) cmd = ['ld', '-t'] @@ -142,7 +133,7 @@ diff -Nru Python-3.11.5.orig/Lib/ctypes/util.py Python-3.11.5/Lib/ctypes/util.py cmd.extend(['-o', os.devnull, '-l%s' % name]) result = None try: -@@ -325,9 +352,15 @@ +@@ -431,9 +458,15 @@ return result def find_library(name): @@ -159,5 +150,5 @@ diff -Nru Python-3.11.5.orig/Lib/ctypes/util.py Python-3.11.5/Lib/ctypes/util.py + _findLib_gcc(name) or _findLib_ld(name) or \ + _findLib_gcc(name, name_is_fullname=True) - ################################################################ - # test code + + # Listing loaded libraries on other systems will try to use From 37e455831442a1b81e000ecae5a7ef2da4559ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bob=20Dr=C3=B6ge?= Date: Wed, 21 Jan 2026 10:42:32 +0100 Subject: [PATCH 5/5] update checksum of ctypes patch --- easybuild/easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easybuild/easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb b/easybuild/easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb index 4cd0c9b3bfe3..c881279f8ab7 100644 --- a/easybuild/easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb +++ b/easybuild/easyconfigs/p/Python/Python-3.14.2-GCCcore-15.2.0.eb @@ -26,7 +26,7 @@ checksums = [ {'Python-3.13.1_runshared-ld-preload.patch': 'ca9ec56c71aafa881e7ddf6fba23fbecc016be48c2d912e5ccd92962ddd38edf'}, {'Python-3.14.2_skip-inf-recursion-tests.patch': 'ff9d0951f169fa5c34f883093d7688b1823693e2950b6c4cfb21682ece33646a'}, - {'Python-3.14.2-custom-ctypes.patch': '05191402bbe43f287d5fa64ee019da286f98f7fa80753e7abeff05a57e7ce800'}, + {'Python-3.14.2-custom-ctypes.patch': '4c7659026a88961330c9629b0a1c23e9a8ed38108af765bfc0f537f55b7d610f'}, ] builddependencies = [