Python: fix clingo bootstrapping on Apple M1 (#30834)

This PR fixes several issues I noticed while trying to get Spack working on Apple M1.

- [x] `build_environment.py` attempts to add `spec['foo'].libs` and `spec['foo'].headers` to our compiler wrappers for all dependencies using a try-except that ignores `NoLibrariesError` and `NoHeadersError` respectively. However, The `libs` and `headers` attributes of the Python package were erroneously using `RuntimeError` instead.
- [x] `spack external find python` (used during bootstrapping) currently has no way to determine whether or not an installation is `+shared`, so previously we would only search for static Python libs. However, most distributions including XCode/Conda/Intel ship shared Python libs. I updated `libs` to search for both shared and static (order based on variant) as a fallback.
- [x] The `headers` attribute was recursively searching in `prefix.include` for `pyconfig.h`, but this could lead to non-deterministic behavior if multiple versions of Python are installed and `pyconfig.h` files exist in multiple `<prefix>/include/pythonX.Y` locations. It's safer to search in `sysconfig.get_path('include')` instead.
- [x] The Python installation that comes with XCode is broken, and `sysconfig.get_paths` is hard-coded to return specific directories. This meant that our logic for `platlib`/`purelib`/`include` where we replace `platbase`/`base`/`installed_base` with `prefix` wasn't working and the `mkdirp` in `setup_dependent_package` was trying to create a directory in root, giving permissions issues. Even if you commented out those `mkdirp` calls, Spack would add the wrong directories to `PYTHONPATH`. Added a fallback hard-coded to `lib/pythonX.Y/site-packages` if sysconfig is broken (this is what distutils always did).
This commit is contained in:
Adam J. Stewart 2022-05-27 03:18:20 -07:00 committed by GitHub
parent 0bf3a9c2af
commit a3a8710cbe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -18,7 +18,7 @@
is_nonsymlink_exe_with_shebang, is_nonsymlink_exe_with_shebang,
path_contains_subdirectory, path_contains_subdirectory,
) )
from llnl.util.lang import match_predicate from llnl.util.lang import dedupe, match_predicate
from spack import * from spack import *
from spack.build_environment import dso_suffix from spack.build_environment import dso_suffix
@ -1019,16 +1019,13 @@ def home(self):
""" """
return Prefix(self.config_vars['prefix']) return Prefix(self.config_vars['prefix'])
@property def find_library(self, library):
def libs(self): # Spack installs libraries into lib, except on openSUSE where it installs them
# Spack installs libraries into lib, except on openSUSE where it # into lib64. If the user is using an externally installed package, it may be
# installs them into lib64. If the user is using an externally # in either lib or lib64, so we need to ask Python where its LIBDIR is.
# installed package, it may be in either lib or lib64, so we need
# to ask Python where its LIBDIR is.
libdir = self.config_vars['LIBDIR'] libdir = self.config_vars['LIBDIR']
# In Ubuntu 16.04.6 and python 2.7.12 from the system, lib could be # In Ubuntu 16.04.6 and python 2.7.12 from the system, lib could be in LBPL
# in LBPL
# https://mail.python.org/pipermail/python-dev/2013-April/125733.html # https://mail.python.org/pipermail/python-dev/2013-April/125733.html
libpl = self.config_vars['LIBPL'] libpl = self.config_vars['LIBPL']
@ -1044,50 +1041,74 @@ def libs(self):
else: else:
macos_developerdir = '' macos_developerdir = ''
if '+shared' in self.spec: # Windows libraries are installed directly to BINDIR
ldlibrary = self.config_vars['LDLIBRARY'] win_bin_dir = self.config_vars['BINDIR']
win_bin_dir = self.config_vars['BINDIR']
if os.path.exists(os.path.join(libdir, ldlibrary)):
return LibraryList(os.path.join(libdir, ldlibrary))
elif os.path.exists(os.path.join(libpl, ldlibrary)):
return LibraryList(os.path.join(libpl, ldlibrary))
elif os.path.exists(os.path.join(frameworkprefix, ldlibrary)):
return LibraryList(os.path.join(frameworkprefix, ldlibrary))
elif macos_developerdir and \
os.path.exists(os.path.join(macos_developerdir, ldlibrary)):
return LibraryList(os.path.join(macos_developerdir, ldlibrary))
elif is_windows and \
os.path.exists(os.path.join(win_bin_dir, ldlibrary)):
return LibraryList(os.path.join(win_bin_dir, ldlibrary))
else:
msg = 'Unable to locate {0} libraries in {1}'
raise RuntimeError(msg.format(ldlibrary, libdir))
else:
library = self.config_vars['LIBRARY']
if os.path.exists(os.path.join(libdir, library)): directories = [libdir, libpl, frameworkprefix, macos_developerdir, win_bin_dir]
return LibraryList(os.path.join(libdir, library)) for directory in directories:
elif os.path.exists(os.path.join(frameworkprefix, library)): path = os.path.join(directory, library)
return LibraryList(os.path.join(frameworkprefix, library)) if os.path.exists(path):
else: return LibraryList(path)
msg = 'Unable to locate {0} libraries in {1}'
raise RuntimeError(msg.format(library, libdir)) @property
def libs(self):
# The +shared variant isn't always reliable, as `spack external find`
# currently can't detect it. If +shared, prefer the shared libraries, but check
# for static if those aren't found. Vice versa for ~shared.
# The values of LDLIBRARY and LIBRARY also aren't reliable. Intel Python uses a
# static binary but installs shared libraries, so sysconfig reports
# libpythonX.Y.a but only libpythonX.Y.so exists.
shared_libs = [
self.config_vars['LDLIBRARY'],
'libpython{}.{}'.format(self.version.up_to(2), dso_suffix),
]
static_libs = [
self.config_vars['LIBRARY'],
'libpython{}.a'.format(self.version.up_to(2)),
]
if '+shared' in self.spec:
libraries = shared_libs + static_libs
else:
libraries = static_libs + shared_libs
libraries = dedupe(libraries)
for library in libraries:
lib = self.find_library(library)
if lib:
return lib
msg = 'Unable to locate {} libraries in {}'
libdir = self.config_vars['LIBDIR']
raise spack.error.NoLibrariesError(msg.format(self.name, libdir))
@property @property
def headers(self): def headers(self):
directory = self.config_vars['include']
config_h = self.config_vars['config_h_filename'] config_h = self.config_vars['config_h_filename']
if os.path.exists(config_h): if os.path.exists(config_h):
headers = HeaderList(config_h) headers = HeaderList(config_h)
else: else:
headers = find_headers( headers = find_headers('pyconfig', directory)
'pyconfig', self.prefix.include, recursive=True) if headers:
config_h = headers[0] config_h = headers[0]
else:
msg = 'Unable to locate {} headers in {}'
raise spack.error.NoHeadersError(msg.format(self.name, directory))
headers.directories = [os.path.dirname(config_h)] headers.directories = [os.path.dirname(config_h)]
return headers return headers
# https://docs.python.org/3/library/sysconfig.html#installation-paths # https://docs.python.org/3/library/sysconfig.html#installation-paths
# https://discuss.python.org/t/understanding-site-packages-directories/12959
# https://github.com/pypa/pip/blob/22.1/src/pip/_internal/locations/__init__.py
# https://github.com/pypa/installer/pull/103
# NOTE: XCode Python's sysconfing module was incorrectly patched, and hard-codes
# everything to be installed in /Library/Python. Therefore, we need to use a
# fallback in the following methods. For more information, see:
# https://github.com/pypa/pip/blob/22.1/src/pip/_internal/locations/__init__.py#L486
@property @property
def platlib(self): def platlib(self):
@ -1104,8 +1125,12 @@ def platlib(self):
Returns: Returns:
str: platform-specific site-packages directory str: platform-specific site-packages directory
""" """
return self.config_vars['platlib'].replace( prefix = self.config_vars['platbase'] + os.sep
self.config_vars['platbase'] + os.sep, '' path = self.config_vars['platlib']
if path.startswith(prefix):
return path.replace(prefix, '')
return os.path.join(
'lib64', 'python{}'.format(self.version.up_to(2)), 'site-packages'
) )
@property @property
@ -1122,8 +1147,12 @@ def purelib(self):
Returns: Returns:
str: platform-independent site-packages directory str: platform-independent site-packages directory
""" """
return self.config_vars['purelib'].replace( prefix = self.config_vars['base'] + os.sep
self.config_vars['base'] + os.sep, '' path = self.config_vars['purelib']
if path.startswith(prefix):
return path.replace(prefix, '')
return os.path.join(
'lib', 'python{}'.format(self.version.up_to(2)), 'site-packages'
) )
@property @property
@ -1142,9 +1171,11 @@ def include(self):
Returns: Returns:
str: platform-independent header file directory str: platform-independent header file directory
""" """
return self.config_vars['include'].replace( prefix = self.config_vars['installed_base'] + os.sep
self.config_vars['installed_base'] + os.sep, '' path = self.config_vars['include']
) if path.startswith(prefix):
return path.replace(prefix, '')
return os.path.join('include', 'python{}'.format(self.version.up_to(2)))
@property @property
def easy_install_file(self): def easy_install_file(self):