diff options
Diffstat (limited to 'lib/spack/llnl/util/link_tree.py')
-rw-r--r-- | lib/spack/llnl/util/link_tree.py | 170 |
1 files changed, 98 insertions, 72 deletions
diff --git a/lib/spack/llnl/util/link_tree.py b/lib/spack/llnl/util/link_tree.py index 60ab5cb2b0..e675523015 100644 --- a/lib/spack/llnl/util/link_tree.py +++ b/lib/spack/llnl/util/link_tree.py @@ -1,27 +1,8 @@ -############################################################################## -# Copyright (c) 2013-2017, Lawrence Livermore National Security, LLC. -# Produced at the Lawrence Livermore National Laboratory. +# Copyright 2013-2018 Lawrence Livermore National Security, LLC and other +# Spack Project Developers. See the top-level COPYRIGHT file for details. # -# This file is part of Spack. -# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved. -# LLNL-CODE-647188 -# -# For details, see https://github.com/spack/spack -# Please also see the NOTICE and LICENSE files for our notice and the LGPL. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License (as -# published by the Free Software Foundation) version 2.1, February 1999. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and -# conditions of the GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -############################################################################## +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + """LinkTree class for setting up trees of symbolic links.""" import os @@ -29,6 +10,7 @@ import shutil import filecmp from llnl.util.filesystem import traverse_tree, mkdirp, touch +import llnl.util.tty as tty __all__ = ['LinkTree'] @@ -44,37 +26,49 @@ class LinkTree(object): Trees comprise symlinks only to files; directries are never symlinked to, to prevent the source directory from ever being modified. - """ - def __init__(self, source_root): if not os.path.exists(source_root): raise IOError("No such file or directory: '%s'", source_root) self._root = source_root - def find_conflict(self, dest_root, **kwargs): + def find_conflict(self, dest_root, ignore=None, + ignore_file_conflicts=False): """Returns the first file in dest that conflicts with src""" - kwargs['follow_nonexisting'] = False + ignore = ignore or (lambda x: False) + conflicts = self.find_dir_conflicts(dest_root, ignore) + + if not ignore_file_conflicts: + conflicts.extend( + dst for src, dst + in self.get_file_map(dest_root, ignore).items() + if os.path.exists(dst)) + + if conflicts: + return conflicts[0] + + def find_dir_conflicts(self, dest_root, ignore): + conflicts = [] + kwargs = {'follow_nonexisting': False, 'ignore': ignore} for src, dest in traverse_tree(self._root, dest_root, **kwargs): if os.path.isdir(src): if os.path.exists(dest) and not os.path.isdir(dest): - return dest - elif os.path.exists(dest): - return dest - return None - - def merge(self, dest_root, link=os.symlink, **kwargs): - """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. - """ - kwargs['order'] = 'pre' - ignore_conflicts = kwargs.get("ignore_conflicts", False) - existing = [] + conflicts.append("File blocks directory: %s" % dest) + elif os.path.exists(dest) and os.path.isdir(dest): + conflicts.append("Directory blocks directory: %s" % dest) + return conflicts + + def get_file_map(self, dest_root, ignore): + merge_map = {} + kwargs = {'follow_nonexisting': True, 'ignore': ignore} for src, dest in traverse_tree(self._root, dest_root, **kwargs): + if not os.path.isdir(src): + merge_map[src] = dest + return merge_map + + def merge_directories(self, dest_root, ignore): + for src, dest in traverse_tree(self._root, dest_root, ignore=ignore): if os.path.isdir(src): if not os.path.exists(dest): mkdirp(dest) @@ -88,31 +82,13 @@ class LinkTree(object): marker = os.path.join(dest, empty_file_name) touch(marker) - else: - if os.path.exists(dest): - if ignore_conflicts: - existing.append(src) - else: - raise AssertionError("File already exists: %s" % dest) - else: - link(src, dest) - if ignore_conflicts: - return existing - - def unmerge(self, dest_root, **kwargs): - """Unlink all files in dest that exist in src. - - Unlinks directories in dest if they are empty. - - """ - kwargs['order'] = 'post' - for src, dest in traverse_tree(self._root, dest_root, **kwargs): + def unmerge_directories(self, dest_root, ignore): + for src, dest in traverse_tree( + self._root, dest_root, ignore=ignore, order='post'): if os.path.isdir(src): - # Skip non-existing links. if not os.path.exists(dest): continue - - if not os.path.isdir(dest): + elif not os.path.isdir(dest): raise ValueError("File blocks directory: %s" % dest) # remove directory if it is empty. @@ -124,11 +100,61 @@ class LinkTree(object): if os.path.exists(marker): os.remove(marker) - elif os.path.exists(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) + def merge(self, dest_root, **kwargs): + """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) + conflict = self.find_conflict( + dest_root, ignore=ignore, ignore_file_conflicts=ignore_conflicts) + if conflict: + raise MergeConflictError(conflict) + + 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) + else: + merge_file(src, dst) + + for c in existing: + tty.warn("Could not merge: %s" % c) + + def unmerge(self, dest_root, **kwargs): + """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) + 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): + super(MergeConflictError, self).__init__( + "Package merge blocked by file: %s" % path) |