Repository: spack/spack

Issue:
conflict directive not in lmod module files
Hi,

I have the following YAML in my `spack/etc/spack/defaults/modules.yaml`:
```
  lmod:
    naming_scheme: '{name}/{version}'
    verbose: True
    hash_length: 0
    all:
      conflict:
        - '{name}'
      autoload: direct
```
I then refresh my modules:

```
$ spack module lmod refresh --delete-tree -y
==> Regenerating lmod module files
$
```
When I open a module file, e.g. `/home/admwm1/spack/share/spack/lmod/linux-rhel7-x86_64/intel/18.0.1/autoconf/2.69.lua` there is no `conflict` directive in it, but exists for TCL module files, but not for lmod module file. Should be an easy fix :-) 




Discussion:
Your documentation seems to think the `conflict` directive is TCL specific, but it's not - it also exists for Lmod:

https://lmod.readthedocs.io/en/latest/050_lua_modulefiles.html

Check your documentation in:

https://spack-tutorial.readthedocs.io/en/latest/tutorial_modules.html

in section "Core/Compiler/MPI", where it says "remove the tcl specific conflict directive". 
+1 for this request. Module conflicts are still very useful in lmod hierarchies (e.g., between MPI and serial versions of libraries with different names).
@xdelaruelle fyi 

Resolving patch:
diff --git a/lib/spack/spack/cmd/modules/__init__.py b/lib/spack/spack/cmd/modules/__init__.py
--- a/lib/spack/spack/cmd/modules/__init__.py
+++ b/lib/spack/spack/cmd/modules/__init__.py
@@ -11,6 +11,7 @@
 import sys
 
 from llnl.util import filesystem, tty
+from llnl.util.tty import color
 
 import spack.cmd
 import spack.cmd.common.arguments as arguments
@@ -347,14 +348,20 @@ def refresh(module_type, specs, args):
     spack.modules.common.generate_module_index(
         module_type_root, writers, overwrite=args.delete_tree
     )
+    errors = []
     for x in writers:
         try:
             x.write(overwrite=True)
+        except spack.error.SpackError as e:
+            msg = f"{x.layout.filename}: {e.message}"
+            errors.append(msg)
         except Exception as e:
-            tty.debug(e)
-            msg = "Could not write module file [{0}]"
-            tty.warn(msg.format(x.layout.filename))
-            tty.warn("\t--> {0} <--".format(str(e)))
+            msg = f"{x.layout.filename}: {str(e)}"
+            errors.append(msg)
+
+    if errors:
+        errors.insert(0, color.colorize("@*{some module files could not be written}"))
+        tty.warn("\n".join(errors))
 
 
 #: Dictionary populated with the list of sub-commands.
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
@@ -35,6 +35,7 @@
 import os.path
 import pathlib
 import re
+import string
 import warnings
 from typing import Optional
 
@@ -470,6 +471,11 @@ def hash(self):
             return self.spec.dag_hash(length=hash_length)
         return None
 
+    @property
+    def conflicts(self):
+        """Conflicts for this module file"""
+        return self.conf.get("conflict", [])
+
     @property
     def excluded(self):
         """Returns True if the module has been excluded, False otherwise."""
@@ -763,6 +769,36 @@ def has_manpath_modifications(self):
         else:
             return False
 
+    @tengine.context_property
+    def conflicts(self):
+        """List of conflicts for the module file."""
+        fmts = []
+        projection = proj.get_projection(self.conf.projections, self.spec)
+        for item in self.conf.conflicts:
+            self._verify_conflict_naming_consistency_or_raise(item, projection)
+            item = self.spec.format(item)
+            fmts.append(item)
+        return fmts
+
+    def _verify_conflict_naming_consistency_or_raise(self, item, projection):
+        f = string.Formatter()
+        errors = []
+        if len([x for x in f.parse(item)]) > 1:
+            for naming_dir, conflict_dir in zip(projection.split("/"), item.split("/")):
+                if naming_dir != conflict_dir:
+                    errors.extend(
+                        [
+                            f"spec={self.spec.cshort_spec}",
+                            f"conflict_scheme={item}",
+                            f"naming_scheme={projection}",
+                        ]
+                    )
+        if errors:
+            raise ModulesError(
+                message="conflict scheme does not match naming scheme",
+                long_message="\n    ".join(errors),
+            )
+
     @tengine.context_property
     def autoload(self):
         """List of modules that needs to be loaded automatically."""
diff --git a/lib/spack/spack/modules/tcl.py b/lib/spack/spack/modules/tcl.py
--- a/lib/spack/spack/modules/tcl.py
+++ b/lib/spack/spack/modules/tcl.py
@@ -7,13 +7,9 @@
 non-hierarchical modules.
 """
 import posixpath
-import string
 from typing import Any, Dict
 
-import llnl.util.tty as tty
-
 import spack.config
-import spack.projections as proj
 import spack.tengine as tengine
 
 from .common import BaseConfiguration, BaseContext, BaseFileLayout, BaseModuleFileWriter
@@ -56,11 +52,6 @@ def make_context(spec, module_set_name, explicit):
 class TclConfiguration(BaseConfiguration):
     """Configuration class for tcl module files."""
 
-    @property
-    def conflicts(self):
-        """Conflicts for this module file"""
-        return self.conf.get("conflict", [])
-
 
 class TclFileLayout(BaseFileLayout):
     """File layout for tcl module files."""
@@ -74,29 +65,6 @@ def prerequisites(self):
         """List of modules that needs to be loaded automatically."""
         return self._create_module_list_of("specs_to_prereq")
 
-    @tengine.context_property
-    def conflicts(self):
-        """List of conflicts for the tcl module file."""
-        fmts = []
-        projection = proj.get_projection(self.conf.projections, self.spec)
-        f = string.Formatter()
-        for item in self.conf.conflicts:
-            if len([x for x in f.parse(item)]) > 1:
-                for naming_dir, conflict_dir in zip(projection.split("/"), item.split("/")):
-                    if naming_dir != conflict_dir:
-                        message = "conflict scheme does not match naming "
-                        message += "scheme [{spec}]\n\n"
-                        message += 'naming scheme   : "{nformat}"\n'
-                        message += 'conflict scheme : "{cformat}"\n\n'
-                        message += "** You may want to check your "
-                        message += "`modules.yaml` configuration file **\n"
-                        tty.error(message.format(spec=self.spec, nformat=projection, cformat=item))
-                        raise SystemExit("Module generation aborted.")
-                item = self.spec.format(item)
-            fmts.append(item)
-        # Substitute spec tokens if present
-        return [self.spec.format(x) for x in fmts]
-
 
 class TclModulefileWriter(BaseModuleFileWriter):
     """Writer class for tcl module files."""


Test patch:
diff --git a/lib/spack/spack/test/data/modules/lmod/conflicts.yaml b/lib/spack/spack/test/data/modules/lmod/conflicts.yaml
new file mode 100644
--- /dev/null
+++ b/lib/spack/spack/test/data/modules/lmod/conflicts.yaml
@@ -0,0 +1,10 @@
+enable:
+  - lmod
+lmod:
+  core_compilers:
+    - 'clang@3.3'
+  all:
+    autoload: none
+    conflict:
+      - '{name}'
+      - 'intel/14.0.1'
diff --git a/lib/spack/spack/test/data/modules/lmod/wrong_conflicts.yaml b/lib/spack/spack/test/data/modules/lmod/wrong_conflicts.yaml
new file mode 100644
--- /dev/null
+++ b/lib/spack/spack/test/data/modules/lmod/wrong_conflicts.yaml
@@ -0,0 +1,11 @@
+enable:
+  - lmod
+lmod:
+  core_compilers:
+    - 'clang@3.3'
+  projections:
+    all: '{name}/{version}-{compiler.name}'
+  all:
+    autoload: none
+    conflict:
+      - '{name}/{compiler.name}'
diff --git a/lib/spack/spack/test/modules/lmod.py b/lib/spack/spack/test/modules/lmod.py
--- a/lib/spack/spack/test/modules/lmod.py
+++ b/lib/spack/spack/test/modules/lmod.py
@@ -290,6 +290,26 @@ def test_non_virtual_in_hierarchy(self, factory, module_configuration):
         with pytest.raises(spack.modules.lmod.NonVirtualInHierarchyError):
             module.write()
 
+    def test_conflicts(self, modulefile_content, module_configuration):
+        """Tests adding conflicts to the module."""
+
+        # This configuration has no error, so check the conflicts directives
+        # are there
+        module_configuration("conflicts")
+        content = modulefile_content("mpileaks")
+
+        assert len([x for x in content if x.startswith("conflict")]) == 2
+        assert len([x for x in content if x == 'conflict("mpileaks")']) == 1
+        assert len([x for x in content if x == 'conflict("intel/14.0.1")']) == 1
+
+    def test_inconsistent_conflict_in_modules_yaml(self, modulefile_content, module_configuration):
+        """Tests inconsistent conflict definition in `modules.yaml`."""
+
+        # This configuration is inconsistent, check an error is raised
+        module_configuration("wrong_conflicts")
+        with pytest.raises(spack.modules.common.ModulesError):
+            modulefile_content("mpileaks")
+
     def test_override_template_in_package(self, modulefile_content, module_configuration):
         """Tests overriding a template from and attribute in the package."""
 
diff --git a/lib/spack/spack/test/modules/tcl.py b/lib/spack/spack/test/modules/tcl.py
--- a/lib/spack/spack/test/modules/tcl.py
+++ b/lib/spack/spack/test/modules/tcl.py
@@ -311,9 +311,12 @@ def test_conflicts(self, modulefile_content, module_configuration):
         assert len([x for x in content if x == "conflict mpileaks"]) == 1
         assert len([x for x in content if x == "conflict intel/14.0.1"]) == 1
 
+    def test_inconsistent_conflict_in_modules_yaml(self, modulefile_content, module_configuration):
+        """Tests inconsistent conflict definition in `modules.yaml`."""
+
         # This configuration is inconsistent, check an error is raised
         module_configuration("wrong_conflicts")
-        with pytest.raises(SystemExit):
+        with pytest.raises(spack.modules.common.ModulesError):
             modulefile_content("mpileaks")
 
     def test_module_index(self, module_configuration, factory, tmpdir_factory):