diff --git a/lib/spack/llnl/util/link_tree.py b/lib/spack/llnl/util/link_tree.py index 8b32253297..e9d8e72161 100644 --- a/lib/spack/llnl/util/link_tree.py +++ b/lib/spack/llnl/util/link_tree.py @@ -5,6 +5,8 @@ """LinkTree class for setting up trees of symbolic links.""" +from __future__ import print_function + import os import shutil import filecmp @@ -17,6 +19,16 @@ empty_file_name = '.spack-empty' +def remove_link(src, dest): + if not os.path.islink(dest): + raise ValueError("%s is not a link tree!" % dest) + # remove if dest is a hardlink/symlink to src; this will only + # be false if two packages are merged into a prefix and have a + # conflicting file + if filecmp.cmp(src, dest, shallow=True): + os.remove(dest) + + class LinkTree(object): """Class to create trees of symbolic links from a source directory. @@ -100,16 +112,28 @@ def unmerge_directories(self, dest_root, ignore): if os.path.exists(marker): os.remove(marker) - def merge(self, dest_root, **kwargs): + def merge(self, dest_root, ignore_conflicts=False, ignore=None, + link=os.symlink, relative=False): """Link all files in src into dest, creating directories if necessary. - If ignore_conflicts is True, do not break when the target exists but - rather return a list of files that could not be linked. - Note that files blocking directories will still cause an error. - """ - ignore_conflicts = kwargs.get("ignore_conflicts", False) - ignore = kwargs.get('ignore', lambda x: False) + Keyword Args: + + ignore_conflicts (bool): if True, do not break when the target exists; + return a list of files that could not be linked + + ignore (callable): callable that returns True if a file is to be + ignored in the merge (by default ignore nothing) + + link (callable): function to create links with (defaults to os.symlink) + + relative (bool): create all symlinks relative to the target + (default False) + + """ + if ignore is None: + ignore = lambda x: False + conflict = self.find_conflict( dest_root, ignore=ignore, ignore_file_conflicts=ignore_conflicts) if conflict: @@ -117,42 +141,33 @@ def merge(self, dest_root, **kwargs): self.merge_directories(dest_root, ignore) existing = [] - merge_file = kwargs.get('merge_file', merge_link) for src, dst in self.get_file_map(dest_root, ignore).items(): if os.path.exists(dst): existing.append(dst) + elif relative: + abs_src = os.path.abspath(src) + dst_dir = os.path.dirname(os.path.abspath(dst)) + rel = os.path.relpath(abs_src, dst_dir) + link(rel, dst) else: - merge_file(src, dst) + link(src, dst) for c in existing: tty.warn("Could not merge: %s" % c) - def unmerge(self, dest_root, **kwargs): + def unmerge(self, dest_root, ignore=None, remove_file=remove_link): """Unlink all files in dest that exist in src. Unlinks directories in dest if they are empty. """ - remove_file = kwargs.get('remove_file', remove_link) - ignore = kwargs.get('ignore', lambda x: False) + if ignore is None: + ignore = lambda x: False + for src, dst in self.get_file_map(dest_root, ignore).items(): remove_file(src, dst) self.unmerge_directories(dest_root, ignore) -def merge_link(src, dest): - os.symlink(src, dest) - - -def remove_link(src, dest): - if not os.path.islink(dest): - raise ValueError("%s is not a link tree!" % dest) - # remove if dest is a hardlink/symlink to src; this will only - # be false if two packages are merged into a prefix and have a - # conflicting file - if filecmp.cmp(src, dest, shallow=True): - os.remove(dest) - - class MergeConflictError(Exception): def __init__(self, path): diff --git a/lib/spack/spack/test/llnl/util/link_tree.py b/lib/spack/spack/test/llnl/util/link_tree.py index 884e950778..6b00afb3fe 100644 --- a/lib/spack/spack/test/llnl/util/link_tree.py +++ b/lib/spack/spack/test/llnl/util/link_tree.py @@ -38,9 +38,11 @@ def link_tree(stage): return LinkTree(source_path) -def check_file_link(filename): +def check_file_link(filename, expected_target): assert os.path.isfile(filename) assert os.path.islink(filename) + assert (os.path.abspath(os.path.realpath(filename)) == + os.path.abspath(expected_target)) def check_dir(filename): @@ -51,13 +53,46 @@ def test_merge_to_new_directory(stage, link_tree): with working_dir(stage.path): link_tree.merge('dest') - check_file_link('dest/1') - check_file_link('dest/a/b/2') - check_file_link('dest/a/b/3') - check_file_link('dest/c/4') - check_file_link('dest/c/d/5') - check_file_link('dest/c/d/6') - check_file_link('dest/c/d/e/7') + check_file_link('dest/1', 'source/1') + check_file_link('dest/a/b/2', 'source/a/b/2') + check_file_link('dest/a/b/3', 'source/a/b/3') + check_file_link('dest/c/4', 'source/c/4') + check_file_link('dest/c/d/5', 'source/c/d/5') + check_file_link('dest/c/d/6', 'source/c/d/6') + check_file_link('dest/c/d/e/7', 'source/c/d/e/7') + + assert os.path.isabs(os.readlink('dest/1')) + assert os.path.isabs(os.readlink('dest/a/b/2')) + assert os.path.isabs(os.readlink('dest/a/b/3')) + assert os.path.isabs(os.readlink('dest/c/4')) + assert os.path.isabs(os.readlink('dest/c/d/5')) + assert os.path.isabs(os.readlink('dest/c/d/6')) + assert os.path.isabs(os.readlink('dest/c/d/e/7')) + + link_tree.unmerge('dest') + + assert not os.path.exists('dest') + + +def test_merge_to_new_directory_relative(stage, link_tree): + with working_dir(stage.path): + link_tree.merge('dest', relative=True) + + check_file_link('dest/1', 'source/1') + check_file_link('dest/a/b/2', 'source/a/b/2') + check_file_link('dest/a/b/3', 'source/a/b/3') + check_file_link('dest/c/4', 'source/c/4') + check_file_link('dest/c/d/5', 'source/c/d/5') + check_file_link('dest/c/d/6', 'source/c/d/6') + check_file_link('dest/c/d/e/7', 'source/c/d/e/7') + + assert not os.path.isabs(os.readlink('dest/1')) + assert not os.path.isabs(os.readlink('dest/a/b/2')) + assert not os.path.isabs(os.readlink('dest/a/b/3')) + assert not os.path.isabs(os.readlink('dest/c/4')) + assert not os.path.isabs(os.readlink('dest/c/d/5')) + assert not os.path.isabs(os.readlink('dest/c/d/6')) + assert not os.path.isabs(os.readlink('dest/c/d/e/7')) link_tree.unmerge('dest') @@ -72,13 +107,13 @@ def test_merge_to_existing_directory(stage, link_tree): link_tree.merge('dest') - check_file_link('dest/1') - check_file_link('dest/a/b/2') - check_file_link('dest/a/b/3') - check_file_link('dest/c/4') - check_file_link('dest/c/d/5') - check_file_link('dest/c/d/6') - check_file_link('dest/c/d/e/7') + check_file_link('dest/1', 'source/1') + check_file_link('dest/a/b/2', 'source/a/b/2') + check_file_link('dest/a/b/3', 'source/a/b/3') + check_file_link('dest/c/4', 'source/c/4') + check_file_link('dest/c/d/5', 'source/c/d/5') + check_file_link('dest/c/d/6', 'source/c/d/6') + check_file_link('dest/c/d/e/7', 'source/c/d/e/7') assert os.path.isfile('dest/x') assert os.path.isfile('dest/a/b/y')