Repository: easybuilders/easybuild-framework

Issue:
module function check does not support Environment Modules
When trying to use EB with Environment Modules, the module function check raises an error:

```console
$ eb --modules-tool EnvironmentModules --module-syntax Tcl 
== Temporary log file in case of crash /tmp/eb-snqwtxzg/easybuild-gvwalsbq.log
ERROR: pattern '.*modulecmd.tcl' (Environment Modules) not found in defined 'module' function.
Specify the correct modules tool to avoid weird problems due to this mismatch, see the --modules-tool and --avail-modules-tools command line options.
Or alternatively, use --allow-modules-tool-mismatch to stop treating this as an error. Obtained definition of 'module' function: module is a function
module ()  
{ 
    local _mlredir=1;
    if [ -n "${MODULES_REDIRECT_OUTPUT+x}" ]; then
    |   if [ "$MODULES_REDIRECT_OUTPUT" = '0' ]; then
    |   ¦   _mlredir=0;
    |   else
    |   ¦   if [ "$MODULES_REDIRECT_OUTPUT" = '1' ]; then
    |   ¦   ┆   _mlredir=1;
    |   ¦   fi;
    |   fi;
    fi; 
    case " $@ " in 
    |   *' --no-redirect '*)
    |   ¦   _mlredir=0
    |   ;;  
    |   *' --redirect '*)
    |   ¦   _mlredir=1
    |   ;;
    esac;
    if [ $_mlredir -eq 0 ]; then
    |   _module_raw "$@";
    else
    |   _module_raw "$@" 2>&1;
    fi
}

```

Module command cannot be found in `module` shell function definition:
* starting version 5.1.0: module command is always called from `_module_raw` shell function
* starting version 4.0: module command is called from `_module_raw` shell function when it has been initialized in an interactive shell session



Discussion:
Defining a specific `check_module_function` function in EnvironmentModules class should be able to fix this issue.

I will provide such fix that tests `_module_raw` shell function prior `module` shell function.

Resolving patch:
diff --git a/easybuild/tools/modules.py b/easybuild/tools/modules.py
--- a/easybuild/tools/modules.py
+++ b/easybuild/tools/modules.py
@@ -1328,6 +1328,31 @@ class EnvironmentModules(EnvironmentModulesTcl):
     MAX_VERSION = None
     VERSION_REGEXP = r'^Modules\s+Release\s+(?P<version>\d\S*)\s'
 
+    def check_module_function(self, allow_mismatch=False, regex=None):
+        """Check whether selected module tool matches 'module' function definition."""
+        # Modules 5.1.0+: module command is called from _module_raw shell function
+        # Modules 4.2.0..5.0.1: module command is called from _module_raw shell function if it has
+        #   been initialized in an interactive shell session (i.e., a session attached to a tty)
+        if self.testing:
+            if '_module_raw' in os.environ:
+                out, ec = os.environ['_module_raw'], 0
+            else:
+                out, ec = None, 1
+        else:
+            cmd = "type _module_raw"
+            out, ec = run_cmd(cmd, simple=False, log_ok=False, log_all=False, force_in_dry_run=True, trace=False)
+
+        if regex is None:
+            regex = r".*%s" % os.path.basename(self.cmd)
+        mod_cmd_re = re.compile(regex, re.M)
+
+        if ec == 0 and mod_cmd_re.search(out):
+            self.log.debug("Found pattern '%s' in defined '_module_raw' function." % mod_cmd_re.pattern)
+        else:
+            self.log.debug("Pattern '%s' not found in '_module_raw' function, falling back to 'module' function",
+                           mod_cmd_re.pattern)
+            super(EnvironmentModules, self).check_module_function(allow_mismatch, regex)
+
     def check_module_output(self, cmd, stdout, stderr):
         """Check output of 'module' command, see if if is potentially invalid."""
         if "_mlstatus = False" in stdout:
diff --git a/easybuild/tools/run.py b/easybuild/tools/run.py
--- a/easybuild/tools/run.py
+++ b/easybuild/tools/run.py
@@ -71,6 +71,7 @@
     "sysctl -n machdep.cpu.brand_string",  # used in get_cpu_model (OS X)
     "sysctl -n machdep.cpu.vendor",  # used in get_cpu_vendor (OS X)
     "type module",  # used in ModulesTool.check_module_function
+    "type _module_raw",  # used in EnvironmentModules.check_module_function
     "ulimit -u",  # used in det_parallelism
 ]
 


Test patch:
diff --git a/test/framework/modulestool.py b/test/framework/modulestool.py
--- a/test/framework/modulestool.py
+++ b/test/framework/modulestool.py
@@ -39,7 +39,7 @@
 from easybuild.tools import modules, StrictVersion
 from easybuild.tools.build_log import EasyBuildError
 from easybuild.tools.filetools import read_file, which, write_file
-from easybuild.tools.modules import Lmod
+from easybuild.tools.modules import EnvironmentModules, Lmod
 from test.framework.utilities import init_config
 
 
@@ -192,6 +192,23 @@ def test_lmod_specific(self):
             # test updating local spider cache (but don't actually update the local cache file!)
             self.assertTrue(lmod.update(), "Updated local Lmod spider cache is non-empty")
 
+    def test_environment_modules_specific(self):
+        """Environment Modules-specific test (skipped unless installed)."""
+        modulecmd_abspath = which(EnvironmentModules.COMMAND)
+        # only run this test if 'modulecmd.tcl' is installed
+        if modulecmd_abspath is not None:
+            # redefine 'module' and '_module_raw' function (deliberate mismatch with used module
+            # command in EnvironmentModules)
+            os.environ['_module_raw'] = "() {  eval `/usr/share/Modules/libexec/foo.tcl' bash $*`;\n}"
+            os.environ['module'] = "() {  _module_raw \"$@\" 2>&1;\n}"
+            error_regex = ".*pattern .* not found in defined 'module' function"
+            self.assertErrorRegex(EasyBuildError, error_regex, EnvironmentModules, testing=True)
+
+            # redefine '_module_raw' function with correct module command
+            os.environ['_module_raw'] = "() {  eval `/usr/share/Modules/libexec/modulecmd.tcl' bash $*`;\n}"
+            mt = EnvironmentModules(testing=True)
+            self.assertIsInstance(mt.loaded_modules(), list)  # dummy usage
+
     def tearDown(self):
         """Testcase cleanup."""
         super(ModulesToolTest, self).tearDown()