A hook changes a file you are not reading

What you will be able to do

  1. Say what a hook is and when it runs.

  2. Say what condition a hook puts on An easyconfig is Python’s way of reading a failure.

  3. Spot the one-line hook bug that silently discards an easyconfig’s own parameter.

Each preceding chapter has read an easyconfig and derived reasoning from it. This chapter discusses the mechanism that renders that reasoning incomplete and explains why a site still adopts it.

The problem a hook solves

An easyconfig originates from upstream. A site defines policies that apply to each build it runs. These policies cover installation locations, module naming, required compiler flags, and local machine requirements.

Such information does not belong in an easyconfig. Placing it there causes the site to fork the upstream file. Each version bump then becomes a merge, and each policy change triggers a sweep across thousands of files.

Thus EasyBuild takes a file of Python functions and calls them at defined points:

eb GROMACS-2025.2-foss-2025a.eb -r --hooks=/my/site/hooks.py

Functions are located by their names. parse_hook executes after an easyconfig is parsed. pre_configure_hook executes before the configure step. post_module_hook executes after the modulefile is written. Each step has a single such name. No registration or decorator is required; the name is the subscription. parse_hook is the most significant. A modification there alters what is built rather than only how.

A hook sees everything, so it starts with a guard

Below is the outline of an actual example, simplified to its structure.

def parse_hook(ec, *args, **kwargs):
    if ec.name != 'NVHPC':
        return
    if 'oldcluster' not in socket.getfqdn():
        return
    ec['postinstallcmds'] = ['echo "set LIBDIR=/usr/lib64;" >> %(installdir)s/localrc']

Three lines of guard precede one line of work, and the guards constitute the interesting part. The first guard tests the package. A hook is called for every easyconfig the site builds, so without that test the guard would fire on all of them. The second guard tests the hostname. That guard is a hook performing the purpose of a hook: a workaround for a specific set of machines, expressed once, applied to any easyconfig that meets it. Both guards are ordinary Python, evaluated at build time, and neither appears in any easyconfig.

The line of work is where hooks go wrong

Look at the assignment:

ec['postinstallcmds'] = [...]

The parameter is replaced. Anything declared in postinstallcmds by the easyconfig disappears without diagnostic. Compare:

ec['postinstallcmds'] = ec['postinstallcmds'] + [...]

Two versions of a single hook differ by a few characters, and the difference determines whether an easyconfig’s own post-install steps survive. The hazard is quiet in the ordinary case. Most easyconfigs declare no postinstallcmds. Replacing an empty list with a one-element list is exactly the intent. It becomes a defect when someone adds postinstallcmds to an easyconfig on which the hook fires. The easyconfig is correct, the hook is unchanged, and the commands do not run. This applies to every list- or dictionary-valued parameter a hook touches. dependencies, configopts, modextravars, patches: assignment discards, and nothing warns.

Which qualifies An easyconfig is Python

An easyconfig is Python resolved a failure by using this inference:

The easyconfig is a Python file, so whatever expected that path is a
value somebody assigned in it.

A hook makes that false. The value may have been assigned in the hook, after the file was parsed, and the file on disk will not mention it. So the inference needs a condition attached, and the condition is not optional at a site that uses hooks. Reading a failure means knowing whether a hook is loaded and what it touches, before concluding that an easyconfig says what it appears to say. The practical form: find out what --hooks is set to, in the configuration or the wrapper script that invoked eb, and read that file once. Most are short, and the guards report immediately whether the hook can have applied to your build.

EB-Hook-1 — The commands that never ran

A site hook has worked for two years:

def parse_hook(ec, *args, **kwargs):
    if ec.name != 'NVHPC':
        return
    ec['postinstallcmds'] = ['echo "set LIBDIR=/usr/lib64;" >> ...']

Somebody adds a postinstallcmds line to the NVHPC easyconfig, to fix up a set of symlinks. The build passes. The symlinks are not there.

  1. Why not?

  2. Why did nobody notice this in the two years before?

  3. What is the one-line change, and what would you grep for to find the same bug elsewhere in the hook?

Solution

The hook assigns postinstallcmds, so whatever the easyconfig declared is replaced. Assignment to a list parameter is not an error and produces no diagnostic, so the build is green and the commands never ran.

Nobody noticed postinstallcmds, and replacing an empty list with a one-element list is indistinguishable from appending to it. The defect was there the whole time, dormant until the value it discards stopped being empty.

The change is ec['postinstallcmds'] = ec['postinstallcmds'] + [...].

To find the rest, grep the hook for assignment to any parameter holding a list or a dictionary: dependencies, builddependencies, configopts, patches, modextravars, sanity_check_commands. Every one has the same failure mode and the same silence.

Hooks and overlays solve different problems

The robot’s robot path and the hooks described in this chapter constitute the two methods by which a site alters EasyBuild’s behaviour, and they are not interchangeable. An overlay modifies which easyconfig is discovered. It is appropriate when the site requires a different file, such as a newer version, a local patch, or a variant not provided upstream. A hook alters what happens to the easyconfig that was found. It is appropriate when the same modification must affect files that the site does not own and prefers not to adopt. Choosing the incorrect method yields a characteristic outcome in each case. An overlay employed for policy creates a fork to maintain, with one copy per package, each diverging from upstream at its own pace. A hook used to apply a package-specific change provides a global mechanism whose behaviour depends on a guard. The subsequent reader examines the easyconfig and observes none of the change.

The condition this puts on An easyconfig is Python

An easyconfig is Python states that any expected path originates from a value assigned in the easyconfig. On a site employing hooks, this is true only after confirming that no hook has modified it. Determining this requires reading a short Python file and its guards. Two rules follow, with the second being problematic. A hook is invoked for each easyconfig. Its guards as integral to its behavior as its body. Assigning a list parameter overwrites the existing value. ec['postinstallcmds'] = [...] = discards any declaration from the easyconfig without diagnostic output, remaining hidden until =postinstallcmds is added to a package on which the hook runs. Append. Replace only when replacement is intended. The subsequent three chapters cover the actual implementation steps. Install into a prefix owned by the operator, apply it atop EESSI, and then examine how the engine behind these examples determines its established state.