Skip to content

Commit

Permalink
Merge pull request #4119 from casparvl/skip_rpath_check
Browse files Browse the repository at this point in the history
add support for --filter-rpath-sanity-libs to skip RPATH sanity check for designated libraries
  • Loading branch information
boegel authored Dec 22, 2022
2 parents c70d81c + fa7375a commit 1d55137
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 22 deletions.
26 changes: 21 additions & 5 deletions easybuild/framework/easyblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
:author: Damian Alvarez (Forschungszentrum Juelich GmbH)
:author: Maxime Boissonneault (Compute Canada)
:author: Davide Vanzo (Vanderbilt University)
:author: Caspar van Leeuwen (SURF)
"""

import copy
Expand Down Expand Up @@ -3047,9 +3048,16 @@ def sanity_check_rpath(self, rpath_dirs=None):
self.log.debug("$LD_LIBRARY_PATH during RPATH sanity check: %s", os.getenv('LD_LIBRARY_PATH', '(empty)'))
self.log.debug("List of loaded modules: %s", self.modules_tool.list())

not_found_regex = re.compile('not found', re.M)
not_found_regex = re.compile(r'(\S+)\s*\=\>\s*not found')
readelf_rpath_regex = re.compile('(RPATH)', re.M)

# List of libraries that should be exempt from the RPATH sanity check;
# For example, libcuda.so.1 should never be RPATH-ed by design,
# see https://github.com/easybuilders/easybuild-framework/issues/4095
filter_rpath_sanity_libs = build_option('filter_rpath_sanity_libs')
msg = "Ignoring the following libraries if they are not found by RPATH sanity check: %s"
self.log.info(msg, filter_rpath_sanity_libs)

if rpath_dirs is None:
rpath_dirs = self.cfg['bin_lib_subdirs'] or self.bin_lib_subdirs()

Expand All @@ -3076,10 +3084,18 @@ def sanity_check_rpath(self, rpath_dirs=None):
self.log.debug(msg, path)
else:
# check whether all required libraries are found via 'ldd'
if not_found_regex.search(out):
fail_msg = "One or more required libraries not found for %s: %s" % (path, out)
self.log.warning(fail_msg)
fails.append(fail_msg)
matches = re.findall(not_found_regex, out)
if len(matches) > 0: # Some libraries are not found via 'ldd'
# For each match, check if the library is in the exception list
for match in matches:
if match in filter_rpath_sanity_libs:
msg = "Library %s not found for %s, but ignored "
msg += "since it is on the rpath exception list: %s"
self.log.info(msg, match, path, filter_rpath_sanity_libs)
else:
fail_msg = "Library %s not found for %s" % (match, path)
self.log.warning(fail_msg)
fails.append(fail_msg)
else:
self.log.debug("Output of 'ldd %s' checked, looks OK", path)

Expand Down
11 changes: 11 additions & 0 deletions easybuild/tools/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@
DEFAULT_PR_TARGET_ACCOUNT = 'easybuilders'
DEFAULT_PREFIX = os.path.join(os.path.expanduser('~'), ".local", "easybuild")
DEFAULT_REPOSITORY = 'FileRepository'
# Filter these CUDA libraries by default from the RPATH sanity check.
# These are the only four libraries for which the CUDA toolkit ships stubs. By design, one is supposed to build
# against the stub versions, but use the libraries that come with the CUDA driver at runtime. That means they should
# never be RPATH-ed, and thus the sanity check should also accept that they aren't RPATH-ed.
DEFAULT_FILTER_RPATH_SANITY_LIBS = (
'libcuda.so',
'libcuda.so.1',
'libnvidia-ml.so',
'libnvidia-ml.so.1'
)
DEFAULT_WAIT_ON_LOCK_INTERVAL = 60
DEFAULT_WAIT_ON_LOCK_LIMIT = 0

Expand Down Expand Up @@ -205,6 +215,7 @@ def mk_full_default_path(name, prefix=DEFAULT_PREFIX):
'filter_deps',
'filter_ecs',
'filter_env_vars',
'filter_rpath_sanity_libs',
'force_download',
'git_working_dirs_path',
'github_user',
Expand Down
6 changes: 6 additions & 0 deletions easybuild/tools/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
from easybuild.tools.config import DEFAULT_MODULECLASSES, DEFAULT_PATH_SUBDIRS, DEFAULT_PKG_RELEASE, DEFAULT_PKG_TOOL
from easybuild.tools.config import DEFAULT_PKG_TYPE, DEFAULT_PNS, DEFAULT_PREFIX, DEFAULT_PR_TARGET_ACCOUNT
from easybuild.tools.config import DEFAULT_REPOSITORY, DEFAULT_WAIT_ON_LOCK_INTERVAL, DEFAULT_WAIT_ON_LOCK_LIMIT
from easybuild.tools.config import DEFAULT_FILTER_RPATH_SANITY_LIBS
from easybuild.tools.config import EBROOT_ENV_VAR_ACTIONS, ERROR, FORCE_DOWNLOAD_CHOICES, GENERAL_CLASS, IGNORE
from easybuild.tools.config import JOB_DEPS_TYPE_ABORT_ON_ERROR, JOB_DEPS_TYPE_ALWAYS_RUN, LOADED_MODULES_ACTIONS
from easybuild.tools.config import LOCAL_VAR_NAMING_CHECK_WARN, LOCAL_VAR_NAMING_CHECKS
Expand Down Expand Up @@ -409,6 +410,11 @@ def override_options(self):
'strlist', 'extend', None),
'filter-env-vars': ("List of names of environment variables that should *not* be defined/updated by "
"module files generated by EasyBuild", 'strlist', 'extend', None),
'filter-rpath-sanity-libs': ("List of libraries that should be ignored by the RPATH sanity check. "
"I.e. if these libraries are not RPATH-ed, that will be accepted "
"by the RPATH sanity check. Note that you'll need to provide the exact "
"library name, as it is returned by 'ldd', including any version ",
'strlist', 'store', DEFAULT_FILTER_RPATH_SANITY_LIBS),
'fixed-installdir-naming-scheme': ("Use fixed naming scheme for installation directories", None,
'store_true', True),
'force-download': ("Force re-downloading of sources and/or patches, "
Expand Down
30 changes: 30 additions & 0 deletions test/framework/easyconfigs/test_ecs/t/toy-app/toy-app-0.0.eb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
easyblock = 'EB_toy'

name = 'toy-app'
version = '0.0'

homepage = 'https://easybuilders.github.io/easybuild'
description = "Toy C program, 100% toy. This toy has a main function that depends on libtoy."

toolchain = SYSTEM

sources = [SOURCE_TAR_GZ]
checksums = [
'9559393c0d747a4940a79be54e82fa8f14dbb0c32979a3e61e9db305f32dad49', # default (SHA256)
]

dependencies = [
('libtoy', '0.0')
]

buildopts = '-ltoy'

sanity_check_paths = {
'files': [('bin/toy-app')],
'dirs': ['bin'],
}

postinstallcmds = ["echo TOY > %(installdir)s/README"]

moduleclass = 'tools'
# trailing comment, leave this here, it may trigger bugs with extract_comments()
2 changes: 1 addition & 1 deletion test/framework/filetools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2387,7 +2387,7 @@ def test_index_functions(self):
# test with specified path with and without trailing '/'s
for path in [test_ecs, test_ecs + '/', test_ecs + '//']:
index = ft.create_index(path)
self.assertEqual(len(index), 90)
self.assertEqual(len(index), 91)

expected = [
os.path.join('b', 'bzip2', 'bzip2-1.0.6-GCC-4.9.2.eb'),
Expand Down
Binary file not shown.
93 changes: 77 additions & 16 deletions test/framework/toy_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def tearDown(self):
if os.path.exists(self.dummylogfn):
os.remove(self.dummylogfn)

def check_toy(self, installpath, outtxt, version='0.0', versionprefix='', versionsuffix='', error=None):
def check_toy(self, installpath, outtxt, name='toy', version='0.0', versionprefix='', versionsuffix='', error=None):
"""Check whether toy build succeeded."""

full_version = ''.join([versionprefix, version, versionsuffix])
Expand All @@ -121,38 +121,39 @@ def check_toy(self, installpath, outtxt, version='0.0', versionprefix='', versio
self.assertTrue(success.search(outtxt), "COMPLETED message found in '%s'%s" % (outtxt, error_msg))

# if the module exists, it should be fine
toy_module = os.path.join(installpath, 'modules', 'all', 'toy', full_version)
toy_module = os.path.join(installpath, 'modules', 'all', name, full_version)
msg = "module for toy build toy/%s found (path %s)" % (full_version, toy_module)
if get_module_syntax() == 'Lua':
toy_module += '.lua'
self.assertTrue(os.path.exists(toy_module), msg + error_msg)

# module file is symlinked according to moduleclass
toy_module_symlink = os.path.join(installpath, 'modules', 'tools', 'toy', full_version)
toy_module_symlink = os.path.join(installpath, 'modules', 'tools', name, full_version)
if get_module_syntax() == 'Lua':
toy_module_symlink += '.lua'
self.assertTrue(os.path.islink(toy_module_symlink))
self.assertTrue(os.path.exists(toy_module_symlink))

# make sure installation log file and easyconfig file are copied to install dir
software_path = os.path.join(installpath, 'software', 'toy', full_version)
install_log_path_pattern = os.path.join(software_path, 'easybuild', 'easybuild-toy-%s*.log' % version)
software_path = os.path.join(installpath, 'software', name, full_version)
install_log_path_pattern = os.path.join(software_path, 'easybuild', 'easybuild-%s-%s*.log' % (name, version))
self.assertTrue(len(glob.glob(install_log_path_pattern)) >= 1,
"Found at least 1 file at %s" % install_log_path_pattern)

# make sure test report is available
test_report_path_pattern = os.path.join(software_path, 'easybuild', 'easybuild-toy-%s*test_report.md' % version)
report_name = 'easybuild-%s-%s*test_report.md' % (name, version)
test_report_path_pattern = os.path.join(software_path, 'easybuild', report_name)
self.assertTrue(len(glob.glob(test_report_path_pattern)) >= 1,
"Found at least 1 file at %s" % test_report_path_pattern)

ec_file_path = os.path.join(software_path, 'easybuild', 'toy-%s.eb' % full_version)
ec_file_path = os.path.join(software_path, 'easybuild', '%s-%s.eb' % (name, full_version))
self.assertTrue(os.path.exists(ec_file_path))

devel_module_path = os.path.join(software_path, 'easybuild', 'toy-%s-easybuild-devel' % full_version)
devel_module_path = os.path.join(software_path, 'easybuild', '%s-%s-easybuild-devel' % (name, full_version))
self.assertTrue(os.path.exists(devel_module_path))

def test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, verify=True, fails=False, verbose=True,
raise_error=False, test_report=None, versionsuffix='', testing=True,
raise_error=False, test_report=None, name='toy', versionsuffix='', testing=True,
raise_systemexit=False, force=True, test_report_regexs=None):
"""Perform a toy build."""
if extra_args is None:
Expand Down Expand Up @@ -188,7 +189,7 @@ def test_toy_build(self, extra_args=None, ec_file=None, tmpdir=None, verify=True
raise myerr

if verify:
self.check_toy(self.test_installpath, outtxt, versionsuffix=versionsuffix, error=myerr)
self.check_toy(self.test_installpath, outtxt, name=name, versionsuffix=versionsuffix, error=myerr)

if test_readme:
# make sure postinstallcmds were used
Expand Down Expand Up @@ -2660,7 +2661,7 @@ def grab_gcc_rpath_wrapper_args():

return {'filter_paths': res_filter.group(1), 'include_paths': res_include.group(1)}

args = ['--rpath', '--experimental']
args = ['--rpath']
self.test_toy_build(extra_args=args, raise_error=True)

# by default, /lib and /usr are included in RPATH filter,
Expand All @@ -2672,23 +2673,23 @@ def grab_gcc_rpath_wrapper_args():
self.assertTrue(any(p.startswith(self.test_buildpath) for p in rpath_filter_paths))

# Check that we can use --rpath-override-dirs
args = ['--rpath', '--experimental', '--rpath-override-dirs=/opt/eessi/2021.03/lib:/opt/eessi/lib']
args = ['--rpath', '--rpath-override-dirs=/opt/eessi/2021.03/lib:/opt/eessi/lib']
self.test_toy_build(extra_args=args, raise_error=True)
rpath_include_paths = grab_gcc_rpath_wrapper_args()['include_paths'].split(',')
# Make sure our directories appear in dirs to be included in the rpath (and in the right order)
self.assertEqual(rpath_include_paths[0], '/opt/eessi/2021.03/lib')
self.assertEqual(rpath_include_paths[1], '/opt/eessi/lib')

# Check that when we use --rpath-override-dirs empty values are filtered
args = ['--rpath', '--experimental', '--rpath-override-dirs=/opt/eessi/2021.03/lib::/opt/eessi/lib']
args = ['--rpath', '--rpath-override-dirs=/opt/eessi/2021.03/lib::/opt/eessi/lib']
self.test_toy_build(extra_args=args, raise_error=True)
rpath_include_paths = grab_gcc_rpath_wrapper_args()['include_paths'].split(',')
# Make sure our directories appear in dirs to be included in the rpath (and in the right order)
self.assertEqual(rpath_include_paths[0], '/opt/eessi/2021.03/lib')
self.assertEqual(rpath_include_paths[1], '/opt/eessi/lib')

# Check that when we use --rpath-override-dirs we can only provide absolute paths
eb_args = ['--rpath', '--experimental', '--rpath-override-dirs=/opt/eessi/2021.03/lib:eessi/lib']
eb_args = ['--rpath', '--rpath-override-dirs=/opt/eessi/2021.03/lib:eessi/lib']
error_pattern = r"Path used in rpath_override_dirs is not an absolute path: eessi/lib"
self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, extra_args=eb_args, raise_error=True,
verbose=False)
Expand All @@ -2710,7 +2711,67 @@ def grab_gcc_rpath_wrapper_args():
toy_ec_txt += "\ntoolchainopts = {'rpath': False}\n"
toy_ec = os.path.join(self.test_prefix, 'toy.eb')
write_file(toy_ec, toy_ec_txt)
self.test_toy_build(ec_file=toy_ec, extra_args=['--rpath', '--experimental'], raise_error=True)
self.test_toy_build(ec_file=toy_ec, extra_args=['--rpath'], raise_error=True)

def test_toy_filter_rpath_sanity_libs(self):
"""Test use of --filter-rpath-sanity-libs."""

test_ecs = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')
toy_ec = os.path.join(test_ecs, 't', 'toy-app', 'toy-app-0.0.eb')

# This should just build succesfully
args = ['--rpath']
self.test_toy_build(ec_file=toy_ec, name='toy-app', extra_args=args, raise_error=True)

libtoy_libdir = os.path.join(self.test_installpath, 'software', 'libtoy', '0.0', 'lib')
toyapp_bin = os.path.join(self.test_installpath, 'software', 'toy-app', '0.0', 'bin', 'toy-app')
rpath_regex = re.compile(r"RPATH.*%s" % libtoy_libdir, re.M)
out, ec = run_cmd("readelf -d %s" % toyapp_bin, simple=False)
self.assertTrue(rpath_regex.search(out), "Pattern '%s' should be found in: %s" % (rpath_regex.pattern, out))

out, ec = run_cmd("ldd %s" % toyapp_bin, simple=False)
libtoy_regex = re.compile(r"libtoy.so => /.*/libtoy.so", re.M)
notfound = re.compile(r"libtoy\.so\s*=>\s*not found", re.M)
self.assertTrue(libtoy_regex.search(out), "Pattern '%s' should be found in: %s" % (libtoy_regex.pattern, out))
self.assertFalse(notfound.search(out), "Pattern '%s' should not be found in: %s" % (notfound.pattern, out))

# test sanity error when --rpath-filter is used to filter a required library
# In this test, libtoy.so will be linked, but not RPATH-ed due to the --rpath-filter
# Thus, the RPATH sanity check is expected to fail with libtoy.so not being found
error_pattern = r"Sanity check failed\: Library libtoy\.so not found"
self.assertErrorRegex(EasyBuildError, error_pattern, self.test_toy_build, ec_file=toy_ec,
extra_args=['--rpath', '--rpath-filter=.*libtoy.*'],
name='toy-app', raise_error=True, verbose=False)

# test use of --filter-rpath-sanity-libs option. In this test, we use --rpath-filter to make sure libtoy.so is
# not rpath-ed. Then, we use --filter-rpath-sanity-libs to make sure the RPATH sanity checks ignores
# the fact that libtoy.so is not found. Thus, this build should complete succesfully
args = ['--rpath', '--rpath-filter=.*libtoy.*', '--filter-rpath-sanity-libs=libtoy.so']
self.test_toy_build(ec_file=toy_ec, name='toy-app', extra_args=args, raise_error=True)

out, ec = run_cmd("readelf -d %s" % toyapp_bin, simple=False)
self.assertFalse(rpath_regex.search(out),
"Pattern '%s' should not be found in: %s" % (rpath_regex.pattern, out))

out, ec = run_cmd("ldd %s" % toyapp_bin, simple=False)
self.assertFalse(libtoy_regex.search(out),
"Pattern '%s' should not be found in: %s" % (libtoy_regex.pattern, out))
self.assertTrue(notfound.search(out),
"Pattern '%s' should be found in: %s" % (notfound.pattern, out))

# test again with list of library names passed to --filter-rpath-sanity-libs
args = ['--rpath', '--rpath-filter=.*libtoy.*', '--filter-rpath-sanity-libs=libfoo.so,libtoy.so,libbar.so']
self.test_toy_build(ec_file=toy_ec, name='toy-app', extra_args=args, raise_error=True)

out, ec = run_cmd("readelf -d %s" % toyapp_bin, simple=False)
self.assertFalse(rpath_regex.search(out),
"Pattern '%s' should not be found in: %s" % (rpath_regex.pattern, out))

out, ec = run_cmd("ldd %s" % toyapp_bin, simple=False)
self.assertFalse(libtoy_regex.search(out),
"Pattern '%s' should not be found in: %s" % (libtoy_regex.pattern, out))
self.assertTrue(notfound.search(out),
"Pattern '%s' should be found in: %s" % (notfound.pattern, out))

def test_toy_modaltsoftname(self):
"""Build two dependent toys as in test_toy_toy but using modaltsoftname"""
Expand Down Expand Up @@ -2780,7 +2841,7 @@ def test_toy_build_trace(self):

self.mock_stderr(True)
self.mock_stdout(True)
self.test_toy_build(ec_file=test_ec, extra_args=['--trace', '--experimental'], verify=False, testing=False)
self.test_toy_build(ec_file=test_ec, extra_args=['--trace'], verify=False, testing=False)
stderr = self.get_stderr()
stdout = self.get_stdout()
self.mock_stderr(False)
Expand Down

0 comments on commit 1d55137

Please sign in to comment.