spack external find: fix multi-arch troubles (#33973)

This commit is contained in:
Harmen Stoppels 2023-11-02 09:45:31 +01:00 committed by GitHub
parent f56efaff3e
commit 80944d22f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 97 additions and 15 deletions

View file

@ -15,9 +15,12 @@
from typing import Dict, List, Optional, Set, Tuple from typing import Dict, List, Optional, Set, Tuple
import llnl.util.filesystem import llnl.util.filesystem
import llnl.util.lang
import llnl.util.tty import llnl.util.tty
import spack.util.elf as elf_utils
import spack.util.environment import spack.util.environment
import spack.util.environment as environment
import spack.util.ld_so_conf import spack.util.ld_so_conf
from .common import ( from .common import (
@ -57,6 +60,11 @@ def common_windows_package_paths(pkg_cls=None) -> List[str]:
return paths 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]: def executables_in_path(path_hints: List[str]) -> Dict[str, str]:
"""Get the paths of all executables available from the current PATH. """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) 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( def libraries_in_ld_and_system_library_path(
path_hints: Optional[List[str]] = None, path_hints: Optional[List[str]] = None,
) -> Dict[str, str]: ) -> Dict[str, str]:
"""Get the paths of all libraries available from LD_LIBRARY_PATH, """Get the paths of all libraries available from ``path_hints`` or the
LIBRARY_PATH, DYLD_LIBRARY_PATH, DYLD_FALLBACK_LIBRARY_PATH, and following defaults:
standard system library paths.
- 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 For convenience, this is constructed as a dictionary where the keys are
the library paths and the values are the names of the libraries 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, constructed based on the set of LD_LIBRARY_PATH, LIBRARY_PATH,
DYLD_LIBRARY_PATH, and DYLD_FALLBACK_LIBRARY_PATH environment DYLD_LIBRARY_PATH, and DYLD_FALLBACK_LIBRARY_PATH environment
variables as well as the standard system library paths. 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 = ( if path_hints:
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) search_paths = llnl.util.filesystem.search_paths_for_libraries(*path_hints)
return path_to_dict(search_paths) 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]: def libraries_in_windows_paths(path_hints: Optional[List[str]] = None) -> Dict[str, str]:

View file

@ -120,6 +120,21 @@ def test_parser_doesnt_deal_with_nonzero_offset():
elf.parse_elf(elf_at_offset_one) 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") @pytest.mark.requires_executables("gcc")
@skip_unless_linux @skip_unless_linux
def test_elf_get_and_replace_rpaths(binary_with_rpaths): def test_elf_get_and_replace_rpaths(binary_with_rpaths):

View file

@ -377,7 +377,7 @@ def parse_header(f, elf):
elf.elf_hdr = ElfHeader._make(unpack(elf_header_fmt, data)) 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 # 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. # jump to absolute offsets as they are specified in the ELF file.
if f.tell() != 0: if f.tell() != 0:
@ -386,6 +386,9 @@ def _do_parse_elf(f, interpreter=True, dynamic_section=True):
elf = ElfFile() elf = ElfFile()
parse_header(f, elf) parse_header(f, elf)
if only_header:
return elf
# We don't handle anything but executables and shared libraries now. # 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): 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") 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 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 """Given a file handle f for an ELF file opened in binary mode, return an ElfFile
object that is stores data about rpaths""" object that is stores data about rpaths"""
try: try:
return _do_parse_elf(f, interpreter, dynamic_section) return _do_parse_elf(f, interpreter, dynamic_section, only_header)
except (DeprecationWarning, struct.error): except (DeprecationWarning, struct.error):
# According to the docs old versions of Python can throw DeprecationWarning # According to the docs old versions of Python can throw DeprecationWarning
# instead of struct.error. # instead of struct.error.