Improved binary relocation: error checking and reporting (#7314)
Fixes #7237 Fixes #6404 Fixes #6418 Fixes #6369 Identify when binary relocation fails to remove all instances of the source prefix (and report an error to the user unless they specify -a to allow the old root to appear). This check occurs at two stages: during "bincache create" all instances of the root are replaced with a special placeholder string ("@@@@..."), and a failure occurs if the root is detected at this point; when the binary package is extracted there is a second check. This addresses #7237 and #6418. This is intended to be compatible with previously-created binary packages. This also adds: * Better error messages for "spack install --use-cache" (#6404) * Faster relocation on Mac OS (using a single call to install_name_tool for all files rather than a call for each file) * Clean up when "buildcache create" fails (addresses #6369) * Explicit error message when the spack instance extracting the binary package uses a different install layout than the spack instance that created the binary package (since this is currently not supported) * Remove the option to create unsigned binary packages with -y
This commit is contained in:
		 Patrick Gartung
					Patrick Gartung
				
			
				
					committed by
					
						 scheibelp
						scheibelp
					
				
			
			
				
	
			
			
			 scheibelp
						scheibelp
					
				
			
						parent
						
							360feb4193
						
					
				
				
					commit
					f5467957bc
				
			| @@ -29,6 +29,7 @@ | ||||
| import yaml | ||||
| import shutil | ||||
| import platform | ||||
| import tempfile | ||||
|  | ||||
| import llnl.util.tty as tty | ||||
| from spack.util.gpg import Gpg | ||||
| @@ -46,26 +47,57 @@ | ||||
|  | ||||
|  | ||||
| class NoOverwriteException(Exception): | ||||
|     """ | ||||
|     Raised when a file exists and must be overwritten. | ||||
|     """ | ||||
|     def __init__(self, file_path): | ||||
|         err_msg = "\n%s\nexists\n" % file_path | ||||
|         err_msg += "Use -f option to overwrite." | ||||
|         super(NoOverwriteException, self).__init__(err_msg) | ||||
|  | ||||
|  | ||||
| class NoGpgException(spack.error.SpackError): | ||||
|     """ | ||||
|     Raised when gpg2 is not in PATH | ||||
|     """ | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class NoGpgException(Exception): | ||||
| class NoKeyException(spack.error.SpackError): | ||||
|     """ | ||||
|     Raised when gpg has no default key added. | ||||
|     """ | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class PickKeyException(Exception): | ||||
| class PickKeyException(spack.error.SpackError): | ||||
|     """ | ||||
|     Raised when multiple keys can be used to sign. | ||||
|     """ | ||||
|     def __init__(self, keys): | ||||
|         err_msg = "Multi 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) | ||||
|  | ||||
|  | ||||
| class NoVerifyException(spack.error.SpackError): | ||||
|     """ | ||||
|     Raised if file fails signature verification. | ||||
|     """ | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class NoKeyException(Exception): | ||||
| class NoChecksumException(spack.error.SpackError): | ||||
|     """ | ||||
|     Raised if file fails checksum verification. | ||||
|     """ | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class NoVerifyException(Exception): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class NoChecksumException(Exception): | ||||
| class NewLayoutException(spack.error.SpackError): | ||||
|     """ | ||||
|     Raised if directory layout is different from buildcache. | ||||
|     """ | ||||
|     pass | ||||
|  | ||||
|  | ||||
| @@ -114,7 +146,8 @@ def write_buildinfo_file(prefix, workdir, rel=False): | ||||
|             #  Check if the file contains a string with the installroot. | ||||
|             #  This cuts down on the number of files added to the list | ||||
|             #  of files potentially needing relocation | ||||
|             if relocate.strings_contains_installroot(path_name): | ||||
|             if relocate.strings_contains_installroot(path_name, | ||||
|                                                      spack.store.layout.root): | ||||
|                 filetype = relocate.get_filetype(path_name) | ||||
|                 if relocate.needs_binary_relocation(filetype, os_id): | ||||
|                     rel_path_name = os.path.relpath(path_name, prefix) | ||||
| @@ -127,6 +160,8 @@ def write_buildinfo_file(prefix, workdir, rel=False): | ||||
|     buildinfo = {} | ||||
|     buildinfo['relative_rpaths'] = rel | ||||
|     buildinfo['buildpath'] = spack.store.layout.root | ||||
|     buildinfo['relative_prefix'] = os.path.relpath(prefix, | ||||
|                                                    spack.store.layout.root) | ||||
|     buildinfo['relocate_textfiles'] = text_to_relocate | ||||
|     buildinfo['relocate_binaries'] = binary_to_relocate | ||||
|     filename = buildinfo_file_name(workdir) | ||||
| @@ -178,19 +213,24 @@ def checksum_tarball(file): | ||||
|     return hasher.hexdigest() | ||||
|  | ||||
|  | ||||
| def sign_tarball(yes_to_all, key, force, specfile_path): | ||||
| def sign_tarball(key, force, specfile_path): | ||||
|     # Sign the packages if keys available | ||||
|     if not has_gnupg2(): | ||||
|         raise NoGpgException() | ||||
|         raise NoGpgException( | ||||
|             "gpg2 is not available in $PATH .\n" | ||||
|             "Use spack install gnupg and spack load gnupg.") | ||||
|     else: | ||||
|         if key is None: | ||||
|             keys = Gpg.signing_keys() | ||||
|             if len(keys) == 1: | ||||
|                 key = keys[0] | ||||
|             if len(keys) > 1: | ||||
|                 raise PickKeyException() | ||||
|                 raise PickKeyException(str(keys)) | ||||
|             if len(keys) == 0: | ||||
|                 raise NoKeyException() | ||||
|                 msg = "No default key available for signing.\n" | ||||
|                 msg += "Use spack gpg init and spack gpg create" | ||||
|                 msg += " to create a default key." | ||||
|                 raise NoKeyException(msg) | ||||
|     if os.path.exists('%s.asc' % specfile_path): | ||||
|         if force: | ||||
|             os.remove('%s.asc' % specfile_path) | ||||
| @@ -214,7 +254,7 @@ def generate_index(outdir, indexfile_path): | ||||
|     f.close() | ||||
|  | ||||
|  | ||||
| def build_tarball(spec, outdir, force=False, rel=False, yes_to_all=False, | ||||
| def build_tarball(spec, outdir, force=False, rel=False, allow_root=False, | ||||
|                   key=None): | ||||
|     """ | ||||
|     Build a tarball from given spec and put it into the directory structure | ||||
| @@ -247,9 +287,7 @@ def build_tarball(spec, outdir, force=False, rel=False, yes_to_all=False, | ||||
|         else: | ||||
|             raise NoOverwriteException(str(specfile_path)) | ||||
|     # make a copy of the install directory to work with | ||||
|     workdir = join_path(outdir, os.path.basename(spec.prefix)) | ||||
|     if os.path.exists(workdir): | ||||
|         shutil.rmtree(workdir) | ||||
|     workdir = join_path(tempfile.mkdtemp(), os.path.basename(spec.prefix)) | ||||
|     install_tree(spec.prefix, workdir, symlinks=True) | ||||
|  | ||||
|     # create info for later relocation and create tar | ||||
| @@ -258,11 +296,23 @@ def build_tarball(spec, outdir, force=False, rel=False, yes_to_all=False, | ||||
|     # optinally make the paths in the binaries relative to each other | ||||
|     # in the spack install tree before creating tarball | ||||
|     if rel: | ||||
|         make_package_relative(workdir, spec.prefix) | ||||
|         try: | ||||
|             make_package_relative(workdir, spec.prefix, allow_root) | ||||
|         except Exception as e: | ||||
|             shutil.rmtree(workdir) | ||||
|             shutil.rmtree(tarfile_dir) | ||||
|             tty.die(str(e)) | ||||
|     else: | ||||
|         try: | ||||
|             make_package_placeholder(workdir, allow_root) | ||||
|         except Exception as e: | ||||
|             shutil.rmtree(workdir) | ||||
|             shutil.rmtree(tarfile_dir) | ||||
|             tty.die(str(e)) | ||||
|     # create compressed tarball of the install prefix | ||||
|     with closing(tarfile.open(tarfile_path, 'w:gz')) as tar: | ||||
|         tar.add(name='%s' % workdir, | ||||
|                 arcname='%s' % os.path.basename(workdir)) | ||||
|                 arcname='%s' % os.path.basename(spec.prefix)) | ||||
|     # remove copy of install directory | ||||
|     shutil.rmtree(workdir) | ||||
|  | ||||
| @@ -278,32 +328,26 @@ def build_tarball(spec, outdir, force=False, rel=False, yes_to_all=False, | ||||
|     bchecksum['hash_algorithm'] = 'sha256' | ||||
|     bchecksum['hash'] = checksum | ||||
|     spec_dict['binary_cache_checksum'] = bchecksum | ||||
|     # Add original install prefix relative to layout root to spec.yaml. | ||||
|     # This will be used to determine is the directory layout has changed. | ||||
|     buildinfo = {} | ||||
|     buildinfo['relative_prefix'] = os.path.relpath(spec.prefix, | ||||
|                                                    spack.store.layout.root) | ||||
|     spec_dict['buildinfo'] = buildinfo | ||||
|     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() | ||||
|     # sign the tarball and spec file with gpg | ||||
|     sign_tarball(key, force, specfile_path) | ||||
|     # 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) | ||||
|         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) | ||||
|     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): | ||||
| @@ -334,7 +378,7 @@ def download_tarball(spec): | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def make_package_relative(workdir, prefix): | ||||
| def make_package_relative(workdir, prefix, allow_root): | ||||
|     """ | ||||
|     Change paths in binaries to relative paths | ||||
|     """ | ||||
| @@ -345,15 +389,26 @@ def make_package_relative(workdir, prefix): | ||||
|     for filename in buildinfo['relocate_binaries']: | ||||
|         orig_path_names.append(os.path.join(prefix, filename)) | ||||
|         cur_path_names.append(os.path.join(workdir, filename)) | ||||
|         relocate.make_binary_relative(cur_path_names, orig_path_names, | ||||
|                                       old_path) | ||||
|     relocate.make_binary_relative(cur_path_names, orig_path_names, | ||||
|                                   old_path, allow_root) | ||||
|  | ||||
|  | ||||
| def relocate_package(prefix): | ||||
| def make_package_placeholder(workdir, allow_root): | ||||
|     """ | ||||
|     Change paths in binaries to placeholder paths | ||||
|     """ | ||||
|     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.make_binary_placeholder(cur_path_names, allow_root) | ||||
|  | ||||
|  | ||||
| def relocate_package(workdir, allow_root): | ||||
|     """ | ||||
|     Relocate the given package | ||||
|     """ | ||||
|     buildinfo = read_buildinfo_file(prefix) | ||||
|     buildinfo = read_buildinfo_file(workdir) | ||||
|     new_path = spack.store.layout.root | ||||
|     old_path = buildinfo['buildpath'] | ||||
|     rel = buildinfo.get('relative_rpaths', False) | ||||
| @@ -364,7 +419,7 @@ def relocate_package(prefix): | ||||
|             "%s to %s." % (old_path, new_path)) | ||||
|     path_names = set() | ||||
|     for filename in buildinfo['relocate_textfiles']: | ||||
|         path_name = os.path.join(prefix, filename) | ||||
|         path_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) | ||||
| @@ -374,39 +429,46 @@ def relocate_package(prefix): | ||||
|     if not rel: | ||||
|         path_names = set() | ||||
|         for filename in buildinfo['relocate_binaries']: | ||||
|             path_name = os.path.join(prefix, filename) | ||||
|             path_name = os.path.join(workdir, filename) | ||||
|             path_names.add(path_name) | ||||
|         relocate.relocate_binary(path_names, old_path, new_path) | ||||
|         relocate.relocate_binary(path_names, old_path, new_path, | ||||
|                                  allow_root) | ||||
|  | ||||
|  | ||||
| def extract_tarball(spec, filename, yes_to_all=False, force=False): | ||||
| def extract_tarball(spec, filename, allow_root=False, force=False): | ||||
|     """ | ||||
|     extract binary tarball for given package into install area | ||||
|     """ | ||||
|     installpath = spec.prefix | ||||
|     if os.path.exists(installpath): | ||||
|     if os.path.exists(spec.prefix): | ||||
|         if force: | ||||
|             shutil.rmtree(installpath) | ||||
|             shutil.rmtree(spec.prefix) | ||||
|         else: | ||||
|             raise NoOverwriteException(str(installpath)) | ||||
|             raise NoOverwriteException(str(spec.prefix)) | ||||
|  | ||||
|     tmpdir = tempfile.mkdtemp() | ||||
|     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) | ||||
|     tarfile_path = os.path.join(tmpdir, tarfile_name) | ||||
|     specfile_name = tarball_name(spec, '.spec.yaml') | ||||
|     specfile_path = os.path.join(stagepath, specfile_name) | ||||
|     specfile_path = os.path.join(tmpdir, specfile_name) | ||||
|  | ||||
|     with closing(tarfile.open(spackfile_path, 'r')) as tar: | ||||
|         tar.extractall(stagepath) | ||||
|         tar.extractall(tmpdir) | ||||
|  | ||||
|     if not yes_to_all: | ||||
|         if os.path.exists('%s.asc' % specfile_path): | ||||
|     if os.path.exists('%s.asc' % specfile_path): | ||||
|         try: | ||||
|             Gpg.verify('%s.asc' % specfile_path, specfile_path) | ||||
|             os.remove(specfile_path + '.asc') | ||||
|         else: | ||||
|             raise NoVerifyException() | ||||
|  | ||||
|         except Exception as e: | ||||
|             shutil.rmtree(tmpdir) | ||||
|             tty.die(str(e)) | ||||
|     else: | ||||
|         shutil.rmtree(tmpdir) | ||||
|         raise NoVerifyException( | ||||
|             "Package spec file failed signature verification.\n" | ||||
|             "Use spack buildcache keys to download " | ||||
|             "and install a key for verification from the mirror.") | ||||
|     # get the sha256 checksum of the tarball | ||||
|     checksum = checksum_tarball(tarfile_path) | ||||
|  | ||||
| @@ -419,16 +481,48 @@ def extract_tarball(spec, filename, yes_to_all=False, force=False): | ||||
|  | ||||
|     # if the checksums don't match don't install | ||||
|     if bchecksum['hash'] != checksum: | ||||
|         raise NoChecksumException() | ||||
|         shutil.rmtree(tmpdir) | ||||
|         raise NoChecksumException( | ||||
|             "Package tarball failed checksum verification.\n" | ||||
|             "It cannot be installed.") | ||||
|  | ||||
|     # delay creating installpath until verification is complete | ||||
|     mkdirp(installpath) | ||||
|     new_relative_prefix = str(os.path.relpath(spec.prefix, | ||||
|                                               spack.store.layout.root)) | ||||
|     # 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) | ||||
|     # 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) | ||||
|  | ||||
|     # extract the tarball in a temp directory | ||||
|     with closing(tarfile.open(tarfile_path, 'r')) as tar: | ||||
|         tar.extractall(path=join_path(installpath, '..')) | ||||
|         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 = join_path(tmpdir, os.path.basename(spec.prefix)) | ||||
|  | ||||
|     # cleanup | ||||
|     os.remove(tarfile_path) | ||||
|     os.remove(specfile_path) | ||||
|     relocate_package(installpath) | ||||
|  | ||||
|     try: | ||||
|         relocate_package(workdir, allow_root) | ||||
|     except Exception as e: | ||||
|         shutil.rmtree(workdir) | ||||
|         tty.die(str(e)) | ||||
|     # Delay creating spec.prefix until verification is complete | ||||
|     # and any relocation has been done. | ||||
|     else: | ||||
|         install_tree(workdir, spec.prefix, symlinks=True) | ||||
|     finally: | ||||
|         shutil.rmtree(tmpdir) | ||||
|  | ||||
|  | ||||
| def get_specs(force=False): | ||||
|   | ||||
| @@ -24,15 +24,11 @@ | ||||
| ############################################################################## | ||||
| import argparse | ||||
|  | ||||
| import os | ||||
| 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 binary packages" | ||||
| section = "packaging" | ||||
| @@ -49,9 +45,9 @@ def setup_parser(subparser): | ||||
|                              " 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('-a', '--allow_root', action='store_true', | ||||
|                         help="allow install root string in binary files " + | ||||
|                              "after RPATH substitution") | ||||
|     create.add_argument('-k', '--key', metavar='key', | ||||
|                         type=str, default=None, | ||||
|                         help="Key for signing.") | ||||
| @@ -66,9 +62,11 @@ def setup_parser(subparser): | ||||
|     install = subparsers.add_parser('install', help=installtarball.__doc__) | ||||
|     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('-m', '--multiple', action='store_true', | ||||
|                          help="allow all matching packages ") | ||||
|     install.add_argument('-a', '--allow_root', action='store_true', | ||||
|                          help="allow install root string in binary files " + | ||||
|                               "after RPATH substitution") | ||||
|     install.add_argument( | ||||
|         'packages', nargs=argparse.REMAINDER, | ||||
|         help="specs of packages to install biuldache for") | ||||
| @@ -87,7 +85,7 @@ def setup_parser(subparser): | ||||
|         '-i', '--install', action='store_true', | ||||
|         help="install Keys pulled from mirror") | ||||
|     dlkeys.add_argument( | ||||
|         '-y', '--yes-to-all', action='store_true', | ||||
|         '-y', '--yes_to_all', action='store_true', | ||||
|         help="answer yes to all trust questions") | ||||
|     dlkeys.add_argument('-f', '--force', action='store_true', | ||||
|                         help="force new download of keys") | ||||
| @@ -185,17 +183,17 @@ def createtarball(args): | ||||
|                 " installed package argument") | ||||
|     pkgs = set(args.packages) | ||||
|     specs = set() | ||||
|     outdir = os.getcwd() | ||||
|     outdir = '.' | ||||
|     if args.directory: | ||||
|         outdir = args.directory | ||||
|     signkey = None | ||||
|     if args.key: | ||||
|         signkey = args.key | ||||
|     yes_to_all = False | ||||
|     allow_root = False | ||||
|     force = False | ||||
|     relative = False | ||||
|     if args.yes_to_all: | ||||
|         yes_to_all = True | ||||
|     if args.allow_root: | ||||
|         allow_root = True | ||||
|     if args.force: | ||||
|         force = True | ||||
|     if args.rel: | ||||
| @@ -220,24 +218,12 @@ def createtarball(args): | ||||
|                     tty.msg('adding dependency %s' % node.format()) | ||||
|                     specs.add(node) | ||||
|  | ||||
|     tty.msg('writing tarballs to %s/build_cache' % outdir) | ||||
|  | ||||
|     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.die("gpg2 is not available," | ||||
|                     " use -y to create unsigned build caches") | ||||
|         except NoKeyException: | ||||
|             tty.die("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.die("multi keys available for signing," | ||||
|                     " use -y to create unsigned build caches" | ||||
|                     " or -k <key hash> to pick a key") | ||||
|         bindist.build_tarball(spec, outdir, force, | ||||
|                               relative, allow_root, signkey) | ||||
|  | ||||
|  | ||||
| def installtarball(args): | ||||
| @@ -246,13 +232,13 @@ def installtarball(args): | ||||
|         tty.die("build cache file installation requires" + | ||||
|                 " at least one package spec argument") | ||||
|     pkgs = set(args.packages) | ||||
|     yes_to_all = False | ||||
|     if args.yes_to_all: | ||||
|         yes_to_all = True | ||||
|     multiple = False | ||||
|     if args.multiple: | ||||
|         multiple = True | ||||
|     force = False | ||||
|     if args.force: | ||||
|         force = True | ||||
|     matches = match_downloaded_specs(pkgs, yes_to_all, force) | ||||
|     matches = match_downloaded_specs(pkgs, multiple, force) | ||||
|  | ||||
|     for match in matches: | ||||
|         install_tarball(match, args) | ||||
| @@ -263,9 +249,9 @@ def install_tarball(spec, args): | ||||
|     if s.external or s.virtual: | ||||
|         tty.warn("Skipping external or virtual package %s" % spec.format()) | ||||
|         return | ||||
|     yes_to_all = False | ||||
|     if args.yes_to_all: | ||||
|         yes_to_all = True | ||||
|     allow_root = False | ||||
|     if args.allow_root: | ||||
|         allow_root = True | ||||
|     force = False | ||||
|     if args.force: | ||||
|         force = True | ||||
| @@ -274,24 +260,13 @@ def install_tarball(spec, args): | ||||
|         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.") | ||||
|         tty.warn("Package for spec %s already installed." % spec.format()) | ||||
|     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) | ||||
|             bindist.extract_tarball(spec, tarball, allow_root, force) | ||||
|             spack.store.db.reindex(spack.store.layout) | ||||
|         else: | ||||
|             tty.die('Download of binary cache file for spec %s failed.' % | ||||
|                     spec.format()) | ||||
|   | ||||
| @@ -1288,7 +1288,7 @@ def try_install_from_binary_cache(self, explicit): | ||||
|         tty.msg('Installing %s from binary cache' % self.name) | ||||
|         tarball = binary_distribution.download_tarball(binary_spec) | ||||
|         binary_distribution.extract_tarball( | ||||
|             binary_spec, tarball, yes_to_all=False, force=False) | ||||
|             binary_spec, tarball, allow_root=False, force=False) | ||||
|         spack.store.db.add(self.spec, spack.store.layout, explicit=explicit) | ||||
|         return True | ||||
|  | ||||
|   | ||||
| @@ -33,6 +33,18 @@ | ||||
| import llnl.util.tty as tty | ||||
|  | ||||
|  | ||||
| class InstallRootStringException(spack.error.SpackError): | ||||
|     """ | ||||
|     Raised when the relocated binary still has the install root string. | ||||
|     """ | ||||
|     def __init__(self, file_path, root_path): | ||||
|         super(InstallRootStringException, self).__init__( | ||||
|             "\n %s \ncontains string\n %s \n" | ||||
|             "after replacing it in rpaths.\n" | ||||
|             "Package should not be relocated.\n Use -a to override." % | ||||
|             (file_path, root_path)) | ||||
|  | ||||
|  | ||||
| def get_patchelf(): | ||||
|     """ | ||||
|     Builds and installs spack patchelf package on linux platforms | ||||
| @@ -86,6 +98,29 @@ def get_relative_rpaths(path_name, orig_dir, orig_rpaths): | ||||
|     return rel_rpaths | ||||
|  | ||||
|  | ||||
| def set_placeholder(dirname): | ||||
|     """ | ||||
|     return string of @'s with same length | ||||
|     """ | ||||
|     return '@' * len(dirname) | ||||
|  | ||||
|  | ||||
| def get_placeholder_rpaths(path_name, orig_rpaths): | ||||
|     """ | ||||
|     Replaces original layout root dir with a placeholder string in all rpaths. | ||||
|     """ | ||||
|     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: | ||||
| @@ -121,7 +156,7 @@ def macho_get_paths(path_name): | ||||
| 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); | ||||
|     in rpaths and deps; idpaths are replaced with @rpath/libname as needed; | ||||
|     replacement are returned. | ||||
|     """ | ||||
|     new_idpath = None | ||||
| @@ -144,6 +179,34 @@ def macho_make_paths_relative(path_name, old_dir, rpaths, deps, idpath): | ||||
|     return (new_rpaths, new_deps, new_idpath) | ||||
|  | ||||
|  | ||||
| def macho_make_paths_placeholder(rpaths, deps, idpath): | ||||
|     """ | ||||
|     Replace old_dir with a placeholder of the same length | ||||
|     in rpaths and deps and idpaths is needed. | ||||
|     replacement are returned. | ||||
|     """ | ||||
|     new_idpath = None | ||||
|     old_dir = spack.store.layout.root | ||||
|     placeholder = set_placeholder(old_dir) | ||||
|     if idpath: | ||||
|         new_idpath = re.sub(old_dir, placeholder, idpath) | ||||
|     new_rpaths = list() | ||||
|     new_deps = list() | ||||
|     for rpath in rpaths: | ||||
|         if re.match(old_dir, rpath): | ||||
|             ph = re.sub(old_dir, placeholder, rpath) | ||||
|             new_rpaths.append('%s' % ph) | ||||
|         else: | ||||
|             new_rpaths.append(rpath) | ||||
|     for dep in deps: | ||||
|         if re.match(old_dir, dep): | ||||
|             ph = re.sub(old_dir, placeholder, dep) | ||||
|             new_deps.append('%s' % ph) | ||||
|         else: | ||||
|             new_deps.append(dep) | ||||
|     return (new_rpaths, new_deps, new_idpath) | ||||
|  | ||||
|  | ||||
| def macho_replace_paths(old_dir, new_dir, rpaths, deps, idpath): | ||||
|     """ | ||||
|     Replace old_dir with new_dir in rpaths, deps and idpath | ||||
| @@ -179,15 +242,17 @@ def modify_macho_object(cur_path, rpaths, deps, idpath, | ||||
|     if 'libgcc_' in cur_path: | ||||
|         return | ||||
|     install_name_tool = Executable('install_name_tool') | ||||
|     args = [] | ||||
|     if new_idpath: | ||||
|         install_name_tool('-id', new_idpath, str(cur_path), | ||||
|                           output=str, err=str) | ||||
|         args.extend(['-id', new_idpath]) | ||||
|  | ||||
|     for orig, new in zip(deps, new_deps): | ||||
|         install_name_tool('-change', orig, new, str(cur_path)) | ||||
|         args.extend(['-change', orig, new]) | ||||
|  | ||||
|     for orig, new in zip(rpaths, new_rpaths): | ||||
|         install_name_tool('-rpath', orig, new, str(cur_path)) | ||||
|         args.extend(['-rpath', orig, new]) | ||||
|     args.append(str(cur_path)) | ||||
|     install_name_tool(*args) | ||||
|     return | ||||
|  | ||||
|  | ||||
| @@ -202,14 +267,14 @@ def get_filetype(path_name): | ||||
|     return output.strip() | ||||
|  | ||||
|  | ||||
| def strings_contains_installroot(path_name): | ||||
| def strings_contains_installroot(path_name, root_dir): | ||||
|     """ | ||||
|     Check if the file contain the install root string. | ||||
|     """ | ||||
|     strings = Executable('strings') | ||||
|     output = strings('%s' % path_name, | ||||
|                      output=str, err=str) | ||||
|     return (spack.store.layout.root in output) | ||||
|     return (root_dir in output) | ||||
|  | ||||
|  | ||||
| def modify_elf_object(path_name, new_rpaths): | ||||
| @@ -257,34 +322,66 @@ def needs_text_relocation(filetype): | ||||
|     return ("text" in filetype) | ||||
|  | ||||
|  | ||||
| def relocate_binary(path_names, old_dir, new_dir): | ||||
| def relocate_binary(path_names, old_dir, new_dir, allow_root): | ||||
|     """ | ||||
|     Change old_dir to new_dir in RPATHs of elf or mach-o files | ||||
|     Account for the case where old_dir is now a placeholder | ||||
|     """ | ||||
|     placeholder = set_placeholder(old_dir) | ||||
|     if platform.system() == 'Darwin': | ||||
|         for path_name in path_names: | ||||
|             rpaths, deps, idpath = macho_get_paths(path_name) | ||||
|             new_rpaths, new_deps, new_idpath = macho_replace_paths(old_dir, | ||||
|                                                                    new_dir, | ||||
|                                                                    rpaths, | ||||
|                                                                    deps, | ||||
|                                                                    idpath) | ||||
|             (rpaths, deps, idpath) = macho_get_paths(path_name) | ||||
|             # new style buildaches with placeholder in binaries | ||||
|             if (deps[0].startswith(placeholder) or | ||||
|                 rpaths[0].startswith(placeholder) or | ||||
|                 (idpath and idpath.startswith(placeholder))): | ||||
|                 (new_rpaths, | ||||
|                  new_deps, | ||||
|                  new_idpath) = macho_replace_paths(placeholder, | ||||
|                                                    new_dir, | ||||
|                                                    rpaths, | ||||
|                                                    deps, | ||||
|                                                    idpath) | ||||
|             # old style buildcaches with original install root in binaries | ||||
|             else: | ||||
|                 (new_rpaths, | ||||
|                  new_deps, | ||||
|                  new_idpath) = macho_replace_paths(old_dir, | ||||
|                                                    new_dir, | ||||
|                                                    rpaths, | ||||
|                                                    deps, | ||||
|                                                    idpath) | ||||
|             modify_macho_object(path_name, | ||||
|                                 rpaths, deps, idpath, | ||||
|                                 new_rpaths, new_deps, new_idpath) | ||||
|             if (not allow_root and | ||||
|                 strings_contains_installroot(path_name, old_dir)): | ||||
|                     raise InstallRootStringException(path_name, old_dir) | ||||
|  | ||||
|     elif platform.system() == 'Linux': | ||||
|         for path_name in path_names: | ||||
|             orig_rpaths = get_existing_elf_rpaths(path_name) | ||||
|             if orig_rpaths: | ||||
|                 new_rpaths = substitute_rpath(orig_rpaths, old_dir, new_dir) | ||||
|                 if orig_rpaths[0].startswith(placeholder): | ||||
|                     # new style buildaches with placeholder in binaries | ||||
|                     new_rpaths = substitute_rpath(orig_rpaths, | ||||
|                                                   placeholder, new_dir) | ||||
|                 else: | ||||
|                     # old style buildcaches with original install | ||||
|                     # root in binaries | ||||
|                     new_rpaths = substitute_rpath(orig_rpaths, | ||||
|                                                   old_dir, new_dir) | ||||
|                 modify_elf_object(path_name, new_rpaths) | ||||
|                 if (not allow_root and | ||||
|                     strings_contains_installroot(path_name, old_dir)): | ||||
|                         raise InstallRootStringException(path_name, old_dir) | ||||
|     else: | ||||
|         tty.die("Relocation not implemented for %s" % platform.system()) | ||||
|  | ||||
|  | ||||
| def make_binary_relative(cur_path_names, orig_path_names, old_dir): | ||||
| def make_binary_relative(cur_path_names, orig_path_names, old_dir, allow_root): | ||||
|     """ | ||||
|     Make RPATHs relative to old_dir in given elf or mach-o files | ||||
|     Replace old RPATHs with paths relative to old_dir in binary files | ||||
|     """ | ||||
|     if platform.system() == 'Darwin': | ||||
|         for cur_path, orig_path in zip(cur_path_names, orig_path_names): | ||||
| @@ -296,6 +393,9 @@ def make_binary_relative(cur_path_names, orig_path_names, old_dir): | ||||
|             modify_macho_object(cur_path, | ||||
|                                 rpaths, deps, idpath, | ||||
|                                 new_rpaths, new_deps, new_idpath) | ||||
|             if (not allow_root and | ||||
|                 strings_contains_installroot(cur_path)): | ||||
|                     raise InstallRootStringException(cur_path) | ||||
|     elif platform.system() == 'Linux': | ||||
|         for cur_path, orig_path in zip(cur_path_names, orig_path_names): | ||||
|             orig_rpaths = get_existing_elf_rpaths(cur_path) | ||||
| @@ -303,10 +403,46 @@ def make_binary_relative(cur_path_names, orig_path_names, old_dir): | ||||
|                 new_rpaths = get_relative_rpaths(orig_path, old_dir, | ||||
|                                                  orig_rpaths) | ||||
|                 modify_elf_object(cur_path, new_rpaths) | ||||
|                 if (not allow_root and | ||||
|                     strings_contains_installroot(cur_path, old_dir)): | ||||
|                         raise InstallRootStringException(cur_path, old_dir) | ||||
|     else: | ||||
|         tty.die("Prelocation not implemented for %s" % platform.system()) | ||||
|  | ||||
|  | ||||
| def make_binary_placeholder(cur_path_names, allow_root): | ||||
|     """ | ||||
|     Replace old install root in RPATHs with placeholder in binary files | ||||
|     """ | ||||
|     if platform.system() == 'Darwin': | ||||
|         for cur_path in cur_path_names: | ||||
|             rpaths, deps, idpath = macho_get_paths(cur_path) | ||||
|             (new_rpaths, | ||||
|              new_deps, | ||||
|              new_idpath) = macho_make_paths_placeholder(rpaths, deps, idpath) | ||||
|             modify_macho_object(cur_path, | ||||
|                                 rpaths, deps, idpath, | ||||
|                                 new_rpaths, new_deps, new_idpath) | ||||
|             if (not allow_root and | ||||
|                 strings_contains_installroot(cur_path, | ||||
|                                              spack.store.layout.root)): | ||||
|                 raise InstallRootStringException(cur_path, | ||||
|                                                  spack.store.layout.root) | ||||
|     elif platform.system() == 'Linux': | ||||
|         for cur_path in cur_path_names: | ||||
|             orig_rpaths = get_existing_elf_rpaths(cur_path) | ||||
|             if orig_rpaths: | ||||
|                 new_rpaths = get_placeholder_rpaths(cur_path, orig_rpaths) | ||||
|                 modify_elf_object(cur_path, new_rpaths) | ||||
|                 if (not allow_root and | ||||
|                     strings_contains_installroot(cur_path, | ||||
|                                                  spack.store.layout.root)): | ||||
|                     raise InstallRootStringException(cur_path, | ||||
|                                                      spack.store.layout.root) | ||||
|     else: | ||||
|         tty.die("Placeholder not implemented for %s" % platform.system()) | ||||
|  | ||||
|  | ||||
| def relocate_text(path_names, old_dir, new_dir): | ||||
|     """ | ||||
|     Replace old path with new path in text file path_name | ||||
|   | ||||
| @@ -73,7 +73,7 @@ def fake_fetchify(url, pkg): | ||||
|  | ||||
|  | ||||
| @pytest.mark.usefixtures('install_mockery', 'testing_gpg_directory') | ||||
| def test_packaging(mock_archive, tmpdir): | ||||
| def test_buildcache(mock_archive, tmpdir): | ||||
|     # tweak patchelf to only do a download | ||||
|     spec = Spec("patchelf") | ||||
|     spec.concretize() | ||||
| @@ -218,22 +218,20 @@ def test_packaging(mock_archive, tmpdir): | ||||
|  | ||||
|  | ||||
| def test_relocate_text(tmpdir): | ||||
|     # Validate the text path replacement | ||||
|     old_dir = '/home/spack/opt/spack' | ||||
|     filename = str(tmpdir) + '/dummy.txt' | ||||
|     with open(filename, "w") as script: | ||||
|         script.write(old_dir) | ||||
|         script.close() | ||||
|  | ||||
|     filenames = [filename] | ||||
|     new_dir = '/opt/rh/devtoolset/' | ||||
|     relocate_text(filenames, old_dir, new_dir) | ||||
|  | ||||
|     assert(strings_contains_installroot(filename) is False) | ||||
|  | ||||
|     with open(filename, "r") as script: | ||||
|         for line in script: | ||||
|             assert(new_dir in line) | ||||
|     with tmpdir.as_cwd(): | ||||
|         # Validate the text path replacement | ||||
|         old_dir = '/home/spack/opt/spack' | ||||
|         filename = 'dummy.txt' | ||||
|         with open(filename, "w") as script: | ||||
|             script.write(old_dir) | ||||
|             script.close() | ||||
|         filenames = [filename] | ||||
|         new_dir = '/opt/rh/devtoolset/' | ||||
|         relocate_text(filenames, 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) | ||||
|  | ||||
|  | ||||
| def test_needs_relocation(): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user