Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation and example for write-module-hook #714

Merged
merged 3 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/Deprecated-functionality.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ rid of, together with supporting a better alternative.
ERROR: EasyBuild encountered an exception (at easybuild/framework/easyconfig/easyconfig.py:937 in process_easyconfig):
Failed to process easyconfig /home/example/gzip-1.5-goolf-1.4.10.eb:
DEPRECATED (since v2.0) functionality used: Easyconfig parameter 'makeopts' is deprecated, use 'buildopts' instead;
see http://easybuild.readthedocs.org/en/latest/Deprecated-functionality.html for more informatio
see http://easybuild.readthedocs.org/en/latest/Deprecated-functionality.html for more information.

* the code supporting the deprecated functionality is *removed* in EasyBuild version ``X.(Y+1)`` (i.e., the first
non-bugfix-only release after version ``X.Y``), see also :ref:`removed_functionality`
Expand Down
68 changes: 45 additions & 23 deletions docs/Hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,15 @@ Available hooks
Currently (since EasyBuild v3.7.0), three types of hooks are supported:

* ``start_hook`` and ``end_hook``, which are triggered *once* before starting software installations,
and *once* right after completing all installations, respectfully;
* ``parse_hook``, which is triggered when an easyconfig file is being parsed;
and *once* right after completing all installations, respectively
* ``parse_hook``, which is triggered when an easyconfig file is being parsed
* ``module_write_hook``, which is triggered right before a module file is written.
This includes the temporary module file used when installing extensions and during the sanity check,
as well as the devel module.
* "*step*" hooks that are triggered before and after every step of each installation procedure that is performed,
also aptly named '``pre``'- and '``post``'-hooks

The list of currently available hooks in order of exeuction,
The list of currently available hooks in order of execution,
which can also be consulted using ``eb --avail-hooks``, is:

* ``start_hook`` *(only called once in an EasyBuild session)*
Expand All @@ -87,11 +90,12 @@ which can also be consulted using ``eb --avail-hooks``, is:
* ``pre_package_hook``, ``post_package_hook``
* ``pre_testcases_hook``, ``post_testcases_hook``
* ``end_hook`` *(only called once in an EasyBuild session)*
* ``module_write_hook`` *(called multiple times per installation, available since EasyBuild v4.4.1)*

All functions implemented in the provided Python module for which the name ends with ``_hook`` are considered.

If any ``*_hook`` functions are encountered that do not match with any of the available hooks, an error is reported.
EasyBuild will try to provide suggestions for available hooks that closely match with the encountered unknown hook.
If any ``*_hook`` functions are encountered that do not match any of the available hooks, an error is reported.
EasyBuild will try to provide suggestions for available hooks that closely match the encountered unknown hook.

For example::

Expand Down Expand Up @@ -119,6 +123,12 @@ Do take into account the following:
* for ``parse_hook``, one argument is provided: the ``EasyConfig`` instance
that corresponds to the easyconfig file being parsed (usually referred to as ``ec``)

* for ``module_write_hook``, 3 arguments are provided:
* the ``EasyBlock`` instance used to perform the installation (usually referred to as ``self``)
* the filepath of the module that will be written
* the module text as a string
The return value of this hook (if any) will then be appended to the contents of the module file.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@boegel You changed this sentence and introduced a mistake: The contents will be written to the module file. NOT appended, just as I wrote initially. The whole return value will be used and written directly, so this is misleading

Or what did you mean by "appended" here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and I even checked the code before changing this... Will fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #715, thanks for double checking and catching this!


* for the step hooks, one argument is provided:
the ``EasyBlock`` instance used to perform the installation (usually referred to as ``self``)

Expand All @@ -133,7 +143,7 @@ avoids that your hook implementations may break when updating to future EasyBuil
def pre_configure_hook(self, *args, **kwargs):
...

In hooks, you have access to the full functionality provided by the EasyBuild framework,
In hooks you have access to the full functionality provided by the EasyBuild framework,
so do ``import`` from ``easybuild.tools.*`` (or other ``easybuild.*`` namespaces) to leverage
those functions.

Expand All @@ -147,7 +157,7 @@ before further parsing of some easyconfig parameters (like ``*dependencies``) in
custom data structures is done.

This is important since it allows to dynamically modify easyconfig files
while they are still "raw", i.e., when the easyconfig parameter values are
while they are still "raw", i.e. when the easyconfig parameter values are
still basic Python data structures like lists, dictionaries, etc.
that are easy to manipulate (see also :ref:`hooks_caveats_manipulating`).

Expand All @@ -161,7 +171,8 @@ Caveats
-------

Due to internal details of the EasyBuild framework, you may run into some surprises when
implementing hooks. Here are some things to take into account:
implementing hooks.
Here are some things to take into account:

.. _hooks_caveats_template_values:

Expand All @@ -173,9 +184,10 @@ In all *step* hooks, template values in easyconfig parameters will be resolved w
That is, if the ``%(version)`` template is used in for example the ``sources`` easyconfig parameter,
it will be replaced with the actual value of the ``version`` easyconfig parameter whenever the
``sources`` value is used.
This can be avoided by temporarily disabling templating via ``self.cfg.enable_templating``, should the need arise.
This can be avoided by temporarily disabling templating by wrapping the code in ``with self.cfg.disable_templating:``.

There is one notable exception to this: templates in easyconfig parameters are *not* resolved in ``parse_hook``,
There is one notable exception to this:
Templates in easyconfig parameters are *not* resolved in ``parse_hook``,
because templating has been disabled explicitly before ``parse_hook`` is called;
this helps significantly to simplify manipulating of easyconfig parameter values
(see also :ref:`hooks_caveats_manipulating`).
Expand All @@ -191,7 +203,7 @@ You may run into surprises when trying to manipulate easyconfig parameters, for
First of all, the original easyconfig parameters may already be processed in another data structure
which does not resemble the original format in which the parameter was defined in the easyconfig file.

Moreover, this processing could be done either "in place", i.e. by replacing the original easyconfig parameter value,
Moreover, this processing could be done either "in place" by replacing the original easyconfig parameter value,
or in a separate variable, which effectively means that any changes to the original easyconfig parameter value
are simply ignored.

Expand All @@ -209,8 +221,8 @@ More specifically, the following approach will *not* work in any of the (step) h
self.cfg['patches'].append('example.patch')

The problem here is that the value obtained via ``self.cfg['patches']`` is not a reference
to the actual easyconfig parameter value, but a reference to a temporary copy thereof;
hence, any updates on the copy are effectively lost immediately.
to the actual easyconfig parameter value but a reference to a temporary copy thereof;
hence any updates on the copy are effectively lost immediately.

To achieve the intended effect, you can either:

Expand All @@ -221,12 +233,10 @@ To achieve the intended effect, you can either:
def pre_fetch_hook(self):
"Example of pre-fetch hook to manipulate list of patches."
# temporarily disable templating, so changes to 'patches' easyconfig parameter are picked up
orig_enable_templating = self.cfg.enable_templating
self.cfg.enable_templating = False
# add patch
self.cfg['patches'].append('example.patch')
# restore templating state
self.cfg.enable_templating = orig_enable_templating
with self.cfg.disable_templating:
# add patch
self.cfg['patches'].append('example.patch')
# templating state restored

* or replace the original value entirely:

Expand All @@ -240,7 +250,7 @@ To achieve the intended effect, you can either:
A better approach for manipulating easyconfig parameters is to use the ``parse_hook`` that
was introduced in EasyBuild v3.7.0 (see :ref:`hooks_parse_hook`),
where these kind of surprises will not occur (because templating is automatically disabled
before ``parse_hook`` is called, and restored immediately afterwards).
before ``parse_hook`` is called and restored immediately afterwards).
See also :ref:`hooks_examples_inject_patch`.

.. _hooks_caveats_archived_easyconfig:
Expand All @@ -249,12 +259,12 @@ Archived easyconfig file vs hooks
+++++++++++++++++++++++++++++++++

EasyBuild archives the easyconfig file that was used for a particular installation:
a copy is stored both in the ``easybuild`` subdirectory of the software installation
A copy is stored both in the ``easybuild`` subdirectory of the software installation
directory and in the easyconfigs repository (see :ref:`easyconfigs_repo`).

If any changes were made to the easyconfig file via hooks, these changes will *not* be
reflected in these copies. The assumption here is that the hooks will also be in place
for future (re-)installations.
reflected in these copies.
The assumption here is that the hooks will also be in place for future (re-)installations.

EasyBuild does however store an additional copy of the easyconfig file which includes
any modifications that were done dynamically, for example by hooks.
Expand Down Expand Up @@ -295,3 +305,15 @@ Example hook to inject a custom patch file
patch_file = 'example.patch'
ec.log.info("[parse hook] Injecting additional patch file '%s'", patch_file)
ec['patches'].append(patch_file)

Example hook to replace PYTHONPATH by EBPYTHONPREFIXES in (Lua) modules
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

.. code:: python

def module_write_hook(self, filepath, module_txt, *args, **kwargs):
# note: if `self.mod_filepath == filepath` => final module file
if 'Python' in (dep['name'] for dep in self.cfg.dependencies()):
search = r'prepend_path\("PYTHONPATH", pathJoin\(root, "lib/python\d.\d/site-packages"\)\)'
replace = 'prepend_path("EBPYTHONPREFIXES", root)'
return re.sub(search, replace, module_txt)