Repository: spack/spack

Issue:
Subclassing of a package fails to expose resources such as patches correctly

Summary

When subclassing a package, the resources such as  patches do not get propagated in the right way...

E.g. If I had a package like this...

```
from spack import *
from spack.pkg.builtin.kaldi import Kaldi as KaldiSpack

class Kaldi(KaldiSpack):
    version('master', git='https://github.com/kaldi-asr/kaldi.git')
```

The path to "openfst-1.4.1.patch" is incorrect, a full path would need to be provided to the patch for it to work. Currently if I want to extend an existing package I would need to copy all the resources to the extended package as well.

The above is pretty easy to reproduce reliably, just pick any package that has a patch or additional resource files and extend it.


Discussion:

Resolving patch:
diff --git a/lib/spack/spack/patch.py b/lib/spack/spack/patch.py
--- a/lib/spack/spack/patch.py
+++ b/lib/spack/spack/patch.py
@@ -6,6 +6,7 @@
 import hashlib
 import os
 import os.path
+import inspect
 
 import llnl.util.filesystem
 import llnl.util.lang
@@ -110,7 +111,30 @@ class FilePatch(Patch):
     def __init__(self, pkg, relative_path, level, working_dir,
                  ordering_key=None):
         self.relative_path = relative_path
-        abs_path = os.path.join(pkg.package_dir, self.relative_path)
+
+        # patches may be defined by relative paths to parent classes
+        # search mro to look for the file
+        abs_path = None
+        # At different times we call FilePatch on instances and classes
+        pkg_cls = pkg if inspect.isclass(pkg) else pkg.__class__
+        for cls in inspect.getmro(pkg_cls):
+            if not hasattr(cls, 'module'):
+                # We've gone too far up the MRO
+                break
+
+            # Cannot use pkg.package_dir because it's a property and we have
+            # classes, not instances.
+            pkg_dir = os.path.abspath(os.path.dirname(cls.module.__file__))
+            path = os.path.join(pkg_dir, self.relative_path)
+            if os.path.exists(path):
+                abs_path = path
+                break
+
+        if abs_path is None:
+            msg = 'FilePatch: Patch file %s for ' % relative_path
+            msg += 'package %s.%s does not exist.' % (pkg.namespace, pkg.name)
+            raise ValueError(msg)
+
         super(FilePatch, self).__init__(pkg, abs_path, level, working_dir)
         self.path = abs_path
         self._sha256 = None
diff --git a/var/spack/repos/builtin.mock/packages/patch-inheritance/package.py b/var/spack/repos/builtin.mock/packages/patch-inheritance/package.py
new file mode 100644
--- /dev/null
+++ b/var/spack/repos/builtin.mock/packages/patch-inheritance/package.py
@@ -0,0 +1,11 @@
+# Copyright 2013-2019 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+from spack.pkg.builtin.mock.patch import Patch
+
+
+class PatchInheritance(Patch):
+    def install(self, spec, prefix):
+        Patch.install(self, spec, prefix)


Test patch:
diff --git a/lib/spack/spack/test/packages.py b/lib/spack/spack/test/packages.py
--- a/lib/spack/spack/test/packages.py
+++ b/lib/spack/spack/test/packages.py
@@ -123,6 +123,12 @@ def test_inheritance_of_diretives(self):
         assert '~openblas' in s
         assert 'mpi' in s
 
+    @pytest.mark.regression('11844')
+    def test_inheritance_of_patches(self):
+        s = Spec('patch-inheritance')
+        # Will error if inheritor package cannot find inherited patch files
+        s.concretize()
+
     def test_dependency_extensions(self):
         s = Spec('extension2')
         s.concretize()
diff --git a/lib/spack/spack/test/patch.py b/lib/spack/spack/test/patch.py
--- a/lib/spack/spack/test/patch.py
+++ b/lib/spack/spack/test/patch.py
@@ -6,6 +6,7 @@
 import os
 import filecmp
 import pytest
+import collections
 
 from llnl.util.filesystem import working_dir, mkdirp
 
@@ -314,3 +315,11 @@ def test_write_and_read_sub_dags_with_patched_deps(mock_packages, config):
         libelf, libdwarf, fake,
         'builtin.mock.patch-several-dependencies',
         spec.package.package_dir)
+
+
+def test_file_patch_no_file():
+    # Give it the attributes we need to construct the error message
+    FakePackage = collections.namedtuple('FakePackage', ['name', 'namespace'])
+    fp = FakePackage('fake-package', 'test')
+    with pytest.raises(ValueError, match=r'FilePatch:.*'):
+        spack.patch.FilePatch(fp, 'nonexistent_file', 0, '')