Repository: spack/spack

Issue:
Numpy: Ensure Spack finds Numpy's headers.
The mpi4py package does the same thing.


Discussion:
@becker33 @scheibelp I wonder if we want to do this globally for all Python packages. The headers are almost always installed in `prefix/lib/pythonX.Y/site-packages`, not in `prefix/include`.
@adamjstewart would it make more sense to use `find_headers` with that directory name, or this method using `find_all_headers`?
The problem is we need to recursively search both `prefix.lib` and `prefix.lib64`.
@adamjstewart : Any news about this? Was it fixed globally?
This also relates to #14513. This hasn't yet been fixed globally but I think it should be relatively easy to do after the recent Python refactor I did. I'll try to submit a PR sometime this week to do that.

Resolving patch:
diff --git a/lib/spack/spack/build_systems/python.py b/lib/spack/spack/build_systems/python.py
--- a/lib/spack/spack/build_systems/python.py
+++ b/lib/spack/spack/build_systems/python.py
@@ -12,6 +12,8 @@
 from llnl.util.filesystem import (
     filter_file,
     find,
+    find_all_headers,
+    find_libraries,
     is_nonsymlink_exe_with_shebang,
     path_contains_subdirectory,
     same_path,
@@ -20,6 +22,7 @@
 from llnl.util.lang import match_predicate
 
 from spack.directives import depends_on, extends
+from spack.error import NoHeadersError, NoLibrariesError
 from spack.package_base import PackageBase, run_after
 
 
@@ -178,6 +181,37 @@ def install(self, spec, prefix):
         with working_dir(self.build_directory):
             pip(*args)
 
+    @property
+    def headers(self):
+        """Discover header files in platlib."""
+
+        # Headers may be in either location
+        include = inspect.getmodule(self).include
+        platlib = inspect.getmodule(self).platlib
+        headers = find_all_headers(include) + find_all_headers(platlib)
+
+        if headers:
+            return headers
+
+        msg = 'Unable to locate {} headers in {} or {}'
+        raise NoHeadersError(msg.format(self.spec.name, include, platlib))
+
+    @property
+    def libs(self):
+        """Discover libraries in platlib."""
+
+        # Remove py- prefix in package name
+        library = 'lib' + self.spec.name[3:].replace('-', '?')
+        root = inspect.getmodule(self).platlib
+
+        for shared in [True, False]:
+            libs = find_libraries(library, root, shared=shared, recursive=True)
+            if libs:
+                return libs
+
+        msg = 'Unable to recursively locate {} libraries in {}'
+        raise NoLibrariesError(msg.format(self.spec.name, root))
+
     # Testing
 
     def test(self):
diff --git a/var/spack/repos/builtin/packages/py-mpi4py/package.py b/var/spack/repos/builtin/packages/py-mpi4py/package.py
--- a/var/spack/repos/builtin/packages/py-mpi4py/package.py
+++ b/var/spack/repos/builtin/packages/py-mpi4py/package.py
@@ -35,8 +35,3 @@ class PyMpi4py(PythonPackage):
     @when('@3.1:')
     def install_options(self, spec, prefix):
         return ['--mpicc=%s -shared' % spec['mpi'].mpicc]
-
-    @property
-    def headers(self):
-        headers = find_all_headers(self.prefix.lib)
-        return headers
diff --git a/var/spack/repos/builtin/packages/py-torch/package.py b/var/spack/repos/builtin/packages/py-torch/package.py
--- a/var/spack/repos/builtin/packages/py-torch/package.py
+++ b/var/spack/repos/builtin/packages/py-torch/package.py
@@ -260,24 +260,6 @@ class PyTorch(PythonPackage, CudaPackage):
     patch('https://github.com/pytorch/pytorch/commit/c74c0c571880df886474be297c556562e95c00e0.patch?full_index=1',
           sha256='8ff7d285e52e4718bad1ca01ceb3bb6471d7828329036bb94222717fcaa237da', when='@:1.9.1 ^cuda@11.4.100:')
 
-    @property
-    def libs(self):
-        # TODO: why doesn't `python_platlib` work here?
-        root = join_path(
-            self.prefix, self.spec['python'].package.platlib, 'torch', 'lib'
-        )
-        return find_libraries('libtorch', root)
-
-    @property
-    def headers(self):
-        # TODO: why doesn't `python_platlib` work here?
-        root = join_path(
-            self.prefix, self.spec['python'].package.platlib, 'torch', 'include'
-        )
-        headers = find_all_headers(root)
-        headers.directories = [root]
-        return headers
-
     @when('@1.5.0:')
     def patch(self):
         # https://github.com/pytorch/pytorch/issues/52208


Test patch: