Skip to content

Commit 8d8e70f

Browse files
authored
Merge pull request #4596 from ocaisa/export_rpath_wrappers
allow specifying location for RPATH wrapper scripts via `rpath_wrappers_dir` + also pass `rpath_include_dirs` when preparing build environment for extensions
2 parents d2cfb88 + 71cc6eb commit 8d8e70f

File tree

4 files changed

+103
-16
lines changed

4 files changed

+103
-16
lines changed

easybuild/framework/easyblock.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ def __init__(self, ec, logfile=None):
238238
# list of locations to include in RPATH used by toolchain
239239
self.rpath_include_dirs = []
240240

241+
# directory to export RPATH wrappers to
242+
self.rpath_wrappers_dir = None
243+
241244
# logging
242245
self.log = None
243246
self.logfile = logfile
@@ -2103,7 +2106,9 @@ def install_extensions_sequential(self, install=True):
21032106
# don't reload modules for toolchain, there is no need since they will be loaded already;
21042107
# the (fake) module for the parent software gets loaded before installing extensions
21052108
ext.toolchain.prepare(onlymod=self.cfg['onlytcmod'], silent=True, loadmod=False,
2106-
rpath_filter_dirs=self.rpath_filter_dirs)
2109+
rpath_filter_dirs=self.rpath_filter_dirs,
2110+
rpath_include_dirs=self.rpath_include_dirs,
2111+
rpath_wrappers_dir=self.rpath_wrappers_dir)
21072112

21082113
# actual installation of the extension
21092114
if install:
@@ -2265,7 +2270,9 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
22652270
# don't reload modules for toolchain, there is no need since they will be loaded already;
22662271
# the (fake) module for the parent software gets loaded before installing extensions
22672272
ext.toolchain.prepare(onlymod=self.cfg['onlytcmod'], silent=True, loadmod=False,
2268-
rpath_filter_dirs=self.rpath_filter_dirs)
2273+
rpath_filter_dirs=self.rpath_filter_dirs,
2274+
rpath_include_dirs=self.rpath_include_dirs,
2275+
rpath_wrappers_dir=self.rpath_wrappers_dir)
22692276
if install:
22702277
ext.install_extension_substep("pre_install_extension")
22712278
ext.async_cmd_task = ext.install_extension_substep("install_extension_async", thread_pool)
@@ -2895,6 +2902,14 @@ def prepare_step(self, start_dir=True, load_tc_deps_modules=True):
28952902
'$ORIGIN/../lib64',
28962903
])
28972904

2905+
# Location to store RPATH wrappers
2906+
if self.rpath_wrappers_dir is not None:
2907+
# Verify the path given is absolute
2908+
if os.path.isabs(self.rpath_wrappers_dir):
2909+
_log.info(f"Using {self.rpath_wrappers_dir} to store/use RPATH wrappers")
2910+
else:
2911+
raise EasyBuildError(f"Path used for rpath_wrappers_dir is not an absolute path: {path}")
2912+
28982913
if self.iter_idx > 0:
28992914
# reset toolchain for iterative runs before preparing it again
29002915
self.toolchain.reset()
@@ -2910,9 +2925,11 @@ def prepare_step(self, start_dir=True, load_tc_deps_modules=True):
29102925
self.modules_tool.prepend_module_path(full_mod_path)
29112926

29122927
# prepare toolchain: load toolchain module and dependencies, set up build environment
2913-
self.toolchain.prepare(self.cfg['onlytcmod'], deps=self.cfg.dependencies(), silent=self.silent,
2914-
loadmod=load_tc_deps_modules, rpath_filter_dirs=self.rpath_filter_dirs,
2915-
rpath_include_dirs=self.rpath_include_dirs)
2928+
self.toolchain.prepare(onlymod=self.cfg['onlytcmod'], deps=self.cfg.dependencies(),
2929+
silent=self.silent, loadmod=load_tc_deps_modules,
2930+
rpath_filter_dirs=self.rpath_filter_dirs,
2931+
rpath_include_dirs=self.rpath_include_dirs,
2932+
rpath_wrappers_dir=self.rpath_wrappers_dir)
29162933

29172934
# keep track of environment variables that were tweaked and need to be restored after environment got reset
29182935
# $TMPDIR may be tweaked for OpenMPI 2.x, which doesn't like long $TMPDIR paths...

easybuild/scripts/rpath_wrapper_template.sh.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ function log {
4242

4343
# command name
4444
CMD=`basename $0`
45+
TOPDIR=`dirname $0`
4546

4647
log "found CMD: $CMD | original command: %(orig_cmd)s | orig args: '$(echo \"$@\")'"
4748

easybuild/tools/toolchain/toolchain.py

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
from easybuild.tools.build_log import EasyBuildError, dry_run_msg, print_warning
6363
from easybuild.tools.config import build_option, install_path
6464
from easybuild.tools.environment import setvar
65-
from easybuild.tools.filetools import adjust_permissions, find_eb_script, read_file, which, write_file
65+
from easybuild.tools.filetools import adjust_permissions, copy_file, find_eb_script, mkdir, read_file, which, write_file
6666
from easybuild.tools.module_generator import dependencies_for
6767
from easybuild.tools.modules import get_software_root, get_software_root_env_var_name
6868
from easybuild.tools.modules import get_software_version, get_software_version_env_var_name
@@ -839,7 +839,7 @@ def reset(self):
839839
self.variables_init()
840840

841841
def prepare(self, onlymod=None, deps=None, silent=False, loadmod=True,
842-
rpath_filter_dirs=None, rpath_include_dirs=None):
842+
rpath_filter_dirs=None, rpath_include_dirs=None, rpath_wrappers_dir=None):
843843
"""
844844
Prepare a set of environment parameters based on name/version of toolchain
845845
- load modules for toolchain and dependencies
@@ -853,6 +853,7 @@ def prepare(self, onlymod=None, deps=None, silent=False, loadmod=True,
853853
:param loadmod: whether or not to (re)load the toolchain module, and the modules for the dependencies
854854
:param rpath_filter_dirs: extra directories to include in RPATH filter (e.g. build dir, tmpdir, ...)
855855
:param rpath_include_dirs: extra directories to include in RPATH
856+
:param rpath_wrappers_dir: directory in which to create RPATH wrappers
856857
"""
857858

858859
# take into account --sysroot configuration setting
@@ -906,7 +907,11 @@ def prepare(self, onlymod=None, deps=None, silent=False, loadmod=True,
906907

907908
if build_option('rpath'):
908909
if self.options.get('rpath', True):
909-
self.prepare_rpath_wrappers(rpath_filter_dirs, rpath_include_dirs)
910+
self.prepare_rpath_wrappers(
911+
rpath_filter_dirs=rpath_filter_dirs,
912+
rpath_include_dirs=rpath_include_dirs,
913+
rpath_wrappers_dir=rpath_wrappers_dir
914+
)
910915
self.use_rpath = True
911916
else:
912917
self.log.info("Not putting RPATH wrappers in place, disabled via 'rpath' toolchain option")
@@ -975,11 +980,13 @@ def is_rpath_wrapper(path):
975980
# need to use binary mode to read the file, since it may be an actual compiler command (which is a binary file)
976981
return b'rpath_args.py $CMD' in read_file(path, mode='rb')
977982

978-
def prepare_rpath_wrappers(self, rpath_filter_dirs=None, rpath_include_dirs=None):
983+
def prepare_rpath_wrappers(self, rpath_filter_dirs=None, rpath_include_dirs=None, rpath_wrappers_dir=None):
979984
"""
980985
Put RPATH wrapper script in place for compiler and linker commands
981986
982987
:param rpath_filter_dirs: extra directories to include in RPATH filter (e.g. build dir, tmpdir, ...)
988+
:param rpath_include_dirs: extra directories to include in RPATH
989+
:param rpath_wrappers_dir: directory in which to create RPATH wrappers (tmpdir is created if None)
983990
"""
984991
if get_os_type() == LINUX:
985992
self.log.info("Putting RPATH wrappers in place...")
@@ -989,6 +996,11 @@ def prepare_rpath_wrappers(self, rpath_filter_dirs=None, rpath_include_dirs=None
989996
if rpath_filter_dirs is None:
990997
rpath_filter_dirs = []
991998

999+
# only enable logging by RPATH wrapper scripts in debug mode
1000+
enable_wrapper_log = build_option('debug')
1001+
1002+
copy_rpath_args_py = False
1003+
9921004
# always include filter for 'stubs' library directory,
9931005
# cfr. https://github.com/easybuilders/easybuild-framework/issues/2683
9941006
# (since CUDA 11.something the stubs are in $EBROOTCUDA/stubs/lib64)
@@ -997,13 +1009,35 @@ def prepare_rpath_wrappers(self, rpath_filter_dirs=None, rpath_include_dirs=None
9971009
if lib_stubs_pattern not in rpath_filter_dirs:
9981010
rpath_filter_dirs.append(lib_stubs_pattern)
9991011

1000-
# directory where all wrappers will be placed
1001-
wrappers_dir = os.path.join(tempfile.mkdtemp(), RPATH_WRAPPERS_SUBDIR)
1012+
# directory where all RPATH wrapper script will be placed;
1013+
if rpath_wrappers_dir is None:
1014+
wrappers_dir = tempfile.mkdtemp()
1015+
else:
1016+
wrappers_dir = rpath_wrappers_dir
1017+
# disable logging in RPATH wrapper scripts when they may be exported for use outside of EasyBuild
1018+
enable_wrapper_log = False
1019+
# copy rpath_args.py script to sit alongside RPATH wrapper scripts
1020+
copy_rpath_args_py = True
1021+
1022+
# it's important to honor RPATH_WRAPPERS_SUBDIR, see is_rpath_wrapper method
1023+
wrappers_dir = os.path.join(wrappers_dir, RPATH_WRAPPERS_SUBDIR)
1024+
mkdir(wrappers_dir, parents=True)
10021025

10031026
# must also wrap compilers commands, required e.g. for Clang ('gcc' on OS X)?
10041027
c_comps, fortran_comps = self.compilers()
10051028

10061029
rpath_args_py = find_eb_script('rpath_args.py')
1030+
1031+
# copy rpath_args.py script along RPATH wrappers, if desired
1032+
if copy_rpath_args_py:
1033+
copy_file(rpath_args_py, wrappers_dir)
1034+
# use path for %(rpath_args)s template value relative to location of the RPATH wrapper script,
1035+
# to avoid that the RPATH wrapper scripts rely on a script that's located elsewhere;
1036+
# that's mostly important when RPATH wrapper scripts are retained to be used outside of EasyBuild;
1037+
# we assume that each RPATH wrapper script is created in a separate subdirectory (see wrapper_dir below);
1038+
# ${TOPDIR} is defined in template for RPATH wrapper scripts, refers to parent dir of RPATH wrapper script
1039+
rpath_args_py = os.path.join('${TOPDIR}', '..', os.path.basename(rpath_args_py))
1040+
10071041
rpath_wrapper_template = find_eb_script('rpath_wrapper_template.sh.in')
10081042

10091043
# figure out list of patterns to use in rpath filter
@@ -1042,11 +1076,11 @@ def prepare_rpath_wrappers(self, rpath_filter_dirs=None, rpath_include_dirs=None
10421076

10431077
# make *very* sure we don't wrap around ourselves and create a fork bomb...
10441078
if os.path.exists(cmd_wrapper) and os.path.exists(orig_cmd) and os.path.samefile(orig_cmd, cmd_wrapper):
1045-
raise EasyBuildError("Refusing the create a fork bomb, which(%s) == %s", cmd, orig_cmd)
1079+
raise EasyBuildError("Refusing to create a fork bomb, which(%s) == %s", cmd, orig_cmd)
10461080

10471081
# enable debug mode in wrapper script by specifying location for log file
1048-
if build_option('debug'):
1049-
rpath_wrapper_log = os.path.join(tempfile.gettempdir(), 'rpath_wrapper_%s.log' % cmd)
1082+
if enable_wrapper_log:
1083+
rpath_wrapper_log = os.path.join(tempfile.gettempdir(), f'rpath_wrapper_{cmd}.log')
10501084
else:
10511085
rpath_wrapper_log = '/dev/null'
10521086

@@ -1060,7 +1094,15 @@ def prepare_rpath_wrappers(self, rpath_filter_dirs=None, rpath_include_dirs=None
10601094
'rpath_wrapper_log': rpath_wrapper_log,
10611095
'wrapper_dir': wrapper_dir,
10621096
}
1063-
write_file(cmd_wrapper, cmd_wrapper_txt)
1097+
1098+
# it may be the case that the wrapper already exists if the user provides a fixed location to store
1099+
# the RPATH wrappers, in this case the wrappers will be overwritten as they do not yet appear in the
1100+
# PATH (`which(cmd)` does not "see" them). Warn that they will be overwritten.
1101+
if os.path.exists(cmd_wrapper):
1102+
_log.warning(f"Overwriting existing RPATH wrapper {cmd_wrapper}")
1103+
write_file(cmd_wrapper, cmd_wrapper_txt, always_overwrite=True)
1104+
else:
1105+
write_file(cmd_wrapper, cmd_wrapper_txt)
10641106
adjust_permissions(cmd_wrapper, stat.S_IXUSR)
10651107

10661108
# prepend location to this wrapper to $PATH

test/framework/toolchain.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
from easybuild.tools.run import run_shell_cmd
5555
from easybuild.tools.systemtools import get_shared_lib_ext
5656
from easybuild.tools.toolchain.mpi import get_mpi_cmd_template
57-
from easybuild.tools.toolchain.toolchain import env_vars_external_module
57+
from easybuild.tools.toolchain.toolchain import env_vars_external_module, RPATH_WRAPPERS_SUBDIR
5858
from easybuild.tools.toolchain.utilities import get_toolchain, search_toolchain
5959
from easybuild.toolchains.compiler.clang import Clang
6060

@@ -3137,6 +3137,33 @@ def test_toolchain_prepare_rpath(self):
31373137
self.assertTrue(os.path.samefile(res[1], fake_gxx))
31383138
self.assertFalse(any(os.path.samefile(x, fake_gxx) for x in res[2:]))
31393139

3140+
def test_toolchain_prepare_rpath_external(self):
3141+
"""Test toolchain.prepare under --rpath with rpath_wrappers_dir argument"""
3142+
3143+
# put fake 'g++' command in place that just echos its arguments
3144+
fake_gxx = os.path.join(self.test_prefix, 'fake', 'g++')
3145+
write_file(fake_gxx, '#!/bin/bash\necho "$@"')
3146+
adjust_permissions(fake_gxx, stat.S_IXUSR)
3147+
os.environ['PATH'] = '%s:%s' % (os.path.join(self.test_prefix, 'fake'), os.getenv('PATH', ''))
3148+
3149+
# export the wrappers to a target location
3150+
target_wrapper_dir = os.path.abspath(os.path.join(self.test_prefix, 'target'))
3151+
# enable --rpath for a toolchain so we test against it
3152+
init_config(build_options={'rpath': True, 'silent': True})
3153+
tc = self.get_toolchain('gompi', version='2018a')
3154+
tc.set_options({'rpath': True})
3155+
# allow the underlying toolchain to be in a prepared state (which may include rpath wrapping)
3156+
with self.mocked_stdout_stderr():
3157+
tc.prepare(rpath_wrappers_dir=target_wrapper_dir)
3158+
3159+
# check that wrapper was created
3160+
target_wrapper = os.path.join(target_wrapper_dir, RPATH_WRAPPERS_SUBDIR, 'gxx_wrapper', 'g++')
3161+
self.assertTrue(os.path.exists(target_wrapper))
3162+
# Make sure it is a wrapper
3163+
self.assertTrue(b'rpath_args.py $CMD' in read_file(target_wrapper, mode='rb'))
3164+
# Make sure it wraps our fake 'g++'
3165+
self.assertTrue(fake_gxx.encode(encoding="utf-8") in read_file(target_wrapper, mode='rb'))
3166+
31403167
def test_prepare_openmpi_tmpdir(self):
31413168
"""Test handling of long $TMPDIR path for OpenMPI 2.x"""
31423169

0 commit comments

Comments
 (0)