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:
parent
f32843528e
commit
d6f2ff1426
2 changed files with 91 additions and 41 deletions
|
@ -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):
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in a new issue