summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorPatrick Gartung <gartung@fnal.gov>2017-08-14 16:32:27 -0500
committerscheibelp <scheibel1@llnl.gov>2017-08-14 14:32:27 -0700
commitab56c742ca1768fef484a02bfb542d6df449c1f6 (patch)
tree104be454c48ed4240dc0a2dfb59d834aca5fd1e1 /lib
parent5184edb15f2cf260d561d8fcb7bf56ad147b6555 (diff)
downloadspack-ab56c742ca1768fef484a02bfb542d6df449c1f6.tar.gz
spack-ab56c742ca1768fef484a02bfb542d6df449c1f6.tar.bz2
spack-ab56c742ca1768fef484a02bfb542d6df449c1f6.tar.xz
spack-ab56c742ca1768fef484a02bfb542d6df449c1f6.zip
Create, install and relocate tarballs of installed packages
Adds the "buildcache" command to spack. The buildcache command is used to create gpg signatures for archives of installed spack packages; the signatures and archives are placed together in a directory that can be added to a spack mirror. A user can retrieve the archives from a mirror and verify their integrity using the buildcache command. It is often the case that the user's Spack instance is located in a different path compared to the Spack instance used to generate the package archive and signature, so this includes logic to relocate the RPATHs generated by Spack.
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/docs/binary_caches.rst108
-rw-r--r--lib/spack/docs/index.rst1
-rw-r--r--lib/spack/spack/binary_distribution.py494
-rw-r--r--lib/spack/spack/cmd/buildcache.py230
-rw-r--r--lib/spack/spack/relocate.py289
-rw-r--r--lib/spack/spack/test/packaging.py304
6 files changed, 1426 insertions, 0 deletions
diff --git a/lib/spack/docs/binary_caches.rst b/lib/spack/docs/binary_caches.rst
new file mode 100644
index 0000000000..d655d2ce58
--- /dev/null
+++ b/lib/spack/docs/binary_caches.rst
@@ -0,0 +1,108 @@
+.. _binary_caches:
+
+Build caches
+============
+
+Some sites may encourage users to set up their own test environments
+before carrying out central installations, or some users prefer to set
+up these environments on their own motivation. To reduce the load of
+recompiling otherwise identical package specs in different installations,
+installed packages can be put into build cache tarballs, uploaded to
+your spack mirror and then downloaded and installed by others.
+
+
+Creating build cache files
+--------------------------
+
+A compressed tarball of an installed package is created. Tarballs are created
+for all of its link and run dependency packages as well. Compressed tarballs are
+signed with gpg and signature and tarball and put in a ".spack" file. Optionally
+, the rpaths ( and ids and deps on macOS ) can be changed to paths relative to
+the spack install tree before the tarball is created.
+
+Build caches are created via:
+
+.. code-block:: sh
+
+ $ spack buildcache create
+
+
+Finding or installing build cache files
+---------------------------------------
+
+To find build caches or install build caches, a spack mirror must be configured
+with
+
+``spack mirror add <name> <url>``.
+
+Build caches are found via:
+
+.. code-block:: sh
+
+ $ spack buildcache list
+
+Build caches are installed via:
+
+.. code-block:: sh
+
+ $ spack buildcache install
+
+
+Relocation
+----------
+
+Initial build and later installation do not necessarily happen at the same
+location. Spack provides a relocation capability and corrects for RPATHs and
+non-relocatable scripts. However, many packages compile paths into binary
+artificats directly. In such cases, the build instructions of this package would
+need to be adjusted for better re-locatability.
+
+
+Usage
+-----
+spack buildcache create <>
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+Create tarball of installed spack package and all dependencies.
+Tarballs is checksummed and signed if gpg2 is available.
+Places them in a directory build_cache that can be copied to a mirror.
+Commands like "spack buildcache install" will search it for pre-compiled packages.
+
+
+options:
+
+-d <path> : directory in which "build_cache" direcory is created, defaults to "."
+-f : overwrite ".spack" file in "build_cache" directory if it exists
+-k <key> : the key to sign package with. In the case where multiple keys exist, the package will be unsigned unless -k is used.
+-r : make paths in binaries relative before creating tarball
+-y : answer yes to all create unsigned "build_cache" questions
+<> : list of package specs or package hashes with leading /
+
+spack buildcache list <>
+^^^^^^^^^^^^^^^^^^^^^^^^
+Retrieves all specs for build caches available on a spack mirror.
+
+options:
+
+<> string to be matched to matched to begining of listed concretized short
+specs, eg. "spack buildcache list gcc" with print only commands to install gcc
+package(s)
+
+spack buildcache install <>
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Retrieves all specs for build caches available on a spack mirror and installs build caches
+with specs matching the specs or hashes input.
+
+options:
+
+-f : remove install directory if it exists before unpacking tarball
+-y : answer yes to all to don't verify package with gpg questions
+<> : list of package specs or package hashes with leading /
+
+spack buildcache keys
+^^^^^^^^^^^^^^^^^^^^^
+List public keys available on spack mirror.
+
+options:
+
+-i : trust the keys downloaded with prompt for each
+-y : answer yes to all trust all keys downloaded
diff --git a/lib/spack/docs/index.rst b/lib/spack/docs/index.rst
index f4918b58db..e86ab0ca85 100644
--- a/lib/spack/docs/index.rst
+++ b/lib/spack/docs/index.rst
@@ -65,6 +65,7 @@ or refer to the full manual below.
repositories
command_index
package_list
+ binary_caches
.. toctree::
:maxdepth: 2
diff --git a/lib/spack/spack/binary_distribution.py b/lib/spack/spack/binary_distribution.py
new file mode 100644
index 0000000000..e2ae89ac19
--- /dev/null
+++ b/lib/spack/spack/binary_distribution.py
@@ -0,0 +1,494 @@
+##############################################################################
+# Copyright (c) 2013-2016, Lawrence Livermore National Security, LLC.
+# Produced at the Lawrence Livermore National Laboratory.
+#
+# This file is part of Spack.
+# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
+# LLNL-CODE-647188
+#
+# For details, see https://github.com/llnl/spack
+# Please also see the NOTICE and LICENSE files for our notice and the LGPL.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License (as
+# published by the Free Software Foundation) version 2.1, February 1999.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and
+# conditions of the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##############################################################################
+
+import os
+import re
+import tarfile
+import yaml
+import shutil
+
+import llnl.util.tty as tty
+from spack.util.gpg import Gpg
+from llnl.util.filesystem import mkdirp, join_path, install_tree
+from spack.util.web import spider
+import spack.cmd
+import spack
+from spack.stage import Stage
+import spack.fetch_strategy as fs
+from contextlib import closing
+import spack.util.gpg as gpg_util
+import hashlib
+from spack.util.executable import ProcessError
+import spack.relocate as relocate
+
+
+class NoOverwriteException(Exception):
+ pass
+
+
+class NoGpgException(Exception):
+ pass
+
+
+class PickKeyException(Exception):
+ pass
+
+
+class NoKeyException(Exception):
+ pass
+
+
+class NoVerifyException(Exception):
+ pass
+
+
+class NoChecksumException(Exception):
+ pass
+
+
+def has_gnupg2():
+ try:
+ gpg_util.Gpg.gpg()('--version', output=os.devnull)
+ return True
+ except ProcessError:
+ return False
+
+
+def buildinfo_file_name(prefix):
+ """
+ Filename of the binary package meta-data file
+ """
+ name = prefix + "/.spack/binary_distribution"
+ return name
+
+
+def read_buildinfo_file(prefix):
+ """
+ Read buildinfo file
+ """
+ filename = buildinfo_file_name(prefix)
+ with open(filename, 'r') as inputfile:
+ content = inputfile.read()
+ buildinfo = yaml.load(content)
+ return buildinfo
+
+
+def write_buildinfo_file(prefix):
+ """
+ Create a cache file containing information
+ required for the relocation
+ """
+ text_to_relocate = []
+ binary_to_relocate = []
+ blacklist = (".spack", "man")
+ # 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):
+ dirs[:] = [d for d in dirs if d not in blacklist]
+ for filename in files:
+ path_name = os.path.join(root, filename)
+ filetype = relocate.get_filetype(path_name)
+ if relocate.needs_binary_relocation(filetype):
+ rel_path_name = os.path.relpath(path_name, prefix)
+ binary_to_relocate.append(rel_path_name)
+ elif relocate.needs_text_relocation(filetype):
+ rel_path_name = os.path.relpath(path_name, prefix)
+ text_to_relocate.append(rel_path_name)
+
+ # Create buildinfo data and write it to disk
+ buildinfo = {}
+ buildinfo['buildpath'] = spack.store.layout.root
+ buildinfo['relocate_textfiles'] = text_to_relocate
+ buildinfo['relocate_binaries'] = binary_to_relocate
+ filename = buildinfo_file_name(prefix)
+ with open(filename, 'w') as outfile:
+ outfile.write(yaml.dump(buildinfo, default_flow_style=True))
+
+
+def tarball_directory_name(spec):
+ """
+ Return name of the tarball directory according to the convention
+ <os>-<architecture>/<compiler>/<package>-<version>/
+ """
+ return "%s/%s/%s-%s" % (spack.architecture.sys_type(),
+ str(spec.compiler).replace("@", "-"),
+ spec.name, spec.version)
+
+
+def tarball_name(spec, ext):
+ """
+ Return the name of the tarfile according to the convention
+ <os>-<architecture>-<package>-<dag_hash><ext>
+ """
+ return "%s-%s-%s-%s-%s%s" % (spack.architecture.sys_type(),
+ str(spec.compiler).replace("@", "-"),
+ spec.name,
+ spec.version,
+ spec.dag_hash(),
+ ext)
+
+
+def tarball_path_name(spec, ext):
+ """
+ Return the full path+name for a given spec according to the convention
+ <tarball_directory_name>/<tarball_name>
+ """
+ return os.path.join(tarball_directory_name(spec),
+ tarball_name(spec, ext))
+
+
+def checksum_tarball(file):
+ # calculate sha256 hash of tar file
+ BLOCKSIZE = 65536
+ hasher = hashlib.sha256()
+ with open(file, 'rb') as tfile:
+ buf = tfile.read(BLOCKSIZE)
+ while len(buf) > 0:
+ hasher.update(buf)
+ buf = tfile.read(BLOCKSIZE)
+ return hasher.hexdigest()
+
+
+def sign_tarball(yes_to_all, key, force, specfile_path):
+ # Sign the packages if keys available
+ if not has_gnupg2():
+ raise NoGpgException()
+ else:
+ if key is None:
+ keys = Gpg.signing_keys()
+ if len(keys) == 1:
+ key = keys[0]
+ if len(keys) > 1:
+ raise PickKeyException()
+ if len(keys) == 0:
+ raise NoKeyException()
+ if os.path.exists('%s.asc' % specfile_path):
+ if force:
+ os.remove('%s.asc' % specfile_path)
+ else:
+ raise NoOverwriteException('%s.asc' % specfile_path)
+ Gpg.sign(key, specfile_path, '%s.asc' % specfile_path)
+
+
+def generate_index(outdir, indexfile_path):
+ f = open(indexfile_path, 'w')
+ header = """<html>
+<head></head>
+<list>"""
+ footer = "</list></html>"
+ paths = os.listdir(outdir + '/build_cache')
+ f.write(header)
+ for path in paths:
+ rel = os.path.basename(path)
+ f.write('<li><a href="%s" %s</a>' % (rel, rel))
+ f.write(footer)
+ f.close()
+
+
+def build_tarball(spec, outdir, force=False, rel=False, yes_to_all=False,
+ key=None):
+ """
+ Build a tarball from given spec and put it into the directory structure
+ used at the mirror (following <tarball_directory_name>).
+ """
+ # set up some paths
+ tarfile_name = tarball_name(spec, '.tar.gz')
+ tarfile_dir = join_path(outdir, "build_cache",
+ tarball_directory_name(spec))
+ tarfile_path = join_path(tarfile_dir, tarfile_name)
+ mkdirp(tarfile_dir)
+ spackfile_path = os.path.join(
+ outdir, "build_cache", tarball_path_name(spec, '.spack'))
+ if os.path.exists(spackfile_path):
+ if force:
+ os.remove(spackfile_path)
+ else:
+ raise NoOverwriteException(str(spackfile_path))
+ # need to copy the spec file so the build cache can be downloaded
+ # without concretizing with the current spack packages
+ # and preferences
+ spec_file = join_path(spec.prefix, ".spack", "spec.yaml")
+ specfile_name = tarball_name(spec, '.spec.yaml')
+ specfile_path = os.path.realpath(
+ join_path(outdir, "build_cache", specfile_name))
+ indexfile_path = join_path(outdir, "build_cache", "index.html")
+ if os.path.exists(specfile_path):
+ if force:
+ os.remove(specfile_path)
+ else:
+ raise NoOverwriteException(str(specfile_path))
+ # make a copy of the install directory to work with
+ prefix = join_path(outdir, os.path.basename(spec.prefix))
+ if os.path.exists(prefix):
+ shutil.rmtree(prefix)
+ install_tree(spec.prefix, prefix)
+
+ # create info for later relocation and create tar
+ write_buildinfo_file(prefix)
+
+ # optinally make the paths in the binaries relative to each other
+ # in the spack install tree before creating tarball
+ if rel:
+ make_package_relative(prefix)
+ # create compressed tarball of the install prefix
+ with closing(tarfile.open(tarfile_path, 'w:gz')) as tar:
+ tar.add(name='%s' % prefix,
+ arcname='%s' % os.path.basename(prefix))
+ # remove copy of install directory
+ shutil.rmtree(prefix)
+
+ # get the sha256 checksum of the tarball
+ checksum = checksum_tarball(tarfile_path)
+
+ # add sha256 checksum to spec.yaml
+ spec_dict = {}
+ with open(spec_file, 'r') as inputfile:
+ content = inputfile.read()
+ spec_dict = yaml.load(content)
+ bchecksum = {}
+ bchecksum['hash_algorithm'] = 'sha256'
+ bchecksum['hash'] = checksum
+ spec_dict['binary_cache_checksum'] = bchecksum
+ with open(specfile_path, 'w') as outfile:
+ outfile.write(yaml.dump(spec_dict))
+ signed = False
+ if not yes_to_all:
+ # sign the tarball and spec file with gpg
+ try:
+ sign_tarball(yes_to_all, key, force, specfile_path)
+ signed = True
+ except NoGpgException:
+ raise NoGpgException()
+ except PickKeyException:
+ raise PickKeyException()
+ except NoKeyException():
+ raise NoKeyException()
+ # put tarball, spec and signature files in .spack archive
+ with closing(tarfile.open(spackfile_path, 'w')) as tar:
+ tar.add(name='%s' % tarfile_path, arcname='%s' % tarfile_name)
+ tar.add(name='%s' % specfile_path, arcname='%s' % specfile_name)
+ if signed:
+ tar.add(name='%s.asc' % specfile_path,
+ arcname='%s.asc' % specfile_name)
+
+ # cleanup file moved to archive
+ os.remove(tarfile_path)
+ if signed:
+ os.remove('%s.asc' % specfile_path)
+
+ # create an index.html for the build_cache directory so specs can be found
+ if os.path.exists(indexfile_path):
+ os.remove(indexfile_path)
+ generate_index(outdir, indexfile_path)
+ return None
+
+
+def download_tarball(spec):
+ """
+ Download binary tarball for given package into stage area
+ Return True if successful
+ """
+ mirrors = spack.config.get_config('mirrors')
+ if len(mirrors) == 0:
+ tty.die("Please add a spack mirror to allow " +
+ "download of pre-compiled packages.")
+ tarball = tarball_path_name(spec, '.spack')
+ for key in mirrors:
+ url = mirrors[key] + "/build_cache/" + tarball
+ # stage the tarball into standard place
+ stage = Stage(url, name="build_cache", keep=True)
+ try:
+ stage.fetch()
+ return stage.save_filename
+ except fs.FetchError:
+ continue
+ return None
+
+
+def make_package_relative(prefix):
+ """
+ Change paths in binaries to relative paths
+ """
+ buildinfo = read_buildinfo_file(prefix)
+ old_path = buildinfo['buildpath']
+ for filename in buildinfo['relocate_binaries']:
+ path_name = os.path.join(prefix, filename)
+ relocate.make_binary_relative(path_name, old_path)
+
+
+def relocate_package(prefix):
+ """
+ Relocate the given package
+ """
+ buildinfo = read_buildinfo_file(prefix)
+ new_path = spack.store.layout.root
+ old_path = buildinfo['buildpath']
+ if new_path == old_path:
+ return
+
+ tty.msg("Relocating package from",
+ "%s to %s." % (old_path, new_path))
+ for filename in buildinfo['relocate_binaries']:
+ path_name = os.path.join(prefix, filename)
+ relocate.relocate_binary(path_name, old_path, new_path)
+
+ for filename in buildinfo['relocate_textfiles']:
+ path_name = os.path.join(prefix, filename)
+ relocate.relocate_text(path_name, old_path, new_path)
+
+
+def extract_tarball(spec, filename, yes_to_all=False, force=False):
+ """
+ extract binary tarball for given package into install area
+ """
+ installpath = spec.prefix
+ if os.path.exists(installpath):
+ if force:
+ shutil.rmtree(installpath)
+ else:
+ raise NoOverwriteException(str(installpath))
+ mkdirp(installpath)
+ stagepath = os.path.dirname(filename)
+ spackfile_name = tarball_name(spec, '.spack')
+ spackfile_path = os.path.join(stagepath, spackfile_name)
+ tarfile_name = tarball_name(spec, '.tar.gz')
+ tarfile_path = os.path.join(stagepath, tarfile_name)
+ specfile_name = tarball_name(spec, '.spec.yaml')
+ specfile_path = os.path.join(stagepath, specfile_name)
+
+ with closing(tarfile.open(spackfile_path, 'r')) as tar:
+ tar.extractall(stagepath)
+
+ if os.path.exists('%s.asc' % specfile_path):
+ Gpg.verify('%s.asc' % specfile_path, specfile_path)
+ os.remove(specfile_path + '.asc')
+ else:
+ if not yes_to_all:
+ raise NoVerifyException()
+
+ # get the sha256 checksum of the tarball
+ checksum = checksum_tarball(tarfile_path)
+
+ # get the sha256 checksum recorded at creation
+ spec_dict = {}
+ with open(specfile_path, 'r') as inputfile:
+ content = inputfile.read()
+ spec_dict = yaml.load(content)
+ bchecksum = spec_dict['binary_cache_checksum']
+
+ # if the checksums don't match don't install
+ if bchecksum['hash'] != checksum:
+ raise NoChecksumException()
+
+ with closing(tarfile.open(tarfile_path, 'r')) as tar:
+ tar.extractall(path=join_path(installpath, '..'))
+
+ os.remove(tarfile_path)
+ os.remove(specfile_path)
+ relocate_package(installpath)
+
+
+def get_specs():
+ """
+ Get spec.yaml's for build caches available on mirror
+ """
+ mirrors = spack.config.get_config('mirrors')
+ if len(mirrors) == 0:
+ tty.die("Please add a spack mirror to allow " +
+ "download of build caches.")
+ path = str(spack.architecture.sys_type())
+ specs = set()
+ urls = set()
+ from collections import defaultdict
+ durls = defaultdict(list)
+ for key in mirrors:
+ url = mirrors[key]
+ if url.startswith('file'):
+ mirror = url.replace('file://', '') + '/build_cache'
+ tty.msg("Finding buildcaches in %s" % mirror)
+ files = os.listdir(mirror)
+ for file in files:
+ if re.search('spec.yaml', file):
+ link = 'file://' + mirror + '/' + file
+ urls.add(link)
+ else:
+ tty.msg("Finding buildcaches on %s" % url)
+ p, links = spider(url + "/build_cache")
+ for link in links:
+ if re.search("spec.yaml", link) and re.search(path, link):
+ urls.add(link)
+ for link in urls:
+ with Stage(link, name="build_cache", keep=True) as stage:
+ try:
+ stage.fetch()
+ except fs.FetchError:
+ continue
+ with open(stage.save_filename, 'r') as f:
+ spec = spack.spec.Spec.from_yaml(f)
+ specs.add(spec)
+ durls[spec].append(link)
+ return specs, durls
+
+
+def get_keys(install=False, yes_to_all=False):
+ """
+ Get pgp public keys available on mirror
+ """
+ mirrors = spack.config.get_config('mirrors')
+ if len(mirrors) == 0:
+ tty.die("Please add a spack mirror to allow " +
+ "download of build caches.")
+
+ keys = set()
+ for key in mirrors:
+ url = mirrors[key]
+ if url.startswith('file'):
+ mirror = url.replace('file://', '') + '/build_cache'
+ tty.msg("Finding public keys in %s" % mirror)
+ files = os.listdir(mirror)
+ for file in files:
+ if re.search('\.key', file):
+ link = 'file://' + mirror + '/' + file
+ keys.add(link)
+ else:
+ tty.msg("Finding public keys on %s" % url)
+ p, links = spider(url + "/build_cache", depth=1)
+ for link in links:
+ if re.search("\.key", link):
+ keys.add(link)
+ for link in keys:
+ with Stage(link, name="build_cache", keep=True) as stage:
+ try:
+ stage.fetch()
+ except fs.FetchError:
+ continue
+ tty.msg('Found key %s' % link)
+ if install:
+ if yes_to_all:
+ Gpg.trust(stage.save_filename)
+ tty.msg('Added this key to trusted keys.')
+ else:
+ tty.msg('Will not add this key to trusted keys.'
+ 'Use -y to override')
diff --git a/lib/spack/spack/cmd/buildcache.py b/lib/spack/spack/cmd/buildcache.py
new file mode 100644
index 0000000000..213442acef
--- /dev/null
+++ b/lib/spack/spack/cmd/buildcache.py
@@ -0,0 +1,230 @@
+##############################################################################
+# Copyright (c) 2013, Lawrence Livermore National Security, LLC.
+# Produced at the Lawrence Livermore National Laboratory.
+#
+# This file is part of Spack.
+# Written by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
+# LLNL-CODE-647188
+#
+# For details, see https://github.com/llnl/spack
+# Please also see the LICENSE file for our notice and the LGPL.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License (as published by
+# the Free Software Foundation) version 2.1 dated February 1999.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and
+# conditions of the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##############################################################################
+import argparse
+
+import os
+import re
+import llnl.util.tty as tty
+
+import spack
+import spack.cmd
+import spack.binary_distribution as bindist
+from spack.binary_distribution import NoOverwriteException, NoGpgException
+from spack.binary_distribution import NoKeyException, PickKeyException
+from spack.binary_distribution import NoVerifyException, NoChecksumException
+
+description = "Create, download and install build cache files."
+section = "caching"
+level = "long"
+
+
+def setup_parser(subparser):
+ setup_parser.parser = subparser
+ subparsers = subparser.add_subparsers(help='buildcache sub-commands')
+
+ create = subparsers.add_parser('create')
+ create.add_argument('-r', '--rel', action='store_true',
+ help="make all rpaths relative" +
+ " before creating tarballs.")
+ create.add_argument('-f', '--force', action='store_true',
+ help="overwrite tarball if it exists.")
+ create.add_argument('-y', '--yes-to-all', action='store_true',
+ help="answer yes to all create unsigned " +
+ "buildcache questions")
+ create.add_argument('-k', '--key', metavar='key',
+ type=str, default=None,
+ help="Key for signing.")
+ create.add_argument('-d', '--directory', metavar='directory',
+ type=str, default='.',
+ help="directory in which to save the tarballs.")
+ create.add_argument(
+ 'packages', nargs=argparse.REMAINDER,
+ help="specs of packages to create buildcache for")
+ create.set_defaults(func=createtarball)
+
+ install = subparsers.add_parser('install')
+ install.add_argument('-f', '--force', action='store_true',
+ help="overwrite install directory if it exists.")
+ install.add_argument('-y', '--yes-to-all', action='store_true',
+ help="answer yes to all install unsigned " +
+ "buildcache questions")
+ install.add_argument(
+ 'packages', nargs=argparse.REMAINDER,
+ help="specs of packages to install biuldache for")
+ install.set_defaults(func=installtarball)
+
+ listcache = subparsers.add_parser('list')
+ listcache.add_argument(
+ 'packages', nargs=argparse.REMAINDER,
+ help="specs of packages to search for")
+ listcache.set_defaults(func=listspecs)
+
+ dlkeys = subparsers.add_parser('keys')
+ dlkeys.add_argument(
+ '-i', '--install', action='store_true',
+ help="install Keys pulled from mirror")
+ dlkeys.add_argument(
+ '-y', '--yes-to-all', action='store_true',
+ help="answer yes to all trust questions")
+ dlkeys.set_defaults(func=getkeys)
+
+
+def createtarball(args):
+ if not args.packages:
+ tty.die("build cache file creation requires at least one" +
+ " installed package argument")
+ pkgs = set(args.packages)
+ specs = set()
+ outdir = os.getcwd()
+ if args.directory:
+ outdir = args.directory
+ signkey = None
+ if args.key:
+ signkey = args.key
+ yes_to_all = False
+ force = False
+ relative = False
+ if args.yes_to_all:
+ yes_to_all = True
+ if args.force:
+ force = True
+ if args.rel:
+ relative = True
+ for pkg in pkgs:
+ for spec in spack.cmd.parse_specs(pkg, concretize=True):
+ specs.add(spec)
+ tty.msg('recursing dependencies')
+ for d, node in spec.traverse(order='post',
+ depth=True,
+ deptype=('link', 'run'),
+ deptype_query='run'):
+ if not node.external:
+ tty.msg('adding dependency %s' % node.format())
+ specs.add(node)
+ for spec in specs:
+ tty.msg('creating binary cache file for package %s ' % spec.format())
+ try:
+ bindist.build_tarball(spec, outdir, force,
+ relative, yes_to_all, signkey)
+ except NoOverwriteException as e:
+ tty.warn("%s exists, use -f to force overwrite." % e)
+ except NoGpgException:
+ tty.warn("gpg2 is not available,"
+ " use -y to create unsigned build caches")
+ except NoKeyException:
+ tty.warn("no default key available for signing,"
+ " use -y to create unsigned build caches"
+ " or spack gpg init to create a default key")
+ except PickKeyException:
+ tty.warn("multi keys available for signing,"
+ " use -y to create unsigned build caches"
+ " or -k <key hash> to pick a key")
+
+
+def installtarball(args):
+ if not args.packages:
+ tty.die("build cache file installation requires" +
+ " at least one package spec argument")
+ pkgs = set(args.packages)
+ specs, links = bindist.get_specs()
+ matches = set()
+ for spec in specs:
+ for pkg in pkgs:
+ if re.match(re.escape(pkg), str(spec)):
+ matches.add(spec)
+ if re.match(re.escape(pkg), '/%s' % spec.dag_hash()):
+ matches.add(spec)
+
+ for match in matches:
+ install_tarball(match, args)
+
+
+def install_tarball(spec, args):
+ s = spack.spec.Spec(spec)
+ yes_to_all = False
+ force = False
+ if args.yes_to_all:
+ yes_to_all = True
+ if args.force:
+ force = True
+ for d in s.dependencies():
+ tty.msg("Installing buildcache for dependency spec %s" % d)
+ install_tarball(d, args)
+ package = spack.repo.get(spec)
+ if s.concrete and package.installed and not force:
+ tty.warn("Package for spec %s already installed." % spec.format(),
+ " Use -f flag to overwrite.")
+ else:
+ tarball = bindist.download_tarball(spec)
+ if tarball:
+ tty.msg('Installing buildcache for spec %s' % spec.format())
+ try:
+ bindist.extract_tarball(spec, tarball, yes_to_all, force)
+ except NoOverwriteException as e:
+ tty.warn("%s exists. use -f to force overwrite." % e.args)
+ except NoVerifyException:
+ tty.die("Package spec file failed signature verification,"
+ " use -y flag to install build cache")
+ except NoChecksumException:
+ tty.die("Package tarball failed checksum verification,"
+ " use -y flag to install build cache")
+ finally:
+ spack.store.db.reindex(spack.store.layout)
+ else:
+ tty.die('Download of binary cache file for spec %s failed.' %
+ spec.format())
+
+
+def listspecs(args):
+ specs, links = bindist.get_specs()
+ if args.packages:
+ pkgs = set(args.packages)
+ for pkg in pkgs:
+ tty.msg("buildcache spec(s) matching %s \n" % pkg)
+ for spec in sorted(specs):
+ if re.search("^" + re.escape(pkg), str(spec)):
+ tty.msg('run "spack buildcache install /%s"' %
+ spec.dag_hash(7) + ' to install %s\n' %
+ spec.format())
+ else:
+ tty.msg("buildcache specs ")
+ for spec in sorted(specs):
+ tty.msg('run "spack buildcache install /%s" to install %s\n' %
+ (spec.dag_hash(7), spec.format()))
+
+
+def getkeys(args):
+ install = False
+ if args.install:
+ install = True
+ yes_to_all = False
+ if args.yes_to_all:
+ yes_to_all = True
+ bindist.get_keys(install, yes_to_all)
+
+
+def buildcache(parser, args):
+ if args.func:
+ args.func(args)
diff --git a/lib/spack/spack/relocate.py b/lib/spack/spack/relocate.py
new file mode 100644
index 0000000000..b4251d05ca
--- /dev/null
+++ b/lib/spack/spack/relocate.py
@@ -0,0 +1,289 @@
+##############################################################################
+# Copyright (c) 2013-2016, Lawrence Livermore National Security, LLC.
+# Produced at the Lawrence Livermore National Laboratory.
+#
+# This file is part of Spack.
+# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
+# LLNL-CODE-647188
+#
+# For details, see https://github.com/llnl/spack
+# Please also see the NOTICE and LICENSE files for our notice and the LGPL.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License (as
+# published by the Free Software Foundation) version 2.1, February 1999.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and
+# conditions of the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##############################################################################
+
+import os
+import platform
+import re
+import spack
+import spack.cmd
+from spack.util.executable import Executable
+from llnl.util.filesystem import filter_file
+import llnl.util.tty as tty
+
+
+def get_patchelf():
+ """
+ Builds and installs spack patchelf package on linux platforms
+ using the first concretized spec.
+ Returns the full patchelf binary path.
+ """
+ # as we may need patchelf, find out where it is
+ if platform.system() == '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()
+ patchelf_executable = os.path.join(patchelf.prefix.bin, "patchelf")
+ return patchelf_executable
+
+
+def get_existing_elf_rpaths(path_name):
+ """
+ Return the RPATHS returned by patchelf --print-rpath path_name
+ as a list of strings.
+ """
+ if platform.system() == 'Linux':
+ command = Executable(get_patchelf())
+ output = command('--print-rpath', '%s' %
+ path_name, output=str, err=str)
+ return output.rstrip('\n').split(':')
+ else:
+ tty.die('relocation not supported for this platform')
+ return
+
+
+def get_relative_rpaths(path_name, orig_dir, orig_rpaths):
+ """
+ Replaces orig_dir with relative path from dirname(path_name) if an rpath
+ in orig_rpaths contains orig_path. Prefixes $ORIGIN
+ to relative paths and returns replacement rpaths.
+ """
+ rel_rpaths = []
+ for rpath in orig_rpaths:
+ if re.match(orig_dir, rpath):
+ rel = os.path.relpath(rpath, start=os.path.dirname(path_name))
+ rel_rpaths.append('$ORIGIN/%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 = ''
+ 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('(.*) \(.*\)', 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/basebane(path_name);
+ replacement are returned.
+ """
+ id = None
+ nrpaths = []
+ ndeps = []
+ if idpath:
+ id = '@rpath/%s' % os.path.basename(idpath)
+ for rpath in rpaths:
+ if re.match(old_dir, rpath):
+ rel = os.path.relpath(rpath, start=os.path.dirname(path_name))
+ nrpaths.append('@loader_path/%s' % rel)
+ else:
+ nrpaths.append(rpath)
+ for dep in deps:
+ if re.match(old_dir, dep):
+ rel = os.path.relpath(dep, start=os.path.dirname(path_name))
+ ndeps.append('@loader_path/%s' % rel)
+ else:
+ ndeps.append(dep)
+ return nrpaths, ndeps, id
+
+
+def macho_replace_paths(old_dir, new_dir, rpaths, deps, idpath):
+ """
+ Replace old_dir with new_dir in rpaths, deps and idpath
+ and return replacements
+ """
+ id = None
+ nrpaths = []
+ ndeps = []
+ if idpath:
+ id = idpath.replace(old_dir, new_dir)
+ for rpath in rpaths:
+ nrpath = rpath.replace(old_dir, new_dir)
+ nrpaths.append(nrpath)
+ for dep in deps:
+ ndep = dep.replace(old_dir, new_dir)
+ ndeps.append(ndep)
+ return nrpaths, ndeps, id
+
+
+def modify_macho_object(path_name, old_dir, new_dir, relative):
+ """
+ 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
+ """
+ # avoid error message for libgcc_s
+ if 'libgcc_' in path_name:
+ return
+ rpaths, deps, idpath = macho_get_paths(path_name)
+ id = None
+ nrpaths = []
+ ndeps = []
+ if relative:
+ nrpaths, ndeps, id = macho_make_paths_relative(path_name,
+ old_dir, rpaths,
+ deps, idpath)
+ else:
+ nrpaths, ndeps, id = macho_replace_paths(old_dir, new_dir, rpaths,
+ deps, idpath)
+ install_name_tool = Executable('install_name_tool')
+ if id:
+ install_name_tool('-id', id, path_name, output=str, err=str)
+
+ for orig, new in zip(deps, ndeps):
+ install_name_tool('-change', orig, new, path_name)
+
+ for orig, new in zip(rpaths, nrpaths):
+ install_name_tool('-rpath', orig, new, path_name)
+ return
+
+
+def get_filetype(path_name):
+ """
+ Return the output of file path_name as a string to identify file type.
+ """
+ file = Executable('file')
+ file.add_default_env('LC_ALL', 'C')
+ output = file('-b', '-h', '%s' % path_name,
+ output=str, err=str)
+ return output.strip()
+
+
+def modify_elf_object(path_name, orig_rpath, new_rpath):
+ """
+ Replace orig_rpath with new_rpath in RPATH of elf object path_name
+ """
+ if platform.system() == 'Linux':
+ new_joined = ':'.join(new_rpath)
+ patchelf = Executable(get_patchelf())
+ patchelf('--force-rpath', '--set-rpath', '%s' % new_joined,
+ '%s' % path_name, output=str, cmd=str)
+ else:
+ tty.die('relocation not supported for this platform')
+
+
+def needs_binary_relocation(filetype):
+ """
+ Check whether the given filetype is a binary that may need relocation.
+ """
+ retval = False
+ if "relocatable" in filetype:
+ return False
+ if platform.system() == 'Darwin':
+ return ('Mach-O' in filetype)
+ elif platform.system() == 'Linux':
+ return ('ELF' in filetype)
+ else:
+ tty.die("Relocation not implemented for %s" % platform.system())
+ return retval
+
+
+def needs_text_relocation(filetype):
+ """
+ Check whether the given filetype is text that may need relocation.
+ """
+ return ("text" in filetype)
+
+
+def relocate_binary(path_name, old_dir, new_dir):
+ """
+ Change old_dir to new_dir in RPATHs of elf or mach-o file path_name
+ """
+ if platform.system() == 'Darwin':
+ modify_macho_object(path_name, old_dir, new_dir, relative=False)
+ elif platform.system() == 'Linux':
+ orig_rpaths = get_existing_elf_rpaths(path_name)
+ new_rpaths = substitute_rpath(orig_rpaths, old_dir, new_dir)
+ modify_elf_object(path_name, orig_rpaths, new_rpaths)
+ else:
+ tty.die("Relocation not implemented for %s" % platform.system())
+
+
+def make_binary_relative(path_name, old_dir):
+ """
+ Make RPATHs relative to old_dir in given elf or mach-o file path_name
+ """
+ if platform.system() == 'Darwin':
+ new_dir = ''
+ modify_macho_object(path_name, old_dir, new_dir, relative=True)
+ elif platform.system() == 'Linux':
+ orig_rpaths = get_existing_elf_rpaths(path_name)
+ new_rpaths = get_relative_rpaths(path_name, old_dir, orig_rpaths)
+ modify_elf_object(path_name, orig_rpaths, new_rpaths)
+ else:
+ tty.die("Prelocation not implemented for %s" % platform.system())
+
+
+def relocate_text(path_name, old_dir, new_dir):
+ """
+ Replace old path with new path in text file path_name
+ """
+ filter_file("r'%s'" % old_dir, "r'%s'" % new_dir, path_name)
+
+
+def substitute_rpath(orig_rpath, topdir, new_root_path):
+ """
+ Replace topdir with new_root_path RPATH list orig_rpath
+ """
+ new_rpaths = []
+ for path in orig_rpath:
+ new_rpath = path.replace(topdir, new_root_path)
+ new_rpaths.append(new_rpath)
+ return new_rpaths
diff --git a/lib/spack/spack/test/packaging.py b/lib/spack/spack/test/packaging.py
new file mode 100644
index 0000000000..bac8bf980f
--- /dev/null
+++ b/lib/spack/spack/test/packaging.py
@@ -0,0 +1,304 @@
+##############################################################################
+# Copyright (c) 2013-2016, Lawrence Livermore National Security, LLC.
+# Produced at the Lawrence Livermore National Laboratory.
+#
+# This file is part of Spack.
+# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
+# LLNL-CODE-647188
+#
+# For details, see https://github.com/llnl/spack
+# Please also see the NOTICE and LICENSE files for our notice and the LGPL.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License (as
+# published by the Free Software Foundation) version 2.1, February 1999.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and
+# conditions of the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##############################################################################
+"""
+This test checks the binary packaging infrastructure
+"""
+import pytest
+import spack
+import spack.store
+from spack.fetch_strategy import URLFetchStrategy, FetchStrategyComposite
+from spack.spec import Spec
+import spack.binary_distribution as bindist
+from llnl.util.filesystem import join_path, mkdirp
+import argparse
+import spack.cmd.buildcache as buildcache
+from spack.relocate import *
+import os
+import stat
+import sys
+import shutil
+from spack.util.executable import ProcessError
+
+
+@pytest.fixture(scope='function')
+def testing_gpg_directory(tmpdir):
+ old_gpg_path = spack.util.gpg.GNUPGHOME
+ spack.util.gpg.GNUPGHOME = str(tmpdir.join('gpg'))
+ yield
+ spack.util.gpg.GNUPGHOME = old_gpg_path
+
+
+def has_gnupg2():
+ try:
+ spack.util.gpg.Gpg.gpg()('--version', output=os.devnull)
+ return True
+ except ProcessError:
+ return False
+
+
+def fake_fetchify(url, pkg):
+ """Fake the URL for a package so it downloads from a file."""
+ fetcher = FetchStrategyComposite()
+ fetcher.append(URLFetchStrategy(url))
+ pkg.fetcher = fetcher
+
+
+@pytest.mark.usefixtures('install_mockery', 'testing_gpg_directory')
+def test_packaging(mock_archive, tmpdir):
+ # tweak patchelf to only do a download
+ spec = Spec("patchelf")
+ spec.concretize()
+ pkg = spack.repo.get(spec)
+ fake_fetchify(pkg.fetcher, pkg)
+ mkdirp(join_path(pkg.prefix, "bin"))
+ patchelfscr = join_path(pkg.prefix, "bin", "patchelf")
+ f = open(patchelfscr, 'w')
+ body = """#!/bin/bash
+echo $PATH"""
+ f.write(body)
+ f.close()
+ st = os.stat(patchelfscr)
+ os.chmod(patchelfscr, st.st_mode | stat.S_IEXEC)
+
+ # Install the test package
+ spec = Spec('trivial-install-test-package')
+ spec.concretize()
+ assert spec.concrete
+ pkg = spack.repo.get(spec)
+ fake_fetchify(mock_archive.url, pkg)
+ pkg.do_install()
+
+ # Put some non-relocatable file in there
+ filename = join_path(spec.prefix, "dummy.txt")
+ with open(filename, "w") as script:
+ script.write(spec.prefix)
+
+ # Create the build cache and
+ # put it directly into the mirror
+
+ mirror_path = join_path(tmpdir, 'test-mirror')
+ specs = [spec]
+ spack.mirror.create(
+ mirror_path, specs, no_checksum=True
+ )
+
+ # register mirror with spack config
+ mirrors = {'spack-mirror-test': 'file://' + mirror_path}
+ spack.config.update_config('mirrors', mirrors)
+
+ stage = spack.stage.Stage(
+ mirrors['spack-mirror-test'], name="build_cache", keep=True)
+ stage.create()
+ # setup argument parser
+ parser = argparse.ArgumentParser()
+ buildcache.setup_parser(parser)
+
+ # Create a private key to sign package with if gpg2 available
+ if has_gnupg2():
+ 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(spec)])
+ buildcache.buildcache(parser, args)
+
+ # 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(spec)])
+ buildcache.buildcache(parser, args)
+
+ else:
+ # create build cache without signing
+ args = parser.parse_args(
+ ['create', '-d', mirror_path, '-y', str(spec)])
+ buildcache.buildcache(parser, args)
+
+ # Uninstall the package
+ pkg.do_uninstall(force=True)
+
+ # install build cache without verification
+ args = parser.parse_args(['install', '-y', str(spec)])
+ buildcache.install_tarball(spec, args)
+
+ # test overwrite install without verification
+ args = parser.parse_args(['install', '-f', '-y', str(spec)])
+ buildcache.buildcache(parser, args)
+
+ # create build cache with relative path
+ args = parser.parse_args(
+ ['create', '-d', mirror_path, '-f', '-r', '-y', str(spec)])
+ buildcache.buildcache(parser, args)
+
+ # Uninstall the package
+ pkg.do_uninstall(force=True)
+
+ # install build cache
+ args = parser.parse_args(['install', '-y', str(spec)])
+ buildcache.install_tarball(spec, args)
+
+ # test overwrite install
+ args = parser.parse_args(['install', '-f', '-y', str(spec)])
+ buildcache.buildcache(parser, args)
+
+ # Validate the relocation information
+ buildinfo = bindist.read_buildinfo_file(spec.prefix)
+ assert(buildinfo['relocate_textfiles'] == ['dummy.txt'])
+
+ args = parser.parse_args(['list'])
+ buildcache.buildcache(parser, args)
+
+ args = parser.parse_args(['list', 'trivial'])
+ buildcache.buildcache(parser, args)
+
+ # Copy a key to the mirror to have something to download
+ shutil.copyfile(spack.mock_gpg_keys_path + '/external.key',
+ mirror_path + '/external.key')
+
+ args = parser.parse_args(['keys'])
+ buildcache.buildcache(parser, args)
+
+ # unregister mirror with spack config
+ mirrors = {}
+ spack.config.update_config('mirrors', mirrors)
+ shutil.rmtree(mirror_path)
+ stage.destroy()
+
+
+def test_relocate():
+ assert (needs_binary_relocation('relocatable') is False)
+
+ out = macho_make_paths_relative('/Users/Shares/spack/pkgC/lib/libC.dylib',
+ '/Users/Shared/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 == (['@loader_path/../../../../Shared/spack/pkgA/lib',
+ '@loader_path/../../../../Shared/spack/pkgB/lib',
+ '/usr/local/lib'],
+ ['@loader_path/../../../../Shared/spack/pkgA/libA.dylib',
+ '@loader_path/../../../../Shared/spack/pkgB/libB.dylib',
+ '/usr/local/lib/libloco.dylib'],
+ '@rpath/libC.dylib')
+
+ out = macho_make_paths_relative('/Users/Shared/spack/pkgC/bin/exeC',
+ '/Users/Shared/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 == (['@loader_path/../../pkgA/lib',
+ '@loader_path/../../pkgB/lib',
+ '/usr/local/lib'],
+ ['@loader_path/../../pkgA/libA.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)
+
+ out = get_relative_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():
+ get_patchelf()
+ assert (needs_binary_relocation('Mach-O') is True)
+ macho_get_paths('/bin/bash')
+ shutil.copyfile('/bin/bash', 'bash')
+ modify_macho_object('bash', '/usr', '/opt', False)
+ modify_macho_object('bash', '/usr', '/opt', True)
+ shutil.copyfile('/usr/lib/libncurses.5.4.dylib', 'libncurses.5.4.dylib')
+ modify_macho_object('libncurses.5.4.dylib', '/usr', '/opt', False)
+ modify_macho_object('libncurses.5.4.dylib', '/usr', '/opt', True)
+
+
+@pytest.mark.skipif(sys.platform != 'linux2',
+ reason="only works with Elf objects")
+def test_relocate_elf():
+ assert (needs_binary_relocation('ELF') is True)