Skip to content

Commit b61190d

Browse files
committed
Add module-write hook
This hook will be called anytime a module is written, so even for the fake modules used for sanity checks etc. It gets passed the easyblock as the first argument and the filename and content to be written as the next 2 and may return a new text to be used for the module file. If nothing or None is returned the original text will be used.
1 parent 22b9603 commit b61190d

File tree

3 files changed

+82
-57
lines changed

3 files changed

+82
-57
lines changed

easybuild/framework/easyblock.py

Lines changed: 5 additions & 1 deletion
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
@@ -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: 12 additions & 14 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()))
@@ -120,10 +121,7 @@ def load_hooks(hooks_path):
120121

121122
def verify_hooks(hooks):
122123
"""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)
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,18 +160,16 @@ 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 hook or None.
177173
178174
:param label: name of hook
179175
:param hooks: list of defined hooks
@@ -183,6 +179,7 @@ def run_hook(label, hooks, pre_step_hook=False, post_step_hook=False, args=None,
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)