Skip to content

Commit 774d514

Browse files
committed
Merge branch 'develop' into fake-module-per-extension
2 parents bd183b3 + 79e458a commit 774d514

File tree

6 files changed

+156
-27
lines changed

6 files changed

+156
-27
lines changed

easybuild/framework/easyblock.py

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

242+
# directory to export RPATH wrappers to
243+
self.rpath_wrappers_dir = None
244+
242245
# logging
243246
self.log = None
244247
self.logfile = logfile
@@ -2097,7 +2100,9 @@ def install_extensions_sequential(self, install=True):
20972100
# don't reload modules for toolchain, there is no need since they will be loaded already;
20982101
# the (fake) module for the parent software gets loaded before installing extensions
20992102
ext.toolchain.prepare(onlymod=self.cfg['onlytcmod'], silent=True, loadmod=False,
2100-
rpath_filter_dirs=self.rpath_filter_dirs)
2103+
rpath_filter_dirs=self.rpath_filter_dirs,
2104+
rpath_include_dirs=self.rpath_include_dirs,
2105+
rpath_wrappers_dir=self.rpath_wrappers_dir)
21012106

21022107
# actual installation of the extension
21032108
if install and not self.dry_run:
@@ -2259,7 +2264,9 @@ def update_exts_progress_bar_helper(running_exts, progress_size):
22592264
# don't reload modules for toolchain, there is no need since they will be loaded already;
22602265
# the (fake) module for the parent software gets loaded before installing extensions
22612266
ext.toolchain.prepare(onlymod=self.cfg['onlytcmod'], silent=True, loadmod=False,
2262-
rpath_filter_dirs=self.rpath_filter_dirs)
2267+
rpath_filter_dirs=self.rpath_filter_dirs,
2268+
rpath_include_dirs=self.rpath_include_dirs,
2269+
rpath_wrappers_dir=self.rpath_wrappers_dir)
22632270
if install and not self.dry_run:
22642271
with self.fake_module_environment(with_build_deps=True):
22652272
ext.install_extension_substep("pre_install_extension")
@@ -2797,7 +2804,8 @@ def check_checksums(self):
27972804
# take into account that extension may be a 2-tuple with just name/version
27982805
ext_opts = ext[2] if len(ext) == 3 else {}
27992806
# only a single source per extension is supported (see source_tmpl)
2800-
res = self.check_checksums_for(ext_opts, sub="of extension %s" % ext_name, source_cnt=1)
2807+
source_cnt = 1 if not ext_opts.get('nosource') else 0
2808+
res = self.check_checksums_for(ext_opts, sub="of extension %s" % ext_name, source_cnt=source_cnt)
28012809
checksum_issues.extend(res)
28022810

28032811
return checksum_issues
@@ -2908,6 +2916,14 @@ def prepare_step(self, start_dir=True, load_tc_deps_modules=True):
29082916
'$ORIGIN/../lib64',
29092917
])
29102918

2919+
# Location to store RPATH wrappers
2920+
if self.rpath_wrappers_dir is not None:
2921+
# Verify the path given is absolute
2922+
if os.path.isabs(self.rpath_wrappers_dir):
2923+
_log.info(f"Using {self.rpath_wrappers_dir} to store/use RPATH wrappers")
2924+
else:
2925+
raise EasyBuildError(f"Path used for rpath_wrappers_dir is not an absolute path: {path}")
2926+
29112927
if self.iter_idx > 0:
29122928
# reset toolchain for iterative runs before preparing it again
29132929
self.toolchain.reset()
@@ -2923,9 +2939,11 @@ def prepare_step(self, start_dir=True, load_tc_deps_modules=True):
29232939
self.modules_tool.prepend_module_path(full_mod_path)
29242940

29252941
# prepare toolchain: load toolchain module and dependencies, set up build environment
2926-
self.toolchain.prepare(self.cfg['onlytcmod'], deps=self.cfg.dependencies(), silent=self.silent,
2927-
loadmod=load_tc_deps_modules, rpath_filter_dirs=self.rpath_filter_dirs,
2928-
rpath_include_dirs=self.rpath_include_dirs)
2942+
self.toolchain.prepare(onlymod=self.cfg['onlytcmod'], deps=self.cfg.dependencies(),
2943+
silent=self.silent, loadmod=load_tc_deps_modules,
2944+
rpath_filter_dirs=self.rpath_filter_dirs,
2945+
rpath_include_dirs=self.rpath_include_dirs,
2946+
rpath_wrappers_dir=self.rpath_wrappers_dir)
29292947

29302948
# keep track of environment variables that were tweaked and need to be restored after environment got reset
29312949
# $TMPDIR may be tweaked for OpenMPI 2.x, which doesn't like long $TMPDIR paths...
@@ -3395,6 +3413,14 @@ def sanity_check_rpath(self, rpath_dirs=None, check_readelf_rpath=True):
33953413
self.log.debug(f"Sanity checking RPATH for files in {dirpath}")
33963414

33973415
for path in [os.path.join(dirpath, x) for x in os.listdir(dirpath)]:
3416+
# skip the check for any symlinks that resolve to outside the installation directory
3417+
if not is_parent_path(self.installdir, path):
3418+
realpath = os.path.realpath(path)
3419+
msg = (f"Skipping RPATH sanity check for {path}, since its absolute path {realpath} resolves to"
3420+
f" outside the installation directory {self.installdir}")
3421+
self.log.info(msg)
3422+
continue
3423+
33983424
self.log.debug(f"Sanity checking RPATH for {path}")
33993425

34003426
out = get_linked_libs_raw(path)

easybuild/main.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,26 @@ def find_easyconfigs_by_specs(build_specs, robot_path, try_to_generate, testing=
111111
return [(ec_file, generated)]
112112

113113

114-
def build_and_install_software(ecs, init_session_state, exit_on_failure=True):
114+
def summary(ecs_with_res):
115+
"""
116+
Compose summary of the build:
117+
* [S] for a successful build
118+
* [F] for a failed build
119+
* [-] for a build that didn’t run
120+
121+
:param ecs_with_res: list of tuples (ec, ec_res), ec is an EasyConfig object, and ec_res is a dict of the result
122+
"""
123+
summary_fmt = " * [{}] {}"
124+
success_map = {True: 'S', False: 'F', None: '-'}
125+
lines = ["Summary:"]
126+
lines.extend([
127+
summary_fmt.format(success_map[ec_res.get('success', False)], ec['full_mod_name'])
128+
for ec, ec_res in ecs_with_res
129+
])
130+
return '\n'.join(lines)
131+
132+
133+
def build_and_install_software(ecs, init_session_state, exit_on_failure=True, testing=False):
115134
"""
116135
Build and install software for all provided parsed easyconfig files.
117136
@@ -126,7 +145,7 @@ def build_and_install_software(ecs, init_session_state, exit_on_failure=True):
126145

127146
start_progress_bar(STATUS_BAR, size=len(ecs))
128147

129-
res = []
148+
ecs_with_res = []
130149
ec_results = []
131150
failed_cnt = 0
132151

@@ -171,14 +190,17 @@ def build_and_install_software(ecs, init_session_state, exit_on_failure=True):
171190
write_file(test_report_fp, test_report_txt['full'])
172191
adjust_permissions(parent_dir, stat.S_IWUSR, add=False, recursive=False)
173192

193+
ecs_with_res.append((ec, ec_res))
194+
174195
if not ec_res['success'] and exit_on_failure:
196+
ecs_in_res = [res[0] for res in ecs_with_res]
197+
ecs_without_res = [(ec, {'success': None}) for ec in ecs if ec not in ecs_in_res]
198+
print_msg(summary(ecs_with_res + ecs_without_res), log=_log, silent=testing)
175199
error = ec_res['err']
176200
if isinstance(error, EasyBuildError):
177201
error = EasyBuildError(test_msg, exit_code=error.exit_code)
178202
raise error
179203

180-
res.append((ec, ec_res))
181-
182204
if failed_cnt:
183205
# if installations failed: indicate th
184206
status_label = ' (%s): ' % colorize('%s failed!' % failed_cnt, COLOR_RED)
@@ -192,7 +214,7 @@ def build_and_install_software(ecs, init_session_state, exit_on_failure=True):
192214

193215
stop_progress_bar(STATUS_BAR)
194216

195-
return res
217+
return ecs_with_res
196218

197219

198220
def run_contrib_style_checks(ecs, check_contrib, check_style):
@@ -563,11 +585,11 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session
563585

564586
with rich_live_cm():
565587
run_hook(PRE_PREF + BUILD_AND_INSTALL_LOOP, hooks, args=[ordered_ecs])
566-
ecs_with_res = build_and_install_software(ordered_ecs, init_session_state,
567-
exit_on_failure=exit_on_failure)
588+
ecs_with_res = build_and_install_software(
589+
ordered_ecs, init_session_state, exit_on_failure=exit_on_failure, testing=testing)
568590
run_hook(POST_PREF + BUILD_AND_INSTALL_LOOP, hooks, args=[ecs_with_res])
569591
else:
570-
ecs_with_res = [(ec, {}) for ec in ordered_ecs]
592+
ecs_with_res = [(ec, {'success': None}) for ec in ordered_ecs]
571593

572594
correct_builds_cnt = len([ec_res for (_, ec_res) in ecs_with_res if ec_res.get('success', False)])
573595
overall_success = correct_builds_cnt == len(ordered_ecs)
@@ -585,6 +607,8 @@ def process_eb_args(eb_args, eb_go, cfg_settings, modtool, testing, init_session
585607
print_msg(test_report_msg)
586608

587609
print_msg(success_msg, log=_log, silent=testing)
610+
if ecs_with_res:
611+
print_msg(summary(ecs_with_res), log=_log, silent=testing)
588612

589613
# cleanup and spec files
590614
for ec in easyconfigs:

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/easyconfig.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4197,10 +4197,10 @@ def test_check_sha256_checksums(self):
41974197
# re-test with right checksum in place
41984198
toy_sha256 = '44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc'
41994199
test_ec_txt = checksums_regex.sub('checksums = ["%s"]' % toy_sha256, toy_ec_txt)
4200-
test_ec_txt = re.sub(r'patches = \[(.|\n)*\]', '', test_ec_txt)
4200+
passing_test_ec_txt = re.sub(r'patches = \[(.|\n)*\]', '', test_ec_txt)
42014201

42024202
test_ec = os.path.join(self.test_prefix, 'toy-0.0-ok.eb')
4203-
write_file(test_ec, test_ec_txt)
4203+
write_file(test_ec, passing_test_ec_txt)
42044204
ecs, _ = parse_easyconfigs([(test_ec, False)])
42054205
ecs = [ec['ec'] for ec in ecs]
42064206

@@ -4236,6 +4236,15 @@ def test_check_sha256_checksums(self):
42364236
regex = re.compile(r"Non-SHA256 checksum\(s\) found for toy-0.0.tar.gz:.*not_really_a_sha256_checksum")
42374237
self.assertTrue(regex.match(res[0]), "Pattern '%s' found in: %s" % (regex.pattern, res[0]))
42384238

4239+
# Extension with nosource: True
4240+
test_ec_txt = passing_test_ec_txt + "exts_list = [('bar', '0.0', { 'nosource': True })]"
4241+
toy_sha256 = '44332000aa33b99ad1e00cbd1a7da769220d74647060a10e807b916d73ea27bc'
4242+
test_ec = os.path.join(self.test_prefix, 'toy-0.0-nosource.eb')
4243+
write_file(test_ec, test_ec_txt)
4244+
ecs, _ = parse_easyconfigs([(test_ec, False)])
4245+
ecs = [ec['ec'] for ec in ecs]
4246+
self.assertEqual(check_sha256_checksums(ecs), [])
4247+
42394248
def test_deprecated(self):
42404249
"""Test use of 'deprecated' easyconfig parameter."""
42414250
topdir = os.path.dirname(os.path.abspath(__file__))

0 commit comments

Comments
 (0)