407 lines
13 KiB
Python
407 lines
13 KiB
Python
# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
|
|
# Spack Project Developers. See the top-level COPYRIGHT file for details.
|
|
#
|
|
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
|
import os
|
|
import os.path
|
|
import re
|
|
import shutil
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
import llnl.util.filesystem
|
|
|
|
import spack.concretize
|
|
import spack.paths
|
|
import spack.platforms
|
|
import spack.relocate
|
|
import spack.relocate_text as relocate_text
|
|
import spack.spec
|
|
import spack.store
|
|
import spack.tengine
|
|
import spack.util.executable
|
|
|
|
pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="Tests fail on Windows")
|
|
|
|
|
|
def skip_unless_linux(f):
|
|
return pytest.mark.skipif(
|
|
str(spack.platforms.real_host()) != "linux",
|
|
reason="implementation currently requires linux",
|
|
)(f)
|
|
|
|
|
|
def rpaths_for(new_binary):
|
|
"""Return the RPATHs or RUNPATHs of a binary."""
|
|
patchelf = spack.util.executable.which("patchelf")
|
|
output = patchelf("--print-rpath", str(new_binary), output=str)
|
|
return output.strip()
|
|
|
|
|
|
def text_in_bin(text, binary):
|
|
with open(str(binary), "rb") as f:
|
|
data = f.read()
|
|
f.seek(0)
|
|
pat = re.compile(text.encode("utf-8"))
|
|
if not pat.search(data):
|
|
return False
|
|
return True
|
|
|
|
|
|
@pytest.fixture(params=[True, False])
|
|
def is_relocatable(request):
|
|
return request.param
|
|
|
|
|
|
@pytest.fixture()
|
|
def source_file(tmpdir, is_relocatable):
|
|
"""Returns the path to a source file of a relocatable executable."""
|
|
if is_relocatable:
|
|
template_src = os.path.join(spack.paths.test_path, "data", "templates", "relocatable.c")
|
|
src = tmpdir.join("relocatable.c")
|
|
shutil.copy(template_src, str(src))
|
|
else:
|
|
template_dirs = (os.path.join(spack.paths.test_path, "data", "templates"),)
|
|
env = spack.tengine.make_environment(template_dirs)
|
|
template = env.get_template("non_relocatable.c")
|
|
text = template.render({"prefix": spack.store.layout.root})
|
|
|
|
src = tmpdir.join("non_relocatable.c")
|
|
src.write(text)
|
|
|
|
return src
|
|
|
|
|
|
@pytest.fixture()
|
|
def mock_patchelf(tmpdir, mock_executable):
|
|
def _factory(output):
|
|
return mock_executable("patchelf", output=output)
|
|
|
|
return _factory
|
|
|
|
|
|
@pytest.fixture()
|
|
def make_dylib(tmpdir_factory):
|
|
"""Create a shared library with unfriendly qualities.
|
|
|
|
- Writes the same rpath twice
|
|
- Writes its install path as an absolute path
|
|
"""
|
|
cc = spack.util.executable.which("cc")
|
|
|
|
def _factory(abs_install_name="abs", extra_rpaths=[]):
|
|
assert all(extra_rpaths)
|
|
|
|
tmpdir = tmpdir_factory.mktemp(abs_install_name + "-".join(extra_rpaths).replace("/", ""))
|
|
src = tmpdir.join("foo.c")
|
|
src.write("int foo() { return 1; }\n")
|
|
|
|
filename = "foo.dylib"
|
|
lib = tmpdir.join(filename)
|
|
|
|
args = ["-shared", str(src), "-o", str(lib)]
|
|
rpaths = list(extra_rpaths)
|
|
if abs_install_name.startswith("abs"):
|
|
args += ["-install_name", str(lib)]
|
|
else:
|
|
args += ["-install_name", "@rpath/" + filename]
|
|
|
|
if abs_install_name.endswith("rpath"):
|
|
rpaths.append(str(tmpdir))
|
|
|
|
args.extend("-Wl,-rpath," + s for s in rpaths)
|
|
|
|
cc(*args)
|
|
|
|
return (str(tmpdir), filename)
|
|
|
|
return _factory
|
|
|
|
|
|
@pytest.fixture()
|
|
def make_object_file(tmpdir):
|
|
cc = spack.util.executable.which("cc")
|
|
|
|
def _factory():
|
|
src = tmpdir.join("bar.c")
|
|
src.write("int bar() { return 2; }\n")
|
|
|
|
filename = "bar.o"
|
|
lib = tmpdir.join(filename)
|
|
|
|
args = ["-c", str(src), "-o", str(lib)]
|
|
|
|
cc(*args)
|
|
|
|
return (str(tmpdir), filename)
|
|
|
|
return _factory
|
|
|
|
|
|
@pytest.fixture()
|
|
def copy_binary(prefix_like):
|
|
"""Returns a function that copies a binary somewhere and
|
|
returns the new location.
|
|
"""
|
|
|
|
def _copy_somewhere(orig_binary):
|
|
new_root = orig_binary.mkdtemp().mkdir(prefix_like)
|
|
new_binary = new_root.join("main.x")
|
|
shutil.copy(str(orig_binary), str(new_binary))
|
|
return new_binary
|
|
|
|
return _copy_somewhere
|
|
|
|
|
|
@pytest.mark.requires_executables("/usr/bin/gcc", "patchelf", "strings", "file")
|
|
@skip_unless_linux
|
|
def test_ensure_binary_is_relocatable(source_file, is_relocatable):
|
|
compiler = spack.util.executable.Executable("/usr/bin/gcc")
|
|
executable = str(source_file).replace(".c", ".x")
|
|
compiler_env = {"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}
|
|
compiler(str(source_file), "-o", executable, env=compiler_env)
|
|
|
|
assert spack.relocate.is_binary(executable)
|
|
|
|
try:
|
|
spack.relocate.ensure_binary_is_relocatable(executable)
|
|
relocatable = True
|
|
except spack.relocate.InstallRootStringError:
|
|
relocatable = False
|
|
|
|
assert relocatable == is_relocatable
|
|
|
|
|
|
@skip_unless_linux
|
|
def test_ensure_binary_is_relocatable_errors(tmpdir):
|
|
# The file passed in as argument must exist...
|
|
with pytest.raises(ValueError) as exc_info:
|
|
spack.relocate.ensure_binary_is_relocatable("/usr/bin/does_not_exist")
|
|
assert "does not exist" in str(exc_info.value)
|
|
|
|
# ...and the argument must be an absolute path to it
|
|
file = tmpdir.join("delete.me")
|
|
file.write("foo")
|
|
|
|
with llnl.util.filesystem.working_dir(str(tmpdir)):
|
|
with pytest.raises(ValueError) as exc_info:
|
|
spack.relocate.ensure_binary_is_relocatable("delete.me")
|
|
assert "is not an absolute path" in str(exc_info.value)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"start_path,path_root,paths,expected",
|
|
[
|
|
(
|
|
"/usr/bin/test",
|
|
"/usr",
|
|
["/usr/lib", "/usr/lib64", "/opt/local/lib"],
|
|
[
|
|
os.path.join("$ORIGIN", "..", "lib"),
|
|
os.path.join("$ORIGIN", "..", "lib64"),
|
|
"/opt/local/lib",
|
|
],
|
|
)
|
|
],
|
|
)
|
|
def test_make_relative_paths(start_path, path_root, paths, expected):
|
|
relatives = spack.relocate._make_relative(start_path, path_root, paths)
|
|
assert relatives == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"start_path,relative_paths,expected",
|
|
[
|
|
# $ORIGIN will be replaced with os.path.dirname('usr/bin/test')
|
|
# and then normalized
|
|
(
|
|
"/usr/bin/test",
|
|
["$ORIGIN/../lib", "$ORIGIN/../lib64", "/opt/local/lib"],
|
|
[
|
|
os.sep + os.path.join("usr", "lib"),
|
|
os.sep + os.path.join("usr", "lib64"),
|
|
"/opt/local/lib",
|
|
],
|
|
),
|
|
# Relative path without $ORIGIN
|
|
("/usr/bin/test", ["../local/lib"], ["../local/lib"]),
|
|
],
|
|
)
|
|
def test_normalize_relative_paths(start_path, relative_paths, expected):
|
|
normalized = spack.relocate._normalize_relative_paths(start_path, relative_paths)
|
|
assert normalized == expected
|
|
|
|
|
|
@pytest.mark.requires_executables("patchelf", "strings", "file", "gcc")
|
|
@skip_unless_linux
|
|
def test_relocate_text_bin(binary_with_rpaths, prefix_like):
|
|
prefix = "/usr/" + prefix_like
|
|
prefix_bytes = prefix.encode("utf-8")
|
|
new_prefix = "/foo/" + prefix_like
|
|
new_prefix_bytes = new_prefix.encode("utf-8")
|
|
# Compile an "Hello world!" executable and set RPATHs
|
|
executable = binary_with_rpaths(rpaths=[prefix + "/lib", prefix + "/lib64"])
|
|
|
|
# Relocate the RPATHs
|
|
spack.relocate.relocate_text_bin([str(executable)], {prefix_bytes: new_prefix_bytes})
|
|
|
|
# Some compilers add rpaths so ensure changes included in final result
|
|
assert "%s/lib:%s/lib64" % (new_prefix, new_prefix) in rpaths_for(executable)
|
|
|
|
|
|
@pytest.mark.requires_executables("patchelf", "strings", "file", "gcc")
|
|
@skip_unless_linux
|
|
def test_relocate_elf_binaries_absolute_paths(binary_with_rpaths, copy_binary, prefix_tmpdir):
|
|
# Create an executable, set some RPATHs, copy it to another location
|
|
orig_binary = binary_with_rpaths(rpaths=[str(prefix_tmpdir.mkdir("lib")), "/usr/lib64"])
|
|
new_binary = copy_binary(orig_binary)
|
|
|
|
spack.relocate.relocate_elf_binaries(
|
|
binaries=[str(new_binary)],
|
|
orig_root=str(orig_binary.dirpath()),
|
|
new_root=None, # Not needed when relocating absolute paths
|
|
new_prefixes={str(orig_binary.dirpath()): "/foo"},
|
|
rel=False,
|
|
# Not needed when relocating absolute paths
|
|
orig_prefix=None,
|
|
new_prefix=None,
|
|
)
|
|
|
|
# Some compilers add rpaths so ensure changes included in final result
|
|
assert "/foo/lib:/usr/lib64" in rpaths_for(new_binary)
|
|
|
|
|
|
@pytest.mark.requires_executables("patchelf", "strings", "file", "gcc")
|
|
@skip_unless_linux
|
|
def test_relocate_elf_binaries_relative_paths(binary_with_rpaths, copy_binary):
|
|
# Create an executable, set some RPATHs, copy it to another location
|
|
orig_binary = binary_with_rpaths(rpaths=["lib", "lib64", "/opt/local/lib"])
|
|
new_binary = copy_binary(orig_binary)
|
|
|
|
spack.relocate.relocate_elf_binaries(
|
|
binaries=[str(new_binary)],
|
|
orig_root=str(orig_binary.dirpath()),
|
|
new_root=str(new_binary.dirpath()),
|
|
new_prefixes={str(orig_binary.dirpath()): "/foo"},
|
|
rel=True,
|
|
orig_prefix=str(orig_binary.dirpath()),
|
|
new_prefix=str(new_binary.dirpath()),
|
|
)
|
|
|
|
# Some compilers add rpaths so ensure changes included in final result
|
|
assert "/foo/lib:/foo/lib64:/opt/local/lib" in rpaths_for(new_binary)
|
|
|
|
|
|
@pytest.mark.requires_executables("patchelf", "strings", "file", "gcc")
|
|
@skip_unless_linux
|
|
def test_make_elf_binaries_relative(binary_with_rpaths, copy_binary, prefix_tmpdir):
|
|
orig_binary = binary_with_rpaths(
|
|
rpaths=[
|
|
str(prefix_tmpdir.mkdir("lib")),
|
|
str(prefix_tmpdir.mkdir("lib64")),
|
|
"/opt/local/lib",
|
|
]
|
|
)
|
|
new_binary = copy_binary(orig_binary)
|
|
|
|
spack.relocate.make_elf_binaries_relative(
|
|
[str(new_binary)], [str(orig_binary)], str(orig_binary.dirpath())
|
|
)
|
|
|
|
# Some compilers add rpaths so ensure changes included in final result
|
|
assert "$ORIGIN/lib:$ORIGIN/lib64:/opt/local/lib" in rpaths_for(new_binary)
|
|
|
|
|
|
@pytest.mark.requires_executables("patchelf", "strings", "file", "gcc")
|
|
@skip_unless_linux
|
|
def test_relocate_text_bin_with_message(binary_with_rpaths, copy_binary, prefix_tmpdir):
|
|
orig_binary = binary_with_rpaths(
|
|
rpaths=[
|
|
str(prefix_tmpdir.mkdir("lib")),
|
|
str(prefix_tmpdir.mkdir("lib64")),
|
|
"/opt/local/lib",
|
|
],
|
|
message=str(prefix_tmpdir),
|
|
)
|
|
new_binary = copy_binary(orig_binary)
|
|
|
|
# Check original directory is in the executable and the new one is not
|
|
assert text_in_bin(str(prefix_tmpdir), new_binary)
|
|
assert not text_in_bin(str(new_binary.dirpath()), new_binary)
|
|
|
|
# Check this call succeed
|
|
orig_path_bytes = str(orig_binary.dirpath()).encode("utf-8")
|
|
new_path_bytes = str(new_binary.dirpath()).encode("utf-8")
|
|
|
|
spack.relocate.relocate_text_bin([str(new_binary)], {orig_path_bytes: new_path_bytes})
|
|
|
|
# Check original directory is not there anymore and it was
|
|
# substituted with the new one
|
|
assert not text_in_bin(str(prefix_tmpdir), new_binary)
|
|
assert text_in_bin(str(new_binary.dirpath()), new_binary)
|
|
|
|
|
|
def test_relocate_text_bin_raise_if_new_prefix_is_longer(tmpdir):
|
|
short_prefix = b"/short"
|
|
long_prefix = b"/much/longer"
|
|
fpath = str(tmpdir.join("fakebin"))
|
|
with open(fpath, "w") as f:
|
|
f.write("/short")
|
|
with pytest.raises(relocate_text.BinaryTextReplaceError):
|
|
spack.relocate.relocate_text_bin([fpath], {short_prefix: long_prefix})
|
|
|
|
|
|
@pytest.mark.requires_executables("install_name_tool", "file", "cc")
|
|
def test_fixup_macos_rpaths(make_dylib, make_object_file):
|
|
# For each of these tests except for the "correct" case, the first fixup
|
|
# should make changes, and the second fixup should be a null-op.
|
|
fixup_rpath = spack.relocate.fixup_macos_rpath
|
|
|
|
no_rpath = []
|
|
duplicate_rpaths = ["/usr", "/usr"]
|
|
bad_rpath = ["/nonexistent/path"]
|
|
|
|
# Non-relocatable library id and duplicate rpaths
|
|
(root, filename) = make_dylib("abs", duplicate_rpaths)
|
|
assert fixup_rpath(root, filename)
|
|
assert not fixup_rpath(root, filename)
|
|
|
|
# Hardcoded but relocatable library id (but we do NOT relocate)
|
|
(root, filename) = make_dylib("abs_with_rpath", no_rpath)
|
|
assert not fixup_rpath(root, filename)
|
|
|
|
# Library id uses rpath but there are extra duplicate rpaths
|
|
(root, filename) = make_dylib("rpath", duplicate_rpaths)
|
|
assert fixup_rpath(root, filename)
|
|
assert not fixup_rpath(root, filename)
|
|
|
|
# Shared library was constructed with relocatable id from the get-go
|
|
(root, filename) = make_dylib("rpath", no_rpath)
|
|
assert not fixup_rpath(root, filename)
|
|
|
|
# Non-relocatable library id
|
|
(root, filename) = make_dylib("abs", no_rpath)
|
|
assert not fixup_rpath(root, filename)
|
|
|
|
# Relocatable with executable paths and loader paths
|
|
(root, filename) = make_dylib("rpath", ["@executable_path/../lib", "@loader_path"])
|
|
assert not fixup_rpath(root, filename)
|
|
|
|
# Non-relocatable library id but nonexistent rpath
|
|
(root, filename) = make_dylib("abs", bad_rpath)
|
|
assert fixup_rpath(root, filename)
|
|
assert not fixup_rpath(root, filename)
|
|
|
|
# Duplicate nonexistent rpath will need *two* passes
|
|
(root, filename) = make_dylib("rpath", bad_rpath * 2)
|
|
assert fixup_rpath(root, filename)
|
|
assert fixup_rpath(root, filename)
|
|
assert not fixup_rpath(root, filename)
|
|
|
|
# Test on an object file, which *also* has type 'application/x-mach-binary'
|
|
# but should be ignored (no ID headers, no RPATH)
|
|
# (this is a corner case for GCC installation)
|
|
(root, filename) = make_object_file()
|
|
assert not fixup_rpath(root, filename)
|