Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for --filter-rpath-sanity-libs to skip RPATH sanity check for designated libraries #4119

Merged
merged 32 commits into from
Dec 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8f934eb
First attempt at skipping the RPATH check for designated libraries su…
Nov 1, 2022
052fca1
Now made the list of libraries that will be ignored in the RPATH sani…
Nov 2, 2022
066c31d
Add test that 1) checks if the correct error message is produced when…
Nov 2, 2022
9602395
Updated default list for --filter-rpath-sanity-libs
Nov 2, 2022
236c785
Removed trailing whitespace
Nov 2, 2022
0d233ab
rpath-filter based on libtoy might not work, if ltoy.so isn't install…
Nov 14, 2022
e3308ce
Revert previous change, as the library itself is of course also calle…
Nov 14, 2022
9bb059f
Removed stray blank linke
Nov 14, 2022
e421a0e
Let's see if I can print the build log from the CI... It's hacky and …
Nov 15, 2022
fa8be4b
Let's see if it works if I put these two tests in a seperate unit test
Nov 15, 2022
00d50e0
Add missing line to creat the path from which the test ECs are read
Nov 15, 2022
3399f25
Further debugging of the CI pipeline: lets see if we can print an ldd…
Nov 15, 2022
4450eca
Debugging, trying to find out which libtoy is being linked and see if…
Nov 16, 2022
8f70f31
Try to explicitely link to the shared library, to make sure the linke…
Nov 16, 2022
27370e3
Further debugging added. Seeing if the linker version in CI maybe doe…
Nov 16, 2022
08972e1
Order of --no-as-needed and -l might matter, let's make sure it's app…
Nov 16, 2022
f3d5e03
Printing the env. Somehow, the linker is doing an --as-needed. One of…
Nov 16, 2022
69e9123
Added a new toy program, toy2, which has a real dependency on libtoy.…
Nov 16, 2022
0cdf4f0
Now using the new toy program for the test_toy_rpath_filter tests. St…
Nov 16, 2022
c7c8383
Fixed formatting for the hound
Nov 16, 2022
f543e9a
Added all nvidia libs that are supposed to be used at runtime from th…
Nov 16, 2022
fcf96ce
Removed define which was only there for initial development of the fu…
Nov 16, 2022
25064a9
Since we added a test Easyconfig, we also need to update the count fo…
Nov 16, 2022
856cde7
Fix lazy logging, us \S+ in regex, replace rpath_exception_libs by fi…
Nov 18, 2022
16c7440
Renamed toy2 to toy-app
Nov 18, 2022
d10eb08
Start with a test that only does rpath and doesnt use rpath-filter, f…
Nov 18, 2022
7666f2f
Merge branch 'develop' into skip_rpath_check
boegel Dec 21, 2022
a086fb1
fix typo in comment above DEFAULT_FILTER_RPATH_SANITY_LIBS
boegel Dec 21, 2022
28ad073
slightly relax pattern to catch missing libraries in 'ldd' output
boegel Dec 21, 2022
05073de
remove --experimental where it's no longer needed
boegel Dec 21, 2022
0ce1f3a
extend test for --filter-rpath-sanity-libs with an extra test case
boegel Dec 21, 2022
fa7375a
enhance test_toy_filter_rpath_sanity_libs by also checking output of …
boegel Dec 21, 2022
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
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 @@ -3048,9 +3049,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 @@ -3077,10 +3085,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 @@ -69,6 +69,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 @@ -410,6 +411,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