Skip to content

Commit b9abfd5

Browse files
committed
add pre/post extension hook (triggered before/after individual extension installations) (fixes #4191)
1 parent e9b7cea commit b9abfd5

File tree

5 files changed

+155
-11
lines changed

5 files changed

+155
-11
lines changed

easybuild/framework/easyblock.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@
8383
from easybuild.tools.filetools import find_backup_name_candidate, get_source_tarball_from_git, is_alt_pypi_url
8484
from easybuild.tools.filetools import is_binary, is_sha256_checksum, mkdir, move_file, move_logs, read_file, remove_dir
8585
from easybuild.tools.filetools import remove_file, remove_lock, verify_checksum, weld_paths, write_file, symlink
86-
from easybuild.tools.hooks import BUILD_STEP, CLEANUP_STEP, CONFIGURE_STEP, EXTENSIONS_STEP, FETCH_STEP, INSTALL_STEP
87-
from easybuild.tools.hooks import MODULE_STEP, PACKAGE_STEP, PATCH_STEP, PERMISSIONS_STEP, POSTITER_STEP, POSTPROC_STEP
88-
from easybuild.tools.hooks import PREPARE_STEP, READY_STEP, SANITYCHECK_STEP, SOURCE_STEP, TEST_STEP, TESTCASES_STEP
89-
from easybuild.tools.hooks import MODULE_WRITE, load_hooks, run_hook
86+
from easybuild.tools.hooks import BUILD_STEP, CLEANUP_STEP, CONFIGURE_STEP, EXTENSION_INSTALL, EXTENSIONS_STEP
87+
from easybuild.tools.hooks import FETCH_STEP, INSTALL_STEP, MODULE_STEP, MODULE_WRITE, PACKAGE_STEP, PATCH_STEP
88+
from easybuild.tools.hooks import PERMISSIONS_STEP, POSTITER_STEP, POSTPROC_STEP, PREPARE_STEP, READY_STEP
89+
from easybuild.tools.hooks import SANITYCHECK_STEP, SOURCE_STEP, TEST_STEP, TESTCASES_STEP, load_hooks, run_hook
9090
from easybuild.tools.run import check_async_cmd, run_cmd
9191
from easybuild.tools.jenkins import write_to_xml
9292
from easybuild.tools.module_generator import ModuleGeneratorLua, ModuleGeneratorTcl, module_generator, dependencies_for
@@ -1851,6 +1851,8 @@ def install_extensions_sequential(self, install=True):
18511851

18521852
self.log.info("Starting extension %s", ext.name)
18531853

1854+
run_hook(EXTENSION_INSTALL, self.hooks, pre_step_hook=True, args=[ext])
1855+
18541856
# always go back to original work dir to avoid running stuff from a dir that no longer exists
18551857
change_dir(self.orig_workdir)
18561858

@@ -1897,6 +1899,8 @@ def install_extensions_sequential(self, install=True):
18971899

18981900
self.update_exts_progress_bar(progress_info, progress_size=1)
18991901

1902+
run_hook(EXTENSION_INSTALL, self.hooks, post_step_hook=True, args=[ext])
1903+
19001904
def install_extensions_parallel(self, install=True):
19011905
"""
19021906
Install extensions in parallel.

easybuild/tools/hooks.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161

6262
START = 'start'
6363
PARSE = 'parse'
64+
EXTENSION_INSTALL = 'extension'
6465
MODULE_WRITE = 'module_write'
6566
END = 'end'
6667

@@ -73,7 +74,31 @@
7374
INSTALL_STEP, EXTENSIONS_STEP, POSTPROC_STEP, SANITYCHECK_STEP, CLEANUP_STEP, MODULE_STEP,
7475
PERMISSIONS_STEP, PACKAGE_STEP, TESTCASES_STEP]
7576

76-
HOOK_NAMES = [START, PARSE, MODULE_WRITE] + [p + s for s in STEP_NAMES for p in [PRE_PREF, POST_PREF]] + [END]
77+
# hook names (in order of being triggered)
78+
HOOK_NAMES = [
79+
START,
80+
PARSE,
81+
] + [p + x for x in STEP_NAMES[:STEP_NAMES.index(EXTENSIONS_STEP)]
82+
for p in [PRE_PREF, POST_PREF]] + [
83+
# pre-extensions hook is triggered before starting installation of extensions,
84+
# pre/post extension (singular) hook is triggered when installing individual extensions,
85+
# post-extensions hook is triggered after installation of extensions
86+
PRE_PREF + EXTENSIONS_STEP,
87+
PRE_PREF + EXTENSION_INSTALL,
88+
POST_PREF + EXTENSION_INSTALL,
89+
POST_PREF + EXTENSIONS_STEP,
90+
] + [p + x for x in STEP_NAMES[STEP_NAMES.index(EXTENSIONS_STEP)+1:STEP_NAMES.index(MODULE_STEP)]
91+
for p in [PRE_PREF, POST_PREF]] + [
92+
# pre-module hook hook is triggered before starting module step which creates module file,
93+
# module_write hook is triggered when module file has been written,
94+
# post-module hook hook is triggered before after running module step
95+
PRE_PREF + MODULE_STEP,
96+
MODULE_WRITE,
97+
POST_PREF + MODULE_STEP,
98+
] + [p + x for x in STEP_NAMES[STEP_NAMES.index(MODULE_STEP)+1:]
99+
for p in [PRE_PREF, POST_PREF]] + [
100+
END,
101+
]
77102
KNOWN_HOOKS = [h + HOOK_SUFF for h in HOOK_NAMES]
78103

79104

test/framework/hooks.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ def setUp(self):
6161
'',
6262
'def pre_install_hook(self):',
6363
' print("this is run before install step")',
64+
'',
65+
'def pre_extension_hook(ext):',
66+
' print("this is run before installing an extension")',
6467
])
6568
write_file(self.test_hooks_pymod, test_hooks_pymod_txt)
6669

@@ -71,8 +74,9 @@ def test_load_hooks(self):
7174

7275
hooks = load_hooks(self.test_hooks_pymod)
7376

74-
self.assertEqual(len(hooks), 4)
75-
self.assertEqual(sorted(hooks.keys()), ['parse_hook', 'post_configure_hook', 'pre_install_hook', 'start_hook'])
77+
self.assertEqual(len(hooks), 5)
78+
expected = ['parse_hook', 'post_configure_hook', 'pre_extension_hook', 'pre_install_hook', 'start_hook']
79+
self.assertEqual(sorted(hooks.keys()), expected)
7680
self.assertTrue(all(callable(h) for h in hooks.values()))
7781

7882
# test caching of hooks
@@ -101,6 +105,7 @@ def test_find_hook(self):
101105

102106
post_configure_hook = [hooks[k] for k in hooks if k == 'post_configure_hook'][0]
103107
pre_install_hook = [hooks[k] for k in hooks if k == 'pre_install_hook'][0]
108+
pre_extension_hook = [hooks[k] for k in hooks if k == 'pre_extension_hook'][0]
104109
start_hook = [hooks[k] for k in hooks if k == 'start_hook'][0]
105110

106111
self.assertEqual(find_hook('configure', hooks), None)
@@ -111,6 +116,14 @@ def test_find_hook(self):
111116
self.assertEqual(find_hook('install', hooks, pre_step_hook=True), pre_install_hook)
112117
self.assertEqual(find_hook('install', hooks, post_step_hook=True), None)
113118

119+
self.assertEqual(find_hook('extension', hooks), None)
120+
self.assertEqual(find_hook('extension', hooks, pre_step_hook=True), pre_extension_hook)
121+
self.assertEqual(find_hook('extension', hooks, post_step_hook=True), None)
122+
123+
self.assertEqual(find_hook('extensions', hooks), None)
124+
self.assertEqual(find_hook('extensions', hooks, pre_step_hook=True), None)
125+
self.assertEqual(find_hook('extensions', hooks, post_step_hook=True), None)
126+
114127
self.assertEqual(find_hook('build', hooks), None)
115128
self.assertEqual(find_hook('build', hooks, pre_step_hook=True), None)
116129
self.assertEqual(find_hook('build', hooks, post_step_hook=True), None)
@@ -136,6 +149,11 @@ def test_run_hook(self):
136149
run_hook('build', hooks, post_step_hook=True, args=[None])
137150
run_hook('install', hooks, pre_step_hook=True, args=[None])
138151
run_hook('install', hooks, post_step_hook=True, args=[None])
152+
run_hook('extensions', hooks, pre_step_hook=True, args=[None])
153+
for x in range(3):
154+
run_hook('extension', hooks, pre_step_hook=True, args=[None])
155+
run_hook('extension', hooks, post_step_hook=True, args=[None])
156+
run_hook('extensions', hooks, post_step_hook=True, args=[None])
139157
stdout = self.get_stdout()
140158
stderr = self.get_stderr()
141159
self.mock_stdout(False)
@@ -151,6 +169,12 @@ def test_run_hook(self):
151169
"running foo helper method",
152170
"== Running pre-install hook...",
153171
"this is run before install step",
172+
"== Running pre-extension hook...",
173+
"this is run before installing an extension",
174+
"== Running pre-extension hook...",
175+
"this is run before installing an extension",
176+
"== Running pre-extension hook...",
177+
"this is run before installing an extension",
154178
])
155179

156180
self.assertEqual(stdout.strip(), expected_stdout)

test/framework/options.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,66 @@ def run_test(custom=None, extra_params=[], fmt=None):
659659
run_test(custom='bar', extra_params=['bar_extra1', 'bar_extra2'], fmt=fmt)
660660
run_test(custom='EB_foofoo', extra_params=['foofoo_extra1', 'foofoo_extra2'], fmt=fmt)
661661

662+
def test_avail_hooks(self):
663+
"""
664+
Test listing available hooks via --avail-hooks
665+
"""
666+
667+
self.mock_stderr(True)
668+
self.mock_stdout(True)
669+
self.eb_main(['--avail-hooks'], verbose=True, raise_error=True)
670+
stderr, stdout = self.get_stderr(), self.get_stdout()
671+
self.mock_stderr(False)
672+
self.mock_stdout(False)
673+
674+
self.assertFalse(stderr)
675+
676+
expected = '\n'.join([
677+
"List of supported hooks (in order of execution):",
678+
" start_hook",
679+
" parse_hook",
680+
" pre_fetch_hook",
681+
" post_fetch_hook",
682+
" pre_ready_hook",
683+
" post_ready_hook",
684+
" pre_source_hook",
685+
" post_source_hook",
686+
" pre_patch_hook",
687+
" post_patch_hook",
688+
" pre_prepare_hook",
689+
" post_prepare_hook",
690+
" pre_configure_hook",
691+
" post_configure_hook",
692+
" pre_build_hook",
693+
" post_build_hook",
694+
" pre_test_hook",
695+
" post_test_hook",
696+
" pre_install_hook",
697+
" post_install_hook",
698+
" pre_extensions_hook",
699+
" pre_extension_hook",
700+
" post_extension_hook",
701+
" post_extensions_hook",
702+
" pre_postproc_hook",
703+
" post_postproc_hook",
704+
" pre_sanitycheck_hook",
705+
" post_sanitycheck_hook",
706+
" pre_cleanup_hook",
707+
" post_cleanup_hook",
708+
" pre_module_hook",
709+
" module_write_hook",
710+
" post_module_hook",
711+
" pre_permissions_hook",
712+
" post_permissions_hook",
713+
" pre_package_hook",
714+
" post_package_hook",
715+
" pre_testcases_hook",
716+
" post_testcases_hook",
717+
" end_hook",
718+
'',
719+
])
720+
self.assertEqual(stdout, expected)
721+
662722
# double underscore to make sure it runs first, which is required to detect certain types of bugs,
663723
# e.g. running with non-initialized EasyBuild config (truly mimicing 'eb --list-toolchains')
664724
def test__list_toolchains(self):

test/framework/toy_build.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2878,6 +2878,11 @@ def test_toy_build_trace(self):
28782878

28792879
def test_toy_build_hooks(self):
28802880
"""Test use of --hooks."""
2881+
toy_ec = os.path.join(os.path.dirname(__file__), 'easyconfigs', 'test_ecs', 't', 'toy', 'toy-0.0.eb')
2882+
test_ec = os.path.join(self.test_prefix, 'test.eb')
2883+
test_ec_txt = read_file(toy_ec) + "\nexts_list = [('bar', '0.0'), ('toy', '0.0')]"
2884+
write_file(test_ec, test_ec_txt)
2885+
28812886
hooks_file = os.path.join(self.test_prefix, 'my_hooks.py')
28822887
hooks_file_txt = textwrap.dedent("""
28832888
import os
@@ -2908,14 +2913,20 @@ def module_write_hook(self, module_path, module_txt):
29082913
print('in module-write hook hook for %s' % os.path.basename(module_path))
29092914
return module_txt.replace('Toy C program, 100% toy.', 'Not a toy anymore')
29102915
2916+
def post_extension_hook(ext):
2917+
print('installing of extension %s is done!' % ext.name)
2918+
2919+
def pre_sanitycheck_hook(self):
2920+
print('pre_sanity_check_hook')
2921+
29112922
def end_hook():
29122923
print('end hook triggered, all done!')
29132924
""")
29142925
write_file(hooks_file, hooks_file_txt)
29152926

29162927
self.mock_stderr(True)
29172928
self.mock_stdout(True)
2918-
self.test_toy_build(extra_args=['--hooks=%s' % hooks_file], raise_error=True)
2929+
self.test_toy_build(ec_file=test_ec, extra_args=['--hooks=%s' % hooks_file], raise_error=True)
29192930
stderr = self.get_stderr()
29202931
stdout = self.get_stdout()
29212932
self.mock_stderr(False)
@@ -2927,12 +2938,16 @@ def end_hook():
29272938
toy_mod_file += '.lua'
29282939

29292940
self.assertEqual(stderr, '')
2930-
# There are 4 modules written:
2931-
# Sanitycheck for extensions and main easyblock (1 each), main and devel module
2941+
# parse hook is triggered 3 times: once for main install, and then again for each extension;
2942+
# module write hook is triggered 5 times:
2943+
# - before installing extensions
2944+
# - for fake module file being created during sanity check (triggered twice, for main + toy install)
2945+
# - for final module file
2946+
# - for devel module file
29322947
expected_output = textwrap.dedent("""
29332948
== Running start hook...
29342949
start hook triggered
2935-
== Running parse hook for toy-0.0.eb...
2950+
== Running parse hook for test.eb...
29362951
toy 0.0
29372952
['%(name)s-%(version)s.tar.gz']
29382953
echo toy
@@ -2945,6 +2960,22 @@ def end_hook():
29452960
bin, lib
29462961
== Running module_write hook...
29472962
in module-write hook hook for {mod_name}
2963+
== Running parse hook...
2964+
toy 0.0
2965+
['%(name)s-%(version)s.tar.gz']
2966+
echo toy
2967+
== Running parse hook...
2968+
toy 0.0
2969+
['%(name)s-%(version)s.tar.gz']
2970+
echo toy
2971+
== Running post-extension hook...
2972+
installing of extension bar is done!
2973+
== Running post-extension hook...
2974+
installing of extension toy is done!
2975+
== Running pre-sanitycheck hook...
2976+
pre_sanity_check_hook
2977+
== Running module_write hook...
2978+
in module-write hook hook for {mod_name}
29482979
== Running module_write hook...
29492980
in module-write hook hook for {mod_name}
29502981
== Running module_write hook...

0 commit comments

Comments
 (0)