Repository: spack/spack

Issue:
Rebuilding modules fails for dependents of `gdbm`
### Summary

Rebuilding lmod module tree fails for dependents of `gdbm` (but succeeds for `gdbm`, and most other packages).

### Expected Result

`spack module refresh -m lmod --delete-tree -y` regenerates all Lmod modules

### Actual Result

Any module that depends on `gdbm` (e.g., `autoconf`) will throw a warning:

```console
==> Warning: Could not write module file [/Users/oxberry1/spack/share/spack/lmod/darwin-sierra-x86_64/Core/autoconf/2.69.lua]
==> Warning:    --> Cannot create a consistent method resolution
order (MRO) for bases Spec, SpecBuildInterface <--
```

### Steps to reproduce the issue

```console
$ spack install autoconf
$ spack module refresh -m lmod --delete-tree -y
...
```

### Information on your system

This includes:

 1. macOS / OS X 10.12.6
 2. I don't have any customizations related to autoconf, but would be happy to provide config files on request if they might be useful.

### History

I think this issue is a continuation of #6089 and #5864. Moving use of `spack_cc` into `setup_environment` solved issues relating to the expansion of `spack_cc`, so now instead of `gdbm` and its dependents throwing errors, only dependents of `gdbm` throw errors. The problem seemed to be introduced by #5893, and #7225 improved the situation slightly -- efore #7225, module files for `gdbm` could not be regenerated either.

### Narrowing down the problem

I tried running `spack -D module refresh -m lmod --delete-tree -y` and got as far as tracking down the error to within a method call at `spack/tengine.py:85`. Set a breakpoint at `spack/cmd/module.py:317` conditional on `'automake' in x.spec.name`. Once this breakpoint triggers, set a breakpoint in `spack/tengine.py:85`. Around the 15th-ish execution of that line (after the breakpoint in `module.py`), the method resolution order error throws. I couldn't narrow down the problem further.

### Workaround

A workaround for now is to reinstall the packages that throw warnings, because the module file written upon installation is apparently fine.


Discussion:
I am also interested in the resolution of this issue.
I'm seeing this a lot now since I've been managing modules with Spack.
I had some time this morning to track down the error, and have to say it's a "funny" one. The error  comes from `perl`, in particular from the line:
```python
def setup_dependent_package(self, module, dependent_spec):
    """Called before perl modules' install() methods.
        In most cases, extensions will only need to have one line:
       perl('Makefile.PL','INSTALL_BASE=%s' % self.prefix)
     """
     # perl extension builds can have a global perl executable function
     module.perl = self.spec['perl'].command # <-- ERROR
     ...
```
and is due to the fact that when building module files we pass to this function a class that *is already* a `SpecBuildInterface`, and building a `SpecBuildInterface` from another `SpecBuildInterface` creates an inconsistent MRO. 

I'll try to work on a permanent fix to this (shouldn't be a lot of lines), but a workaround for who needs it is to change:
```python
module.perl = self.spec['perl'].command # <-- ERROR
```
with
```python
module.perl = self.spec.command
```
before regenerating module files. The change must be undone before installing a new package that depends on `perl`.

Resolving patch:
diff --git a/lib/spack/llnl/util/lang.py b/lib/spack/llnl/util/lang.py
--- a/lib/spack/llnl/util/lang.py
+++ b/lib/spack/llnl/util/lang.py
@@ -458,5 +458,17 @@ class ObjectWrapper(object):
     def __init__(self, wrapped_object):
         wrapped_cls = type(wrapped_object)
         wrapped_name = wrapped_cls.__name__
-        self.__class__ = type(wrapped_name, (type(self), wrapped_cls), {})
+
+        # If the wrapped object is already an ObjectWrapper, or a derived class
+        # of it, adding type(self) in front of type(wrapped_object)
+        # results in an inconsistent MRO.
+        #
+        # TODO: the implementation below doesn't account for the case where we
+        # TODO: have different base classes of ObjectWrapper, say A and B, and
+        # TODO: we want to wrap an instance of A with B.
+        if type(self) not in wrapped_cls.__mro__:
+            self.__class__ = type(wrapped_name, (type(self), wrapped_cls), {})
+        else:
+            self.__class__ = type(wrapped_name, (wrapped_cls,), {})
+
         self.__dict__ = wrapped_object.__dict__


Test patch:
diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py
--- a/lib/spack/spack/test/concretize.py
+++ b/lib/spack/spack/test/concretize.py
@@ -23,8 +23,10 @@
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ##############################################################################
 import pytest
+import llnl.util.lang
 import spack
 import spack.architecture
+
 from spack.concretize import find_spec
 from spack.spec import Spec, CompilerSpec
 from spack.spec import ConflictsInSpecError, SpecError
@@ -444,3 +446,22 @@ def test_regression_issue_4492(self):
         s._concrete = False
 
         assert not s.concrete
+
+    @pytest.mark.regression('7239')
+    def test_regression_issue_7239(self):
+        # Constructing a SpecBuildInterface from another SpecBuildInterface
+        # results in an inconsistent MRO
+
+        # Normal Spec
+        s = Spec('mpileaks')
+        s.concretize()
+
+        assert llnl.util.lang.ObjectWrapper not in type(s).__mro__
+
+        # Spec wrapped in a build interface
+        build_interface = s['mpileaks']
+        assert llnl.util.lang.ObjectWrapper in type(build_interface).__mro__
+
+        # Mimics asking the build interface from a build interface
+        build_interface = s['mpileaks']['mpileaks']
+        assert llnl.util.lang.ObjectWrapper in type(build_interface).__mro__