spack.relocate: add unit test (#15610)
* relocate: removed import from statements * relocate: renamed *Exception to *Error This aims at consistency in naming with both the standard library (ValueError, AttributeError, etc.) and other errors in 'spack.error'. Improved existing docstrings * relocate: simplified search function by un-nesting conditionals The search function that searches for patchelf has been refactored to remove deeply nested conditionals. Extended docstring. * relocate: removed a condition specific to unit tests * relocate: added test for _patchelf
This commit is contained in:
parent
7e0ec0ec09
commit
1b4de7813a
2 changed files with 130 additions and 67 deletions
|
@ -2,55 +2,63 @@
|
||||||
# Spack Project Developers. See the top-level COPYRIGHT file for details.
|
# Spack Project Developers. See the top-level COPYRIGHT file for details.
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import platform
|
|
||||||
import spack.repo
|
|
||||||
import spack.cmd
|
|
||||||
import llnl.util.lang
|
import llnl.util.lang
|
||||||
from spack.util.executable import Executable, ProcessError
|
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
from macholib.MachO import MachO
|
import macholib.MachO
|
||||||
from spack.spec import Spec
|
|
||||||
import macholib.mach_o
|
import macholib.mach_o
|
||||||
|
import spack.cmd
|
||||||
|
import spack.repo
|
||||||
|
import spack.spec
|
||||||
|
import spack.util.executable as executable
|
||||||
|
|
||||||
|
|
||||||
class InstallRootStringException(spack.error.SpackError):
|
class InstallRootStringError(spack.error.SpackError):
|
||||||
"""
|
|
||||||
Raised when the relocated binary still has the install root string.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, file_path, root_path):
|
def __init__(self, file_path, root_path):
|
||||||
super(InstallRootStringException, self).__init__(
|
"""Signal that the relocated binary still has the original
|
||||||
|
Spack's store root string
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): path of the binary
|
||||||
|
root_path (str): original Spack's store root string
|
||||||
|
"""
|
||||||
|
super(InstallRootStringError, self).__init__(
|
||||||
"\n %s \ncontains string\n %s \n"
|
"\n %s \ncontains string\n %s \n"
|
||||||
"after replacing it in rpaths.\n"
|
"after replacing it in rpaths.\n"
|
||||||
"Package should not be relocated.\n Use -a to override." %
|
"Package should not be relocated.\n Use -a to override." %
|
||||||
(file_path, root_path))
|
(file_path, root_path))
|
||||||
|
|
||||||
|
|
||||||
class BinaryStringReplacementException(spack.error.SpackError):
|
class BinaryStringReplacementError(spack.error.SpackError):
|
||||||
"""
|
|
||||||
Raised when the size of the file changes after binary path substitution.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, file_path, old_len, new_len):
|
def __init__(self, file_path, old_len, new_len):
|
||||||
super(BinaryStringReplacementException, self).__init__(
|
"""The size of the file changed after binary path substitution
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path (str): file with changing size
|
||||||
|
old_len (str): original length of the file
|
||||||
|
new_len (str): length of the file after substitution
|
||||||
|
"""
|
||||||
|
super(BinaryStringReplacementError, self).__init__(
|
||||||
"Doing a binary string replacement in %s failed.\n"
|
"Doing a binary string replacement in %s failed.\n"
|
||||||
"The size of the file changed from %s to %s\n"
|
"The size of the file changed from %s to %s\n"
|
||||||
"when it should have remanined the same." %
|
"when it should have remanined the same." %
|
||||||
(file_path, old_len, new_len))
|
(file_path, old_len, new_len))
|
||||||
|
|
||||||
|
|
||||||
class BinaryTextReplaceException(spack.error.SpackError):
|
class BinaryTextReplaceError(spack.error.SpackError):
|
||||||
"""
|
|
||||||
Raised when the new install path is shorter than the old install path
|
|
||||||
so binary text replacement cannot occur.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, old_path, new_path):
|
def __init__(self, old_path, new_path):
|
||||||
|
"""Raised when the new install path is longer than the
|
||||||
|
old one, so binary text replacement cannot occur.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
old_path (str): original path to be substituted
|
||||||
|
new_path (str): candidate path for substitution
|
||||||
|
"""
|
||||||
|
|
||||||
msg = "New path longer than old path: binary text"
|
msg = "New path longer than old path: binary text"
|
||||||
msg += " replacement not possible."
|
msg += " replacement not possible."
|
||||||
err_msg = "The new path %s" % new_path
|
err_msg = "The new path %s" % new_path
|
||||||
|
@ -58,33 +66,35 @@ def __init__(self, old_path, new_path):
|
||||||
err_msg += "Text replacement in binaries will not work.\n"
|
err_msg += "Text replacement in binaries will not work.\n"
|
||||||
err_msg += "Create buildcache from an install path "
|
err_msg += "Create buildcache from an install path "
|
||||||
err_msg += "longer than new path."
|
err_msg += "longer than new path."
|
||||||
super(BinaryTextReplaceException, self).__init__(msg, err_msg)
|
super(BinaryTextReplaceError, self).__init__(msg, err_msg)
|
||||||
|
|
||||||
|
|
||||||
def get_patchelf():
|
def _patchelf():
|
||||||
|
"""Return the full path to the patchelf binary, if available, else None.
|
||||||
|
|
||||||
|
Search first the current PATH for patchelf. If not found, try to look
|
||||||
|
if the default patchelf spec is installed and if not install it.
|
||||||
|
|
||||||
|
Return None on Darwin or if patchelf cannot be found.
|
||||||
"""
|
"""
|
||||||
Returns the full patchelf binary path if available in $PATH.
|
# Check if patchelf is already in the PATH
|
||||||
Builds and installs spack patchelf package on linux platforms
|
|
||||||
using the first concretized spec if it is not installed and
|
|
||||||
returns the full patchelf binary path.
|
|
||||||
"""
|
|
||||||
# as we may need patchelf, find out where it is
|
|
||||||
patchelf = spack.util.executable.which('patchelf')
|
patchelf = spack.util.executable.which('patchelf')
|
||||||
if patchelf is not None:
|
if patchelf is not None:
|
||||||
return patchelf.path
|
return patchelf.path
|
||||||
patchelf_spec = Spec('patchelf').concretized()
|
|
||||||
patchelf = patchelf_spec.package
|
# Check if patchelf spec is installed
|
||||||
if patchelf.installed:
|
spec = spack.spec.Spec('patchelf').concretized()
|
||||||
patchelf_executable = os.path.join(patchelf.prefix.bin, "patchelf")
|
exe_path = os.path.join(spec.prefix.bin, "patchelf")
|
||||||
return patchelf_executable
|
if spec.package.installed and os.path.exists(exe_path):
|
||||||
else:
|
return exe_path
|
||||||
if (str(spack.architecture.platform()) == 'test' or
|
|
||||||
str(spack.architecture.platform()) == 'darwin'):
|
# Skip darwin
|
||||||
return None
|
if str(spack.architecture.platform()) == 'darwin':
|
||||||
else:
|
return None
|
||||||
patchelf.do_install()
|
|
||||||
patchelf_executable = os.path.join(patchelf.prefix.bin, "patchelf")
|
# Install the spec and return its path
|
||||||
return patchelf_executable
|
spec.package.do_install()
|
||||||
|
return exe_path if os.path.exists(exe_path) else None
|
||||||
|
|
||||||
|
|
||||||
def get_existing_elf_rpaths(path_name):
|
def get_existing_elf_rpaths(path_name):
|
||||||
|
@ -95,17 +105,17 @@ def get_existing_elf_rpaths(path_name):
|
||||||
|
|
||||||
# if we're relocating patchelf itself, use it
|
# if we're relocating patchelf itself, use it
|
||||||
|
|
||||||
if path_name[-13:] == "/bin/patchelf":
|
if path_name.endswith("/bin/patchelf"):
|
||||||
patchelf = Executable(path_name)
|
patchelf = executable.Executable(path_name)
|
||||||
else:
|
else:
|
||||||
patchelf = Executable(get_patchelf())
|
patchelf = executable.Executable(_patchelf())
|
||||||
|
|
||||||
rpaths = list()
|
rpaths = list()
|
||||||
try:
|
try:
|
||||||
output = patchelf('--print-rpath', '%s' %
|
output = patchelf('--print-rpath', '%s' %
|
||||||
path_name, output=str, error=str)
|
path_name, output=str, error=str)
|
||||||
rpaths = output.rstrip('\n').split(':')
|
rpaths = output.rstrip('\n').split(':')
|
||||||
except ProcessError as e:
|
except executable.ProcessError as e:
|
||||||
msg = 'patchelf --print-rpath %s produced an error %s' % (path_name, e)
|
msg = 'patchelf --print-rpath %s produced an error %s' % (path_name, e)
|
||||||
tty.warn(msg)
|
tty.warn(msg)
|
||||||
return rpaths
|
return rpaths
|
||||||
|
@ -266,7 +276,7 @@ def modify_macho_object(cur_path, rpaths, deps, idpath,
|
||||||
# avoid error message for libgcc_s
|
# avoid error message for libgcc_s
|
||||||
if 'libgcc_' in cur_path:
|
if 'libgcc_' in cur_path:
|
||||||
return
|
return
|
||||||
install_name_tool = Executable('install_name_tool')
|
install_name_tool = executable.Executable('install_name_tool')
|
||||||
|
|
||||||
if idpath:
|
if idpath:
|
||||||
new_idpath = paths_to_paths.get(idpath, None)
|
new_idpath = paths_to_paths.get(idpath, None)
|
||||||
|
@ -295,7 +305,7 @@ def modify_object_macholib(cur_path, paths_to_paths):
|
||||||
dictionary mapping paths in old install layout to new install layout
|
dictionary mapping paths in old install layout to new install layout
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dll = MachO(cur_path)
|
dll = macholib.MachO.MachO(cur_path)
|
||||||
|
|
||||||
changedict = paths_to_paths
|
changedict = paths_to_paths
|
||||||
|
|
||||||
|
@ -324,7 +334,7 @@ def macholib_get_paths(cur_path):
|
||||||
Get rpaths, dependencies and id of mach-o objects
|
Get rpaths, dependencies and id of mach-o objects
|
||||||
using python macholib package
|
using python macholib package
|
||||||
"""
|
"""
|
||||||
dll = MachO(cur_path)
|
dll = macholib.MachO.MachO(cur_path)
|
||||||
|
|
||||||
ident = None
|
ident = None
|
||||||
rpaths = list()
|
rpaths = list()
|
||||||
|
@ -359,14 +369,14 @@ def modify_elf_object(path_name, new_rpaths):
|
||||||
|
|
||||||
if path_name[-13:] == "/bin/patchelf":
|
if path_name[-13:] == "/bin/patchelf":
|
||||||
shutil.copy(path_name, bak_path)
|
shutil.copy(path_name, bak_path)
|
||||||
patchelf = Executable(bak_path)
|
patchelf = executable.Executable(bak_path)
|
||||||
else:
|
else:
|
||||||
patchelf = Executable(get_patchelf())
|
patchelf = executable.Executable(_patchelf())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
patchelf('--force-rpath', '--set-rpath', '%s' % new_joined,
|
patchelf('--force-rpath', '--set-rpath', '%s' % new_joined,
|
||||||
'%s' % path_name, output=str, error=str)
|
'%s' % path_name, output=str, error=str)
|
||||||
except ProcessError as e:
|
except executable.ProcessError as e:
|
||||||
msg = 'patchelf --force-rpath --set-rpath %s failed with error %s' % (
|
msg = 'patchelf --force-rpath --set-rpath %s failed with error %s' % (
|
||||||
path_name, e)
|
path_name, e)
|
||||||
tty.warn(msg)
|
tty.warn(msg)
|
||||||
|
@ -443,7 +453,7 @@ def replace(match):
|
||||||
return
|
return
|
||||||
ndata = pat.sub(replace, data)
|
ndata = pat.sub(replace, data)
|
||||||
if not len(ndata) == original_data_len:
|
if not len(ndata) == original_data_len:
|
||||||
raise BinaryStringReplacementException(
|
raise BinaryStringReplacementError(
|
||||||
path_name, original_data_len, len(ndata))
|
path_name, original_data_len, len(ndata))
|
||||||
f.write(ndata)
|
f.write(ndata)
|
||||||
f.truncate()
|
f.truncate()
|
||||||
|
@ -468,7 +478,7 @@ def replace(match):
|
||||||
new_dir.encode('utf-8')) + b'\0' * padding
|
new_dir.encode('utf-8')) + b'\0' * padding
|
||||||
|
|
||||||
if len(new_dir) > len(old_dir):
|
if len(new_dir) > len(old_dir):
|
||||||
raise BinaryTextReplaceException(old_dir, new_dir)
|
raise BinaryTextReplaceError(old_dir, new_dir)
|
||||||
|
|
||||||
with open(path_name, 'rb+') as f:
|
with open(path_name, 'rb+') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
|
@ -479,7 +489,7 @@ def replace(match):
|
||||||
return
|
return
|
||||||
ndata = pat.sub(replace, data)
|
ndata = pat.sub(replace, data)
|
||||||
if not len(ndata) == original_data_len:
|
if not len(ndata) == original_data_len:
|
||||||
raise BinaryStringReplacementException(
|
raise BinaryStringReplacementError(
|
||||||
path_name, original_data_len, len(ndata))
|
path_name, original_data_len, len(ndata))
|
||||||
f.write(ndata)
|
f.write(ndata)
|
||||||
f.truncate()
|
f.truncate()
|
||||||
|
@ -656,7 +666,7 @@ def check_files_relocatable(cur_path_names, allow_root):
|
||||||
for cur_path in cur_path_names:
|
for cur_path in cur_path_names:
|
||||||
if (not allow_root and
|
if (not allow_root and
|
||||||
not file_is_relocatable(cur_path)):
|
not file_is_relocatable(cur_path)):
|
||||||
raise InstallRootStringException(
|
raise InstallRootStringError(
|
||||||
cur_path, spack.store.layout.root)
|
cur_path, spack.store.layout.root)
|
||||||
|
|
||||||
|
|
||||||
|
@ -725,7 +735,7 @@ def relocate_text_bin(path_names, old_layout_root, new_layout_root,
|
||||||
replace_prefix_bin(path_name, old_spack_prefix, new_spack_prefix)
|
replace_prefix_bin(path_name, old_spack_prefix, new_spack_prefix)
|
||||||
else:
|
else:
|
||||||
if len(path_names) > 0:
|
if len(path_names) > 0:
|
||||||
raise BinaryTextReplaceException(
|
raise BinaryTextReplaceError(
|
||||||
old_install_prefix, new_install_prefix)
|
old_install_prefix, new_install_prefix)
|
||||||
|
|
||||||
|
|
||||||
|
@ -789,7 +799,7 @@ def file_is_relocatable(file, paths_to_relocate=None):
|
||||||
if not os.path.isabs(file):
|
if not os.path.isabs(file):
|
||||||
raise ValueError('{0} is not an absolute path'.format(file))
|
raise ValueError('{0} is not an absolute path'.format(file))
|
||||||
|
|
||||||
strings = Executable('strings')
|
strings = executable.Executable('strings')
|
||||||
|
|
||||||
# Remove the RPATHS from the strings in the executable
|
# Remove the RPATHS from the strings in the executable
|
||||||
set_of_strings = set(strings(file, output=str).split())
|
set_of_strings = set(strings(file, output=str).split())
|
||||||
|
@ -851,7 +861,7 @@ def mime_type(file):
|
||||||
Returns:
|
Returns:
|
||||||
Tuple containing the MIME type and subtype
|
Tuple containing the MIME type and subtype
|
||||||
"""
|
"""
|
||||||
file_cmd = Executable('file')
|
file_cmd = executable.Executable('file')
|
||||||
output = file_cmd('-b', '-h', '--mime-type', file, output=str, error=str)
|
output = file_cmd('-b', '-h', '--mime-type', file, output=str, error=str)
|
||||||
tty.debug('[MIME_TYPE] {0} -> {1}'.format(file, output.strip()))
|
tty.debug('[MIME_TYPE] {0} -> {1}'.format(file, output.strip()))
|
||||||
# In corner cases the output does not contain a subtype prefixed with a /
|
# In corner cases the output does not contain a subtype prefixed with a /
|
||||||
|
|
|
@ -3,15 +3,18 @@
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||||
|
|
||||||
|
import collections
|
||||||
import os.path
|
import os.path
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
import llnl.util.filesystem
|
import llnl.util.filesystem
|
||||||
|
import pytest
|
||||||
|
import spack.architecture
|
||||||
|
import spack.concretize
|
||||||
import spack.paths
|
import spack.paths
|
||||||
import spack.relocate
|
import spack.relocate
|
||||||
|
import spack.spec
|
||||||
import spack.store
|
import spack.store
|
||||||
import spack.tengine
|
import spack.tengine
|
||||||
import spack.util.executable
|
import spack.util.executable
|
||||||
|
@ -45,6 +48,47 @@ def source_file(tmpdir, is_relocatable):
|
||||||
return src
|
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.mark.requires_executables(
|
@pytest.mark.requires_executables(
|
||||||
'/usr/bin/gcc', 'patchelf', 'strings', 'file'
|
'/usr/bin/gcc', 'patchelf', 'strings', 'file'
|
||||||
)
|
)
|
||||||
|
@ -64,7 +108,7 @@ def test_file_is_relocatable(source_file, is_relocatable):
|
||||||
'patchelf', 'strings', 'file'
|
'patchelf', 'strings', 'file'
|
||||||
)
|
)
|
||||||
def test_patchelf_is_relocatable():
|
def test_patchelf_is_relocatable():
|
||||||
patchelf = spack.relocate.get_patchelf()
|
patchelf = spack.relocate._patchelf()
|
||||||
assert llnl.util.filesystem.is_exe(patchelf)
|
assert llnl.util.filesystem.is_exe(patchelf)
|
||||||
assert spack.relocate.file_is_relocatable(patchelf)
|
assert spack.relocate.file_is_relocatable(patchelf)
|
||||||
|
|
||||||
|
@ -87,3 +131,12 @@ def test_file_is_relocatable_errors(tmpdir):
|
||||||
with pytest.raises(ValueError) as exc_info:
|
with pytest.raises(ValueError) as exc_info:
|
||||||
spack.relocate.file_is_relocatable('delete.me')
|
spack.relocate.file_is_relocatable('delete.me')
|
||||||
assert 'is not an absolute path' in str(exc_info.value)
|
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
|
||||||
|
|
Loading…
Reference in a new issue