Skip to content

Commit 80944d2

Browse files
authored
spack external find: fix multi-arch troubles (#33973)
1 parent f56efaf commit 80944d2

File tree

3 files changed

+98
-16
lines changed

3 files changed

+98
-16
lines changed

lib/spack/spack/detection/path.py

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
from typing import Dict, List, Optional, Set, Tuple
1616

1717
import llnl.util.filesystem
18+
import llnl.util.lang
1819
import llnl.util.tty
1920

21+
import spack.util.elf as elf_utils
2022
import spack.util.environment
23+
import spack.util.environment as environment
2124
import spack.util.ld_so_conf
2225

2326
from .common import (
@@ -57,6 +60,11 @@ def common_windows_package_paths(pkg_cls=None) -> List[str]:
5760
return paths
5861

5962

63+
def file_identifier(path):
64+
s = os.stat(path)
65+
return (s.st_dev, s.st_ino)
66+
67+
6068
def executables_in_path(path_hints: List[str]) -> Dict[str, str]:
6169
"""Get the paths of all executables available from the current PATH.
6270
@@ -75,12 +83,40 @@ def executables_in_path(path_hints: List[str]) -> Dict[str, str]:
7583
return path_to_dict(search_paths)
7684

7785

86+
def get_elf_compat(path):
87+
"""For ELF files, get a triplet (EI_CLASS, EI_DATA, e_machine) and see if
88+
it is host-compatible."""
89+
# On ELF platforms supporting, we try to be a bit smarter when it comes to shared
90+
# libraries, by dropping those that are not host compatible.
91+
with open(path, "rb") as f:
92+
elf = elf_utils.parse_elf(f, only_header=True)
93+
return (elf.is_64_bit, elf.is_little_endian, elf.elf_hdr.e_machine)
94+
95+
96+
def accept_elf(path, host_compat):
97+
"""Accept an ELF file if the header matches the given compat triplet,
98+
obtained with :py:func:`get_elf_compat`. In case it's not an ELF (e.g.
99+
static library, or some arbitrary file, fall back to is_readable_file)."""
100+
# Fast path: assume libraries at least have .so in their basename.
101+
# Note: don't replace with splitext, because of libsmth.so.1.2.3 file names.
102+
if ".so" not in os.path.basename(path):
103+
return llnl.util.filesystem.is_readable_file(path)
104+
try:
105+
return host_compat == get_elf_compat(path)
106+
except (OSError, elf_utils.ElfParsingError):
107+
return llnl.util.filesystem.is_readable_file(path)
108+
109+
78110
def libraries_in_ld_and_system_library_path(
79111
path_hints: Optional[List[str]] = None,
80112
) -> Dict[str, str]:
81-
"""Get the paths of all libraries available from LD_LIBRARY_PATH,
82-
LIBRARY_PATH, DYLD_LIBRARY_PATH, DYLD_FALLBACK_LIBRARY_PATH, and
83-
standard system library paths.
113+
"""Get the paths of all libraries available from ``path_hints`` or the
114+
following defaults:
115+
116+
- Environment variables (Linux: ``LD_LIBRARY_PATH``, Darwin: ``DYLD_LIBRARY_PATH``,
117+
and ``DYLD_FALLBACK_LIBRARY_PATH``)
118+
- Dynamic linker default paths (glibc: ld.so.conf, musl: ld-musl-<arch>.path)
119+
- Default system library paths.
84120
85121
For convenience, this is constructed as a dictionary where the keys are
86122
the library paths and the values are the names of the libraries
@@ -94,17 +130,45 @@ def libraries_in_ld_and_system_library_path(
94130
constructed based on the set of LD_LIBRARY_PATH, LIBRARY_PATH,
95131
DYLD_LIBRARY_PATH, and DYLD_FALLBACK_LIBRARY_PATH environment
96132
variables as well as the standard system library paths.
133+
path_hints (list): list of paths to be searched. If ``None``, the default
134+
system paths are used.
97135
"""
98-
default_lib_search_paths = (
99-
spack.util.environment.get_path("LD_LIBRARY_PATH")
100-
+ spack.util.environment.get_path("DYLD_LIBRARY_PATH")
101-
+ spack.util.environment.get_path("DYLD_FALLBACK_LIBRARY_PATH")
102-
+ spack.util.ld_so_conf.host_dynamic_linker_search_paths()
103-
)
104-
path_hints = path_hints if path_hints is not None else default_lib_search_paths
105-
106-
search_paths = llnl.util.filesystem.search_paths_for_libraries(*path_hints)
107-
return path_to_dict(search_paths)
136+
if path_hints:
137+
search_paths = llnl.util.filesystem.search_paths_for_libraries(*path_hints)
138+
else:
139+
search_paths = []
140+
141+
# Environment variables
142+
if sys.platform == "darwin":
143+
search_paths.extend(environment.get_path("DYLD_LIBRARY_PATH"))
144+
search_paths.extend(environment.get_path("DYLD_FALLBACK_LIBRARY_PATH"))
145+
elif sys.platform.startswith("linux"):
146+
search_paths.extend(environment.get_path("LD_LIBRARY_PATH"))
147+
148+
# Dynamic linker paths
149+
search_paths.extend(spack.util.ld_so_conf.host_dynamic_linker_search_paths())
150+
151+
# Drop redundant paths
152+
search_paths = list(filter(os.path.isdir, search_paths))
153+
154+
# Make use we don't doubly list /usr/lib and /lib etc
155+
search_paths = list(llnl.util.lang.dedupe(search_paths, key=file_identifier))
156+
157+
try:
158+
host_compat = get_elf_compat(sys.executable)
159+
accept = lambda path: accept_elf(path, host_compat)
160+
except (OSError, elf_utils.ElfParsingError):
161+
accept = llnl.util.filesystem.is_readable_file
162+
163+
path_to_lib = {}
164+
# Reverse order of search directories so that a lib in the first
165+
# search path entry overrides later entries
166+
for search_path in reversed(search_paths):
167+
for lib in os.listdir(search_path):
168+
lib_path = os.path.join(search_path, lib)
169+
if accept(lib_path):
170+
path_to_lib[lib_path] = lib
171+
return path_to_lib
108172

109173

110174
def libraries_in_windows_paths(path_hints: Optional[List[str]] = None) -> Dict[str, str]:

lib/spack/spack/test/util/elf.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,21 @@ def test_parser_doesnt_deal_with_nonzero_offset():
120120
elf.parse_elf(elf_at_offset_one)
121121

122122

123+
def test_only_header():
124+
# When passing only_header=True parsing a file that is literally just a header
125+
# without any sections/segments should not error.
126+
127+
# 32 bit
128+
elf_32 = elf.parse_elf(io.BytesIO(b"\x7fELF\x01\x01" + b"\x00" * 46), only_header=True)
129+
assert not elf_32.is_64_bit
130+
assert elf_32.is_little_endian
131+
132+
# 64 bit
133+
elf_64 = elf.parse_elf(io.BytesIO(b"\x7fELF\x02\x01" + b"\x00" * 58), only_header=True)
134+
assert elf_64.is_64_bit
135+
assert elf_64.is_little_endian
136+
137+
123138
@pytest.mark.requires_executables("gcc")
124139
@skip_unless_linux
125140
def test_elf_get_and_replace_rpaths(binary_with_rpaths):

lib/spack/spack/util/elf.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ def parse_header(f, elf):
377377
elf.elf_hdr = ElfHeader._make(unpack(elf_header_fmt, data))
378378

379379

380-
def _do_parse_elf(f, interpreter=True, dynamic_section=True):
380+
def _do_parse_elf(f, interpreter=True, dynamic_section=True, only_header=False):
381381
# We don't (yet?) allow parsing ELF files at a nonzero offset, we just
382382
# jump to absolute offsets as they are specified in the ELF file.
383383
if f.tell() != 0:
@@ -386,6 +386,9 @@ def _do_parse_elf(f, interpreter=True, dynamic_section=True):
386386
elf = ElfFile()
387387
parse_header(f, elf)
388388

389+
if only_header:
390+
return elf
391+
389392
# We don't handle anything but executables and shared libraries now.
390393
if elf.elf_hdr.e_type not in (ELF_CONSTANTS.ET_EXEC, ELF_CONSTANTS.ET_DYN):
391394
raise ElfParsingError("Not an ET_DYN or ET_EXEC type")
@@ -403,11 +406,11 @@ def _do_parse_elf(f, interpreter=True, dynamic_section=True):
403406
return elf
404407

405408

406-
def parse_elf(f, interpreter=False, dynamic_section=False):
409+
def parse_elf(f, interpreter=False, dynamic_section=False, only_header=False):
407410
"""Given a file handle f for an ELF file opened in binary mode, return an ElfFile
408411
object that is stores data about rpaths"""
409412
try:
410-
return _do_parse_elf(f, interpreter, dynamic_section)
413+
return _do_parse_elf(f, interpreter, dynamic_section, only_header)
411414
except (DeprecationWarning, struct.error):
412415
# According to the docs old versions of Python can throw DeprecationWarning
413416
# instead of struct.error.

0 commit comments

Comments
 (0)