summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorHarmen Stoppels <me@harmenstoppels.nl>2023-11-02 09:45:31 +0100
committerGitHub <noreply@github.com>2023-11-02 09:45:31 +0100
commit80944d22f775b014cfd201e895fc8777a2038786 (patch)
tree9accb6bd23edc1536c10bd5acedf113c29d4c6cb /lib
parentf56efaff3ee2c706e965659de812a8785803412d (diff)
downloadspack-80944d22f775b014cfd201e895fc8777a2038786.tar.gz
spack-80944d22f775b014cfd201e895fc8777a2038786.tar.bz2
spack-80944d22f775b014cfd201e895fc8777a2038786.tar.xz
spack-80944d22f775b014cfd201e895fc8777a2038786.zip
spack external find: fix multi-arch troubles (#33973)
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/detection/path.py90
-rw-r--r--lib/spack/spack/test/util/elf.py15
-rw-r--r--lib/spack/spack/util/elf.py9
3 files changed, 98 insertions, 16 deletions
diff --git a/lib/spack/spack/detection/path.py b/lib/spack/spack/detection/path.py
index 6531ed62da..f5da02bede 100644
--- a/lib/spack/spack/detection/path.py
+++ b/lib/spack/spack/detection/path.py
@@ -15,9 +15,12 @@ import warnings
from typing import Dict, List, Optional, Set, Tuple
import llnl.util.filesystem
+import llnl.util.lang
import llnl.util.tty
+import spack.util.elf as elf_utils
import spack.util.environment
+import spack.util.environment as environment
import spack.util.ld_so_conf
from .common import (
@@ -57,6 +60,11 @@ def common_windows_package_paths(pkg_cls=None) -> List[str]:
return paths
+def file_identifier(path):
+ s = os.stat(path)
+ return (s.st_dev, s.st_ino)
+
+
def executables_in_path(path_hints: List[str]) -> Dict[str, str]:
"""Get the paths of all executables available from the current PATH.
@@ -75,12 +83,40 @@ def executables_in_path(path_hints: List[str]) -> Dict[str, str]:
return path_to_dict(search_paths)
+def get_elf_compat(path):
+ """For ELF files, get a triplet (EI_CLASS, EI_DATA, e_machine) and see if
+ it is host-compatible."""
+ # On ELF platforms supporting, we try to be a bit smarter when it comes to shared
+ # libraries, by dropping those that are not host compatible.
+ with open(path, "rb") as f:
+ elf = elf_utils.parse_elf(f, only_header=True)
+ return (elf.is_64_bit, elf.is_little_endian, elf.elf_hdr.e_machine)
+
+
+def accept_elf(path, host_compat):
+ """Accept an ELF file if the header matches the given compat triplet,
+ obtained with :py:func:`get_elf_compat`. In case it's not an ELF (e.g.
+ static library, or some arbitrary file, fall back to is_readable_file)."""
+ # Fast path: assume libraries at least have .so in their basename.
+ # Note: don't replace with splitext, because of libsmth.so.1.2.3 file names.
+ if ".so" not in os.path.basename(path):
+ return llnl.util.filesystem.is_readable_file(path)
+ try:
+ return host_compat == get_elf_compat(path)
+ except (OSError, elf_utils.ElfParsingError):
+ return llnl.util.filesystem.is_readable_file(path)
+
+
def libraries_in_ld_and_system_library_path(
path_hints: Optional[List[str]] = None,
) -> Dict[str, str]:
- """Get the paths of all libraries available from LD_LIBRARY_PATH,
- LIBRARY_PATH, DYLD_LIBRARY_PATH, DYLD_FALLBACK_LIBRARY_PATH, and
- standard system library paths.
+ """Get the paths of all libraries available from ``path_hints`` or the
+ following defaults:
+
+ - Environment variables (Linux: ``LD_LIBRARY_PATH``, Darwin: ``DYLD_LIBRARY_PATH``,
+ and ``DYLD_FALLBACK_LIBRARY_PATH``)
+ - Dynamic linker default paths (glibc: ld.so.conf, musl: ld-musl-<arch>.path)
+ - Default system library paths.
For convenience, this is constructed as a dictionary where the keys are
the library paths and the values are the names of the libraries
@@ -94,17 +130,45 @@ def libraries_in_ld_and_system_library_path(
constructed based on the set of LD_LIBRARY_PATH, LIBRARY_PATH,
DYLD_LIBRARY_PATH, and DYLD_FALLBACK_LIBRARY_PATH environment
variables as well as the standard system library paths.
+ path_hints (list): list of paths to be searched. If ``None``, the default
+ system paths are used.
"""
- default_lib_search_paths = (
- spack.util.environment.get_path("LD_LIBRARY_PATH")
- + spack.util.environment.get_path("DYLD_LIBRARY_PATH")
- + spack.util.environment.get_path("DYLD_FALLBACK_LIBRARY_PATH")
- + spack.util.ld_so_conf.host_dynamic_linker_search_paths()
- )
- path_hints = path_hints if path_hints is not None else default_lib_search_paths
-
- search_paths = llnl.util.filesystem.search_paths_for_libraries(*path_hints)
- return path_to_dict(search_paths)
+ if path_hints:
+ search_paths = llnl.util.filesystem.search_paths_for_libraries(*path_hints)
+ else:
+ search_paths = []
+
+ # Environment variables
+ if sys.platform == "darwin":
+ search_paths.extend(environment.get_path("DYLD_LIBRARY_PATH"))
+ search_paths.extend(environment.get_path("DYLD_FALLBACK_LIBRARY_PATH"))
+ elif sys.platform.startswith("linux"):
+ search_paths.extend(environment.get_path("LD_LIBRARY_PATH"))
+
+ # Dynamic linker paths
+ search_paths.extend(spack.util.ld_so_conf.host_dynamic_linker_search_paths())
+
+ # Drop redundant paths
+ search_paths = list(filter(os.path.isdir, search_paths))
+
+ # Make use we don't doubly list /usr/lib and /lib etc
+ search_paths = list(llnl.util.lang.dedupe(search_paths, key=file_identifier))
+
+ try:
+ host_compat = get_elf_compat(sys.executable)
+ accept = lambda path: accept_elf(path, host_compat)
+ except (OSError, elf_utils.ElfParsingError):
+ accept = llnl.util.filesystem.is_readable_file
+
+ path_to_lib = {}
+ # Reverse order of search directories so that a lib in the first
+ # search path entry overrides later entries
+ for search_path in reversed(search_paths):
+ for lib in os.listdir(search_path):
+ lib_path = os.path.join(search_path, lib)
+ if accept(lib_path):
+ path_to_lib[lib_path] = lib
+ return path_to_lib
def libraries_in_windows_paths(path_hints: Optional[List[str]] = None) -> Dict[str, str]:
diff --git a/lib/spack/spack/test/util/elf.py b/lib/spack/spack/test/util/elf.py
index 6380bb7910..db826df173 100644
--- a/lib/spack/spack/test/util/elf.py
+++ b/lib/spack/spack/test/util/elf.py
@@ -120,6 +120,21 @@ def test_parser_doesnt_deal_with_nonzero_offset():
elf.parse_elf(elf_at_offset_one)
+def test_only_header():
+ # When passing only_header=True parsing a file that is literally just a header
+ # without any sections/segments should not error.
+
+ # 32 bit
+ elf_32 = elf.parse_elf(io.BytesIO(b"\x7fELF\x01\x01" + b"\x00" * 46), only_header=True)
+ assert not elf_32.is_64_bit
+ assert elf_32.is_little_endian
+
+ # 64 bit
+ elf_64 = elf.parse_elf(io.BytesIO(b"\x7fELF\x02\x01" + b"\x00" * 58), only_header=True)
+ assert elf_64.is_64_bit
+ assert elf_64.is_little_endian
+
+
@pytest.mark.requires_executables("gcc")
@skip_unless_linux
def test_elf_get_and_replace_rpaths(binary_with_rpaths):
diff --git a/lib/spack/spack/util/elf.py b/lib/spack/spack/util/elf.py
index cab1db0b03..6d0881f494 100644
--- a/lib/spack/spack/util/elf.py
+++ b/lib/spack/spack/util/elf.py
@@ -377,7 +377,7 @@ def parse_header(f, elf):
elf.elf_hdr = ElfHeader._make(unpack(elf_header_fmt, data))
-def _do_parse_elf(f, interpreter=True, dynamic_section=True):
+def _do_parse_elf(f, interpreter=True, dynamic_section=True, only_header=False):
# We don't (yet?) allow parsing ELF files at a nonzero offset, we just
# jump to absolute offsets as they are specified in the ELF file.
if f.tell() != 0:
@@ -386,6 +386,9 @@ def _do_parse_elf(f, interpreter=True, dynamic_section=True):
elf = ElfFile()
parse_header(f, elf)
+ if only_header:
+ return elf
+
# We don't handle anything but executables and shared libraries now.
if elf.elf_hdr.e_type not in (ELF_CONSTANTS.ET_EXEC, ELF_CONSTANTS.ET_DYN):
raise ElfParsingError("Not an ET_DYN or ET_EXEC type")
@@ -403,11 +406,11 @@ def _do_parse_elf(f, interpreter=True, dynamic_section=True):
return elf
-def parse_elf(f, interpreter=False, dynamic_section=False):
+def parse_elf(f, interpreter=False, dynamic_section=False, only_header=False):
"""Given a file handle f for an ELF file opened in binary mode, return an ElfFile
object that is stores data about rpaths"""
try:
- return _do_parse_elf(f, interpreter, dynamic_section)
+ return _do_parse_elf(f, interpreter, dynamic_section, only_header)
except (DeprecationWarning, struct.error):
# According to the docs old versions of Python can throw DeprecationWarning
# instead of struct.error.