link_tree: add option to merge link trees with relative targets
- previous version of link trees would only do absolute symlinks - this version can do relative links using merge(relative=True)
This commit is contained in:
		| @@ -5,6 +5,8 @@ | |||||||
|  |  | ||||||
| """LinkTree class for setting up trees of symbolic links.""" | """LinkTree class for setting up trees of symbolic links.""" | ||||||
|  |  | ||||||
|  | from __future__ import print_function | ||||||
|  |  | ||||||
| import os | import os | ||||||
| import shutil | import shutil | ||||||
| import filecmp | import filecmp | ||||||
| @@ -17,6 +19,16 @@ | |||||||
| empty_file_name = '.spack-empty' | 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 LinkTree(object): | ||||||
|     """Class to create trees of symbolic links from a source directory. |     """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): |                 if os.path.exists(marker): | ||||||
|                     os.remove(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 |         """Link all files in src into dest, creating directories | ||||||
|            if necessary. |            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( |         conflict = self.find_conflict( | ||||||
|             dest_root, ignore=ignore, ignore_file_conflicts=ignore_conflicts) |             dest_root, ignore=ignore, ignore_file_conflicts=ignore_conflicts) | ||||||
|         if conflict: |         if conflict: | ||||||
| @@ -117,42 +141,33 @@ def merge(self, dest_root, **kwargs): | |||||||
|  |  | ||||||
|         self.merge_directories(dest_root, ignore) |         self.merge_directories(dest_root, ignore) | ||||||
|         existing = [] |         existing = [] | ||||||
|         merge_file = kwargs.get('merge_file', merge_link) |  | ||||||
|         for src, dst in self.get_file_map(dest_root, ignore).items(): |         for src, dst in self.get_file_map(dest_root, ignore).items(): | ||||||
|             if os.path.exists(dst): |             if os.path.exists(dst): | ||||||
|                 existing.append(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: |             else: | ||||||
|                 merge_file(src, dst) |                 link(src, dst) | ||||||
|  |  | ||||||
|         for c in existing: |         for c in existing: | ||||||
|             tty.warn("Could not merge: %s" % c) |             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. |         """Unlink all files in dest that exist in src. | ||||||
|  |  | ||||||
|         Unlinks directories in dest if they are empty. |         Unlinks directories in dest if they are empty. | ||||||
|         """ |         """ | ||||||
|         remove_file = kwargs.get('remove_file', remove_link) |         if ignore is None: | ||||||
|         ignore = kwargs.get('ignore', lambda x: False) |             ignore = lambda x: False | ||||||
|  |  | ||||||
|         for src, dst in self.get_file_map(dest_root, ignore).items(): |         for src, dst in self.get_file_map(dest_root, ignore).items(): | ||||||
|             remove_file(src, dst) |             remove_file(src, dst) | ||||||
|         self.unmerge_directories(dest_root, ignore) |         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): | class MergeConflictError(Exception): | ||||||
|  |  | ||||||
|     def __init__(self, path): |     def __init__(self, path): | ||||||
|   | |||||||
| @@ -38,9 +38,11 @@ def link_tree(stage): | |||||||
|     return LinkTree(source_path) |     return LinkTree(source_path) | ||||||
|  |  | ||||||
|  |  | ||||||
| def check_file_link(filename): | def check_file_link(filename, expected_target): | ||||||
|     assert os.path.isfile(filename) |     assert os.path.isfile(filename) | ||||||
|     assert os.path.islink(filename) |     assert os.path.islink(filename) | ||||||
|  |     assert (os.path.abspath(os.path.realpath(filename)) == | ||||||
|  |             os.path.abspath(expected_target)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def check_dir(filename): | def check_dir(filename): | ||||||
| @@ -51,13 +53,46 @@ def test_merge_to_new_directory(stage, link_tree): | |||||||
|     with working_dir(stage.path): |     with working_dir(stage.path): | ||||||
|         link_tree.merge('dest') |         link_tree.merge('dest') | ||||||
|  |  | ||||||
|         check_file_link('dest/1') |         check_file_link('dest/1',       'source/1') | ||||||
|         check_file_link('dest/a/b/2') |         check_file_link('dest/a/b/2',   'source/a/b/2') | ||||||
|         check_file_link('dest/a/b/3') |         check_file_link('dest/a/b/3',   'source/a/b/3') | ||||||
|         check_file_link('dest/c/4') |         check_file_link('dest/c/4',     'source/c/4') | ||||||
|         check_file_link('dest/c/d/5') |         check_file_link('dest/c/d/5',   'source/c/d/5') | ||||||
|         check_file_link('dest/c/d/6') |         check_file_link('dest/c/d/6',   'source/c/d/6') | ||||||
|         check_file_link('dest/c/d/e/7') |         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') |         link_tree.unmerge('dest') | ||||||
|  |  | ||||||
| @@ -72,13 +107,13 @@ def test_merge_to_existing_directory(stage, link_tree): | |||||||
|  |  | ||||||
|         link_tree.merge('dest') |         link_tree.merge('dest') | ||||||
|  |  | ||||||
|         check_file_link('dest/1') |         check_file_link('dest/1',       'source/1') | ||||||
|         check_file_link('dest/a/b/2') |         check_file_link('dest/a/b/2',   'source/a/b/2') | ||||||
|         check_file_link('dest/a/b/3') |         check_file_link('dest/a/b/3',   'source/a/b/3') | ||||||
|         check_file_link('dest/c/4') |         check_file_link('dest/c/4',     'source/c/4') | ||||||
|         check_file_link('dest/c/d/5') |         check_file_link('dest/c/d/5',   'source/c/d/5') | ||||||
|         check_file_link('dest/c/d/6') |         check_file_link('dest/c/d/6',   'source/c/d/6') | ||||||
|         check_file_link('dest/c/d/e/7') |         check_file_link('dest/c/d/e/7', 'source/c/d/e/7') | ||||||
|  |  | ||||||
|         assert os.path.isfile('dest/x') |         assert os.path.isfile('dest/x') | ||||||
|         assert os.path.isfile('dest/a/b/y') |         assert os.path.isfile('dest/a/b/y') | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Todd Gamblin
					Todd Gamblin