Skip to content

Commit 24e657c

Browse files
committed
add support for multiple modulenames in extensions
This changes the allowed types of the `modulename` extension option: - `False` to skip the sanity check and always install it when `--skip` is used - `str`: Value for `%(ext_name)s` in the `exts_filter` template - List of `str`: Multiple names to be used in the `exts_filter` template. All resulting commands must succeed. It replaces `resolve_exts_filter_template` by `construct_exts_filter_cmds` as the method now returns a, potentially empty, list which might cause errors if used without expecting a list.
1 parent fb6ff38 commit 24e657c

File tree

3 files changed

+95
-67
lines changed

3 files changed

+95
-67
lines changed

easybuild/framework/easyblock.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
from easybuild.framework.easyconfig.style import MAX_LINE_LENGTH
6868
from easybuild.framework.easyconfig.tools import dump_env_easyblock, get_paths_for
6969
from easybuild.framework.easyconfig.templates import TEMPLATE_NAMES_EASYBLOCK_RUN_STEP, template_constant_dict
70-
from easybuild.framework.extension import Extension, resolve_exts_filter_template
70+
from easybuild.framework.extension import Extension, construct_exts_filter_cmds
7171
from easybuild.tools import LooseVersion, config
7272
from easybuild.tools.build_details import get_build_stats
7373
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, dry_run_msg, dry_run_warning, dry_run_set_dirs
@@ -1872,10 +1872,15 @@ def skip_extensions_sequential(self, exts_filter):
18721872

18731873
exts = []
18741874
for idx, ext_inst in enumerate(self.ext_instances):
1875-
cmd, stdin = resolve_exts_filter_template(exts_filter, ext_inst)
1876-
res = run_shell_cmd(cmd, stdin=stdin, fail_on_error=False, hidden=True)
1877-
self.log.info(f"exts_filter result for {ext_inst.name}: exit code {res.exit_code}; output: {res.output}")
1878-
if res.exit_code == EasyBuildExit.SUCCESS:
1875+
cmds = construct_exts_filter_cmds(exts_filter, ext_inst) or []
1876+
for cmd, stdin in cmds:
1877+
res = run_shell_cmd(cmd, stdin=stdin, fail_on_error=False, hidden=True)
1878+
self.log.info(f"exts_filter result for {ext_inst.name}:cmd {cmd}; "
1879+
f"exit code {res.exit_code}; output: {res.output}")
1880+
if res.exit_code != EasyBuildExit.SUCCESS:
1881+
break
1882+
# Don't skip extension if there were no commands to check, e.g. modulename=False
1883+
if cmds and res.exit_code == EasyBuildExit.SUCCESS:
18791884
print_msg(f"skipping extension {ext_inst.name}", silent=self.silent, log=self.log)
18801885
else:
18811886
self.log.info(f"Not skipping {ext_inst.name}")
@@ -1893,37 +1898,45 @@ def skip_extensions_parallel(self, exts_filter):
18931898
"""
18941899
print_msg("skipping installed extensions (in parallel)", log=self.log)
18951900

1896-
installed_exts_ids = []
1897-
checked_exts_cnt = 0
1901+
cmds = [construct_exts_filter_cmds(exts_filter, ext) for ext in self.ext_instances]
1902+
# Consider extensions that don't need checking as checked
1903+
checked_exts_cnt = sum(0 if ext_cmds else 1 for ext_cmds in cmds)
18981904
exts_cnt = len(self.ext_instances)
1899-
cmds = [resolve_exts_filter_template(exts_filter, ext) for ext in self.ext_instances]
19001905

19011906
with ThreadPoolExecutor(max_workers=self.cfg['parallel']) as thread_pool:
19021907

19031908
# list of command to run asynchronously
19041909
async_cmds = [thread_pool.submit(run_shell_cmd, cmd, stdin=stdin, hidden=True, fail_on_error=False,
1905-
asynchronous=True, task_id=idx) for (idx, (cmd, stdin)) in enumerate(cmds)]
1910+
asynchronous=True, task_id=idx)
1911+
for (idx, ext_cmds) in enumerate(cmds)
1912+
for (cmd, stdin) in ext_cmds]
19061913

1914+
pending_cmds_per_ext = [len(ext_cmds) for ext_cmds in cmds]
1915+
installed_exts = [True] * exts_cnt
19071916
# process result of commands as they have completed running
19081917
for done_task in concurrent.futures.as_completed(async_cmds):
19091918
res = done_task.result()
19101919
idx = res.task_id
19111920
ext_name = self.ext_instances[idx].name
19121921
self.log.info(f"exts_filter result for {ext_name}: exit code {res.exit_code}; output: {res.output}")
1913-
if res.exit_code == EasyBuildExit.SUCCESS:
1914-
print_msg(f"skipping extension {ext_name}", log=self.log)
1915-
installed_exts_ids.append(idx)
19161922

1917-
checked_exts_cnt += 1
1918-
exts_pbar_label = "skipping installed extensions "
1919-
exts_pbar_label += "(%d/%d checked)" % (checked_exts_cnt, exts_cnt)
1920-
self.update_exts_progress_bar(exts_pbar_label)
1923+
if res.exit_code != EasyBuildExit.SUCCESS:
1924+
installed_exts[idx] = False
1925+
1926+
pending_cmds_per_ext[idx] -= 1
1927+
if pending_cmds_per_ext[idx] == 0:
1928+
checked_exts_cnt += 1
1929+
exts_pbar_label = "skipping installed extensions "
1930+
exts_pbar_label += "(%d/%d checked)" % (checked_exts_cnt, exts_cnt)
1931+
self.update_exts_progress_bar(exts_pbar_label)
19211932

19221933
# compose new list of extensions, skip over the ones that are already installed;
19231934
# note: original order in extensions list should be preserved!
19241935
retained_ext_instances = []
19251936
for idx, ext in enumerate(self.ext_instances):
1926-
if idx not in installed_exts_ids:
1937+
if installed_exts[idx]:
1938+
print_msg(f"skipping extension {ext.name}", log=self.log)
1939+
else:
19271940
retained_ext_instances.append(ext)
19281941
self.log.info("Not skipping %s", ext.name)
19291942

easybuild/framework/extension.py

Lines changed: 49 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,17 @@
4040

4141
from easybuild.framework.easyconfig.easyconfig import resolve_template
4242
from easybuild.framework.easyconfig.templates import TEMPLATE_NAMES_EASYBLOCK_RUN_STEP, template_constant_dict
43-
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit, raise_nosupport
43+
from easybuild.tools.build_log import EasyBuildError, EasyBuildExit
4444
from easybuild.tools.filetools import change_dir
4545
from easybuild.tools.run import run_shell_cmd
4646

4747

48-
def resolve_exts_filter_template(exts_filter, ext):
48+
def construct_exts_filter_cmds(exts_filter, ext):
4949
"""
5050
Resolve the exts_filter tuple by replacing the template values using the extension
5151
:param exts_filter: Tuple of (command, input) using template values (ext_name, ext_version, src)
5252
:param ext: Instance of Extension or dictionary like with 'name' and optionally 'options', 'version', 'source' keys
53-
:return: (cmd, input) as a tuple of strings
53+
:return: (cmd, input) as a tuple of strings for each modulename. Might be empty if no filtering is intented
5454
"""
5555

5656
if isinstance(exts_filter, str) or len(exts_filter) != 2:
@@ -61,25 +61,35 @@ def resolve_exts_filter_template(exts_filter, ext):
6161
if not isinstance(ext, dict):
6262
ext = {'name': ext.name, 'version': ext.version, 'src': ext.src, 'options': ext.options}
6363

64-
name = ext['name']
65-
if 'options' in ext and 'modulename' in ext['options']:
66-
modname = ext['options']['modulename']
67-
else:
68-
modname = name
69-
tmpldict = {
70-
'ext_name': modname,
71-
'ext_version': ext.get('version'),
72-
'src': ext.get('src'),
73-
}
74-
7564
try:
76-
cmd = cmd % tmpldict
77-
cmdinput = cmdinput % tmpldict if cmdinput else None
78-
except KeyError as err:
79-
msg = "KeyError occurred on completing extension filter template: %s; "
80-
msg += "'name'/'version' keys are no longer supported, should use 'ext_name'/'ext_version' instead"
81-
raise_nosupport(msg % err, '2.0')
82-
return cmd, cmdinput
65+
modulenames = ext['options']['modulename']
66+
except KeyError:
67+
modulenames = [ext['name']]
68+
else:
69+
if isinstance(modulenames, list):
70+
if not modulenames:
71+
raise EasyBuildError(f"Empty modulename list for {ext['name']} is not supported."
72+
"Use `False` to skip checking the module!")
73+
elif modulenames is False:
74+
return [] # Skip any checks
75+
elif not isinstance(modulenames, str):
76+
raise EasyBuildError(f"Wrong type of modulename for {ext['name']}: {type(modulenames)}: {modulenames}")
77+
else:
78+
modulenames = [modulenames]
79+
80+
result = []
81+
for modulename in modulenames:
82+
tmpldict = {
83+
'ext_name': modulename,
84+
'ext_version': ext.get('version'),
85+
'src': ext.get('src'),
86+
}
87+
try:
88+
result.append((cmd % tmpldict,
89+
cmdinput % tmpldict if cmdinput else None))
90+
except KeyError as err:
91+
raise EasyBuildError(f"KeyError occurred on completing extension filter template: {err}")
92+
return result
8393

8494

8595
class Extension(object):
@@ -287,31 +297,30 @@ def sanity_check_step(self):
287297

288298
if exts_filter is None:
289299
self.log.debug("no exts_filter setting found, skipping sanitycheck")
300+
return res
290301

291-
if 'modulename' in self.options:
292-
modname = self.options['modulename']
293-
self.log.debug("modulename found in self.options, using it: %s", modname)
294-
else:
295-
modname = self.name
296-
self.log.debug("self.name: %s", modname)
297-
302+
exts_filer_cmds = construct_exts_filter_cmds(exts_filter, self)
298303
# allow skipping of sanity check by setting module name to False
299-
if modname is False:
304+
if exts_filer_cmds is None:
300305
self.log.info("modulename set to False for '%s' extension, so skipping sanity check", self.name)
301-
elif exts_filter:
302-
cmd, stdin = resolve_exts_filter_template(exts_filter, self)
303-
cmd_res = run_shell_cmd(cmd, fail_on_error=False, stdin=stdin)
304-
305-
if cmd_res.exit_code != EasyBuildExit.SUCCESS:
306-
if stdin:
307-
fail_msg = 'command "%s" (stdin: "%s") failed' % (cmd, stdin)
308-
else:
309-
fail_msg = 'command "%s" failed' % cmd
310-
fail_msg += "; output:\n%s" % cmd_res.output.strip()
306+
else:
307+
fail_msgs = []
308+
for cmd, stdin in exts_filer_cmds:
309+
cmd_res = run_shell_cmd(cmd, fail_on_error=False, stdin=stdin)
310+
311+
if cmd_res.exit_code != EasyBuildExit.SUCCESS:
312+
if stdin:
313+
fail_msg = 'command "%s" (stdin: "%s") failed' % (cmd, stdin)
314+
else:
315+
fail_msg = 'command "%s" failed' % cmd
316+
fail_msg += "; output:\n%s" % cmd_res.output.strip()
317+
fail_msgs.append(fail_msg)
318+
if fail_msgs:
319+
fail_msg = '\n'.join(fail_msgs)
311320
self.log.warning("Sanity check for '%s' extension failed: %s", self.name, fail_msg)
312-
res = (False, fail_msg)
313321
# keep track of all reasons of failure
314322
# (only relevant when this extension is installed stand-alone via ExtensionEasyBlock)
315323
self.sanity_check_fail_msgs.append(fail_msg)
324+
res = (False, fail_msg)
316325

317326
return res

test/framework/easyconfig.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
from easybuild.framework.easyconfig.tools import dep_graph, det_copy_ec_specs, find_related_easyconfigs, get_paths_for
6363
from easybuild.framework.easyconfig.tools import parse_easyconfigs
6464
from easybuild.framework.easyconfig.tweak import obtain_ec_for, tweak, tweak_one
65-
from easybuild.framework.extension import resolve_exts_filter_template
65+
from easybuild.framework.extension import construct_exts_filter_cmds
6666
from easybuild.toolchains.system import SystemToolchain
6767
from easybuild.tools.build_log import EasyBuildError
6868
from easybuild.tools.config import build_option, get_module_syntax, module_classes, update_build_option
@@ -4632,8 +4632,8 @@ def test_unexpected_version_keys_caught(self):
46324632

46334633
self.assertRaises(EasyBuildError, EasyConfig, test_ec)
46344634

4635-
def test_resolve_exts_filter_template(self):
4636-
"""Test for resolve_exts_filter_template function."""
4635+
def test_construct_exts_filter_cmds(self):
4636+
"""Test for construct_exts_filter_cmds function."""
46374637
class TestExtension(object):
46384638
def __init__(self, values):
46394639
self.name = values['name']
@@ -4642,11 +4642,11 @@ def __init__(self, values):
46424642
self.options = values.get('options', {})
46434643

46444644
error_msg = 'exts_filter should be a list or tuple'
4645-
self.assertErrorRegex(EasyBuildError, error_msg, resolve_exts_filter_template,
4645+
self.assertErrorRegex(EasyBuildError, error_msg, construct_exts_filter_cmds,
46464646
'[ 1 == 1 ]', {})
4647-
self.assertErrorRegex(EasyBuildError, error_msg, resolve_exts_filter_template,
4647+
self.assertErrorRegex(EasyBuildError, error_msg, construct_exts_filter_cmds,
46484648
['[ 1 == 1 ]'], {})
4649-
self.assertErrorRegex(EasyBuildError, error_msg, resolve_exts_filter_template,
4649+
self.assertErrorRegex(EasyBuildError, error_msg, construct_exts_filter_cmds,
46504650
['[ 1 == 1 ]', 'true', 'false'], {})
46514651

46524652
test_cases = [
@@ -4677,10 +4677,16 @@ def __init__(self, values):
46774677
),
46784678
]
46794679
for exts_filter, ext, expected_value in test_cases:
4680-
value = resolve_exts_filter_template(exts_filter, ext)
4681-
self.assertEqual(value, expected_value)
4682-
value = resolve_exts_filter_template(exts_filter, TestExtension(ext))
4683-
self.assertEqual(value, expected_value)
4680+
value = construct_exts_filter_cmds(exts_filter, ext)
4681+
self.assertEqual(value, [expected_value])
4682+
value = construct_exts_filter_cmds(exts_filter, TestExtension(ext))
4683+
self.assertEqual(value, [expected_value])
4684+
4685+
exts_filter = ('run %(ext_name)s', None)
4686+
value = construct_exts_filter_cmds(exts_filter, {'name': 'foo', 'options': {'modulename': False}})
4687+
self.assertEqual(value, [])
4688+
value = construct_exts_filter_cmds(exts_filter, {'name': 'foo', 'options': {'modulename': ['name', 'alt']}})
4689+
self.assertEqual(value, [('run name', None), ('run alt', None)])
46844690

46854691
def test_cuda_compute_capabilities(self):
46864692
"""Tests that the cuda_compute_capabilities templates are correct"""

0 commit comments

Comments
 (0)