Repository: spack/spack

Issue:
Add setdefault option to tcl environment-modules backend
Spack Tcl environment-modules backend does not have the `setdefault` version as the LMod backend does.
So, when one installs an application and uses the Tcl environment-modules backend, it is left to the environment-modules to pick the default. This may change the current default on the system, which is not ideal.

For completeness
```
spack module lmod -h
usage: spack module lmod [-h] SUBCOMMAND ...

positional arguments:
  SUBCOMMAND
    refresh   regenerate module files
    find      find module files for packages
    rm        remove module files
    loads     prompt the list of modules associated with a constraint
    setdefault
              set the default module file for a package

optional arguments:
  -h, --help  show this help message and exit
```

For the Tcl backend...
```
spack module tcl -h
usage: spack module tcl [-h] SUBCOMMAND ...

positional arguments:
  SUBCOMMAND
    refresh   regenerate module files
    find      find module files for packages
    rm        remove module files
    loads     prompt the list of modules associated with a constraint

optional arguments:
  -h, --help  show this help message and exit
```

### Rationale

Assuming that there is no other GROMACS installation on the system if one uses the Tcl environment-modules backend and he/she performs the operation
```
spack install gromacs@2019.2
```
The "default" module version will be `gromacs/2019.2`. 
After performing the operation
```
spack install gromacs@2018.3
```
The default module version will be `gromacs/2018.3`. 

This is a problem on the HPC centers because care has to be taken when changing default module versions. For this reason, the LMod backend has this option. But this option is also needed by centers that do not use LMod.

### Description

The solution is to add the option to the Tcl backend. I haven't looked at how to implement it yet.




Discussion:

Resolving patch:
diff --git a/lib/spack/spack/cmd/modules/lmod.py b/lib/spack/spack/cmd/modules/lmod.py
--- a/lib/spack/spack/cmd/modules/lmod.py
+++ b/lib/spack/spack/cmd/modules/lmod.py
@@ -4,12 +4,11 @@
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
 
 import functools
-import os
-
-import llnl.util.filesystem
 
 import spack.cmd.common.arguments
 import spack.cmd.modules
+import spack.config
+import spack.modules.lmod
 
 
 def add_command(parser, command_dict):
@@ -41,12 +40,19 @@ def setdefault(module_type, specs, args):
     # https://lmod.readthedocs.io/en/latest/060_locating.html#marking-a-version-as-default
     #
     spack.cmd.modules.one_spec_or_raise(specs)
-    writer = spack.modules.module_types['lmod'](
-        specs[0], args.module_set_name)
-
-    module_folder = os.path.dirname(writer.layout.filename)
-    module_basename = os.path.basename(writer.layout.filename)
-    with llnl.util.filesystem.working_dir(module_folder):
-        if os.path.exists('default') and os.path.islink('default'):
-            os.remove('default')
-        os.symlink(module_basename, 'default')
+    spec = specs[0]
+    data = {
+        'modules': {
+            args.module_set_name: {
+                'lmod': {
+                    'defaults': [str(spec)]
+                }
+            }
+        }
+    }
+    # Need to clear the cache if a SpackCommand is called during scripting
+    spack.modules.lmod.configuration_registry = {}
+    scope = spack.config.InternalConfigScope('lmod-setdefault', data)
+    with spack.config.override(scope):
+        writer = spack.modules.module_types['lmod'](spec, args.module_set_name)
+        writer.update_module_defaults()
diff --git a/lib/spack/spack/cmd/modules/tcl.py b/lib/spack/spack/cmd/modules/tcl.py
--- a/lib/spack/spack/cmd/modules/tcl.py
+++ b/lib/spack/spack/cmd/modules/tcl.py
@@ -2,18 +2,52 @@
 # Spack Project Developers. See the top-level COPYRIGHT file for details.
 #
 # SPDX-License-Identifier: (Apache-2.0 OR MIT)
-
 import functools
 
+import spack.cmd.common.arguments
 import spack.cmd.modules
+import spack.config
+import spack.modules.tcl
 
 
 def add_command(parser, command_dict):
     tcl_parser = parser.add_parser(
         'tcl', help='manipulate non-hierarchical module files'
     )
-    spack.cmd.modules.setup_parser(tcl_parser)
+    sp = spack.cmd.modules.setup_parser(tcl_parser)
+
+    # Set default module file for a package
+    setdefault_parser = sp.add_parser(
+        'setdefault', help='set the default module file for a package'
+    )
+    spack.cmd.common.arguments.add_common_arguments(
+        setdefault_parser, ['constraint']
+    )
+
+    callbacks = dict(spack.cmd.modules.callbacks.items())
+    callbacks['setdefault'] = setdefault
 
     command_dict['tcl'] = functools.partial(
-        spack.cmd.modules.modules_cmd, module_type='tcl'
+        spack.cmd.modules.modules_cmd, module_type='tcl', callbacks=callbacks
     )
+
+
+def setdefault(module_type, specs, args):
+    """Set the default module file, when multiple are present"""
+    # Currently, accepts only a single matching spec
+    spack.cmd.modules.one_spec_or_raise(specs)
+    spec = specs[0]
+    data = {
+        'modules': {
+            args.module_set_name: {
+                'tcl': {
+                    'defaults': [str(spec)]
+                }
+            }
+        }
+    }
+    spack.modules.tcl.configuration_registry = {}
+    scope = spack.config.InternalConfigScope('tcl-setdefault', data)
+    with spack.config.override(scope):
+        writer = spack.modules.module_types['tcl'](spec, args.module_set_name)
+        writer.update_module_defaults()
diff --git a/lib/spack/spack/modules/common.py b/lib/spack/spack/modules/common.py
--- a/lib/spack/spack/modules/common.py
+++ b/lib/spack/spack/modules/common.py
@@ -906,6 +906,9 @@ def write(self, overwrite=False):
             fp.set_permissions_by_spec(self.layout.filename, self.spec)
 
         # Symlink defaults if needed
+        self.update_module_defaults()
+
+    def update_module_defaults(self):
         if any(self.spec.satisfies(default) for default in self.conf.defaults):
             # This spec matches a default, it needs to be symlinked to default
             # Symlink to a tmp location first and move, so that existing


Test patch:
diff --git a/lib/spack/spack/test/cmd/module.py b/lib/spack/spack/test/cmd/module.py
--- a/lib/spack/spack/test/cmd/module.py
+++ b/lib/spack/spack/test/cmd/module.py
@@ -178,10 +178,18 @@ def test_loads_recursive_blacklisted(database, module_configuration):
 
 @pytest.mark.db
 def test_setdefault_command(
-        mutable_database, module_configuration
+        mutable_database, mutable_config
 ):
-    module_configuration('autoload_direct')
-
+    data = {
+        'default': {
+            'enable': ['lmod'],
+            'lmod': {
+                'core_compilers': ['clang@3.3'],
+                'hierarchy': ['mpi']
+            }
+        }
+    }
+    spack.config.set('modules', data)
     # Install two different versions of a package
     other_spec, preferred = 'a@1.0', 'a@2.0'