Buildcache: Install into non-default directory layouts (#13797)

* Buildcache: Install into non-default directory layouts

Store a dictionary mapping of original dependency prefixes to dependency hashes

Use the loaded spec to grab the new dependency prefixes in the new directory layout.

Map the original dependency prefixes to the new dependency prefixes using the dependency hashes.

Use the dependency prefixes map to replace original rpaths with new rpaths preserving the order.
For mach-o binaries, use the dependency prefixes map to replace the dependency library entires for libraries and executables and the replace the library id for libraries.

On Linux, patchelf is used to replace the rpaths of elf binaries.
On macOS, install_name_tool is used to replace the rpaths and  dependency libraries  of mach-o binaries and the id of mach-o libraries.
On Linux, macholib is used to replace the dependency libraries of mach-o binaries and the id of mach-o libraries.

Binary text with padding replacement is attempted for all binaries for the following paths:
spack layout root
spack prefix
sbang script location
dependency prefixes
package prefix
 Text replacement is attempted for all text files using the paths above.

Symbolic links to the absolute path of the package install prefix are replaced, all others produce warnings.
This commit is contained in:
Patrick Gartung 2020-03-16 08:42:23 -05:00 committed by GitHub
parent 0301ec32b4
commit 17e4df1e41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 887 additions and 576 deletions

View file

@ -139,9 +139,12 @@
@contributor: U{Reka Albert <http://www.phys.psu.edu/~ralbert/>}
'''
import pkg_resources
__version__ = pkg_resources.require('altgraph')[0].version
# import pkg_resources
# __version__ = pkg_resources.require('altgraph')[0].version
# pkg_resources is not finding the altgraph import despite the fact that it is in sys.path
# there is no .dist-info or .egg-info for pkg_resources to query the version from
# so it must be set manually
__version__ = '0.16.1'
class GraphError(ValueError):
pass

View file

@ -10,6 +10,9 @@
import shutil
import tempfile
import hashlib
import glob
import platform
from contextlib import closing
import ruamel.yaml as yaml
@ -53,7 +56,7 @@
BUILD_CACHE_INDEX_ENTRY_TEMPLATE = ' <li><a href="{path}">{path}</a></li>'
class NoOverwriteException(Exception):
class NoOverwriteException(spack.error.SpackError):
"""
Raised when a file exists and must be overwritten.
"""
@ -68,14 +71,18 @@ class NoGpgException(spack.error.SpackError):
"""
Raised when gpg2 is not in PATH
"""
pass
def __init__(self, msg):
super(NoGpgException, self).__init__(msg)
class NoKeyException(spack.error.SpackError):
"""
Raised when gpg has no default key added.
"""
pass
def __init__(self, msg):
super(NoKeyException, self).__init__(msg)
class PickKeyException(spack.error.SpackError):
@ -84,7 +91,7 @@ class PickKeyException(spack.error.SpackError):
"""
def __init__(self, keys):
err_msg = "Multi keys available for signing\n%s\n" % keys
err_msg = "Multiple keys available for signing\n%s\n" % keys
err_msg += "Use spack buildcache create -k <key hash> to pick a key."
super(PickKeyException, self).__init__(err_msg)
@ -107,7 +114,9 @@ class NewLayoutException(spack.error.SpackError):
"""
Raised if directory layout is different from buildcache.
"""
pass
def __init__(self, msg):
super(NewLayoutException, self).__init__(msg)
def build_cache_relative_path():
@ -137,15 +146,21 @@ def read_buildinfo_file(prefix):
return buildinfo
def write_buildinfo_file(prefix, workdir, rel=False):
def write_buildinfo_file(spec, workdir, rel=False):
"""
Create a cache file containing information
required for the relocation
"""
prefix = spec.prefix
text_to_relocate = []
binary_to_relocate = []
link_to_relocate = []
blacklist = (".spack", "man")
prefix_to_hash = dict()
prefix_to_hash[str(spec.package.prefix)] = spec.dag_hash()
deps = spack.build_environment.get_rpath_deps(spec.package)
for d in deps:
prefix_to_hash[str(d.prefix)] = d.dag_hash()
# Do this at during tarball creation to save time when tarball unpacked.
# Used by make_package_relative to determine binaries to change.
for root, dirs, files in os.walk(prefix, topdown=True):
@ -162,8 +177,8 @@ def write_buildinfo_file(prefix, workdir, rel=False):
link_to_relocate.append(rel_path_name)
else:
msg = 'Absolute link %s to %s ' % (path_name, link)
msg += 'outside of stage %s ' % prefix
msg += 'cannot be relocated.'
msg += 'outside of prefix %s ' % prefix
msg += 'should not be relocated.'
tty.warn(msg)
if relocate.needs_binary_relocation(m_type, m_subtype):
@ -184,6 +199,7 @@ def write_buildinfo_file(prefix, workdir, rel=False):
buildinfo['relocate_textfiles'] = text_to_relocate
buildinfo['relocate_binaries'] = binary_to_relocate
buildinfo['relocate_links'] = link_to_relocate
buildinfo['prefix_to_hash'] = prefix_to_hash
filename = buildinfo_file_name(workdir)
with open(filename, 'w') as outfile:
outfile.write(syaml.dump(buildinfo, default_flow_style=True))
@ -356,7 +372,7 @@ def build_tarball(spec, outdir, force=False, rel=False, unsigned=False,
os.remove(temp_tarfile_path)
# create info for later relocation and create tar
write_buildinfo_file(spec.prefix, workdir, rel=rel)
write_buildinfo_file(spec, workdir, rel)
# optionally make the paths in the binaries relative to each other
# in the spack install tree before creating tarball
@ -370,7 +386,7 @@ def build_tarball(spec, outdir, force=False, rel=False, unsigned=False,
tty.die(e)
else:
try:
make_package_placeholder(workdir, spec, allow_root)
check_package_relocatable(workdir, spec, allow_root)
except Exception as e:
shutil.rmtree(workdir)
shutil.rmtree(tarfile_dir)
@ -400,6 +416,7 @@ def build_tarball(spec, outdir, force=False, rel=False, unsigned=False,
buildinfo = {}
buildinfo['relative_prefix'] = os.path.relpath(
spec.prefix, spack.store.layout.root)
buildinfo['relative_rpaths'] = rel
spec_dict['buildinfo'] = buildinfo
spec_dict['full_hash'] = spec.full_hash()
@ -481,100 +498,149 @@ def make_package_relative(workdir, spec, allow_root):
"""
prefix = spec.prefix
buildinfo = read_buildinfo_file(workdir)
old_path = buildinfo['buildpath']
old_layout_root = buildinfo['buildpath']
orig_path_names = list()
cur_path_names = list()
for filename in buildinfo['relocate_binaries']:
orig_path_names.append(os.path.join(prefix, filename))
cur_path_names.append(os.path.join(workdir, filename))
if spec.architecture.platform == 'darwin':
if (spec.architecture.platform == 'darwin' or
spec.architecture.platform == 'test' and
platform.system().lower() == 'darwin'):
relocate.make_macho_binaries_relative(cur_path_names, orig_path_names,
old_path, allow_root)
else:
old_layout_root)
if (spec.architecture.platform == 'linux' or
spec.architecture.platform == 'test' and
platform.system().lower() == 'linux'):
relocate.make_elf_binaries_relative(cur_path_names, orig_path_names,
old_path, allow_root)
old_layout_root)
relocate.check_files_relocatable(cur_path_names, allow_root)
orig_path_names = list()
cur_path_names = list()
for filename in buildinfo.get('relocate_links', []):
orig_path_names.append(os.path.join(prefix, filename))
cur_path_names.append(os.path.join(workdir, filename))
for linkname in buildinfo.get('relocate_links', []):
orig_path_names.append(os.path.join(prefix, linkname))
cur_path_names.append(os.path.join(workdir, linkname))
relocate.make_link_relative(cur_path_names, orig_path_names)
def make_package_placeholder(workdir, spec, allow_root):
def check_package_relocatable(workdir, spec, allow_root):
"""
Check if package binaries are relocatable.
Change links to placeholder links.
"""
prefix = spec.prefix
buildinfo = read_buildinfo_file(workdir)
cur_path_names = list()
for filename in buildinfo['relocate_binaries']:
cur_path_names.append(os.path.join(workdir, filename))
relocate.check_files_relocatable(cur_path_names, allow_root)
cur_path_names = list()
for filename in buildinfo.get('relocate_links', []):
cur_path_names.append(os.path.join(workdir, filename))
relocate.make_link_placeholder(cur_path_names, workdir, prefix)
def relocate_package(workdir, spec, allow_root):
def relocate_package(spec, allow_root):
"""
Relocate the given package
"""
workdir = str(spec.prefix)
buildinfo = read_buildinfo_file(workdir)
new_path = str(spack.store.layout.root)
new_prefix = str(spack.paths.prefix)
old_path = str(buildinfo['buildpath'])
old_prefix = str(buildinfo.get('spackprefix',
'/not/in/buildinfo/dictionary'))
rel = buildinfo.get('relative_rpaths', False)
new_layout_root = str(spack.store.layout.root)
new_prefix = str(spec.prefix)
new_rel_prefix = str(os.path.relpath(new_prefix, new_layout_root))
new_spack_prefix = str(spack.paths.prefix)
old_layout_root = str(buildinfo['buildpath'])
old_spack_prefix = str(buildinfo.get('spackprefix'))
old_rel_prefix = buildinfo.get('relative_prefix')
old_prefix = os.path.join(old_layout_root, old_rel_prefix)
rel = buildinfo.get('relative_rpaths')
prefix_to_hash = buildinfo.get('prefix_to_hash', None)
if (old_rel_prefix != new_rel_prefix and not prefix_to_hash):
msg = "Package tarball was created from an install "
msg += "prefix with a different directory layout and an older "
msg += "buildcache create implementation. It cannot be relocated."
raise NewLayoutException(msg)
# older buildcaches do not have the prefix_to_hash dictionary
# need to set an empty dictionary and add one entry to
# prefix_to_prefix to reproduce the old behavior
if not prefix_to_hash:
prefix_to_hash = dict()
hash_to_prefix = dict()
hash_to_prefix[spec.format('{hash}')] = str(spec.package.prefix)
new_deps = spack.build_environment.get_rpath_deps(spec.package)
for d in new_deps:
hash_to_prefix[d.format('{hash}')] = str(d.prefix)
prefix_to_prefix = dict()
for orig_prefix, hash in prefix_to_hash.items():
prefix_to_prefix[orig_prefix] = hash_to_prefix.get(hash, None)
prefix_to_prefix[old_prefix] = new_prefix
prefix_to_prefix[old_layout_root] = new_layout_root
tty.msg("Relocating package from",
"%s to %s." % (old_path, new_path))
path_names = set()
tty.debug("Relocating package from",
"%s to %s." % (old_layout_root, new_layout_root))
def is_backup_file(file):
return file.endswith('~')
# Text files containing the prefix text
text_names = list()
for filename in buildinfo['relocate_textfiles']:
path_name = os.path.join(workdir, filename)
text_name = os.path.join(workdir, filename)
# Don't add backup files generated by filter_file during install step.
if not path_name.endswith('~'):
path_names.add(path_name)
relocate.relocate_text(path_names, oldpath=old_path,
newpath=new_path, oldprefix=old_prefix,
newprefix=new_prefix)
# If the binary files in the package were not edited to use
# relative RPATHs, then the RPATHs need to be relocated
if rel:
if old_path != new_path:
if not is_backup_file(text_name):
text_names.append(text_name)
# If we are installing back to the same location don't replace anything
if old_layout_root != new_layout_root:
paths_to_relocate = [old_spack_prefix, old_layout_root]
paths_to_relocate.extend(prefix_to_hash.keys())
files_to_relocate = list(filter(
lambda pathname: not relocate.file_is_relocatable(
pathname, paths_to_relocate=[old_path, old_prefix]),
pathname, paths_to_relocate=paths_to_relocate),
map(lambda filename: os.path.join(workdir, filename),
buildinfo['relocate_binaries'])))
# If the buildcache was not created with relativized rpaths
# do the relocation of path in binaries
if (spec.architecture.platform == 'darwin' or
spec.architecture.platform == 'test' and
platform.system().lower() == 'darwin'):
relocate.relocate_macho_binaries(files_to_relocate,
old_layout_root,
new_layout_root,
prefix_to_prefix, rel,
old_prefix,
new_prefix)
if (spec.architecture.platform == 'linux' or
spec.architecture.platform == 'test' and
platform.system().lower() == 'linux'):
relocate.relocate_elf_binaries(files_to_relocate,
old_layout_root,
new_layout_root,
prefix_to_prefix, rel,
old_prefix,
new_prefix)
# Relocate links to the new install prefix
link_names = [linkname
for linkname in buildinfo.get('relocate_links', [])]
relocate.relocate_links(link_names,
old_layout_root,
new_layout_root,
old_prefix,
new_prefix,
prefix_to_prefix)
if len(old_path) < len(new_path) and files_to_relocate:
tty.debug('Cannot do a binary string replacement with padding '
'for package because %s is longer than %s.' %
(new_path, old_path))
else:
for path_name in files_to_relocate:
relocate.replace_prefix_bin(path_name, old_path, new_path)
else:
path_names = set()
for filename in buildinfo['relocate_binaries']:
path_name = os.path.join(workdir, filename)
path_names.add(path_name)
if spec.architecture.platform == 'darwin':
relocate.relocate_macho_binaries(path_names, old_path,
new_path, allow_root)
else:
relocate.relocate_elf_binaries(path_names, old_path,
new_path, allow_root)
path_names = set()
for filename in buildinfo.get('relocate_links', []):
path_name = os.path.join(workdir, filename)
path_names.add(path_name)
relocate.relocate_links(path_names, old_path, new_path)
# For all buildcaches
# relocate the install prefixes in text files including dependencies
relocate.relocate_text(text_names,
old_layout_root, new_layout_root,
old_prefix, new_prefix,
old_spack_prefix,
new_spack_prefix,
prefix_to_prefix)
# relocate the install prefixes in binary files including dependencies
relocate.relocate_text_bin(files_to_relocate,
old_layout_root, new_layout_root,
old_prefix, new_prefix,
old_spack_prefix,
new_spack_prefix,
prefix_to_prefix)
def extract_tarball(spec, filename, allow_root=False, unsigned=False,
@ -610,7 +676,7 @@ def extract_tarball(spec, filename, allow_root=False, unsigned=False,
Gpg.verify('%s.asc' % specfile_path, specfile_path, suppress)
except Exception as e:
shutil.rmtree(tmpdir)
tty.die(e)
raise e
else:
shutil.rmtree(tmpdir)
raise NoVerifyException(
@ -639,22 +705,30 @@ def extract_tarball(spec, filename, allow_root=False, unsigned=False,
# if the original relative prefix is in the spec file use it
buildinfo = spec_dict.get('buildinfo', {})
old_relative_prefix = buildinfo.get('relative_prefix', new_relative_prefix)
rel = buildinfo.get('relative_rpaths')
# if the original relative prefix and new relative prefix differ the
# directory layout has changed and the buildcache cannot be installed
if old_relative_prefix != new_relative_prefix:
shutil.rmtree(tmpdir)
msg = "Package tarball was created from an install "
msg += "prefix with a different directory layout.\n"
msg += "It cannot be relocated."
raise NewLayoutException(msg)
# if it was created with relative rpaths
info = 'old relative prefix %s\nnew relative prefix %s\nrelative rpaths %s'
tty.debug(info %
(old_relative_prefix, new_relative_prefix, rel))
# if (old_relative_prefix != new_relative_prefix and (rel)):
# shutil.rmtree(tmpdir)
# msg = "Package tarball was created from an install "
# msg += "prefix with a different directory layout. "
# msg += "It cannot be relocated because it "
# msg += "uses relative rpaths."
# raise NewLayoutException(msg)
# extract the tarball in a temp directory
with closing(tarfile.open(tarfile_path, 'r')) as tar:
tar.extractall(path=tmpdir)
# the base of the install prefix is used when creating the tarball
# so the pathname should be the same now that the directory layout
# is confirmed
workdir = os.path.join(tmpdir, os.path.basename(spec.prefix))
# get the parent directory of the file .spack/binary_distribution
# this should the directory unpacked from the tarball whose
# name is unknown because the prefix naming is unknown
bindist_file = glob.glob('%s/*/.spack/binary_distribution' % tmpdir)[0]
workdir = re.sub('/.spack/binary_distribution$', '', bindist_file)
tty.debug('workdir %s' % workdir)
# install_tree copies hardlinks
# create a temporary tarfile from prefix and exract it to workdir
# tarfile preserves hardlinks
@ -672,10 +746,10 @@ def extract_tarball(spec, filename, allow_root=False, unsigned=False,
os.remove(specfile_path)
try:
relocate_package(spec.prefix, spec, allow_root)
relocate_package(spec, allow_root)
except Exception as e:
shutil.rmtree(spec.prefix)
tty.die(e)
raise e
else:
manifest_file = os.path.join(spec.prefix,
spack.store.layout.metadata_dir,
@ -685,6 +759,8 @@ def extract_tarball(spec, filename, allow_root=False, unsigned=False,
tty.warn('No manifest file in tarball for spec %s' % spec_id)
finally:
shutil.rmtree(tmpdir)
if os.path.exists(filename):
os.remove(filename)
# Internal cache for downloaded specs
@ -732,7 +808,7 @@ def get_spec(spec=None, force=False):
tty.debug("No Spack mirrors are currently configured")
return {}
if spec in _cached_specs:
if _cached_specs and spec in _cached_specs:
return _cached_specs
for mirror in spack.mirror.MirrorCollection().values():
@ -817,7 +893,7 @@ def get_keys(install=False, trust=False, force=False):
mirror_dir = url_util.local_file_path(fetch_url_build_cache)
if mirror_dir:
tty.msg("Finding public keys in %s" % mirror_dir)
files = os.listdir(mirror_dir)
files = os.listdir(str(mirror_dir))
for file in files:
if re.search(r'\.key', file) or re.search(r'\.pub', file):
link = url_util.join(fetch_url_build_cache, file)

View file

@ -13,6 +13,9 @@
import llnl.util.lang
from spack.util.executable import Executable, ProcessError
import llnl.util.tty as tty
from macholib.MachO import MachO
from spack.spec import Spec
import macholib.mach_o
class InstallRootStringException(spack.error.SpackError):
@ -41,43 +44,54 @@ def __init__(self, file_path, old_len, new_len):
(file_path, old_len, new_len))
class MissingMacholibException(spack.error.SpackError):
class BinaryTextReplaceException(spack.error.SpackError):
"""
Raised when the size of the file changes after binary path substitution.
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):
msg = "New path longer than old path: binary text"
msg += " replacement not possible."
err_msg = "The new path %s" % new_path
err_msg += " is longer than the old path %s.\n" % old_path
err_msg += "Text replacement in binaries will not work.\n"
err_msg += "Create buildcache from an install path "
err_msg += "longer than new path."
super(BinaryTextReplaceException, self).__init__(msg, err_msg)
class PatchelfError(spack.error.SpackError):
"""
Raised when patchelf command returns a ProcessError.
"""
def __init__(self, error):
super(MissingMacholibException, self).__init__(
"%s\n"
"Python package macholib needs to be avaiable to list\n"
"and modify a mach-o binary's rpaths, deps and id.\n"
"Use virtualenv with pip install macholib or\n"
"use spack to install the py-macholib package\n"
"spack install py-macholib\n"
"spack activate py-macholib\n"
"spack load python\n"
% error)
super(PatchelfError, self).__init__(error)
def get_patchelf():
"""
Returns the full patchelf binary path if available in $PATH.
Builds and installs spack patchelf package on linux platforms
using the first concretized spec.
Returns the full patchelf binary path.
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')
if patchelf is not None:
return patchelf.path
patchelf_spec = Spec('patchelf').concretized()
patchelf = patchelf_spec.package
if patchelf.installed:
patchelf_executable = os.path.join(patchelf.prefix.bin, "patchelf")
return patchelf_executable
else:
if str(spack.architecture.platform()) == 'test':
if (str(spack.architecture.platform()) == 'test' or
str(spack.architecture.platform()) == 'darwin'):
return None
if str(spack.architecture.platform()) == 'darwin':
return None
patchelf_spec = spack.cmd.parse_specs("patchelf", concretize=True)[0]
patchelf = spack.repo.get(patchelf_spec)
if not patchelf.installed:
patchelf.do_install(use_cache=False)
else:
patchelf.do_install()
patchelf_executable = os.path.join(patchelf.prefix.bin, "patchelf")
return patchelf_executable
@ -95,33 +109,53 @@ def get_existing_elf_rpaths(path_name):
else:
patchelf = Executable(get_patchelf())
rpaths = list()
try:
output = patchelf('--print-rpath', '%s' %
path_name, output=str, error=str)
return output.rstrip('\n').split(':')
rpaths = output.rstrip('\n').split(':')
except ProcessError as e:
tty.debug('patchelf --print-rpath produced an error on %s' %
path_name, e)
return []
return
msg = 'patchelf --print-rpath %s produced an error %s' % (path_name, e)
raise PatchelfError(msg)
return rpaths
def get_relative_rpaths(path_name, orig_dir, orig_rpaths):
def get_relative_elf_rpaths(path_name, orig_layout_root, orig_rpaths):
"""
Replaces orig_dir with relative path from dirname(path_name) if an rpath
in orig_rpaths contains orig_path. Prefixes $ORIGIN
Replaces orig rpath with relative path from dirname(path_name) if an rpath
in orig_rpaths contains orig_layout_root. Prefixes $ORIGIN
to relative paths and returns replacement rpaths.
"""
rel_rpaths = []
for rpath in orig_rpaths:
if re.match(orig_dir, rpath):
if re.match(orig_layout_root, rpath):
rel = os.path.relpath(rpath, start=os.path.dirname(path_name))
rel_rpaths.append('$ORIGIN/%s' % rel)
rel_rpaths.append(os.path.join('$ORIGIN', '%s' % rel))
else:
rel_rpaths.append(rpath)
return rel_rpaths
def get_normalized_elf_rpaths(orig_path_name, rel_rpaths):
"""
Normalize the relative rpaths with respect to the original path name
of the file. If the rpath starts with $ORIGIN replace $ORIGIN with the
dirname of the original path name and then normalize the rpath.
A dictionary mapping relativized rpaths to normalized rpaths is returned.
"""
norm_rpaths = list()
for rpath in rel_rpaths:
if rpath.startswith('$ORIGIN'):
sub = re.sub('$ORIGIN',
os.path.dirname(orig_path_name),
rpath)
norm = os.path.normpath(sub)
norm_rpaths.append(norm)
else:
norm_rpaths.append(rpath)
return norm_rpaths
def set_placeholder(dirname):
"""
return string of @'s with same length
@ -129,183 +163,157 @@ def set_placeholder(dirname):
return '@' * len(dirname)
def get_placeholder_rpaths(path_name, orig_rpaths):
def macho_make_paths_relative(path_name, old_layout_root,
rpaths, deps, idpath):
"""
Replaces original layout root dir with a placeholder string in all rpaths.
Return a dictionary mapping the original rpaths to the relativized rpaths.
This dictionary is used to replace paths in mach-o binaries.
Replace old_dir with relative path from dirname of path name
in rpaths and deps; idpath is replaced with @rpath/libname.
"""
rel_rpaths = []
orig_dir = spack.store.layout.root
for rpath in orig_rpaths:
if re.match(orig_dir, rpath):
placeholder = set_placeholder(orig_dir)
rel = re.sub(orig_dir, placeholder, rpath)
rel_rpaths.append('%s' % rel)
else:
rel_rpaths.append(rpath)
return rel_rpaths
def macho_get_paths(path_name):
"""
Examines the output of otool -l path_name for these three fields:
LC_ID_DYLIB, LC_LOAD_DYLIB, LC_RPATH and parses out the rpaths,
dependiencies and library id.
Returns these values.
"""
otool = Executable('otool')
output = otool("-l", path_name, output=str, err=str)
last_cmd = None
idpath = None
rpaths = []
deps = []
for line in output.split('\n'):
match = re.search('( *[a-zA-Z]+ )(.*)', line)
if match:
lhs = match.group(1).lstrip().rstrip()
rhs = match.group(2)
match2 = re.search(r'(.*) \(.*\)', rhs)
if match2:
rhs = match2.group(1)
if lhs == 'cmd':
last_cmd = rhs
if lhs == 'path' and last_cmd == 'LC_RPATH':
rpaths.append(rhs)
if lhs == 'name' and last_cmd == 'LC_ID_DYLIB':
idpath = rhs
if lhs == 'name' and last_cmd == 'LC_LOAD_DYLIB':
deps.append(rhs)
return rpaths, deps, idpath
def macho_make_paths_relative(path_name, old_dir, rpaths, deps, idpath):
"""
Replace old_dir with relative path from dirname(path_name)
in rpaths and deps; idpaths are replaced with @rpath/libname as needed;
replacement are returned.
"""
new_idpath = None
paths_to_paths = dict()
if idpath:
new_idpath = '@rpath/%s' % os.path.basename(idpath)
new_rpaths = list()
new_deps = list()
paths_to_paths[idpath] = os.path.join(
'@rpath', '%s' % os.path.basename(idpath))
for rpath in rpaths:
if re.match(old_dir, rpath):
if re.match(old_layout_root, rpath):
rel = os.path.relpath(rpath, start=os.path.dirname(path_name))
new_rpaths.append('@loader_path/%s' % rel)
paths_to_paths[rpath] = os.path.join('@loader_path', '%s' % rel)
else:
new_rpaths.append(rpath)
paths_to_paths[rpath] = rpath
for dep in deps:
if re.match(old_dir, dep):
if re.match(old_layout_root, dep):
rel = os.path.relpath(dep, start=os.path.dirname(path_name))
new_deps.append('@loader_path/%s' % rel)
paths_to_paths[dep] = os.path.join('@loader_path', '%s' % rel)
else:
new_deps.append(dep)
return (new_rpaths, new_deps, new_idpath)
paths_to_paths[dep] = dep
return paths_to_paths
def macho_make_paths_placeholder(rpaths, deps, idpath):
def macho_make_paths_normal(orig_path_name, rpaths, deps, idpath):
"""
Replace old_dir with a placeholder of the same length
in rpaths and deps and idpaths is needed.
replacement are returned.
Return a dictionary mapping the relativized rpaths to the original rpaths.
This dictionary is used to replace paths in mach-o binaries.
Replace '@loader_path' with the dirname of the origname path name
in rpaths and deps; idpath is replaced with the original path name
"""
new_idpath = None
old_dir = spack.store.layout.root
placeholder = set_placeholder(old_dir)
rel_to_orig = dict()
if idpath:
new_idpath = re.sub(old_dir, placeholder, idpath)
new_rpaths = list()
new_deps = list()
rel_to_orig[idpath] = orig_path_name
for rpath in rpaths:
if re.match(old_dir, rpath):
ph = re.sub(old_dir, placeholder, rpath)
new_rpaths.append('%s' % ph)
if re.match('@loader_path', rpath):
norm = os.path.normpath(re.sub(re.escape('@loader_path'),
os.path.dirname(orig_path_name),
rpath))
rel_to_orig[rpath] = norm
else:
new_rpaths.append(rpath)
rel_to_orig[rpath] = rpath
for dep in deps:
if re.match(old_dir, dep):
ph = re.sub(old_dir, placeholder, dep)
new_deps.append('%s' % ph)
if re.match('@loader_path', dep):
norm = os.path.normpath(re.sub(re.escape('@loader_path'),
os.path.dirname(orig_path_name),
dep))
rel_to_orig[dep] = norm
else:
new_deps.append(dep)
return (new_rpaths, new_deps, new_idpath)
rel_to_orig[dep] = dep
return rel_to_orig
def macho_replace_paths(old_dir, new_dir, rpaths, deps, idpath):
def macho_find_paths(orig_rpaths, deps, idpath,
old_layout_root, prefix_to_prefix):
"""
Replace old_dir with new_dir in rpaths, deps and idpath
and return replacements
Inputs
original rpaths from mach-o binaries
dependency libraries for mach-o binaries
id path of mach-o libraries
old install directory layout root
prefix_to_prefix dictionary which maps prefixes in the old directory layout
to directories in the new directory layout
Output
paths_to_paths dictionary which maps all of the old paths to new paths
"""
new_idpath = None
paths_to_paths = dict()
for orig_rpath in orig_rpaths:
if orig_rpath.startswith(old_layout_root):
for old_prefix, new_prefix in prefix_to_prefix.items():
if orig_rpath.startswith(old_prefix):
new_rpath = re.sub(re.escape(old_prefix),
new_prefix, orig_rpath)
paths_to_paths[orig_rpath] = new_rpath
else:
paths_to_paths[orig_rpath] = orig_rpath
if idpath:
new_idpath = idpath.replace(old_dir, new_dir)
new_rpaths = list()
new_deps = list()
for rpath in rpaths:
new_rpath = rpath.replace(old_dir, new_dir)
new_rpaths.append(new_rpath)
for old_prefix, new_prefix in prefix_to_prefix.items():
if idpath.startswith(old_prefix):
paths_to_paths[idpath] = re.sub(
re.escape(old_prefix), new_prefix, idpath)
for dep in deps:
new_dep = dep.replace(old_dir, new_dir)
new_deps.append(new_dep)
return new_rpaths, new_deps, new_idpath
for old_prefix, new_prefix in prefix_to_prefix.items():
if dep.startswith(old_prefix):
paths_to_paths[dep] = re.sub(
re.escape(old_prefix), new_prefix, dep)
if dep.startswith('@'):
paths_to_paths[dep] = dep
return paths_to_paths
def modify_macho_object(cur_path, rpaths, deps, idpath,
new_rpaths, new_deps, new_idpath):
paths_to_paths):
"""
Modify MachO binary path_name by replacing old_dir with new_dir
or the relative path to spack install root.
The old install dir in LC_ID_DYLIB is replaced with the new install dir
using install_name_tool -id newid binary
The old install dir in LC_LOAD_DYLIB is replaced with the new install dir
using install_name_tool -change old new binary
The old install dir in LC_RPATH is replaced with the new install dir using
install_name_tool -rpath old new binary
This function is used to make machO buildcaches on macOS by
replacing old paths with new paths using install_name_tool
Inputs:
mach-o binary to be modified
original rpaths
original dependency paths
original id path if a mach-o library
dictionary mapping paths in old install layout to new install layout
"""
# avoid error message for libgcc_s
if 'libgcc_' in cur_path:
return
install_name_tool = Executable('install_name_tool')
if idpath:
new_idpath = paths_to_paths.get(idpath, None)
if new_idpath and not idpath == new_idpath:
install_name_tool('-id', new_idpath, str(cur_path))
for dep in deps:
new_dep = paths_to_paths.get(dep)
if new_dep and dep != new_dep:
install_name_tool('-change', dep, new_dep, str(cur_path))
if len(deps) == len(new_deps):
for orig, new in zip(deps, new_deps):
if not orig == new:
install_name_tool('-change', orig, new, str(cur_path))
if len(rpaths) == len(new_rpaths):
for orig, new in zip(rpaths, new_rpaths):
if not orig == new:
install_name_tool('-rpath', orig, new, str(cur_path))
for orig_rpath in rpaths:
new_rpath = paths_to_paths.get(orig_rpath)
if new_rpath and not orig_rpath == new_rpath:
install_name_tool('-rpath', orig_rpath, new_rpath, str(cur_path))
return
def modify_object_macholib(cur_path, old_dir, new_dir):
def modify_object_macholib(cur_path, paths_to_paths):
"""
Modify MachO binary path_name by replacing old_dir with new_dir
or the relative path to spack install root.
The old install dir in LC_ID_DYLIB is replaced with the new install dir
using py-macholib
The old install dir in LC_LOAD_DYLIB is replaced with the new install dir
using py-macholib
The old install dir in LC_RPATH is replaced with the new install dir using
using py-macholib
This function is used when install machO buildcaches on linux by
rewriting mach-o loader commands for dependency library paths of
mach-o binaries and the id path for mach-o libraries.
Rewritting of rpaths is handled by replace_prefix_bin.
Inputs
mach-o binary to be modified
dictionary mapping paths in old install layout to new install layout
"""
if cur_path.endswith('.o'):
return
try:
from macholib.MachO import MachO
except ImportError as e:
raise MissingMacholibException(e)
def match_func(cpath):
rpath = cpath.replace(old_dir, new_dir)
return rpath
dll = MachO(cur_path)
dll.rewriteLoadCommands(match_func)
changedict = paths_to_paths
def changefunc(path):
npath = changedict.get(path, None)
return npath
dll.rewriteLoadCommands(changefunc)
try:
f = open(dll.filename, 'rb+')
for header in dll.headers:
@ -320,14 +328,32 @@ def match_func(cpath):
return
def strings_contains_installroot(path_name, root_dir):
def macholib_get_paths(cur_path):
"""
Check if the file contain the install root string.
Get rpaths, dependencies and id of mach-o objects
using python macholib package
"""
strings = Executable('strings')
output = strings('%s' % path_name,
output=str, err=str)
return (root_dir in output or spack.paths.prefix in output)
dll = MachO(cur_path)
ident = None
rpaths = list()
deps = list()
for header in dll.headers:
rpaths = [data.rstrip(b'\0').decode('utf-8')
for load_command, dylib_command, data in header.commands if
load_command.cmd == macholib.mach_o.LC_RPATH]
deps = [data.rstrip(b'\0').decode('utf-8')
for load_command, dylib_command, data in header.commands if
load_command.cmd == macholib.mach_o.LC_LOAD_DYLIB]
idents = [data.rstrip(b'\0').decode('utf-8')
for load_command, dylib_command, data in header.commands if
load_command.cmd == macholib.mach_o.LC_ID_DYLIB]
if len(idents) == 1:
ident = idents[0]
tty.debug('ident: %s' % ident)
tty.debug('deps: %s' % deps)
tty.debug('rpaths: %s' % rpaths)
return (rpaths, deps, ident)
def modify_elf_object(path_name, new_rpaths):
@ -338,9 +364,9 @@ def modify_elf_object(path_name, new_rpaths):
new_joined = ':'.join(new_rpaths)
# if we're relocating patchelf itself, use it
bak_path = path_name + ".bak"
if path_name[-13:] == "/bin/patchelf":
bak_path = path_name + ".bak"
shutil.copy(path_name, bak_path)
patchelf = Executable(bak_path)
else:
@ -350,9 +376,11 @@ def modify_elf_object(path_name, new_rpaths):
patchelf('--force-rpath', '--set-rpath', '%s' % new_joined,
'%s' % path_name, output=str, error=str)
except ProcessError as e:
tty.die('patchelf --set-rpath %s failed' %
path_name, e)
msg = 'patchelf --set-rpath %s failed with error %s' % (path_name, e)
raise PatchelfError(msg)
pass
if os.path.exists(bak_path):
os.remove(bak_path)
def needs_binary_relocation(m_type, m_subtype):
@ -447,11 +475,15 @@ def replace(match):
return data
return match.group().replace(old_dir.encode('utf-8'),
new_dir.encode('utf-8')) + b'\0' * padding
if len(new_dir) > len(old_dir):
raise BinaryTextReplaceException(old_dir, new_dir)
with open(path_name, 'rb+') as f:
data = f.read()
f.seek(0)
original_data_len = len(data)
pat = re.compile(old_dir.encode('utf-8') + b'([^\0]*?)\0')
pat = re.compile(re.escape(old_dir).encode('utf-8') + b'([^\0]*?)\0')
if not pat.search(data):
return
ndata = pat.sub(replace, data)
@ -462,80 +494,129 @@ def replace(match):
f.truncate()
def relocate_macho_binaries(path_names, old_dir, new_dir, allow_root):
def relocate_macho_binaries(path_names, old_layout_root, new_layout_root,
prefix_to_prefix, rel, old_prefix, new_prefix):
"""
Change old_dir to new_dir in LC_RPATH of mach-o files (on macOS)
Change old_dir to new_dir in LC_ID and LC_DEP of mach-o files
Account for the case where old_dir is now a placeholder
Use macholib python package to get the rpaths, depedent libraries
and library identity for libraries from the MachO object. Modify them
with the replacement paths queried from the dictionary mapping old layout
prefixes to hashes and the dictionary mapping hashes to the new layout
prefixes.
"""
placeholder = set_placeholder(old_dir)
for path_name in path_names:
# Corner case where macho object file ended up in the path name list
if path_name.endswith('.o'):
continue
if new_dir == old_dir:
continue
if platform.system().lower() == 'darwin':
rpaths, deps, idpath = macho_get_paths(path_name)
# one pass to replace placeholder
(n_rpaths,
n_deps,
n_idpath) = macho_replace_paths(placeholder,
new_dir,
rpaths,
deps,
if rel:
# get the relativized paths
rpaths, deps, idpath = macholib_get_paths(path_name)
# get the file path name in the original prefix
orig_path_name = re.sub(re.escape(new_prefix), old_prefix,
path_name)
# get the mapping of the relativized paths to the original
# normalized paths
rel_to_orig = macho_make_paths_normal(orig_path_name,
rpaths, deps,
idpath)
# another pass to replace old_dir
(new_rpaths,
new_deps,
new_idpath) = macho_replace_paths(old_dir,
new_dir,
n_rpaths,
n_deps,
n_idpath)
modify_macho_object(path_name,
rpaths, deps, idpath,
new_rpaths, new_deps, new_idpath)
# replace the relativized paths with normalized paths
if platform.system().lower() == 'darwin':
modify_macho_object(path_name, rpaths, deps,
idpath, rel_to_orig)
else:
modify_object_macholib(path_name, placeholder, new_dir)
modify_object_macholib(path_name, old_dir, new_dir)
if len(new_dir) <= len(old_dir):
replace_prefix_nullterm(path_name, old_dir, new_dir)
modify_object_macholib(path_name,
rel_to_orig)
# get the normalized paths in the mach-o binary
rpaths, deps, idpath = macholib_get_paths(path_name)
# get the mapping of paths in old prefix to path in new prefix
paths_to_paths = macho_find_paths(rpaths, deps, idpath,
old_layout_root,
prefix_to_prefix)
# replace the old paths with new paths
if platform.system().lower() == 'darwin':
modify_macho_object(path_name, rpaths, deps,
idpath, paths_to_paths)
else:
tty.warn('Cannot do a binary string replacement'
' with padding for %s'
' because %s is longer than %s' %
(path_name, new_dir, old_dir))
modify_object_macholib(path_name,
paths_to_paths)
# get the new normalized path in the mach-o binary
rpaths, deps, idpath = macholib_get_paths(path_name)
# get the mapping of paths to relative paths in the new prefix
paths_to_paths = macho_make_paths_relative(path_name,
new_layout_root,
rpaths, deps, idpath)
# replace the new paths with relativized paths in the new prefix
if platform.system().lower() == 'darwin':
modify_macho_object(path_name, rpaths, deps,
idpath, paths_to_paths)
else:
modify_object_macholib(path_name,
paths_to_paths)
else:
# get the paths in the old prefix
rpaths, deps, idpath = macholib_get_paths(path_name)
# get the mapping of paths in the old prerix to the new prefix
paths_to_paths = macho_find_paths(rpaths, deps, idpath,
old_layout_root,
prefix_to_prefix)
# replace the old paths with new paths
if platform.system().lower() == 'darwin':
modify_macho_object(path_name, rpaths, deps,
idpath, paths_to_paths)
else:
modify_object_macholib(path_name,
paths_to_paths)
def relocate_elf_binaries(path_names, old_dir, new_dir, allow_root):
def elf_find_paths(orig_rpaths, old_layout_root, prefix_to_prefix):
new_rpaths = list()
for orig_rpath in orig_rpaths:
if orig_rpath.startswith(old_layout_root):
for old_prefix, new_prefix in prefix_to_prefix.items():
if orig_rpath.startswith(old_prefix):
new_rpaths.append(re.sub(re.escape(old_prefix),
new_prefix, orig_rpath))
else:
new_rpaths.append(orig_rpath)
return new_rpaths
def relocate_elf_binaries(path_names, old_layout_root, new_layout_root,
prefix_to_prefix, rel, old_prefix, new_prefix):
"""
Change old_dir to new_dir in RPATHs of elf binaries
Account for the case where old_dir is now a placeholder
Use patchelf to get the original rpaths and then replace them with
rpaths in the new directory layout.
New rpaths are determined from a dictionary mapping the prefixes in the
old directory layout to the prefixes in the new directory layout if the
rpath was in the old layout root, i.e. system paths are not replaced.
"""
placeholder = set_placeholder(old_dir)
for path_name in path_names:
orig_rpaths = get_existing_elf_rpaths(path_name)
if orig_rpaths:
# one pass to replace placeholder
n_rpaths = substitute_rpath(orig_rpaths,
placeholder, new_dir)
# one pass to replace old_dir
new_rpaths = substitute_rpath(n_rpaths,
old_dir, new_dir)
new_rpaths = list()
if rel:
# get the file path in the old_prefix
orig_path_name = re.sub(re.escape(new_prefix), old_prefix,
path_name)
# get the normalized rpaths in the old prefix using the file path
# in the orig prefix
orig_norm_rpaths = get_normalized_elf_rpaths(orig_path_name,
orig_rpaths)
# get the normalize rpaths in the new prefix
norm_rpaths = elf_find_paths(orig_norm_rpaths, old_layout_root,
prefix_to_prefix)
# get the relativized rpaths in the new prefix
new_rpaths = get_relative_elf_rpaths(path_name, new_layout_root,
norm_rpaths)
modify_elf_object(path_name, new_rpaths)
if not new_dir == old_dir:
if len(new_dir) <= len(old_dir):
replace_prefix_bin(path_name, old_dir, new_dir)
else:
tty.warn('Cannot do a binary string replacement'
' with padding for %s'
' because %s is longer than %s.' %
(path_name, new_dir, old_dir))
new_rpaths = elf_find_paths(orig_rpaths, old_layout_root,
prefix_to_prefix)
modify_elf_object(path_name, new_rpaths)
def make_link_relative(cur_path_names, orig_path_names):
"""
Change absolute links to be relative.
Change absolute links to relative links.
"""
for cur_path, orig_path in zip(cur_path_names, orig_path_names):
target = os.readlink(orig_path)
@ -545,8 +626,8 @@ def make_link_relative(cur_path_names, orig_path_names):
os.symlink(relative_target, cur_path)
def make_macho_binaries_relative(cur_path_names, orig_path_names, old_dir,
allow_root):
def make_macho_binaries_relative(cur_path_names, orig_path_names,
old_layout_root):
"""
Replace old RPATHs with paths relative to old_dir in binary files
"""
@ -555,33 +636,26 @@ def make_macho_binaries_relative(cur_path_names, orig_path_names, old_dir,
deps = set()
idpath = None
if platform.system().lower() == 'darwin':
(rpaths, deps, idpath) = macho_get_paths(cur_path)
(new_rpaths,
new_deps,
new_idpath) = macho_make_paths_relative(orig_path, old_dir,
(rpaths, deps, idpath) = macholib_get_paths(cur_path)
paths_to_paths = macho_make_paths_relative(orig_path,
old_layout_root,
rpaths, deps, idpath)
modify_macho_object(cur_path,
rpaths, deps, idpath,
new_rpaths, new_deps, new_idpath)
if (not allow_root and
not file_is_relocatable(cur_path)):
raise InstallRootStringException(cur_path, old_dir)
paths_to_paths)
def make_elf_binaries_relative(cur_path_names, orig_path_names, old_dir,
allow_root):
def make_elf_binaries_relative(cur_path_names, orig_path_names,
old_layout_root):
"""
Replace old RPATHs with paths relative to old_dir in binary files
"""
for cur_path, orig_path in zip(cur_path_names, orig_path_names):
orig_rpaths = get_existing_elf_rpaths(cur_path)
if orig_rpaths:
new_rpaths = get_relative_rpaths(orig_path, old_dir,
new_rpaths = get_relative_elf_rpaths(orig_path, old_layout_root,
orig_rpaths)
modify_elf_object(cur_path, new_rpaths)
if (not allow_root and
not file_is_relocatable(cur_path)):
raise InstallRootStringException(cur_path, old_dir)
def check_files_relocatable(cur_path_names, allow_root):
@ -595,63 +669,74 @@ def check_files_relocatable(cur_path_names, allow_root):
cur_path, spack.store.layout.root)
def make_link_placeholder(cur_path_names, cur_dir, old_dir):
def relocate_links(linknames, old_layout_root, new_layout_root,
old_install_prefix, new_install_prefix, prefix_to_prefix):
"""
Replace old install path with placeholder in absolute links.
Links in ``cur_path_names`` must link to absolute paths.
The symbolic links in filenames are absolute links or placeholder links.
The old link target is read and the placeholder is replaced by the old
layout root. If the old link target is in the old install prefix, the new
link target is create by replacing the old install prefix with the new
install prefix.
"""
for cur_path in cur_path_names:
placeholder = set_placeholder(spack.store.layout.root)
placeholder_prefix = old_dir.replace(spack.store.layout.root,
placeholder)
cur_src = os.readlink(cur_path)
rel_src = os.path.relpath(cur_src, cur_dir)
new_src = os.path.join(placeholder_prefix, rel_src)
os.unlink(cur_path)
os.symlink(new_src, cur_path)
placeholder = set_placeholder(old_layout_root)
link_names = [os.path.join(new_install_prefix, linkname)
for linkname in linknames]
for link_name in link_names:
old_link_target = os.readlink(link_name)
old_link_target = re.sub(placeholder, old_layout_root, old_link_target)
if old_link_target.startswith(old_install_prefix):
new_link_target = re.sub(
old_install_prefix, new_install_prefix, old_link_target)
os.unlink(link_name)
os.symlink(new_link_target, link_name)
else:
msg = 'Old link target %s' % old_link_target
msg += ' for symbolic link %s is outside' % link_name
msg += ' of the old install prefix %s.\n' % old_install_prefix
msg += 'This symbolic link will not be relocated'
msg += ' and might break relocation.'
tty.warn(msg)
def relocate_links(path_names, old_dir, new_dir):
def relocate_text(path_names, old_layout_root, new_layout_root,
old_install_prefix, new_install_prefix,
old_spack_prefix, new_spack_prefix,
prefix_to_prefix):
"""
Replace old path with new path in link sources.
Links in ``path_names`` must link to absolute paths or placeholders.
Replace old paths with new paths in text files
including the path the the spack sbang script
"""
placeholder = set_placeholder(old_dir)
sbangre = '#!/bin/bash %s/bin/sbang' % old_spack_prefix
sbangnew = '#!/bin/bash %s/bin/sbang' % new_spack_prefix
for path_name in path_names:
old_src = os.readlink(path_name)
# replace either placeholder or old_dir
new_src = old_src.replace(placeholder, new_dir, 1)
new_src = new_src.replace(old_dir, new_dir, 1)
os.unlink(path_name)
os.symlink(new_src, path_name)
def relocate_text(path_names, oldpath, newpath, oldprefix, newprefix):
"""
Replace old path with new path in text files
including the path the the spack sbang script.
"""
sbangre = '#!/bin/bash %s/bin/sbang' % oldprefix
sbangnew = '#!/bin/bash %s/bin/sbang' % newprefix
for path_name in path_names:
replace_prefix_text(path_name, oldpath, newpath)
replace_prefix_text(path_name, old_install_prefix, new_install_prefix)
for orig_dep_prefix, new_dep_prefix in prefix_to_prefix.items():
replace_prefix_text(path_name, orig_dep_prefix, new_dep_prefix)
replace_prefix_text(path_name, old_layout_root, new_layout_root)
replace_prefix_text(path_name, sbangre, sbangnew)
replace_prefix_text(path_name, oldprefix, newprefix)
def substitute_rpath(orig_rpath, topdir, new_root_path):
def relocate_text_bin(path_names, old_layout_root, new_layout_root,
old_install_prefix, new_install_prefix,
old_spack_prefix, new_spack_prefix,
prefix_to_prefix):
"""
Replace topdir with new_root_path RPATH list orig_rpath
Replace null terminated path strings hard coded into binaries.
Raise an exception when the new path in longer than the old path
because this breaks the binary.
"""
new_rpaths = []
for path in orig_rpath:
new_rpath = path.replace(topdir, new_root_path)
new_rpaths.append(new_rpath)
return new_rpaths
if len(new_install_prefix) <= len(old_install_prefix):
for path_name in path_names:
for old_dep_prefix, new_dep_prefix in prefix_to_prefix.items():
if len(new_dep_prefix) <= len(old_dep_prefix):
replace_prefix_bin(
path_name, old_dep_prefix, new_dep_prefix)
replace_prefix_bin(path_name, old_spack_prefix, new_spack_prefix)
else:
if len(path_names) > 0:
raise BinaryTextReplaceException(
old_install_prefix, new_install_prefix)
def is_relocatable(spec):
@ -729,7 +814,7 @@ def file_is_relocatable(file, paths_to_relocate=None):
set_of_strings.discard(rpaths)
if platform.system().lower() == 'darwin':
if m_subtype == 'x-mach-binary':
rpaths, deps, idpath = macho_get_paths(file)
rpaths, deps, idpath = macholib_get_paths(file)
set_of_strings.discard(set(rpaths))
set_of_strings.discard(set(deps))
if idpath is not None:
@ -779,6 +864,8 @@ def mime_type(file):
file_cmd = Executable('file')
output = file_cmd('-b', '-h', '--mime-type', file, output=str, error=str)
tty.debug('[MIME_TYPE] {0} -> {1}'.format(file, output.strip()))
# In corner cases the output does not contain a subtype prefixed with a /
# In those cases add the / so the tuple can be formed.
if '/' not in output:
output += '/'
split_by_slash = output.strip().split('/')

View file

@ -8,10 +8,11 @@
"""
import os
import stat
import sys
import shutil
import pytest
import argparse
import re
import platform
from llnl.util.filesystem import mkdirp
@ -19,16 +20,15 @@
import spack.store
import spack.binary_distribution as bindist
import spack.cmd.buildcache as buildcache
import spack.util.gpg
from spack.spec import Spec
from spack.paths import mock_gpg_keys_path
from spack.fetch_strategy import URLFetchStrategy, FetchStrategyComposite
from spack.relocate import needs_binary_relocation, needs_text_relocation
from spack.relocate import strings_contains_installroot
from spack.relocate import get_patchelf, relocate_text, relocate_links
from spack.relocate import substitute_rpath, get_relative_rpaths
from spack.relocate import macho_replace_paths, macho_make_paths_relative
from spack.relocate import modify_macho_object, macho_get_paths
from spack.relocate import relocate_text, relocate_links
from spack.relocate import get_relative_elf_rpaths
from spack.relocate import macho_make_paths_relative
from spack.relocate import set_placeholder, macho_find_paths
from spack.relocate import file_is_relocatable
def has_gpg():
@ -50,9 +50,9 @@ def fake_fetchify(url, pkg):
@pytest.mark.usefixtures('install_mockery', 'mock_gnupghome')
def test_buildcache(mock_archive, tmpdir):
# tweak patchelf to only do a download
spec = Spec("patchelf")
spec.concretize()
pkg = spack.repo.get(spec)
pspec = Spec("patchelf")
pspec.concretize()
pkg = spack.repo.get(pspec)
fake_fetchify(pkg.fetcher, pkg)
mkdirp(os.path.join(pkg.prefix, "bin"))
patchelfscr = os.path.join(pkg.prefix, "bin", "patchelf")
@ -71,7 +71,7 @@ def test_buildcache(mock_archive, tmpdir):
pkg = spec.package
fake_fetchify(mock_archive.url, pkg)
pkg.do_install()
pkghash = '/' + spec.dag_hash(7)
pkghash = '/' + str(spec.dag_hash(7))
# Put some non-relocatable file in there
filename = os.path.join(spec.prefix, "dummy.txt")
@ -99,88 +99,69 @@ def test_buildcache(mock_archive, tmpdir):
parser = argparse.ArgumentParser()
buildcache.setup_parser(parser)
create_args = ['create', '-a', '-f', '-d', mirror_path, pkghash]
# Create a private key to sign package with if gpg2 available
if spack.util.gpg.Gpg.gpg():
spack.util.gpg.Gpg.create(name='test key 1', expires='0',
email='spack@googlegroups.com',
comment='Spack test key')
# Create build cache with signing
args = parser.parse_args(['create', '-d', mirror_path, str(spec)])
buildcache.buildcache(parser, args)
# Uninstall the package
pkg.do_uninstall(force=True)
# test overwrite install
args = parser.parse_args(['install', '-f', str(pkghash)])
buildcache.buildcache(parser, args)
files = os.listdir(spec.prefix)
# create build cache with relative path and signing
args = parser.parse_args(
['create', '-d', mirror_path, '-f', '-r', str(spec)])
buildcache.buildcache(parser, args)
# Uninstall the package
pkg.do_uninstall(force=True)
# install build cache with verification
args = parser.parse_args(['install', str(spec)])
buildcache.install_tarball(spec, args)
# test overwrite install
args = parser.parse_args(['install', '-f', str(pkghash)])
buildcache.buildcache(parser, args)
else:
# create build cache without signing
args = parser.parse_args(
['create', '-d', mirror_path, '-f', '-u', str(spec)])
create_args.insert(create_args.index('-a'), '-u')
args = parser.parse_args(create_args)
buildcache.buildcache(parser, args)
# trigger overwrite warning
buildcache.buildcache(parser, args)
# Uninstall the package
pkg.do_uninstall(force=True)
# install build cache without verification
args = parser.parse_args(['install', '-u', str(spec)])
buildcache.install_tarball(spec, args)
files = os.listdir(spec.prefix)
assert 'link_to_dummy.txt' in files
assert 'dummy.txt' in files
# test overwrite install without verification
args = parser.parse_args(['install', '-f', '-u', str(pkghash)])
buildcache.buildcache(parser, args)
# create build cache with relative path
args = parser.parse_args(
['create', '-d', mirror_path, '-f', '-r', '-u', str(pkghash)])
buildcache.buildcache(parser, args)
# Uninstall the package
pkg.do_uninstall(force=True)
# install build cache
args = parser.parse_args(['install', '-u', str(spec)])
buildcache.install_tarball(spec, args)
# test overwrite install
args = parser.parse_args(['install', '-f', '-u', str(pkghash)])
install_args = ['install', '-a', '-f', pkghash]
if not spack.util.gpg.Gpg.gpg():
install_args.insert(install_args.index('-a'), '-u')
args = parser.parse_args(install_args)
# Test install
buildcache.buildcache(parser, args)
files = os.listdir(spec.prefix)
assert 'link_to_dummy.txt' in files
assert 'dummy.txt' in files
assert os.path.realpath(
os.path.join(spec.prefix, 'link_to_dummy.txt')
) == os.path.realpath(os.path.join(spec.prefix, 'dummy.txt'))
# Validate the relocation information
buildinfo = bindist.read_buildinfo_file(spec.prefix)
assert(buildinfo['relocate_textfiles'] == ['dummy.txt'])
assert(buildinfo['relocate_links'] == ['link_to_dummy.txt'])
# create build cache with relative path
create_args.insert(create_args.index('-a'), '-f')
create_args.insert(create_args.index('-a'), '-r')
args = parser.parse_args(create_args)
buildcache.buildcache(parser, args)
# Uninstall the package
pkg.do_uninstall(force=True)
if not spack.util.gpg.Gpg.gpg():
install_args.insert(install_args.index('-a'), '-u')
args = parser.parse_args(install_args)
buildcache.buildcache(parser, args)
# test overwrite install
install_args.insert(install_args.index('-a'), '-f')
args = parser.parse_args(install_args)
buildcache.buildcache(parser, args)
files = os.listdir(spec.prefix)
assert 'link_to_dummy.txt' in files
assert 'dummy.txt' in files
# assert os.path.realpath(
# os.path.join(spec.prefix, 'link_to_dummy.txt')
# ) == os.path.realpath(os.path.join(spec.prefix, 'dummy.txt'))
args = parser.parse_args(['keys'])
buildcache.buildcache(parser, args)
args = parser.parse_args(['list'])
buildcache.buildcache(parser, args)
@ -200,6 +181,9 @@ def test_buildcache(mock_archive, tmpdir):
args = parser.parse_args(['keys', '-f'])
buildcache.buildcache(parser, args)
args = parser.parse_args(['keys', '-i', '-t'])
buildcache.buildcache(parser, args)
# unregister mirror with spack config
mirrors = {}
spack.config.set('mirrors', mirrors)
@ -210,7 +194,10 @@ def test_buildcache(mock_archive, tmpdir):
bindist._cached_specs = set()
@pytest.mark.usefixtures('install_mockery')
def test_relocate_text(tmpdir):
spec = Spec('trivial-install-test-package')
spec.concretize()
with tmpdir.as_cwd():
# Validate the text path replacement
old_dir = '/home/spack/opt/spack'
@ -220,24 +207,46 @@ def test_relocate_text(tmpdir):
script.close()
filenames = [filename]
new_dir = '/opt/rh/devtoolset/'
relocate_text(filenames, oldpath=old_dir, newpath=new_dir,
oldprefix=old_dir, newprefix=new_dir)
relocate_text(filenames, old_dir, new_dir,
old_dir, new_dir,
old_dir, new_dir,
{old_dir: new_dir})
with open(filename, "r")as script:
for line in script:
assert(new_dir in line)
assert(strings_contains_installroot(filename, old_dir) is False)
assert(file_is_relocatable(os.path.realpath(filename)))
# Remove cached binary specs since we deleted the mirror
bindist._cached_specs = set()
def test_relocate_links(tmpdir):
with tmpdir.as_cwd():
old_dir = '/home/spack/opt/spack'
filename = 'link.ln'
old_src = os.path.join(old_dir, filename)
os.symlink(old_src, filename)
filenames = [filename]
new_dir = '/opt/rh/devtoolset'
relocate_links(filenames, old_dir, new_dir)
assert os.path.realpath(filename) == os.path.join(new_dir, filename)
old_layout_root = os.path.join(
'%s' % tmpdir, 'home', 'spack', 'opt', 'spack')
old_install_prefix = os.path.join(
'%s' % old_layout_root, 'debian6', 'test')
old_binname = os.path.join(old_install_prefix, 'binfile')
placeholder = set_placeholder(old_layout_root)
re.sub(old_layout_root, placeholder, old_binname)
filenames = ['link.ln', 'outsideprefix.ln']
new_layout_root = os.path.join(
'%s' % tmpdir, 'opt', 'rh', 'devtoolset')
new_install_prefix = os.path.join(
'%s' % new_layout_root, 'test', 'debian6')
new_linkname = os.path.join(new_install_prefix, 'link.ln')
new_linkname2 = os.path.join(new_install_prefix, 'outsideprefix.ln')
new_binname = os.path.join(new_install_prefix, 'binfile')
mkdirp(new_install_prefix)
with open(new_binname, 'w') as f:
f.write('\n')
os.utime(new_binname, None)
os.symlink(old_binname, new_linkname)
os.symlink('/usr/lib/libc.so', new_linkname2)
relocate_links(filenames, old_layout_root, new_layout_root,
old_install_prefix, new_install_prefix,
{old_install_prefix: new_install_prefix})
assert os.readlink(new_linkname) == new_binname
assert os.readlink(new_linkname2) == '/usr/lib/libc.so'
def test_needs_relocation():
@ -246,15 +255,222 @@ def test_needs_relocation():
assert needs_binary_relocation('application', 'x-executable')
assert not needs_binary_relocation('application', 'x-octet-stream')
assert not needs_binary_relocation('text', 'x-')
assert needs_text_relocation('text', 'x-')
assert not needs_text_relocation('symbolic link to', 'x-')
assert needs_binary_relocation('application', 'x-mach-binary')
def test_macho_paths():
def test_replace_paths(tmpdir):
with tmpdir.as_cwd():
suffix = 'dylib' if platform.system().lower() == 'darwin' else 'so'
hash_a = '53moz6jwnw3xpiztxwhc4us26klribws'
hash_b = 'tk62dzu62kd4oh3h3heelyw23hw2sfee'
hash_c = 'hdkhduizmaddpog6ewdradpobnbjwsjl'
hash_d = 'hukkosc7ahff7o65h6cdhvcoxm57d4bw'
hash_loco = 'zy4oigsc4eovn5yhr2lk4aukwzoespob'
prefix2hash = dict()
old_spack_dir = os.path.join('%s' % tmpdir,
'Users', 'developer', 'spack')
mkdirp(old_spack_dir)
oldprefix_a = os.path.join('%s' % old_spack_dir, 'pkgA-%s' % hash_a)
oldlibdir_a = os.path.join('%s' % oldprefix_a, 'lib')
mkdirp(oldlibdir_a)
prefix2hash[str(oldprefix_a)] = hash_a
oldprefix_b = os.path.join('%s' % old_spack_dir, 'pkgB-%s' % hash_b)
oldlibdir_b = os.path.join('%s' % oldprefix_b, 'lib')
mkdirp(oldlibdir_b)
prefix2hash[str(oldprefix_b)] = hash_b
oldprefix_c = os.path.join('%s' % old_spack_dir, 'pkgC-%s' % hash_c)
oldlibdir_c = os.path.join('%s' % oldprefix_c, 'lib')
oldlibdir_cc = os.path.join('%s' % oldlibdir_c, 'C')
mkdirp(oldlibdir_c)
prefix2hash[str(oldprefix_c)] = hash_c
oldprefix_d = os.path.join('%s' % old_spack_dir, 'pkgD-%s' % hash_d)
oldlibdir_d = os.path.join('%s' % oldprefix_d, 'lib')
mkdirp(oldlibdir_d)
prefix2hash[str(oldprefix_d)] = hash_d
oldprefix_local = os.path.join('%s' % tmpdir, 'usr', 'local')
oldlibdir_local = os.path.join('%s' % oldprefix_local, 'lib')
mkdirp(oldlibdir_local)
prefix2hash[str(oldprefix_local)] = hash_loco
libfile_a = 'libA.%s' % suffix
libfile_b = 'libB.%s' % suffix
libfile_c = 'libC.%s' % suffix
libfile_d = 'libD.%s' % suffix
libfile_loco = 'libloco.%s' % suffix
old_libnames = [os.path.join(oldlibdir_a, libfile_a),
os.path.join(oldlibdir_b, libfile_b),
os.path.join(oldlibdir_c, libfile_c),
os.path.join(oldlibdir_d, libfile_d),
os.path.join(oldlibdir_local, libfile_loco)]
for old_libname in old_libnames:
with open(old_libname, 'a'):
os.utime(old_libname, None)
hash2prefix = dict()
new_spack_dir = os.path.join('%s' % tmpdir, 'Users', 'Shared',
'spack')
mkdirp(new_spack_dir)
prefix_a = os.path.join(new_spack_dir, 'pkgA-%s' % hash_a)
libdir_a = os.path.join(prefix_a, 'lib')
mkdirp(libdir_a)
hash2prefix[hash_a] = str(prefix_a)
prefix_b = os.path.join(new_spack_dir, 'pkgB-%s' % hash_b)
libdir_b = os.path.join(prefix_b, 'lib')
mkdirp(libdir_b)
hash2prefix[hash_b] = str(prefix_b)
prefix_c = os.path.join(new_spack_dir, 'pkgC-%s' % hash_c)
libdir_c = os.path.join(prefix_c, 'lib')
libdir_cc = os.path.join(libdir_c, 'C')
mkdirp(libdir_cc)
hash2prefix[hash_c] = str(prefix_c)
prefix_d = os.path.join(new_spack_dir, 'pkgD-%s' % hash_d)
libdir_d = os.path.join(prefix_d, 'lib')
mkdirp(libdir_d)
hash2prefix[hash_d] = str(prefix_d)
prefix_local = os.path.join('%s' % tmpdir, 'usr', 'local')
libdir_local = os.path.join(prefix_local, 'lib')
mkdirp(libdir_local)
hash2prefix[hash_loco] = str(prefix_local)
new_libnames = [os.path.join(libdir_a, libfile_a),
os.path.join(libdir_b, libfile_b),
os.path.join(libdir_cc, libfile_c),
os.path.join(libdir_d, libfile_d),
os.path.join(libdir_local, libfile_loco)]
for new_libname in new_libnames:
with open(new_libname, 'a'):
os.utime(new_libname, None)
prefix2prefix = dict()
for prefix, hash in prefix2hash.items():
prefix2prefix[prefix] = hash2prefix[hash]
out_dict = macho_find_paths([oldlibdir_a, oldlibdir_b,
oldlibdir_c,
oldlibdir_cc, oldlibdir_local],
[os.path.join(oldlibdir_a,
libfile_a),
os.path.join(oldlibdir_b,
libfile_b),
os.path.join(oldlibdir_local,
libfile_loco)],
os.path.join(oldlibdir_cc,
libfile_c),
old_spack_dir,
prefix2prefix
)
assert out_dict == {oldlibdir_a: libdir_a,
oldlibdir_b: libdir_b,
oldlibdir_c: libdir_c,
oldlibdir_cc: libdir_cc,
libdir_local: libdir_local,
os.path.join(oldlibdir_a, libfile_a):
os.path.join(libdir_a, libfile_a),
os.path.join(oldlibdir_b, libfile_b):
os.path.join(libdir_b, libfile_b),
os.path.join(oldlibdir_local, libfile_loco):
os.path.join(libdir_local, libfile_loco),
os.path.join(oldlibdir_cc, libfile_c):
os.path.join(libdir_cc, libfile_c)}
out_dict = macho_find_paths([oldlibdir_a, oldlibdir_b,
oldlibdir_c,
oldlibdir_cc,
oldlibdir_local],
[os.path.join(oldlibdir_a,
libfile_a),
os.path.join(oldlibdir_b,
libfile_b),
os.path.join(oldlibdir_cc,
libfile_c),
os.path.join(oldlibdir_local,
libfile_loco)],
None,
old_spack_dir,
prefix2prefix
)
assert out_dict == {oldlibdir_a: libdir_a,
oldlibdir_b: libdir_b,
oldlibdir_c: libdir_c,
oldlibdir_cc: libdir_cc,
libdir_local: libdir_local,
os.path.join(oldlibdir_a, libfile_a):
os.path.join(libdir_a, libfile_a),
os.path.join(oldlibdir_b, libfile_b):
os.path.join(libdir_b, libfile_b),
os.path.join(oldlibdir_local, libfile_loco):
os.path.join(libdir_local, libfile_loco),
os.path.join(oldlibdir_cc, libfile_c):
os.path.join(libdir_cc, libfile_c)}
out_dict = macho_find_paths([oldlibdir_a, oldlibdir_b,
oldlibdir_c, oldlibdir_cc,
oldlibdir_local],
['@rpath/%s' % libfile_a,
'@rpath/%s' % libfile_b,
'@rpath/%s' % libfile_c,
'@rpath/%s' % libfile_loco],
None,
old_spack_dir,
prefix2prefix
)
assert out_dict == {'@rpath/%s' % libfile_a:
'@rpath/%s' % libfile_a,
'@rpath/%s' % libfile_b:
'@rpath/%s' % libfile_b,
'@rpath/%s' % libfile_c:
'@rpath/%s' % libfile_c,
'@rpath/%s' % libfile_loco:
'@rpath/%s' % libfile_loco,
oldlibdir_a: libdir_a,
oldlibdir_b: libdir_b,
oldlibdir_c: libdir_c,
oldlibdir_cc: libdir_cc,
libdir_local: libdir_local,
}
out_dict = macho_find_paths([oldlibdir_a,
oldlibdir_b,
oldlibdir_d,
oldlibdir_local],
['@rpath/%s' % libfile_a,
'@rpath/%s' % libfile_b,
'@rpath/%s' % libfile_loco],
None,
old_spack_dir,
prefix2prefix)
assert out_dict == {'@rpath/%s' % libfile_a:
'@rpath/%s' % libfile_a,
'@rpath/%s' % libfile_b:
'@rpath/%s' % libfile_b,
'@rpath/%s' % libfile_loco:
'@rpath/%s' % libfile_loco,
oldlibdir_a: libdir_a,
oldlibdir_b: libdir_b,
oldlibdir_d: libdir_d,
libdir_local: libdir_local,
}
def test_macho_make_paths():
out = macho_make_paths_relative('/Users/Shares/spack/pkgC/lib/libC.dylib',
'/Users/Shared/spack',
('/Users/Shared/spack/pkgA/lib',
@ -264,13 +480,19 @@ def test_macho_paths():
'/Users/Shared/spack/pkgB/libB.dylib',
'/usr/local/lib/libloco.dylib'),
'/Users/Shared/spack/pkgC/lib/libC.dylib')
assert out == (['@loader_path/../../../../Shared/spack/pkgA/lib',
assert out == {'/Users/Shared/spack/pkgA/lib':
'@loader_path/../../../../Shared/spack/pkgA/lib',
'/Users/Shared/spack/pkgB/lib':
'@loader_path/../../../../Shared/spack/pkgB/lib',
'/usr/local/lib'],
['@loader_path/../../../../Shared/spack/pkgA/libA.dylib',
'/usr/local/lib': '/usr/local/lib',
'/Users/Shared/spack/pkgA/libA.dylib':
'@loader_path/../../../../Shared/spack/pkgA/libA.dylib',
'/Users/Shared/spack/pkgB/libB.dylib':
'@loader_path/../../../../Shared/spack/pkgB/libB.dylib',
'/usr/local/lib/libloco.dylib'],
'@rpath/libC.dylib')
'/usr/local/lib/libloco.dylib':
'/usr/local/lib/libloco.dylib',
'/Users/Shared/spack/pkgC/lib/libC.dylib':
'@rpath/libC.dylib'}
out = macho_make_paths_relative('/Users/Shared/spack/pkgC/bin/exeC',
'/Users/Shared/spack',
@ -281,98 +503,21 @@ def test_macho_paths():
'/Users/Shared/spack/pkgB/libB.dylib',
'/usr/local/lib/libloco.dylib'), None)
assert out == (['@loader_path/../../pkgA/lib',
assert out == {'/Users/Shared/spack/pkgA/lib':
'@loader_path/../../pkgA/lib',
'/Users/Shared/spack/pkgB/lib':
'@loader_path/../../pkgB/lib',
'/usr/local/lib'],
['@loader_path/../../pkgA/libA.dylib',
'/usr/local/lib': '/usr/local/lib',
'/Users/Shared/spack/pkgA/libA.dylib':
'@loader_path/../../pkgA/libA.dylib',
'/Users/Shared/spack/pkgB/libB.dylib':
'@loader_path/../../pkgB/libB.dylib',
'/usr/local/lib/libloco.dylib'], None)
out = macho_replace_paths('/Users/Shared/spack',
'/Applications/spack',
('/Users/Shared/spack/pkgA/lib',
'/Users/Shared/spack/pkgB/lib',
'/usr/local/lib'),
('/Users/Shared/spack/pkgA/libA.dylib',
'/Users/Shared/spack/pkgB/libB.dylib',
'/usr/local/lib/libloco.dylib'),
'/Users/Shared/spack/pkgC/lib/libC.dylib')
assert out == (['/Applications/spack/pkgA/lib',
'/Applications/spack/pkgB/lib',
'/usr/local/lib'],
['/Applications/spack/pkgA/libA.dylib',
'/Applications/spack/pkgB/libB.dylib',
'/usr/local/lib/libloco.dylib'],
'/Applications/spack/pkgC/lib/libC.dylib')
out = macho_replace_paths('/Users/Shared/spack',
'/Applications/spack',
('/Users/Shared/spack/pkgA/lib',
'/Users/Shared/spack/pkgB/lib',
'/usr/local/lib'),
('/Users/Shared/spack/pkgA/libA.dylib',
'/Users/Shared/spack/pkgB/libB.dylib',
'/usr/local/lib/libloco.dylib'),
None)
assert out == (['/Applications/spack/pkgA/lib',
'/Applications/spack/pkgB/lib',
'/usr/local/lib'],
['/Applications/spack/pkgA/libA.dylib',
'/Applications/spack/pkgB/libB.dylib',
'/usr/local/lib/libloco.dylib'],
None)
'/usr/local/lib/libloco.dylib':
'/usr/local/lib/libloco.dylib'}
def test_elf_paths():
out = get_relative_rpaths(
out = get_relative_elf_rpaths(
'/usr/bin/test', '/usr',
('/usr/lib', '/usr/lib64', '/opt/local/lib'))
assert out == ['$ORIGIN/../lib', '$ORIGIN/../lib64', '/opt/local/lib']
out = substitute_rpath(
('/usr/lib', '/usr/lib64', '/opt/local/lib'), '/usr', '/opt')
assert out == ['/opt/lib', '/opt/lib64', '/opt/local/lib']
@pytest.mark.skipif(sys.platform != 'darwin',
reason="only works with Mach-o objects")
def test_relocate_macho(tmpdir):
with tmpdir.as_cwd():
get_patchelf() # this does nothing on Darwin
rpaths, deps, idpath = macho_get_paths('/bin/bash')
nrpaths, ndeps, nid = macho_make_paths_relative('/bin/bash', '/usr',
rpaths, deps, idpath)
shutil.copyfile('/bin/bash', 'bash')
modify_macho_object('bash',
rpaths, deps, idpath,
nrpaths, ndeps, nid)
rpaths, deps, idpath = macho_get_paths('/bin/bash')
nrpaths, ndeps, nid = macho_replace_paths('/usr', '/opt',
rpaths, deps, idpath)
shutil.copyfile('/bin/bash', 'bash')
modify_macho_object('bash',
rpaths, deps, idpath,
nrpaths, ndeps, nid)
path = '/usr/lib/libncurses.5.4.dylib'
rpaths, deps, idpath = macho_get_paths(path)
nrpaths, ndeps, nid = macho_make_paths_relative(path, '/usr',
rpaths, deps, idpath)
shutil.copyfile(
'/usr/lib/libncurses.5.4.dylib', 'libncurses.5.4.dylib')
modify_macho_object('libncurses.5.4.dylib',
rpaths, deps, idpath,
nrpaths, ndeps, nid)
rpaths, deps, idpath = macho_get_paths(path)
nrpaths, ndeps, nid = macho_replace_paths('/usr', '/opt',
rpaths, deps, idpath)
shutil.copyfile(
'/usr/lib/libncurses.5.4.dylib', 'libncurses.5.4.dylib')
modify_macho_object(
'libncurses.5.4.dylib',
rpaths, deps, idpath,
nrpaths, ndeps, nid)