Files
spack/lib/spack/spack/test/relocate.py
2020-06-23 17:17:35 -05:00

384 lines
13 KiB
Python

# Copyright 2013-2020 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 collections
import os.path
import platform
import re
import shutil
import llnl.util.filesystem
import pytest
import spack.architecture
import spack.concretize
import spack.paths
import spack.relocate
import spack.spec
import spack.store
import spack.tengine
import spack.util.executable
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(params=['which_found', 'installed', 'to_be_installed'])
def expected_patchelf_path(request, mutable_database, monkeypatch):
"""Prepare the stage to tests different cases that can occur
when searching for patchelf.
"""
case = request.param
# Mock the which function
which_fn = {
'which_found': lambda x: collections.namedtuple(
'_', ['path']
)('/usr/bin/patchelf')
}
monkeypatch.setattr(
spack.util.executable, 'which',
which_fn.setdefault(case, lambda x: None)
)
if case == 'which_found':
return '/usr/bin/patchelf'
# TODO: Mock a case for Darwin architecture
spec = spack.spec.Spec('patchelf')
spec.concretize()
patchelf_cls = type(spec.package)
do_install = patchelf_cls.do_install
expected_path = os.path.join(spec.prefix.bin, 'patchelf')
def do_install_mock(self, **kwargs):
do_install(self, fake=True)
with open(expected_path):
pass
monkeypatch.setattr(patchelf_cls, 'do_install', do_install_mock)
if case == 'installed':
spec.package.do_install()
return expected_path
@pytest.fixture()
def mock_patchelf(tmpdir, mock_executable):
def _factory(output):
return mock_executable('patchelf', output=output)
return _factory
@pytest.fixture()
def hello_world(tmpdir):
"""Factory fixture that compiles an ELF binary setting its RPATH. Relative
paths are encoded with `$ORIGIN` prepended.
"""
def _factory(rpaths, message="Hello world!"):
source = tmpdir.join('main.c')
source.write("""
#include <stdio.h>
int main(){{
printf("{0}");
}}
""".format(message))
gcc = spack.util.executable.which('gcc')
executable = source.dirpath('main.x')
# Encode relative RPATHs using `$ORIGIN` as the root prefix
rpaths = [x if os.path.isabs(x) else os.path.join('$ORIGIN', x)
for x in rpaths]
rpath_str = ':'.join(rpaths)
opts = [
'-Wl,--disable-new-dtags',
'-Wl,-rpath={0}'.format(rpath_str),
str(source), '-o', str(executable)
]
gcc(*opts)
return executable
return _factory
@pytest.fixture()
def copy_binary():
"""Returns a function that copies a binary somewhere and
returns the new location.
"""
def _copy_somewhere(orig_binary):
new_root = orig_binary.mkdtemp()
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'
)
def test_file_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)
assert spack.relocate.file_is_relocatable(executable) is is_relocatable
@pytest.mark.requires_executables('patchelf', 'strings', 'file')
def test_patchelf_is_relocatable():
patchelf = spack.relocate._patchelf()
assert llnl.util.filesystem.is_exe(patchelf)
assert spack.relocate.file_is_relocatable(patchelf)
@pytest.mark.skipif(
platform.system().lower() != 'linux',
reason='implementation for MacOS still missing'
)
def test_file_is_relocatable_errors(tmpdir):
# The file passed in as argument must exist...
with pytest.raises(ValueError) as exc_info:
spack.relocate.file_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.file_is_relocatable('delete.me')
assert 'is not an absolute path' in str(exc_info.value)
@pytest.mark.skipif(
platform.system().lower() != 'linux',
reason='implementation for MacOS still missing'
)
def test_search_patchelf(expected_patchelf_path):
current = spack.relocate._patchelf()
assert current == expected_patchelf_path
@pytest.mark.parametrize('patchelf_behavior,expected', [
('echo ', []),
('echo /opt/foo/lib:/opt/foo/lib64', ['/opt/foo/lib', '/opt/foo/lib64']),
('exit 1', [])
])
def test_existing_rpaths(patchelf_behavior, expected, mock_patchelf):
# Here we are mocking an executable that is always called "patchelf"
# because that will skip the part where we try to build patchelf
# by ourselves. The executable will output some rpaths like
# `patchelf --print-rpath` would.
path = mock_patchelf(patchelf_behavior)
rpaths = spack.relocate._elf_rpaths_for(path)
assert rpaths == expected
@pytest.mark.parametrize('start_path,path_root,paths,expected', [
('/usr/bin/test', '/usr', ['/usr/lib', '/usr/lib64', '/opt/local/lib'],
['$ORIGIN/../lib', '$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'],
['/usr/lib', '/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
def test_set_elf_rpaths(mock_patchelf):
# Try to relocate a mock version of patchelf and check
# the call made to patchelf itself
patchelf = mock_patchelf('echo $@')
rpaths = ['/usr/lib', '/usr/lib64', '/opt/local/lib']
output = spack.relocate._set_elf_rpaths(patchelf, rpaths)
# Assert that the arguments of the call to patchelf are as expected
assert '--force-rpath' in output
assert '--set-rpath ' + ':'.join(rpaths) in output
assert patchelf in output
def test_set_elf_rpaths_warning(mock_patchelf):
# Mock a failing patchelf command and ensure it warns users
patchelf = mock_patchelf('exit 1')
rpaths = ['/usr/lib', '/usr/lib64', '/opt/local/lib']
# To avoid using capfd in order to check if the warning was triggered
# here we just check that output is not set
output = spack.relocate._set_elf_rpaths(patchelf, rpaths)
assert output is None
@pytest.mark.requires_executables('patchelf', 'strings', 'file', 'gcc')
def test_replace_prefix_bin(hello_world):
# Compile an "Hello world!" executable and set RPATHs
executable = hello_world(rpaths=['/usr/lib', '/usr/lib64'])
# Relocate the RPATHs
spack.relocate._replace_prefix_bin(str(executable), '/usr', '/foo')
# Some compilers add rpaths so ensure changes included in final result
assert '/foo/lib:/foo/lib64' in rpaths_for(executable)
@pytest.mark.requires_executables('patchelf', 'strings', 'file', 'gcc')
def test_relocate_elf_binaries_absolute_paths(
hello_world, copy_binary, tmpdir
):
# Create an executable, set some RPATHs, copy it to another location
orig_binary = hello_world(rpaths=[str(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(tmpdir): '/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')
def test_relocate_elf_binaries_relative_paths(hello_world, copy_binary):
# Create an executable, set some RPATHs, copy it to another location
orig_binary = hello_world(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')
def test_make_elf_binaries_relative(hello_world, copy_binary, tmpdir):
orig_binary = hello_world(rpaths=[
str(tmpdir.mkdir('lib')), str(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)
def test_raise_if_not_relocatable(monkeypatch):
monkeypatch.setattr(spack.relocate, 'file_is_relocatable', lambda x: False)
with pytest.raises(spack.relocate.InstallRootStringError):
spack.relocate.raise_if_not_relocatable(
['an_executable'], allow_root=False
)
@pytest.mark.requires_executables('patchelf', 'strings', 'file', 'gcc')
def test_relocate_text_bin(hello_world, copy_binary, tmpdir):
orig_binary = hello_world(rpaths=[
str(tmpdir.mkdir('lib')), str(tmpdir.mkdir('lib64')), '/opt/local/lib'
], message=str(tmpdir))
new_binary = copy_binary(orig_binary)
# Check original directory is in the executabel and the new one is not
assert text_in_bin(str(tmpdir), new_binary)
assert not text_in_bin(str(new_binary.dirpath()), new_binary)
# Check this call succeed
spack.relocate.relocate_text_bin(
[str(new_binary)],
str(orig_binary.dirpath()), str(new_binary.dirpath()),
spack.paths.spack_root, spack.paths.spack_root,
{str(orig_binary.dirpath()): str(new_binary.dirpath())}
)
# Check original directory is not there anymore and it was
# substituted with the new one
assert not text_in_bin(str(tmpdir), new_binary)
assert text_in_bin(str(new_binary.dirpath()), new_binary)
def test_relocate_text_bin_raise_if_new_prefix_is_longer():
short_prefix = '/short'
long_prefix = '/much/longer'
with pytest.raises(spack.relocate.BinaryTextReplaceError):
spack.relocate.relocate_text_bin(
['item'], short_prefix, long_prefix, None, None, None
)