Skip to content

Commit a80f589

Browse files
authored
Merge pull request #3728 from Flamefire/module-write-hook
Add module-write hook
2 parents 545ee2d + a5ff50a commit a80f589

File tree

3 files changed

+85
-60
lines changed

3 files changed

+85
-60
lines changed

easybuild/framework/easyblock.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
from easybuild.tools.hooks import BUILD_STEP, CLEANUP_STEP, CONFIGURE_STEP, EXTENSIONS_STEP, FETCH_STEP, INSTALL_STEP
8282
from easybuild.tools.hooks import MODULE_STEP, PACKAGE_STEP, PATCH_STEP, PERMISSIONS_STEP, POSTITER_STEP, POSTPROC_STEP
8383
from easybuild.tools.hooks import PREPARE_STEP, READY_STEP, SANITYCHECK_STEP, SOURCE_STEP, TEST_STEP, TESTCASES_STEP
84-
from easybuild.tools.hooks import load_hooks, run_hook
84+
from easybuild.tools.hooks import MODULE_WRITE, load_hooks, run_hook
8585
from easybuild.tools.run import run_cmd
8686
from easybuild.tools.jenkins import write_to_xml
8787
from easybuild.tools.module_generator import ModuleGeneratorLua, ModuleGeneratorTcl, module_generator, dependencies_for
@@ -144,7 +144,7 @@ def __init__(self, ec):
144144
# keep track of original working directory, so we can go back there
145145
self.orig_workdir = os.getcwd()
146146

147-
# list of pre- and post-step hooks
147+
# dict of all hooks (mapping of name to function)
148148
self.hooks = load_hooks(build_option('hooks'))
149149

150150
# list of patch/source files, along with checksums
@@ -3165,6 +3165,10 @@ def make_module_step(self, fake=False):
31653165
txt += self.make_module_extra()
31663166
txt += self.make_module_footer()
31673167

3168+
hook_txt = run_hook(MODULE_WRITE, self.hooks, args=[self, mod_filepath, txt])
3169+
if hook_txt is not None:
3170+
txt = hook_txt
3171+
31683172
if self.dry_run:
31693173
# only report generating actual module file during dry run, don't mention temporary module files
31703174
if not fake:

easybuild/tools/hooks.py

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858

5959
START = 'start'
6060
PARSE = 'parse'
61+
MODULE_WRITE = 'module_write'
6162
END = 'end'
6263

6364
PRE_PREF = 'pre_'
@@ -69,7 +70,7 @@
6970
INSTALL_STEP, EXTENSIONS_STEP, POSTPROC_STEP, SANITYCHECK_STEP, CLEANUP_STEP, MODULE_STEP,
7071
PERMISSIONS_STEP, PACKAGE_STEP, TESTCASES_STEP]
7172

72-
HOOK_NAMES = [START, PARSE] + [p + s for s in STEP_NAMES for p in [PRE_PREF, POST_PREF]] + [END]
73+
HOOK_NAMES = [START, PARSE, MODULE_WRITE] + [p + s for s in STEP_NAMES for p in [PRE_PREF, POST_PREF]] + [END]
7374
KNOWN_HOOKS = [h + HOOK_SUFF for h in HOOK_NAMES]
7475

7576

@@ -99,7 +100,7 @@ def load_hooks(hooks_path):
99100
if attr.endswith(HOOK_SUFF):
100101
hook = getattr(imported_hooks, attr)
101102
if callable(hook):
102-
hooks.update({attr: hook})
103+
hooks[attr] = hook
103104
else:
104105
_log.debug("Skipping non-callable attribute '%s' when loading hooks", attr)
105106
_log.info("Found hooks: %s", sorted(hooks.keys()))
@@ -119,11 +120,8 @@ def load_hooks(hooks_path):
119120

120121

121122
def verify_hooks(hooks):
122-
"""Check whether list of obtained hooks only includes known hooks."""
123-
unknown_hooks = []
124-
for key in sorted(hooks):
125-
if key not in KNOWN_HOOKS:
126-
unknown_hooks.append(key)
123+
"""Check whether obtained hooks only includes known hooks."""
124+
unknown_hooks = [key for key in sorted(hooks) if key not in KNOWN_HOOKS]
127125

128126
if unknown_hooks:
129127
error_lines = ["Found one or more unknown hooks:"]
@@ -147,7 +145,7 @@ def find_hook(label, hooks, pre_step_hook=False, post_step_hook=False):
147145
Find hook with specified label.
148146
149147
:param label: name of hook
150-
:param hooks: list of defined hooks
148+
:param hooks: dict of defined hooks
151149
:param pre_step_hook: indicates whether hook to run is a pre-step hook
152150
:param post_step_hook: indicates whether hook to run is a post-step hook
153151
"""
@@ -162,27 +160,26 @@ def find_hook(label, hooks, pre_step_hook=False, post_step_hook=False):
162160

163161
hook_name = hook_prefix + label + HOOK_SUFF
164162

165-
for key in hooks:
166-
if key == hook_name:
167-
_log.info("Found %s hook", hook_name)
168-
res = hooks[key]
169-
break
163+
res = hooks.get(hook_name)
164+
if res:
165+
_log.info("Found %s hook", hook_name)
170166

171167
return res
172168

173169

174170
def run_hook(label, hooks, pre_step_hook=False, post_step_hook=False, args=None, msg=None):
175171
"""
176-
Run hook with specified label.
172+
Run hook with specified label and return result of calling the hook or None.
177173
178174
:param label: name of hook
179-
:param hooks: list of defined hooks
175+
:param hooks: dict of defined hooks
180176
:param pre_step_hook: indicates whether hook to run is a pre-step hook
181177
:param post_step_hook: indicates whether hook to run is a post-step hook
182178
:param args: arguments to pass to hook function
183179
:param msg: custom message that is printed when hook is called
184180
"""
185181
hook = find_hook(label, hooks, pre_step_hook=pre_step_hook, post_step_hook=post_step_hook)
182+
res = None
186183
if hook:
187184
if args is None:
188185
args = []
@@ -197,4 +194,5 @@ def run_hook(label, hooks, pre_step_hook=False, post_step_hook=False, args=None,
197194
print_msg(msg)
198195

199196
_log.info("Running '%s' hook function (arguments: %s)...", hook.__name__, args)
200-
hook(*args)
197+
res = hook(*args)
198+
return res

test/framework/toy_build.py

Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import stat
4040
import sys
4141
import tempfile
42+
import textwrap
4243
from distutils.version import LooseVersion
4344
from test.framework.utilities import EnhancedTestCase, TestLoaderFiltered
4445
from test.framework.package import mock_fpm
@@ -2638,34 +2639,38 @@ def test_toy_build_trace(self):
26382639
def test_toy_build_hooks(self):
26392640
"""Test use of --hooks."""
26402641
hooks_file = os.path.join(self.test_prefix, 'my_hooks.py')
2641-
hooks_file_txt = '\n'.join([
2642-
"import os",
2643-
'',
2644-
"def start_hook():",
2645-
" print('start hook triggered')",
2646-
'',
2647-
"def parse_hook(ec):",
2648-
" print('%s %s' % (ec.name, ec.version))",
2642+
hooks_file_txt = textwrap.dedent("""
2643+
import os
2644+
2645+
def start_hook():
2646+
print('start hook triggered')
2647+
2648+
def parse_hook(ec):
2649+
print('%s %s' % (ec.name, ec.version))
26492650
# print sources value to check that raw untemplated strings are exposed in parse_hook
2650-
" print(ec['sources'])",
2651+
print(ec['sources'])
26512652
# try appending to postinstallcmd to see whether the modification is actually picked up
26522653
# (required templating to be disabled before parse_hook is called)
2653-
" ec['postinstallcmds'].append('echo toy')",
2654-
" print(ec['postinstallcmds'][-1])",
2655-
'',
2656-
"def pre_configure_hook(self):",
2657-
" print('pre-configure: toy.source: %s' % os.path.exists('toy.source'))",
2658-
'',
2659-
"def post_configure_hook(self):",
2660-
" print('post-configure: toy.source: %s' % os.path.exists('toy.source'))",
2661-
'',
2662-
"def post_install_hook(self):",
2663-
" print('in post-install hook for %s v%s' % (self.name, self.version))",
2664-
" print(', '.join(sorted(os.listdir(self.installdir))))",
2665-
'',
2666-
"def end_hook():",
2667-
" print('end hook triggered, all done!')",
2668-
])
2654+
ec['postinstallcmds'].append('echo toy')
2655+
print(ec['postinstallcmds'][-1])
2656+
2657+
def pre_configure_hook(self):
2658+
print('pre-configure: toy.source: %s' % os.path.exists('toy.source'))
2659+
2660+
def post_configure_hook(self):
2661+
print('post-configure: toy.source: %s' % os.path.exists('toy.source'))
2662+
2663+
def post_install_hook(self):
2664+
print('in post-install hook for %s v%s' % (self.name, self.version))
2665+
print(', '.join(sorted(os.listdir(self.installdir))))
2666+
2667+
def module_write_hook(self, module_path, module_txt):
2668+
print('in module-write hook hook for %s' % os.path.basename(module_path))
2669+
return module_txt.replace('Toy C program, 100% toy.', 'Not a toy anymore')
2670+
2671+
def end_hook():
2672+
print('end hook triggered, all done!')
2673+
""")
26692674
write_file(hooks_file, hooks_file_txt)
26702675

26712676
self.mock_stderr(True)
@@ -2676,26 +2681,44 @@ def test_toy_build_hooks(self):
26762681
self.mock_stderr(False)
26772682
self.mock_stdout(False)
26782683

2684+
test_mod_path = os.path.join(self.test_installpath, 'modules', 'all')
2685+
toy_mod_file = os.path.join(test_mod_path, 'toy', '0.0')
2686+
if get_module_syntax() == 'Lua':
2687+
toy_mod_file += '.lua'
2688+
26792689
self.assertEqual(stderr, '')
2680-
expected_output = '\n'.join([
2681-
"== Running start hook...",
2682-
"start hook triggered",
2683-
"== Running parse hook for toy-0.0.eb...",
2684-
"toy 0.0",
2685-
"['%(name)s-%(version)s.tar.gz']",
2686-
"echo toy",
2687-
"== Running pre-configure hook...",
2688-
"pre-configure: toy.source: True",
2689-
"== Running post-configure hook...",
2690-
"post-configure: toy.source: False",
2691-
"== Running post-install hook...",
2692-
"in post-install hook for toy v0.0",
2693-
"bin, lib",
2694-
"== Running end hook...",
2695-
"end hook triggered, all done!",
2696-
])
2690+
# There are 4 modules written:
2691+
# Sanitycheck for extensions and main easyblock (1 each), main and devel module
2692+
expected_output = textwrap.dedent("""
2693+
== Running start hook...
2694+
start hook triggered
2695+
== Running parse hook for toy-0.0.eb...
2696+
toy 0.0
2697+
['%(name)s-%(version)s.tar.gz']
2698+
echo toy
2699+
== Running pre-configure hook...
2700+
pre-configure: toy.source: True
2701+
== Running post-configure hook...
2702+
post-configure: toy.source: False
2703+
== Running post-install hook...
2704+
in post-install hook for toy v0.0
2705+
bin, lib
2706+
== Running module_write hook...
2707+
in module-write hook hook for {mod_name}
2708+
== Running module_write hook...
2709+
in module-write hook hook for {mod_name}
2710+
== Running module_write hook...
2711+
in module-write hook hook for {mod_name}
2712+
== Running module_write hook...
2713+
in module-write hook hook for {mod_name}
2714+
== Running end hook...
2715+
end hook triggered, all done!
2716+
""").strip().format(mod_name=os.path.basename(toy_mod_file))
26972717
self.assertEqual(stdout.strip(), expected_output)
26982718

2719+
toy_mod = read_file(toy_mod_file)
2720+
self.assertIn('Not a toy anymore', toy_mod)
2721+
26992722
def test_toy_multi_deps(self):
27002723
"""Test installation of toy easyconfig that uses multi_deps."""
27012724
test_ecs_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'easyconfigs', 'test_ecs')

0 commit comments

Comments
 (0)