spack external find: fix multi-arch troubles (#33973)
This commit is contained in:
parent
f56efaff3e
commit
80944d22f7
3 changed files with 97 additions and 15 deletions
|
@ -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]:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue