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..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 @@ -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.14.2-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.14.2-custom-ctypes.patch': '4c7659026a88961330c9629b0a1c23e9a8ed38108af765bfc0f537f55b7d610f'}, ] builddependencies = [ 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..a7006f664ce3 --- /dev/null +++ b/easybuild/easyconfigs/p/Python/Python-3.14.2-custom-ctypes.patch @@ -0,0 +1,154 @@ +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-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.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): + # 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: +@@ -221,7 +228,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' +@@ -242,7 +252,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 + +@@ -252,8 +267,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 +@@ -389,7 +407,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'], +@@ -399,7 +418,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 + +@@ -407,10 +427,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: +@@ -431,9 +458,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) + + + # Listing loaded libraries on other systems will try to use