Skip to content
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e1425a4
Disable soname substitution
dagonzalezfo Jul 25, 2025
1e03933
Update python.py
dagonzalezfo Jul 25, 2025
197fb18
Update python.py
dagonzalezfo Jul 25, 2025
55298c1
remove not used import
dagonzalezfo Jul 25, 2025
d8af2d2
Conditionally apply patches based on configuration of easybuild for f…
Jul 29, 2025
125c23f
Add forgotten commas
Jul 29, 2025
b1f3fcd
Use the correct way to append to the patches and checksums lists
Jul 29, 2025
c2bf826
Do this in the fetch step: it needs to be done early enough, otherwis…
Jul 29, 2025
8b17a9a
Clarify descriptions, remove commented section
Jul 30, 2025
d32dc4c
Make hound happy
Jul 30, 2025
993f991
Update easybuild/easyblocks/p/python.py
casparvl Jul 30, 2025
9fd0b9e
Fix too long lines
Jul 30, 2025
0c6d7ab
Changes based on @boegels review. Reducing to 1 new custom configurat…
Aug 27, 2025
4ec46bb
Make explicit remark where the checksum for the custom patch should go
Aug 27, 2025
817c51c
Fix config variable name
Aug 27, 2025
67b35ec
Make sure to handle the default value behaviour correctly
Aug 27, 2025
4510e29
Remove white line
Aug 27, 2025
3b64e2b
Process comments from @boegel. Rename to custom option to patch_ctype…
Oct 13, 2025
c622e77
Fix hound issues
Oct 13, 2025
247c394
Add sanity check to see if a ctypes.CDLL call on libpython3.so works,…
Oct 14, 2025
cbab116
Call the actual function for the specific ctypes sanity check in the …
Oct 14, 2025
13471fd
Rephrase for clarity
Oct 15, 2025
c9f47a3
Add warning for users that filter LD_LIBRARY_PATH, but do not patch c…
Oct 15, 2025
51cb613
First try to do a find_library call with a full library name to see i…
Oct 17, 2025
dd442a3
relax condition in Python easyblock to run sanity check on ctypes cor…
boegel Oct 22, 2025
15fdc3b
Merge pull request #5 from boegel/use_patch_when_filter_ld_library_path
casparvl Oct 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 122 additions & 27 deletions easybuild/easyblocks/p/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,13 @@ def extra_options():
'ulimit_unlimited': [False, "Ensure stack size limit is set to '%s' during build" % UNLIMITED, CUSTOM],
'use_lto': [None, "Build with Link Time Optimization (>= v3.7.0, potentially unstable on some toolchains). "
"If None: auto-detect based on toolchain compiler (version)", CUSTOM],
'patch_ctypes_ld_library_path': [None,
"The ctypes module strongly relies on LD_LIBRARY_PATH to find "
"libraries. This allows specifying a patch that will only be "
"applied if EasyBuild is configured to filter LD_LIBRARY_PATH, in "
"order to make sure ctypes can still find libraries without it. "
"Please make sure to add the checksum for this patch to 'checksums'.",
CUSTOM],
}
return ConfigureMake.extra_options(extra_vars)

Expand Down Expand Up @@ -345,6 +352,64 @@ def _get_pip_ext_version(self):
return ext[1]
return None

def fetch_step(self, *args, **kwargs):
"""
Custom fetch step for Python.

Add patch specified in patch_ctypes_ld_library_path to list of patches if
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EasyBuild is configured to filter $LD_LIBRARY_PATH (and is configured not to filter $LIBRARY_PATH).
This needs to be done in (or before) the fetch step to ensure that those patches are also fetched.
"""
# If we filter out $LD_LIBRARY_PATH (not unusual when using rpath), ctypes is not able to dynamically load
# libraries installed with EasyBuild (see https://github.com/EESSI/software-layer/issues/192).
# If EasyBuild is configured to filter $LD_LIBRARY_PATH the patch specified in 'patch_ctypes_ld_library_path'
# are added to the list of patches. Also, we add the checksums_filter_ld_library_path to the checksums list in
# that case.
# This mechanism e.g. makes sure we can patch ctypes, which normally strongly relies on $LD_LIBRARY_PATH to find
# libraries. But, we want to do the patching conditionally on EasyBuild configuration (i.e. which env vars
# are filtered), hence this setup based on the custom easyconfig parameter 'patch_ctypes_ld_library_path'
filtered_env_vars = build_option('filter_env_vars') or []
patch_ctypes_ld_library_path = self.cfg.get('patch_ctypes_ld_library_path')
if (
'LD_LIBRARY_PATH' in filtered_env_vars and
'LIBRARY_PATH' not in filtered_env_vars and
patch_ctypes_ld_library_path
):
# Some sanity checking so we can raise an early and clear error if needed
# We expect a (one) checksum for the patch_ctypes_ld_library_path
checksums = self.cfg['checksums']
sources = self.cfg['sources']
patches = self.cfg.get('patches')
len_patches = len(patches) if patches else 0
if len_patches + len(sources) + 1 == len(checksums):
msg = "EasyBuild was configured to filter $LD_LIBRARY_PATH (and not to filter $LIBRARY_PATH). "
msg += "The ctypes module relies heavily on $LD_LIBRARY_PATH for locating its libraries. "
msg += "The following patch will be applied to make sure ctypes.CDLL, ctypes.cdll.LoadLibrary "
msg += f"and ctypes.util.find_library will still work correctly: {patch_ctypes_ld_library_path}."
self.log.info(msg)
self.log.info(f"Original list of patches: {self.cfg['patches']}")
self.log.info(f"Patch to be added: {patch_ctypes_ld_library_path}")
self.cfg.update('patches', [patch_ctypes_ld_library_path])
self.log.info(f"Updated list of patches: {self.cfg['patches']}")
else:
msg = "The length of 'checksums' (%s) is not equal to the total amount of sources (%s) + patches (%s). "
msg += "Did you forget to add a checksum for patch_ctypes_ld_library_path?"
raise EasyBuildError(msg, len(checksums), len(sources), len(len_patches + 1))
# If LD_LIBRARY_PATH is filtered, but no patch is specified, warn the user that his may not work
elif (
'LD_LIBRARY_PATH' in filtered_env_vars and
'LIBRARY_PATH' not in filtered_env_vars and
not patch_ctypes_ld_library_path
):
msg = "EasyBuild was configured to filter $LD_LIBRARY_PATH (and not to filter $LIBRARY_PATH). "
msg += "However, no patch for ctypes was specified through 'patch_ctypes_ld_library_path' in the "
msg += "easyconfig. Note that ctypes.util.find_library, ctypes.CDLL and ctypes.cdll.LoadLibrary heavily "
msg += "rely on $LD_LIBRARY_PATH. Without the patch, a setup without $LD_LIBRARY_PATH will likely not work "
msg += "correctly."
self.log.warning(msg)

super().fetch_step(*args, **kwargs)

def patch_step(self, *args, **kwargs):
"""
Custom patch step for Python:
Expand All @@ -357,33 +422,6 @@ def patch_step(self, *args, **kwargs):
# Ignore user site dir. -E ignores PYTHONNOUSERSITE, so we have to add -s
apply_regex_substitutions('configure', [(r"(PYTHON_FOR_BUILD=.*-E)'", r"\1 -s'")])

# If we filter out LD_LIBRARY_PATH (not unusual when using rpath), ctypes is not able to dynamically load
# libraries installed with EasyBuild (see https://github.com/EESSI/software-layer/issues/192).
# ctypes is using GCC (and therefore LIBRARY_PATH) to figure out the full location but then only returns the
# soname, instead let's return the full path in this particular scenario
filtered_env_vars = build_option('filter_env_vars') or []
if 'LD_LIBRARY_PATH' in filtered_env_vars and 'LIBRARY_PATH' not in filtered_env_vars:
ctypes_util_py = os.path.join("Lib", "ctypes", "util.py")
orig_gcc_so_name = None
# Let's do this incrementally since we are going back in time
if LooseVersion(self.version) >= "3.9.1":
# From 3.9.1 to at least v3.12.4 there is only one match for this line
orig_gcc_so_name = "_get_soname(_findLib_gcc(name)) or _get_soname(_findLib_ld(name))"
if orig_gcc_so_name:
orig_gcc_so_name_regex = r'(\s*)' + re.escape(orig_gcc_so_name) + r'(\s*)'
# _get_soname() takes the full path as an argument and uses objdump to get the SONAME field from
# the shared object file. The presence or absence of the SONAME field in the ELF header of a shared
# library is influenced by how the library is compiled and linked. For manually built libraries we
# may be lacking this field, this approach also solves that problem.
updated_gcc_so_name = (
"_findLib_gcc(name) or _findLib_ld(name)"
)
apply_regex_substitutions(
ctypes_util_py,
[(orig_gcc_so_name_regex, r'\1' + updated_gcc_so_name + r'\2')],
on_missing_match=ERROR
)

# if we're installing Python with an alternate sysroot,
# we need to patch setup.py which includes hardcoded paths like /usr/include and /lib64;
# this fixes problems like not being able to build the _ssl module ("Could not build the ssl module")
Expand Down Expand Up @@ -685,6 +723,57 @@ def install_step(self):
symlink(target_lib_dynload, lib_dynload)
change_dir(cwd)

def _sanity_check_ctypes_ld_library_path_patch(self):
"""
Check that ctypes.util.find_library and ctypes.CDLL work as expected.
When $LD_LIBRARY_PATH is filtered, a patch is required for this to work correctly
(see patch_ctypes_ld_library_path).
"""
# Try find_library first, since ctypes.CDLL relies on that to work correctly
cmd = "python -c 'from ctypes import util; print(util.find_library(\"libpython3.so\"))'"
res = run_shell_cmd(cmd)
out = res.output.strip()
escaped_python_root = re.escape(self.installdir)
pattern = rf"^{escaped_python_root}.*libpython3\.so$"
match = re.match(pattern, out)
self.log.debug(f"Matching regular expression pattern {pattern} to string {out}")
if match:
msg = "Call to ctypes.util.find_library('libpython3.so') successfully found libpython3.so under "
msg += f"the installation prefix of the current Python installation ({self.installdir}). "
if self.cfg.get('patch_ctypes_ld_library_path'):
msg += "This indicates that the patch that fixes ctypes when EasyBuild is "
msg += "configured to filter $LD_LIBRARY_PATH was applied succesfully."
self.log.info(msg)
else:
msg = "Finding the library libpython3.so using ctypes.util.find_library('libpython3.so') failed. "
msg += "The ctypes Python module requires a patch when EasyBuild is configured to filter $LD_LIBRARY_PATH. "
msg += "Please check if you specified a patch through patch_ctypes_ld_library_path and check "
msg += "the logs to see if it applied correctly."
raise EasyBuildError(msg)
# Now that we know find_library was patched correctly, check if ctypes.CDLL is also patched correctly
cmd = "python -c 'import ctypes; print(ctypes.CDLL(\"libpython3.so\"))'"
res = run_shell_cmd(cmd)
out = res.output.strip()
pattern = rf"^<CDLL '{escaped_python_root}.*libpython3\.so', handle [a-f0-9]+ at 0x[a-f0-9]+>$"
match = re.match(pattern, out)
self.log.debug(f"Matching regular expression pattern {pattern} to string {out}")
if match:
msg = "Call to ctypes.CDLL('libpython3.so') succesfully opened libpython3.so. "
if self.cfg.get('patch_ctypes_ld_library_path'):
msg += "This indicates that the patch that fixes ctypes when $LD_LIBRARY_PATH is not set "
msg += "was applied successfully."
self.log.info(msg)
msg = "Call to ctypes.CDLL('libpython3.so') succesfully opened libpython3.so. "
if self.cfg.get('patch_ctypes_ld_library_path'):
msg += "This indicates that the patch that fixes ctypes when $LD_LIBRARY_PATH is not set "
msg += "was applied successfully."
else:
msg = "Opening of libpython3.so using ctypes.CDLL('libpython3.so') failed. "
msg += "The ctypes Python module requires a patch when EasyBuild is configured to filter $LD_LIBRARY_PATH. "
msg += "Please check if you specified a patch through patch_ctypes_ld_library_path and check "
msg += "the logs to see if it applied correctly."
raise EasyBuildError(msg)

def _sanity_check_ebpythonprefixes(self):
"""Check that EBPYTHONPREFIXES works"""
temp_prefix = tempfile.mkdtemp(suffix='-tmp-prefix')
Expand Down Expand Up @@ -749,6 +838,12 @@ def sanity_check_step(self):
if self.cfg.get('ebpythonprefixes'):
self._sanity_check_ebpythonprefixes()

# If the conditions for applying the patch specified through patch_ctypes_ld_library_path are met,
# check that a patch was applied and indeed fixed the issue
filtered_env_vars = build_option('filter_env_vars') or []
if 'LD_LIBRARY_PATH' in filtered_env_vars and 'LIBRARY_PATH' not in filtered_env_vars:
self._sanity_check_ctypes_ld_library_path_patch()

pyver = 'python' + self.pyshortver
custom_paths = {
'files': [
Expand Down