Extensions, and what an entry in exts_list inherits

What you will be able to do

  1. Read any exts_list entry, in all three of its forms, and say which options it will actually get.

  2. Say which parameters an extension inherits from its parent easyconfig and which are reset before it runs.

  3. Read an exts_filter and say what it decides, twice.

Before this chapter

An extension is software installed inside another installation rather than as a separate module. The mechanism consists of the easyconfig parameter exts_list and the step extensions, which is the tenth of the eighteen steps. A dedicated chapter is required. It contains parameters and an easyblock. The origins of those parameters are not documented in a single location and are not inferable.

Three forms, and the one that gets nothing

An entry can be a plain string, a (name, version) tuple, or a (name, version, {options}) tuple. Within the framework, as defined in easybuild/framework/easyblock.py:

if len(ext) == 1:
    exts_sources.append({'name': ext_name})
else:
    ext_version = resolve_template(ext[1], self.cfg.template_values)
    ext_options = copy.deepcopy(self.cfg.get_ref('exts_default_options'))
    if len(ext) == 3:
        if isinstance(ext_options, dict):
            ext_options.update(ext[2])

Read the branch: exts_default_options is only fetched in the else, an entry with a name and no version returns ({'name':...}) and gets no options at all, defaults included. A bare string takes the same path. So in this easyconfig the two entries are not treated alike:

name = 'example'
version = '1.0'
toolchain = {'name': 'foss', 'version': '2025a'}

exts_default_options = {
    'source_urls': ['https://example.org/downloads'],
}

exts_list = [
    ('alpha', '1.2'),
    'beta',
]

alpha receives the default source_urls. beta lacks it, and the plan receives no warning. eb -x displays the difference prior to building.

The merge operation is a shallow dict.update. When a collision occurs, the per‑extension dictionary prevails, and a conflicting list is substituted instead of extended, causing a per‑extension source_urls to overwrite the default rather than augment it.

Two kinds of option, and only one of them is checked

Options in that third element are divided into two groups that behave differently. Distinguishing the groups explains most surprises. Read directly by the extensions machinery. These are not easyconfig parameters; they are keys the framework searches for in the options dict.

All other items that qualify as an easyconfig parameter. Any additional key is copied into the extension’s own configuration. The loop performing this also processes the remaining items:

for key, value in self.options.items():
    if key in self.cfg:
        self.cfg[key] = value
        self.log.debug("Customising known easyconfig parameter '%s' ...")
    else:
        self.log.debug("Skipping unknown custom easyconfig parameter '%s' ...")

An unknown key is dropped at debug level. No warning, no error, and no mention appear in a normal run. Thus a misspelled per-extension option in a bundle of two hundred extensions remains invisible unless specifically searched for, and this constitutes the single most useful aspect of this chapter. Alternative parameter names are honoured on input, so install_opts works where installopts is intended, and real easyconfigs in the upstream repository use it.

What an extension does not inherit

The configuration of an extension initially inherits from its parent, which is convenient but would be wrong for parameters that describe the parent’s sources. eight parameters are reset to their defaults first, in easybuild/framework/extension.py.

restore_options = (
    'checksums',
    'data_sources',
    'patches',
    'postinstallcmds',
    'sanity_check_commands',
    'sanity_check_paths',
    'skipsteps',
    'sources',
)

The list should be read as a design statement. Everything on it is a claim about the software being installed, and an extension is different software. A parent’s sanity_check_paths would look for the parent’s files; its checksums belong to the parent’s archive; its skipsteps would silence a step for two hundred extensions at once.

start_dir gets its own treatment two lines later, with the reason in a comment: the parent’s value “will be set, and will most likely be wrong”. It comes from the extension’s own options or is derived, which is Reading a failed build’s descent rule applied one level down.

exts_filter decides the same question twice

exts_filter is a pair (command, input), each half being a template resolved per extension with %(ext_name)s, %(ext_version)s and %(src)s.

Anything that is not a two‑element list or tuple is refused with exts_filter should be a list or tuple of ("command","input").

The framework runs it for two different purposes.

Before installing, to skip what is already there.

EasyBlock.skip_extensions builds one command per extension and runs them sequentially or in parallel to decide which entries can be left alone.

This makes a resumed bundle cheap instead of a re‑run.

After installing each extension, as its sanity check.

Extension.sanity_check_step builds the same commands and runs them with fail_on_error=False.

The exit code settles it, and a failure is collected with the command, its input and its output rather than aborting the run.

Two consequences follow from it being a single mechanism.

An extension whose modulename is False produces no commands, so it is skipped in both roles, not checked before and not checked after.

This provides an escape hatch for an extension whose importable name does not exist.

get_modulenames can return more than one name, so an extension providing three importable modules gets three commands, and each must pass.

For Python, the conventional filter is an import:.

exts_filter = ("python -c 'import %(ext_name)s'", '')

It is a different assertion from sanity_pip_check. Languages that bring their own package manager’s pip check asks whether the environment’s declared versions are mutually consistent. exts_filter asks whether the thing imports. A package can satisfy either one and fail the other: a consistent metadata set whose module fails to import.

EB-Exts-1 — Four entries, four outcomes

An easyconfig sets

exts_default_options = {
    'source_urls': ['https://example.org/dl'],
    'preinstallopts': 'echo DEFAULT &&',
}

and then lists four entries:

exts_list = [
    ('alpha', '1.2'),
    ('beta', '2.0', {'preinstallopts': 'echo MINE &&'}),
    ('gamma', '3.1', {'source_urls': ['https://other.example/dl']}),
    'delta',
]

Identify the source_urls and the preinstallopts associated with each entry. Adding {'instalopts': '--verbose'} causes the verbose option to be used during installation and it appears in the build log. The parent easyconfig defines sanity_check_paths for its binary and skipsteps = ['test']; beta inherits the skipsteps setting. Change exts_filter = ("python -c 'import %(ext_name)s'", '') to reference the actual importable name gamma_core. Setting modulename to False disables the import check and allows the build to continue without verifying the module.

Solution
  1. alpha gets both defaults. beta gets the default source_urls and its own preinstallopts. gamma gets its own source_urls, which replaces the default rather than adding to it, and the default preinstallopts. delta gets neither default never reaches the branch that reads them.

  2. Nothing, visibly. instalopts is not a known parameter, so it is dropped with a debug line reading “Skipping unknown custom easyconfig parameter”. You would see it by running with --debug and searching the log, which is the only place it appears.

  3. Neither. Both are on the reset list, along with checksums, sources, patches, postinstallcmds, sanity_check_commands and data_sources.

  4. Set modulename to 'gamma_core' in gamma’s options, so the filter command imports the name that exists. Setting it to False instead produces no filter commands at all for that extension, which means it is neither skipped when already installed nor checked after installation: you have switched the check off rather than corrected it, which is Testing, and the four things it can mean’s argument in miniature.

What to remember

  • exts_default_options reaches only entries that carry a version; a bare-string entry gets no options at all.

  • The merge is a shallow update, so a per-extension list replaces the default list instead of extending it.

  • An unknown per-extension option is dropped at debug level, with no warning: a typo in a bundle of two hundred extensions is silent.

  • Eight parameters plus start_dir are reset before an extension runs,

.

  • exts_filter is one mechanism doing two jobs, skipping what is already installed and checking what was just installed, and modulename: False switches off both.