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:
Todd Gamblin 2019-05-23 14:28:28 -07:00
parent f32843528e
commit d6f2ff1426
2 changed files with 91 additions and 41 deletions

View file

@ -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):

View file

@ -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')