Skip to content

Commit 1910739

Browse files
authored
Merge pull request #3860 from casparvl/use_patch_when_filter_ld_library_path
add support to Python easyblock for conditionally adding patches when EasyBuild is configured to filter `$LD_LIBRARY_PATH`
2 parents 25ac4a6 + 15fdc3b commit 1910739

File tree

1 file changed

+122
-27
lines changed

1 file changed

+122
-27
lines changed

easybuild/easyblocks/p/python.py

Lines changed: 122 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,13 @@ def extra_options():
302302
'ulimit_unlimited': [False, "Ensure stack size limit is set to '%s' during build" % UNLIMITED, CUSTOM],
303303
'use_lto': [None, "Build with Link Time Optimization (>= v3.7.0, potentially unstable on some toolchains). "
304304
"If None: auto-detect based on toolchain compiler (version)", CUSTOM],
305+
'patch_ctypes_ld_library_path': [None,
306+
"The ctypes module strongly relies on LD_LIBRARY_PATH to find "
307+
"libraries. This allows specifying a patch that will only be "
308+
"applied if EasyBuild is configured to filter LD_LIBRARY_PATH, in "
309+
"order to make sure ctypes can still find libraries without it. "
310+
"Please make sure to add the checksum for this patch to 'checksums'.",
311+
CUSTOM],
305312
}
306313
return ConfigureMake.extra_options(extra_vars)
307314

@@ -345,6 +352,64 @@ def _get_pip_ext_version(self):
345352
return ext[1]
346353
return None
347354

355+
def fetch_step(self, *args, **kwargs):
356+
"""
357+
Custom fetch step for Python.
358+
359+
Add patch specified in patch_ctypes_ld_library_path to list of patches if
360+
EasyBuild is configured to filter $LD_LIBRARY_PATH (and is configured not to filter $LIBRARY_PATH).
361+
This needs to be done in (or before) the fetch step to ensure that those patches are also fetched.
362+
"""
363+
# If we filter out $LD_LIBRARY_PATH (not unusual when using rpath), ctypes is not able to dynamically load
364+
# libraries installed with EasyBuild (see https://github.com/EESSI/software-layer/issues/192).
365+
# If EasyBuild is configured to filter $LD_LIBRARY_PATH the patch specified in 'patch_ctypes_ld_library_path'
366+
# are added to the list of patches. Also, we add the checksums_filter_ld_library_path to the checksums list in
367+
# that case.
368+
# This mechanism e.g. makes sure we can patch ctypes, which normally strongly relies on $LD_LIBRARY_PATH to find
369+
# libraries. But, we want to do the patching conditionally on EasyBuild configuration (i.e. which env vars
370+
# are filtered), hence this setup based on the custom easyconfig parameter 'patch_ctypes_ld_library_path'
371+
filtered_env_vars = build_option('filter_env_vars') or []
372+
patch_ctypes_ld_library_path = self.cfg.get('patch_ctypes_ld_library_path')
373+
if (
374+
'LD_LIBRARY_PATH' in filtered_env_vars and
375+
'LIBRARY_PATH' not in filtered_env_vars and
376+
patch_ctypes_ld_library_path
377+
):
378+
# Some sanity checking so we can raise an early and clear error if needed
379+
# We expect a (one) checksum for the patch_ctypes_ld_library_path
380+
checksums = self.cfg['checksums']
381+
sources = self.cfg['sources']
382+
patches = self.cfg.get('patches')
383+
len_patches = len(patches) if patches else 0
384+
if len_patches + len(sources) + 1 == len(checksums):
385+
msg = "EasyBuild was configured to filter $LD_LIBRARY_PATH (and not to filter $LIBRARY_PATH). "
386+
msg += "The ctypes module relies heavily on $LD_LIBRARY_PATH for locating its libraries. "
387+
msg += "The following patch will be applied to make sure ctypes.CDLL, ctypes.cdll.LoadLibrary "
388+
msg += f"and ctypes.util.find_library will still work correctly: {patch_ctypes_ld_library_path}."
389+
self.log.info(msg)
390+
self.log.info(f"Original list of patches: {self.cfg['patches']}")
391+
self.log.info(f"Patch to be added: {patch_ctypes_ld_library_path}")
392+
self.cfg.update('patches', [patch_ctypes_ld_library_path])
393+
self.log.info(f"Updated list of patches: {self.cfg['patches']}")
394+
else:
395+
msg = "The length of 'checksums' (%s) is not equal to the total amount of sources (%s) + patches (%s). "
396+
msg += "Did you forget to add a checksum for patch_ctypes_ld_library_path?"
397+
raise EasyBuildError(msg, len(checksums), len(sources), len(len_patches + 1))
398+
# If LD_LIBRARY_PATH is filtered, but no patch is specified, warn the user that his may not work
399+
elif (
400+
'LD_LIBRARY_PATH' in filtered_env_vars and
401+
'LIBRARY_PATH' not in filtered_env_vars and
402+
not patch_ctypes_ld_library_path
403+
):
404+
msg = "EasyBuild was configured to filter $LD_LIBRARY_PATH (and not to filter $LIBRARY_PATH). "
405+
msg += "However, no patch for ctypes was specified through 'patch_ctypes_ld_library_path' in the "
406+
msg += "easyconfig. Note that ctypes.util.find_library, ctypes.CDLL and ctypes.cdll.LoadLibrary heavily "
407+
msg += "rely on $LD_LIBRARY_PATH. Without the patch, a setup without $LD_LIBRARY_PATH will likely not work "
408+
msg += "correctly."
409+
self.log.warning(msg)
410+
411+
super().fetch_step(*args, **kwargs)
412+
348413
def patch_step(self, *args, **kwargs):
349414
"""
350415
Custom patch step for Python:
@@ -357,33 +422,6 @@ def patch_step(self, *args, **kwargs):
357422
# Ignore user site dir. -E ignores PYTHONNOUSERSITE, so we have to add -s
358423
apply_regex_substitutions('configure', [(r"(PYTHON_FOR_BUILD=.*-E)'", r"\1 -s'")])
359424

360-
# If we filter out LD_LIBRARY_PATH (not unusual when using rpath), ctypes is not able to dynamically load
361-
# libraries installed with EasyBuild (see https://github.com/EESSI/software-layer/issues/192).
362-
# ctypes is using GCC (and therefore LIBRARY_PATH) to figure out the full location but then only returns the
363-
# soname, instead let's return the full path in this particular scenario
364-
filtered_env_vars = build_option('filter_env_vars') or []
365-
if 'LD_LIBRARY_PATH' in filtered_env_vars and 'LIBRARY_PATH' not in filtered_env_vars:
366-
ctypes_util_py = os.path.join("Lib", "ctypes", "util.py")
367-
orig_gcc_so_name = None
368-
# Let's do this incrementally since we are going back in time
369-
if LooseVersion(self.version) >= "3.9.1":
370-
# From 3.9.1 to at least v3.12.4 there is only one match for this line
371-
orig_gcc_so_name = "_get_soname(_findLib_gcc(name)) or _get_soname(_findLib_ld(name))"
372-
if orig_gcc_so_name:
373-
orig_gcc_so_name_regex = r'(\s*)' + re.escape(orig_gcc_so_name) + r'(\s*)'
374-
# _get_soname() takes the full path as an argument and uses objdump to get the SONAME field from
375-
# the shared object file. The presence or absence of the SONAME field in the ELF header of a shared
376-
# library is influenced by how the library is compiled and linked. For manually built libraries we
377-
# may be lacking this field, this approach also solves that problem.
378-
updated_gcc_so_name = (
379-
"_findLib_gcc(name) or _findLib_ld(name)"
380-
)
381-
apply_regex_substitutions(
382-
ctypes_util_py,
383-
[(orig_gcc_so_name_regex, r'\1' + updated_gcc_so_name + r'\2')],
384-
on_missing_match=ERROR
385-
)
386-
387425
# if we're installing Python with an alternate sysroot,
388426
# we need to patch setup.py which includes hardcoded paths like /usr/include and /lib64;
389427
# this fixes problems like not being able to build the _ssl module ("Could not build the ssl module")
@@ -701,6 +739,57 @@ def install_step(self):
701739
symlink(target_lib_dynload, lib_dynload)
702740
change_dir(cwd)
703741

742+
def _sanity_check_ctypes_ld_library_path_patch(self):
743+
"""
744+
Check that ctypes.util.find_library and ctypes.CDLL work as expected.
745+
When $LD_LIBRARY_PATH is filtered, a patch is required for this to work correctly
746+
(see patch_ctypes_ld_library_path).
747+
"""
748+
# Try find_library first, since ctypes.CDLL relies on that to work correctly
749+
cmd = "python -c 'from ctypes import util; print(util.find_library(\"libpython3.so\"))'"
750+
res = run_shell_cmd(cmd)
751+
out = res.output.strip()
752+
escaped_python_root = re.escape(self.installdir)
753+
pattern = rf"^{escaped_python_root}.*libpython3\.so$"
754+
match = re.match(pattern, out)
755+
self.log.debug(f"Matching regular expression pattern {pattern} to string {out}")
756+
if match:
757+
msg = "Call to ctypes.util.find_library('libpython3.so') successfully found libpython3.so under "
758+
msg += f"the installation prefix of the current Python installation ({self.installdir}). "
759+
if self.cfg.get('patch_ctypes_ld_library_path'):
760+
msg += "This indicates that the patch that fixes ctypes when EasyBuild is "
761+
msg += "configured to filter $LD_LIBRARY_PATH was applied succesfully."
762+
self.log.info(msg)
763+
else:
764+
msg = "Finding the library libpython3.so using ctypes.util.find_library('libpython3.so') failed. "
765+
msg += "The ctypes Python module requires a patch when EasyBuild is configured to filter $LD_LIBRARY_PATH. "
766+
msg += "Please check if you specified a patch through patch_ctypes_ld_library_path and check "
767+
msg += "the logs to see if it applied correctly."
768+
raise EasyBuildError(msg)
769+
# Now that we know find_library was patched correctly, check if ctypes.CDLL is also patched correctly
770+
cmd = "python -c 'import ctypes; print(ctypes.CDLL(\"libpython3.so\"))'"
771+
res = run_shell_cmd(cmd)
772+
out = res.output.strip()
773+
pattern = rf"^<CDLL '{escaped_python_root}.*libpython3\.so', handle [a-f0-9]+ at 0x[a-f0-9]+>$"
774+
match = re.match(pattern, out)
775+
self.log.debug(f"Matching regular expression pattern {pattern} to string {out}")
776+
if match:
777+
msg = "Call to ctypes.CDLL('libpython3.so') succesfully opened libpython3.so. "
778+
if self.cfg.get('patch_ctypes_ld_library_path'):
779+
msg += "This indicates that the patch that fixes ctypes when $LD_LIBRARY_PATH is not set "
780+
msg += "was applied successfully."
781+
self.log.info(msg)
782+
msg = "Call to ctypes.CDLL('libpython3.so') succesfully opened libpython3.so. "
783+
if self.cfg.get('patch_ctypes_ld_library_path'):
784+
msg += "This indicates that the patch that fixes ctypes when $LD_LIBRARY_PATH is not set "
785+
msg += "was applied successfully."
786+
else:
787+
msg = "Opening of libpython3.so using ctypes.CDLL('libpython3.so') failed. "
788+
msg += "The ctypes Python module requires a patch when EasyBuild is configured to filter $LD_LIBRARY_PATH. "
789+
msg += "Please check if you specified a patch through patch_ctypes_ld_library_path and check "
790+
msg += "the logs to see if it applied correctly."
791+
raise EasyBuildError(msg)
792+
704793
def _sanity_check_ebpythonprefixes(self):
705794
"""Check that EBPYTHONPREFIXES works"""
706795
temp_prefix = tempfile.mkdtemp(suffix='-tmp-prefix')
@@ -765,6 +854,12 @@ def sanity_check_step(self):
765854
if self.cfg.get('ebpythonprefixes'):
766855
self._sanity_check_ebpythonprefixes()
767856

857+
# If the conditions for applying the patch specified through patch_ctypes_ld_library_path are met,
858+
# check that a patch was applied and indeed fixed the issue
859+
filtered_env_vars = build_option('filter_env_vars') or []
860+
if 'LD_LIBRARY_PATH' in filtered_env_vars and 'LIBRARY_PATH' not in filtered_env_vars:
861+
self._sanity_check_ctypes_ld_library_path_patch()
862+
768863
pyver = 'python' + self.pyshortver
769864
custom_paths = {
770865
'files': [

0 commit comments

Comments
 (0)