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."""
|
"""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')
|
||||||
|
|
Loading…
Reference in a new issue