From 49b75108ae86153db100f1b02dd470fc974daded Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Thu, 30 Apr 2026 10:22:38 +0200 Subject: [PATCH 01/10] Fix some comments & docstrings --- easybuild/framework/easyblock.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 5f6661116e..b51cca6f32 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -218,7 +218,7 @@ def __init__(self, ec, logfile=None): self.skip = None self.module_extra_extensions = '' # extra stuff for module file required by extensions - # indicates whether or not this instance represents an extension or not; + # indicates whether or not this instance represents an extension # may be set to True by ExtensionEasyBlock self.is_extension = False @@ -684,12 +684,11 @@ def collect_exts_file_info(self, fetch_files=True, verify_checksums=True): 'version': ext_version, 'options': ext_options, 'github_account': ext_options.get('github_account', orig_github_account), + # if a particular easyblock is specified, make sure it's used + # (this is picked up by init_ext_instances) + 'easyblock': ext_options.get('easyblock', None), } - # if a particular easyblock is specified, make sure it's used - # (this is picked up by init_ext_instances) - ext_src['easyblock'] = ext_options.get('easyblock', None) - # construct dictionary with template values; # inherited from parent, except for name/version templates which are specific to this extension template_values = copy.deepcopy(self.cfg.template_values) @@ -1142,14 +1141,14 @@ def obtain_file_raise_on_failure(self, filename, extension=False, urls=None, dow @property def name(self): """ - Shortcut the get the module name. + Shortcut to get the module name. """ return self.cfg['name'] @property def version(self): """ - Shortcut the get the module version. + Shortcut to get the module version. """ return self.cfg['version'] @@ -1863,7 +1862,7 @@ def inject_module_extra_paths(self): msg += f"and paths='{env_var}'" self.log.debug(msg) - def expand_module_search_path(self, search_path, path_type=ModEnvVarType.PATH_WITH_FILES): + def expand_module_search_path(self, *_, **__): """ REMOVED in EasyBuild 5.1, use EasyBlock.module_load_environment.expand_paths instead """ From dab5e255cf133ff41e240ce1113b78d6fa81573b Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Tue, 21 Jul 2026 10:39:44 +0200 Subject: [PATCH 02/10] Use `assertIn` for output check --- test/framework/toy_build.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/test/framework/toy_build.py b/test/framework/toy_build.py index 10beb5a45e..4054b90a66 100644 --- a/test/framework/toy_build.py +++ b/test/framework/toy_build.py @@ -2710,12 +2710,11 @@ def test_toy_build_enhanced_sanity_check(self): stdout = self.get_stdout() self.mock_stdout(False) - pattern_lines = [ - r"^== sanity checking\.\.\.", - r" >> file 'bin/toy' found: OK", - ] - regex = re.compile(r'\n'.join(pattern_lines), re.M) - self.assertTrue(regex.search(stdout), "Pattern '%s' should be found in: %s" % (regex.pattern, stdout)) + expected_out = textwrap.dedent(""" + == sanity checking... + >> file 'bin/toy' found: OK + """) + self.assertIn(expected_out, stdout) # no directories are checked in sanity check now, only files (since dirs is an empty list) regex = re.compile(r"directory .* found:", re.M) @@ -3497,19 +3496,18 @@ def test_toy_build_trace(self): r'', ]), r" >> command completed: exit 0, ran in .*", - r'^' + r'\n'.join([ - r"== sanity checking\.\.\.", - r" >> file 'bin/yot' or 'bin/toy' found: OK", - r" >> \(non-empty\) directory 'bin' found: OK", - r" >> loading modules: toy/0.0\.\.\.", - r" >> running command 'toy' \.\.\.", - r" >> result for command 'toy': OK", - ]) + r'$', r"^== creating module\.\.\.\n >> generating module file @ .*/modules/all/toy/0\.0(?:\.lua)?$", ] - for pattern in patterns: - regex = re.compile(pattern, re.M) - self.assertTrue(regex.search(stdout), "Pattern '%s' found in: %s" % (regex.pattern, stdout)) + self.assert_multi_regex(patterns, stdout) + expected_stdout = textwrap.dedent(""" + == sanity checking... + >> file 'bin/yot' or 'bin/toy' found: OK + >> (non-empty) directory 'bin' found: OK + >> loading modules: toy/0.0... + >> running command 'toy' ... + >> result for command 'toy': OK + """) + self.assertIn(expected_stdout, stdout) def test_toy_build_hooks(self): """Test use of --hooks.""" From 764e07b96e9453595cff4c10b551c1d0ebc5deaf Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Thu, 30 Apr 2026 13:04:46 +0200 Subject: [PATCH 03/10] Add test for custom_commands in extensions --- .../easyblocks/generic/toy_extension.py | 4 ++- test/framework/toy_build.py | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/test/framework/sandbox/easybuild/easyblocks/generic/toy_extension.py b/test/framework/sandbox/easybuild/easyblocks/generic/toy_extension.py index 7855b8f679..110084fd99 100644 --- a/test/framework/sandbox/easybuild/easyblocks/generic/toy_extension.py +++ b/test/framework/sandbox/easybuild/easyblocks/generic/toy_extension.py @@ -44,6 +44,7 @@ def extra_options(): """Custom easyconfig parameters for toy extensions.""" extra_vars = { 'toy_ext_param': ['', "Toy extension parameter", CUSTOM], + 'toy_custom_sanity_check_cmds': [None, "Optional list of custom command to run in sanity check", CUSTOM], } return ExtensionEasyBlock.extra_options(extra_vars=extra_vars) @@ -112,4 +113,5 @@ def sanity_check_step(self, *args, **kwargs): } if self.src: custom_paths['files'].extend(['bin/%s' % self.name, 'lib/lib%s.a' % self.name]) - return super().sanity_check_step(custom_paths=custom_paths) + return super().sanity_check_step(custom_paths=custom_paths, + custom_commands=self.cfg['toy_custom_sanity_check_cmds']) diff --git a/test/framework/toy_build.py b/test/framework/toy_build.py index 4054b90a66..88fea22929 100644 --- a/test/framework/toy_build.py +++ b/test/framework/toy_build.py @@ -2494,6 +2494,35 @@ def test_toy_sanity_check_commands(self): regex = re.compile('^.*/eb-[^/]+/eb-sanity-check-[^/]+\n[ ]*0$') self.assertTrue(regex.match(out_txt), f"Pattern '{regex.pattern}' should match in: {out_txt}") + def test_toy_extension_sanity_check(self): + """Check sanity check for extensions: + Custom_commands from easyblocks are run.""" + test_ec_txt = TOY_EC_TXT + test_ec_txt += '\n' + textwrap.dedent(""" + exts_list = [ + ('barbar', '0.0', { + 'exts_filter': ('ls -l lib/lib%(ext_name)s.a', ''), + 'toy_custom_sanity_check_cmds': ['echo "Run-Custom-Cmd for %(name)s" && false'], + 'sanity_check_paths': {'dirs': [], 'files': ['lib/libbarbar.a']}, + }) + ] + """) + test_ec = os.path.join(self.test_prefix, 'test.eb') + write_file(test_ec, test_ec_txt) + error_pattern = 'sanity check command echo "Run-Custom-Cmd for barbar" && false failed with exit code 1' + with self.mocked_stdout_stderr(): + self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, ec_file=test_ec, + raise_error=True, verbose=False) + + test_ec_txt += ( + "\nexts_list[0][2]['toy_custom_sanity_check_cmds'] = ['echo \"Run-Custom-Cmd for %(name)s\" && true']" + ) + write_file(test_ec, test_ec_txt) + with self.mocked_stdout_stderr(), self.log_to_testlogfile() as logfile: + self._test_toy_build(ec_file=test_ec, raise_error=True) + logtxt = read_file(logfile) + self.assertRegex(logtxt, 'sanity check command .*Run-Custom-Cmd for barbar.*ran successfully',) + def test_sanity_check_paths_lib64(self): """Test whether fallback in sanity check for lib64/ equivalents of library files works.""" # modify test easyconfig: move lib/libtoy.a to lib64/libtoy.a From 32f1d4ff504c77f5612a5906412f1f7877125fd5 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Thu, 30 Apr 2026 10:27:13 +0200 Subject: [PATCH 04/10] Do not create module in sanity check of `extensioneasyblock` Using the "fake module environment" creates a module file. However in `--module-only` setups we want to use the real module file instead. This also avoids potentially loading the real AND the fake module. --- easybuild/framework/easyblock.py | 25 +++++++++++++++++++++++ easybuild/framework/extensioneasyblock.py | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index b51cca6f32..7f0d81c645 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -2406,6 +2406,31 @@ def fake_module_environment(self, extra_modules=None, with_build_deps=False): if fake_mod_data: self.clean_up_fake_module(fake_mod_data) + @contextmanager + def sanity_check_module_environment(self, extra_modules=None, with_build_deps=False, check_loaded=True): + """Load/Unload module for performing sanity checks""" + if self.sanity_check_module_loaded and check_loaded: + raise EasyBuildError("Sanity check module is already loaded and must not be loaded again") + + if self.sanity_check_module_loaded: + unload_module = False + else: + if with_build_deps: + # load modules for build dependencies as extra modules + extra_modules = [dep['short_mod_name'] for dep in self.cfg.dependencies(build_only=True)] + self.sanity_check_load_module(extra_modules=extra_modules) + unload_module = True + + try: + yield + finally: + # cleanup (unload fake module, remove fake module dir) + if unload_module: + if self.fake_mod_data: + self.clean_up_fake_module(self.fake_mod_data) + self.fake_mod_data = None + self.sanity_check_module_loaded = False + def guess_start_dir(self): """ Return the directory where to start the whole configure/make/make install cycle from diff --git a/easybuild/framework/extensioneasyblock.py b/easybuild/framework/extensioneasyblock.py index cdfbf84a51..6a81db18eb 100644 --- a/easybuild/framework/extensioneasyblock.py +++ b/easybuild/framework/extensioneasyblock.py @@ -181,7 +181,7 @@ def sanity_check_step(self, exts_filter=None, custom_paths=None, custom_commands # take into account that module may already be loaded earlier in sanity check if not (self.sanity_check_module_loaded or self.is_extension or self.dry_run): for extra_modules in lists_of_extra_modules: - with self.fake_module_environment(extra_modules=extra_modules): + with self.sanity_check_module_environment(extra_modules=extra_modules): if extra_modules: info_msg = f"Running extension sanity check with extra modules: {', '.join(extra_modules)}" self.log.info(info_msg) From 93bef6ec9fbe88f7be5d8b98e30fd1c3edd62f51 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Thu, 30 Apr 2026 18:18:49 +0200 Subject: [PATCH 05/10] Honor changed output in test --- test/framework/toy_build.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/framework/toy_build.py b/test/framework/toy_build.py index 88fea22929..57ab8c8432 100644 --- a/test/framework/toy_build.py +++ b/test/framework/toy_build.py @@ -2741,6 +2741,7 @@ def test_toy_build_enhanced_sanity_check(self): expected_out = textwrap.dedent(""" == sanity checking... + >> loading modules: toy/0.0... >> file 'bin/toy' found: OK """) self.assertIn(expected_out, stdout) @@ -3530,6 +3531,7 @@ def test_toy_build_trace(self): self.assert_multi_regex(patterns, stdout) expected_stdout = textwrap.dedent(""" == sanity checking... + >> loading modules: toy/0.0... >> file 'bin/yot' or 'bin/toy' found: OK >> (non-empty) directory 'bin' found: OK >> loading modules: toy/0.0... From 9e2ef0b44d209015e030b3d59f41ec725f7f424a Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Wed, 22 Jul 2026 10:15:11 +0200 Subject: [PATCH 06/10] Use placeholder for sanity check cmd success in test EC Co-authored-by: Davide Grassano <34096612+Crivella@users.noreply.github.com> --- test/framework/toy_build.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/test/framework/toy_build.py b/test/framework/toy_build.py index 57ab8c8432..a621923739 100644 --- a/test/framework/toy_build.py +++ b/test/framework/toy_build.py @@ -2502,22 +2502,19 @@ def test_toy_extension_sanity_check(self): exts_list = [ ('barbar', '0.0', { 'exts_filter': ('ls -l lib/lib%(ext_name)s.a', ''), - 'toy_custom_sanity_check_cmds': ['echo "Run-Custom-Cmd for %(name)s" && false'], + 'toy_custom_sanity_check_cmds': ['echo "Run-Custom-Cmd for %(name)s" && {}'], 'sanity_check_paths': {'dirs': [], 'files': ['lib/libbarbar.a']}, }) ] """) test_ec = os.path.join(self.test_prefix, 'test.eb') - write_file(test_ec, test_ec_txt) + write_file(test_ec, test_ec_txt.format("false")) error_pattern = 'sanity check command echo "Run-Custom-Cmd for barbar" && false failed with exit code 1' with self.mocked_stdout_stderr(): self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, ec_file=test_ec, raise_error=True, verbose=False) - test_ec_txt += ( - "\nexts_list[0][2]['toy_custom_sanity_check_cmds'] = ['echo \"Run-Custom-Cmd for %(name)s\" && true']" - ) - write_file(test_ec, test_ec_txt) + write_file(test_ec, test_ec_txt.format("true")) with self.mocked_stdout_stderr(), self.log_to_testlogfile() as logfile: self._test_toy_build(ec_file=test_ec, raise_error=True) logtxt = read_file(logfile) From 911801b5664f75cfa3f4da6b327c457291949e88 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Wed, 22 Jul 2026 11:16:24 +0200 Subject: [PATCH 07/10] Use placeholder and replace by string Co-authored-by: Davide Grassano <34096612+Crivella@users.noreply.github.com> --- test/framework/toy_build.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/framework/toy_build.py b/test/framework/toy_build.py index a621923739..f1d1192291 100644 --- a/test/framework/toy_build.py +++ b/test/framework/toy_build.py @@ -2502,19 +2502,19 @@ def test_toy_extension_sanity_check(self): exts_list = [ ('barbar', '0.0', { 'exts_filter': ('ls -l lib/lib%(ext_name)s.a', ''), - 'toy_custom_sanity_check_cmds': ['echo "Run-Custom-Cmd for %(name)s" && {}'], + 'toy_custom_sanity_check_cmds': ['echo "Run-Custom-Cmd for %(name)s" && PLACEHOLDER'], 'sanity_check_paths': {'dirs': [], 'files': ['lib/libbarbar.a']}, }) ] """) test_ec = os.path.join(self.test_prefix, 'test.eb') - write_file(test_ec, test_ec_txt.format("false")) + write_file(test_ec, test_ec_txt.replace('PLACEHOLDER', 'false')) error_pattern = 'sanity check command echo "Run-Custom-Cmd for barbar" && false failed with exit code 1' with self.mocked_stdout_stderr(): self.assertErrorRegex(EasyBuildError, error_pattern, self._test_toy_build, ec_file=test_ec, raise_error=True, verbose=False) - write_file(test_ec, test_ec_txt.format("true")) + write_file(test_ec, test_ec_txt.replace('PLACEHOLDER', 'true')) with self.mocked_stdout_stderr(), self.log_to_testlogfile() as logfile: self._test_toy_build(ec_file=test_ec, raise_error=True) logtxt = read_file(logfile) From e664f5caf7df314bf126b25bb871153aded786c1 Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Thu, 23 Jul 2026 12:54:21 +0200 Subject: [PATCH 08/10] Show warning when passing `extra_module and `with_build_deps` to `fake_module_environment` --- easybuild/framework/easyblock.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 7f0d81c645..cd2775cb84 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -2394,6 +2394,10 @@ def fake_module_environment(self, extra_modules=None, with_build_deps=False): fake_mod_data = None if with_build_deps: + if extra_modules: + print_warning("`with_build_deps` overwrites `extra_modules` in fake_module_environment", + "Until EasyBuild 6 add the build dependencies to `extra_modules` instead", + log=self.log) # load modules for build dependencies as extra modules extra_modules = [dep['short_mod_name'] for dep in self.cfg.dependencies(build_only=True)] From b9b7e89dc952957e827d93873af77a5047a7875a Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Thu, 23 Jul 2026 12:54:43 +0200 Subject: [PATCH 09/10] Remove `with_build_deps` from `sanity_check_module_environment` --- easybuild/framework/easyblock.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index cd2775cb84..4e61562880 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -2411,7 +2411,7 @@ def fake_module_environment(self, extra_modules=None, with_build_deps=False): self.clean_up_fake_module(fake_mod_data) @contextmanager - def sanity_check_module_environment(self, extra_modules=None, with_build_deps=False, check_loaded=True): + def sanity_check_module_environment(self, extra_modules=None, check_loaded=True): """Load/Unload module for performing sanity checks""" if self.sanity_check_module_loaded and check_loaded: raise EasyBuildError("Sanity check module is already loaded and must not be loaded again") @@ -2419,9 +2419,6 @@ def sanity_check_module_environment(self, extra_modules=None, with_build_deps=Fa if self.sanity_check_module_loaded: unload_module = False else: - if with_build_deps: - # load modules for build dependencies as extra modules - extra_modules = [dep['short_mod_name'] for dep in self.cfg.dependencies(build_only=True)] self.sanity_check_load_module(extra_modules=extra_modules) unload_module = True From 91531c89ce1f0ded9be8b213df97c252b7bba90c Mon Sep 17 00:00:00 2001 From: Alexander Grund Date: Thu, 23 Jul 2026 15:42:24 +0200 Subject: [PATCH 10/10] Remove stray comma Co-authored-by: Davide Grassano <34096612+Crivella@users.noreply.github.com> --- easybuild/framework/easyblock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/easybuild/framework/easyblock.py b/easybuild/framework/easyblock.py index 4e61562880..8778611e00 100644 --- a/easybuild/framework/easyblock.py +++ b/easybuild/framework/easyblock.py @@ -2395,7 +2395,7 @@ def fake_module_environment(self, extra_modules=None, with_build_deps=False): if with_build_deps: if extra_modules: - print_warning("`with_build_deps` overwrites `extra_modules` in fake_module_environment", + print_warning("`with_build_deps` overwrites `extra_modules` in fake_module_environment. " "Until EasyBuild 6 add the build dependencies to `extra_modules` instead", log=self.log) # load modules for build dependencies as extra modules