parent
7ec93a496d
commit
0718e3459a
@ -1377,120 +1377,89 @@ def traverse_tree(
|
|||||||
yield (source_path, dest_path)
|
yield (source_path, dest_path)
|
||||||
|
|
||||||
|
|
||||||
def lexists_islink_isdir(path):
|
|
||||||
"""Computes the tuple (lexists(path), islink(path), isdir(path)) in a minimal
|
|
||||||
number of stat calls on unix. Use os.path and symlink.islink methods for windows."""
|
|
||||||
if sys.platform == "win32":
|
|
||||||
if not os.path.lexists(path):
|
|
||||||
return False, False, False
|
|
||||||
return os.path.lexists(path), islink(path), os.path.isdir(path)
|
|
||||||
# First try to lstat, so we know if it's a link or not.
|
|
||||||
try:
|
|
||||||
lst = os.lstat(path)
|
|
||||||
except (IOError, OSError):
|
|
||||||
return False, False, False
|
|
||||||
|
|
||||||
is_link = stat.S_ISLNK(lst.st_mode)
|
|
||||||
|
|
||||||
# Check whether file is a dir.
|
|
||||||
if not is_link:
|
|
||||||
is_dir = stat.S_ISDIR(lst.st_mode)
|
|
||||||
return True, is_link, is_dir
|
|
||||||
|
|
||||||
# Check whether symlink points to a dir.
|
|
||||||
try:
|
|
||||||
st = os.stat(path)
|
|
||||||
is_dir = stat.S_ISDIR(st.st_mode)
|
|
||||||
except (IOError, OSError):
|
|
||||||
# Dangling symlink (i.e. it lexists but not exists)
|
|
||||||
is_dir = False
|
|
||||||
|
|
||||||
return True, is_link, is_dir
|
|
||||||
|
|
||||||
|
|
||||||
class BaseDirectoryVisitor:
|
class BaseDirectoryVisitor:
|
||||||
"""Base class and interface for :py:func:`visit_directory_tree`."""
|
"""Base class and interface for :py:func:`visit_directory_tree`."""
|
||||||
|
|
||||||
def visit_file(self, root, rel_path, depth):
|
def visit_file(self, root: str, rel_path: str, depth: int) -> None:
|
||||||
"""Handle the non-symlink file at ``os.path.join(root, rel_path)``
|
"""Handle the non-symlink file at ``os.path.join(root, rel_path)``
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
root (str): root directory
|
root: root directory
|
||||||
rel_path (str): relative path to current file from ``root``
|
rel_path: relative path to current file from ``root``
|
||||||
depth (int): depth of current file from the ``root`` directory"""
|
depth (int): depth of current file from the ``root`` directory"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def visit_symlinked_file(self, root, rel_path, depth):
|
def visit_symlinked_file(self, root: str, rel_path: str, depth) -> None:
|
||||||
"""Handle the symlink to a file at ``os.path.join(root, rel_path)``.
|
"""Handle the symlink to a file at ``os.path.join(root, rel_path)``. Note: ``rel_path`` is
|
||||||
Note: ``rel_path`` is the location of the symlink, not to what it is
|
the location of the symlink, not to what it is pointing to. The symlink may be dangling.
|
||||||
pointing to. The symlink may be dangling.
|
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
root (str): root directory
|
root: root directory
|
||||||
rel_path (str): relative path to current symlink from ``root``
|
rel_path: relative path to current symlink from ``root``
|
||||||
depth (int): depth of current symlink from the ``root`` directory"""
|
depth: depth of current symlink from the ``root`` directory"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def before_visit_dir(self, root, rel_path, depth):
|
def before_visit_dir(self, root: str, rel_path: str, depth: int) -> bool:
|
||||||
"""Return True from this function to recurse into the directory at
|
"""Return True from this function to recurse into the directory at
|
||||||
os.path.join(root, rel_path). Return False in order not to recurse further.
|
os.path.join(root, rel_path). Return False in order not to recurse further.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
root (str): root directory
|
root: root directory
|
||||||
rel_path (str): relative path to current directory from ``root``
|
rel_path: relative path to current directory from ``root``
|
||||||
depth (int): depth of current directory from the ``root`` directory
|
depth: depth of current directory from the ``root`` directory
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: ``True`` when the directory should be recursed into. ``False`` when
|
bool: ``True`` when the directory should be recursed into. ``False`` when
|
||||||
not"""
|
not"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def before_visit_symlinked_dir(self, root, rel_path, depth):
|
def before_visit_symlinked_dir(self, root: str, rel_path: str, depth: int) -> bool:
|
||||||
"""Return ``True`` to recurse into the symlinked directory and ``False`` in
|
"""Return ``True`` to recurse into the symlinked directory and ``False`` in order not to.
|
||||||
order not to. Note: ``rel_path`` is the path to the symlink itself.
|
Note: ``rel_path`` is the path to the symlink itself. Following symlinked directories
|
||||||
Following symlinked directories blindly can cause infinite recursion due to
|
blindly can cause infinite recursion due to cycles.
|
||||||
cycles.
|
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
root (str): root directory
|
root: root directory
|
||||||
rel_path (str): relative path to current symlink from ``root``
|
rel_path: relative path to current symlink from ``root``
|
||||||
depth (int): depth of current symlink from the ``root`` directory
|
depth: depth of current symlink from the ``root`` directory
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: ``True`` when the directory should be recursed into. ``False`` when
|
bool: ``True`` when the directory should be recursed into. ``False`` when
|
||||||
not"""
|
not"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def after_visit_dir(self, root, rel_path, depth):
|
def after_visit_dir(self, root: str, rel_path: str, depth: int) -> None:
|
||||||
"""Called after recursion into ``rel_path`` finished. This function is not
|
"""Called after recursion into ``rel_path`` finished. This function is not called when
|
||||||
called when ``rel_path`` was not recursed into.
|
``rel_path`` was not recursed into.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
root (str): root directory
|
root: root directory
|
||||||
rel_path (str): relative path to current directory from ``root``
|
rel_path: relative path to current directory from ``root``
|
||||||
depth (int): depth of current directory from the ``root`` directory"""
|
depth: depth of current directory from the ``root`` directory"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def after_visit_symlinked_dir(self, root, rel_path, depth):
|
def after_visit_symlinked_dir(self, root: str, rel_path: str, depth: int) -> None:
|
||||||
"""Called after recursion into ``rel_path`` finished. This function is not
|
"""Called after recursion into ``rel_path`` finished. This function is not called when
|
||||||
called when ``rel_path`` was not recursed into.
|
``rel_path`` was not recursed into.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
root (str): root directory
|
root: root directory
|
||||||
rel_path (str): relative path to current symlink from ``root``
|
rel_path: relative path to current symlink from ``root``
|
||||||
depth (int): depth of current symlink from the ``root`` directory"""
|
depth: depth of current symlink from the ``root`` directory"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def visit_directory_tree(root, visitor, rel_path="", depth=0):
|
def visit_directory_tree(
|
||||||
"""Recurses the directory root depth-first through a visitor pattern using the
|
root: str, visitor: BaseDirectoryVisitor, rel_path: str = "", depth: int = 0
|
||||||
interface from :py:class:`BaseDirectoryVisitor`
|
):
|
||||||
|
"""Recurses the directory root depth-first through a visitor pattern using the interface from
|
||||||
|
:py:class:`BaseDirectoryVisitor`
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
root (str): path of directory to recurse into
|
root: path of directory to recurse into
|
||||||
visitor (BaseDirectoryVisitor): what visitor to use
|
visitor: what visitor to use
|
||||||
rel_path (str): current relative path from the root
|
rel_path: current relative path from the root
|
||||||
depth (str): current depth from the root
|
depth: current depth from the root
|
||||||
"""
|
"""
|
||||||
dir = os.path.join(root, rel_path)
|
dir = os.path.join(root, rel_path)
|
||||||
dir_entries = sorted(os.scandir(dir), key=lambda d: d.name)
|
dir_entries = sorted(os.scandir(dir), key=lambda d: d.name)
|
||||||
@ -1498,26 +1467,19 @@ def visit_directory_tree(root, visitor, rel_path="", depth=0):
|
|||||||
for f in dir_entries:
|
for f in dir_entries:
|
||||||
rel_child = os.path.join(rel_path, f.name)
|
rel_child = os.path.join(rel_path, f.name)
|
||||||
islink = f.is_symlink()
|
islink = f.is_symlink()
|
||||||
# On Windows, symlinks to directories are distinct from
|
# On Windows, symlinks to directories are distinct from symlinks to files, and it is
|
||||||
# symlinks to files, and it is possible to create a
|
# possible to create a broken symlink to a directory (e.g. using os.symlink without
|
||||||
# broken symlink to a directory (e.g. using os.symlink
|
# `target_is_directory=True`), invoking `isdir` on a symlink on Windows that is broken in
|
||||||
# without `target_is_directory=True`), invoking `isdir`
|
# this manner will result in an error. In this case we can work around the issue by reading
|
||||||
# on a symlink on Windows that is broken in this manner
|
# the target and resolving the directory ourselves
|
||||||
# will result in an error. In this case we can work around
|
|
||||||
# the issue by reading the target and resolving the
|
|
||||||
# directory ourselves
|
|
||||||
try:
|
try:
|
||||||
isdir = f.is_dir()
|
isdir = f.is_dir()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if sys.platform == "win32" and hasattr(e, "winerror") and e.winerror == 5 and islink:
|
if sys.platform == "win32" and hasattr(e, "winerror") and e.winerror == 5 and islink:
|
||||||
# if path is a symlink, determine destination and
|
# if path is a symlink, determine destination and evaluate file vs directory
|
||||||
# evaluate file vs directory
|
|
||||||
link_target = resolve_link_target_relative_to_the_link(f)
|
link_target = resolve_link_target_relative_to_the_link(f)
|
||||||
# link_target might be relative but
|
# link_target might be relative but resolve_link_target_relative_to_the_link
|
||||||
# resolve_link_target_relative_to_the_link
|
# will ensure that if so, that it is relative to the CWD and therefore makes sense
|
||||||
# will ensure that if so, that it is relative
|
|
||||||
# to the CWD and therefore
|
|
||||||
# makes sense
|
|
||||||
isdir = os.path.isdir(link_target)
|
isdir = os.path.isdir(link_target)
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import filecmp
|
import filecmp
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from collections import OrderedDict
|
from typing import Callable, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
from llnl.util.filesystem import BaseDirectoryVisitor, mkdirp, touch, traverse_tree
|
from llnl.util.filesystem import BaseDirectoryVisitor, mkdirp, touch, traverse_tree
|
||||||
@ -51,32 +51,30 @@ class SourceMergeVisitor(BaseDirectoryVisitor):
|
|||||||
- A list of merge conflicts in dst/
|
- A list of merge conflicts in dst/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, ignore=None):
|
def __init__(self, ignore: Optional[Callable[[str], bool]] = None):
|
||||||
self.ignore = ignore if ignore is not None else lambda f: False
|
self.ignore = ignore if ignore is not None else lambda f: False
|
||||||
|
|
||||||
# When mapping <src root> to <dst root>/<projection>, we need
|
# When mapping <src root> to <dst root>/<projection>, we need to prepend the <projection>
|
||||||
# to prepend the <projection> bit to the relative path in the
|
# bit to the relative path in the destination dir.
|
||||||
# destination dir.
|
self.projection: str = ""
|
||||||
self.projection = ""
|
|
||||||
|
|
||||||
# When a file blocks another file, the conflict can sometimes
|
# When a file blocks another file, the conflict can sometimes be resolved / ignored
|
||||||
# be resolved / ignored (e.g. <prefix>/LICENSE or
|
# (e.g. <prefix>/LICENSE or <site-packages>/<namespace>/__init__.py conflicts can be
|
||||||
# or <site-packages>/<namespace>/__init__.py conflicts can be
|
|
||||||
# ignored).
|
# ignored).
|
||||||
self.file_conflicts = []
|
self.file_conflicts: List[MergeConflict] = []
|
||||||
|
|
||||||
# When we have to create a dir where a file is, or a file
|
# When we have to create a dir where a file is, or a file where a dir is, we have fatal
|
||||||
# where a dir is, we have fatal errors, listed here.
|
# errors, listed here.
|
||||||
self.fatal_conflicts = []
|
self.fatal_conflicts: List[MergeConflict] = []
|
||||||
|
|
||||||
# What directories we have to make; this is an ordered set,
|
# What directories we have to make; this is an ordered dict, so that we have a fast lookup
|
||||||
# so that we have a fast lookup and can run mkdir in order.
|
# and can run mkdir in order.
|
||||||
self.directories = OrderedDict()
|
self.directories: Dict[str, Tuple[str, str]] = {}
|
||||||
|
|
||||||
# Files to link. Maps dst_rel to (src_root, src_rel)
|
# Files to link. Maps dst_rel to (src_root, src_rel)
|
||||||
self.files = OrderedDict()
|
self.files: Dict[str, Tuple[str, str]] = {}
|
||||||
|
|
||||||
def before_visit_dir(self, root, rel_path, depth):
|
def before_visit_dir(self, root: str, rel_path: str, depth: int) -> bool:
|
||||||
"""
|
"""
|
||||||
Register a directory if dst / rel_path is not blocked by a file or ignored.
|
Register a directory if dst / rel_path is not blocked by a file or ignored.
|
||||||
"""
|
"""
|
||||||
@ -104,7 +102,7 @@ def before_visit_dir(self, root, rel_path, depth):
|
|||||||
self.directories[proj_rel_path] = (root, rel_path)
|
self.directories[proj_rel_path] = (root, rel_path)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def before_visit_symlinked_dir(self, root, rel_path, depth):
|
def before_visit_symlinked_dir(self, root: str, rel_path: str, depth: int) -> bool:
|
||||||
"""
|
"""
|
||||||
Replace symlinked dirs with actual directories when possible in low depths,
|
Replace symlinked dirs with actual directories when possible in low depths,
|
||||||
otherwise handle it as a file (i.e. we link to the symlink).
|
otherwise handle it as a file (i.e. we link to the symlink).
|
||||||
@ -136,7 +134,7 @@ def before_visit_symlinked_dir(self, root, rel_path, depth):
|
|||||||
self.visit_file(root, rel_path, depth)
|
self.visit_file(root, rel_path, depth)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def visit_file(self, root, rel_path, depth):
|
def visit_file(self, root: str, rel_path: str, depth: int) -> None:
|
||||||
proj_rel_path = os.path.join(self.projection, rel_path)
|
proj_rel_path = os.path.join(self.projection, rel_path)
|
||||||
|
|
||||||
if self.ignore(rel_path):
|
if self.ignore(rel_path):
|
||||||
@ -165,11 +163,11 @@ def visit_file(self, root, rel_path, depth):
|
|||||||
# Otherwise register this file to be linked.
|
# Otherwise register this file to be linked.
|
||||||
self.files[proj_rel_path] = (root, rel_path)
|
self.files[proj_rel_path] = (root, rel_path)
|
||||||
|
|
||||||
def visit_symlinked_file(self, root, rel_path, depth):
|
def visit_symlinked_file(self, root: str, rel_path: str, depth: int) -> None:
|
||||||
# Treat symlinked files as ordinary files (without "dereferencing")
|
# Treat symlinked files as ordinary files (without "dereferencing")
|
||||||
self.visit_file(root, rel_path, depth)
|
self.visit_file(root, rel_path, depth)
|
||||||
|
|
||||||
def set_projection(self, projection):
|
def set_projection(self, projection: str) -> None:
|
||||||
self.projection = os.path.normpath(projection)
|
self.projection = os.path.normpath(projection)
|
||||||
|
|
||||||
# Todo, is this how to check in general for empty projection?
|
# Todo, is this how to check in general for empty projection?
|
||||||
@ -197,24 +195,19 @@ def set_projection(self, projection):
|
|||||||
|
|
||||||
|
|
||||||
class DestinationMergeVisitor(BaseDirectoryVisitor):
|
class DestinationMergeVisitor(BaseDirectoryVisitor):
|
||||||
"""DestinatinoMergeVisitor takes a SourceMergeVisitor
|
"""DestinatinoMergeVisitor takes a SourceMergeVisitor and:
|
||||||
and:
|
|
||||||
|
|
||||||
a. registers additional conflicts when merging
|
a. registers additional conflicts when merging to the destination prefix
|
||||||
to the destination prefix
|
b. removes redundant mkdir operations when directories already exist in the destination prefix.
|
||||||
b. removes redundant mkdir operations when
|
|
||||||
directories already exist in the destination
|
|
||||||
prefix.
|
|
||||||
|
|
||||||
This also makes sure that symlinked directories
|
This also makes sure that symlinked directories in the target prefix will never be merged with
|
||||||
in the target prefix will never be merged with
|
|
||||||
directories in the sources directories.
|
directories in the sources directories.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, source_merge_visitor):
|
def __init__(self, source_merge_visitor: SourceMergeVisitor):
|
||||||
self.src = source_merge_visitor
|
self.src = source_merge_visitor
|
||||||
|
|
||||||
def before_visit_dir(self, root, rel_path, depth):
|
def before_visit_dir(self, root: str, rel_path: str, depth: int) -> bool:
|
||||||
# If destination dir is a file in a src dir, add a conflict,
|
# If destination dir is a file in a src dir, add a conflict,
|
||||||
# and don't traverse deeper
|
# and don't traverse deeper
|
||||||
if rel_path in self.src.files:
|
if rel_path in self.src.files:
|
||||||
@ -236,7 +229,7 @@ def before_visit_dir(self, root, rel_path, depth):
|
|||||||
# don't descend into it.
|
# don't descend into it.
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def before_visit_symlinked_dir(self, root, rel_path, depth):
|
def before_visit_symlinked_dir(self, root: str, rel_path: str, depth: int) -> bool:
|
||||||
"""
|
"""
|
||||||
Symlinked directories in the destination prefix should
|
Symlinked directories in the destination prefix should
|
||||||
be seen as files; we should not accidentally merge
|
be seen as files; we should not accidentally merge
|
||||||
@ -262,7 +255,7 @@ def before_visit_symlinked_dir(self, root, rel_path, depth):
|
|||||||
# Never descend into symlinked target dirs.
|
# Never descend into symlinked target dirs.
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def visit_file(self, root, rel_path, depth):
|
def visit_file(self, root: str, rel_path: str, depth: int) -> None:
|
||||||
# Can't merge a file if target already exists
|
# Can't merge a file if target already exists
|
||||||
if rel_path in self.src.directories:
|
if rel_path in self.src.directories:
|
||||||
src_a_root, src_a_relpath = self.src.directories[rel_path]
|
src_a_root, src_a_relpath = self.src.directories[rel_path]
|
||||||
@ -280,7 +273,7 @@ def visit_file(self, root, rel_path, depth):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def visit_symlinked_file(self, root, rel_path, depth):
|
def visit_symlinked_file(self, root: str, rel_path: str, depth: int) -> None:
|
||||||
# Treat symlinked files as ordinary files (without "dereferencing")
|
# Treat symlinked files as ordinary files (without "dereferencing")
|
||||||
self.visit_file(root, rel_path, depth)
|
self.visit_file(root, rel_path, depth)
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
from llnl.util.tty.color import colorize
|
from llnl.util.tty.color import colorize
|
||||||
|
|
||||||
import spack.config
|
import spack.config
|
||||||
|
import spack.paths
|
||||||
import spack.projections
|
import spack.projections
|
||||||
import spack.relocate
|
import spack.relocate
|
||||||
import spack.schema.projections
|
import spack.schema.projections
|
||||||
@ -91,7 +92,7 @@ def view_copy(src: str, dst: str, view, spec: Optional[spack.spec.Spec] = None):
|
|||||||
prefix_to_projection[spack.store.STORE.layout.root] = view._root
|
prefix_to_projection[spack.store.STORE.layout.root] = view._root
|
||||||
|
|
||||||
# This is vestigial code for the *old* location of sbang.
|
# This is vestigial code for the *old* location of sbang.
|
||||||
prefix_to_projection["#!/bin/bash {0}/bin/sbang".format(spack.paths.spack_root)] = (
|
prefix_to_projection[f"#!/bin/bash {spack.paths.spack_root}/bin/sbang"] = (
|
||||||
sbang.sbang_shebang_line()
|
sbang.sbang_shebang_line()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -100,7 +101,7 @@ def view_copy(src: str, dst: str, view, spec: Optional[spack.spec.Spec] = None):
|
|||||||
try:
|
try:
|
||||||
os.chown(dst, src_stat.st_uid, src_stat.st_gid)
|
os.chown(dst, src_stat.st_uid, src_stat.st_gid)
|
||||||
except OSError:
|
except OSError:
|
||||||
tty.debug("Can't change the permissions for %s" % dst)
|
tty.debug(f"Can't change the permissions for {dst}")
|
||||||
|
|
||||||
|
|
||||||
def view_func_parser(parsed_name):
|
def view_func_parser(parsed_name):
|
||||||
@ -112,7 +113,7 @@ def view_func_parser(parsed_name):
|
|||||||
elif parsed_name in ("add", "symlink", "soft"):
|
elif parsed_name in ("add", "symlink", "soft"):
|
||||||
return view_symlink
|
return view_symlink
|
||||||
else:
|
else:
|
||||||
raise ValueError("invalid link type for view: '%s'" % parsed_name)
|
raise ValueError(f"invalid link type for view: '{parsed_name}'")
|
||||||
|
|
||||||
|
|
||||||
def inverse_view_func_parser(view_type):
|
def inverse_view_func_parser(view_type):
|
||||||
@ -270,9 +271,10 @@ def __init__(self, root, layout, **kwargs):
|
|||||||
# Ensure projections are the same from each source
|
# Ensure projections are the same from each source
|
||||||
# Read projections file from view
|
# Read projections file from view
|
||||||
if self.projections != self.read_projections():
|
if self.projections != self.read_projections():
|
||||||
msg = "View at %s has projections file" % self._root
|
raise ConflictingProjectionsError(
|
||||||
msg += " which does not match projections passed manually."
|
f"View at {self._root} has projections file"
|
||||||
raise ConflictingProjectionsError(msg)
|
" which does not match projections passed manually."
|
||||||
|
)
|
||||||
|
|
||||||
self._croot = colorize_root(self._root) + " "
|
self._croot = colorize_root(self._root) + " "
|
||||||
|
|
||||||
@ -313,11 +315,11 @@ def add_specs(self, *specs, **kwargs):
|
|||||||
|
|
||||||
def add_standalone(self, spec):
|
def add_standalone(self, spec):
|
||||||
if spec.external:
|
if spec.external:
|
||||||
tty.warn(self._croot + "Skipping external package: %s" % colorize_spec(spec))
|
tty.warn(f"{self._croot}Skipping external package: {colorize_spec(spec)}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if self.check_added(spec):
|
if self.check_added(spec):
|
||||||
tty.warn(self._croot + "Skipping already linked package: %s" % colorize_spec(spec))
|
tty.warn(f"{self._croot}Skipping already linked package: {colorize_spec(spec)}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self.merge(spec)
|
self.merge(spec)
|
||||||
@ -325,7 +327,7 @@ def add_standalone(self, spec):
|
|||||||
self.link_meta_folder(spec)
|
self.link_meta_folder(spec)
|
||||||
|
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
tty.info(self._croot + "Linked package: %s" % colorize_spec(spec))
|
tty.info(f"{self._croot}Linked package: {colorize_spec(spec)}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def merge(self, spec, ignore=None):
|
def merge(self, spec, ignore=None):
|
||||||
@ -393,7 +395,7 @@ def needs_file(spec, file):
|
|||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
if not os.path.lexists(file):
|
if not os.path.lexists(file):
|
||||||
tty.warn("Tried to remove %s which does not exist" % file)
|
tty.warn(f"Tried to remove {file} which does not exist")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# remove if file is not owned by any other package in the view
|
# remove if file is not owned by any other package in the view
|
||||||
@ -404,7 +406,7 @@ def needs_file(spec, file):
|
|||||||
# we are currently removing, as we remove files before unlinking the
|
# we are currently removing, as we remove files before unlinking the
|
||||||
# metadata directory.
|
# metadata directory.
|
||||||
if len([s for s in specs if needs_file(s, file)]) <= 1:
|
if len([s for s in specs if needs_file(s, file)]) <= 1:
|
||||||
tty.debug("Removing file " + file)
|
tty.debug(f"Removing file {file}")
|
||||||
os.remove(file)
|
os.remove(file)
|
||||||
|
|
||||||
def check_added(self, spec):
|
def check_added(self, spec):
|
||||||
@ -477,14 +479,14 @@ def remove_standalone(self, spec):
|
|||||||
Remove (unlink) a standalone package from this view.
|
Remove (unlink) a standalone package from this view.
|
||||||
"""
|
"""
|
||||||
if not self.check_added(spec):
|
if not self.check_added(spec):
|
||||||
tty.warn(self._croot + "Skipping package not linked in view: %s" % spec.name)
|
tty.warn(f"{self._croot}Skipping package not linked in view: {spec.name}")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.unmerge(spec)
|
self.unmerge(spec)
|
||||||
self.unlink_meta_folder(spec)
|
self.unlink_meta_folder(spec)
|
||||||
|
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
tty.info(self._croot + "Removed package: %s" % colorize_spec(spec))
|
tty.info(f"{self._croot}Removed package: {colorize_spec(spec)}")
|
||||||
|
|
||||||
def get_projection_for_spec(self, spec):
|
def get_projection_for_spec(self, spec):
|
||||||
"""
|
"""
|
||||||
@ -558,9 +560,9 @@ def print_conflict(self, spec_active, spec_specified, level="error"):
|
|||||||
linked = tty.color.colorize(" (@gLinked@.)", color=color)
|
linked = tty.color.colorize(" (@gLinked@.)", color=color)
|
||||||
specified = tty.color.colorize("(@rSpecified@.)", color=color)
|
specified = tty.color.colorize("(@rSpecified@.)", color=color)
|
||||||
cprint(
|
cprint(
|
||||||
self._croot + "Package conflict detected:\n"
|
f"{self._croot}Package conflict detected:\n"
|
||||||
"%s %s\n" % (linked, colorize_spec(spec_active))
|
f"{linked} {colorize_spec(spec_active)}\n"
|
||||||
+ "%s %s" % (specified, colorize_spec(spec_specified))
|
f"{specified} {colorize_spec(spec_specified)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def print_status(self, *specs, **kwargs):
|
def print_status(self, *specs, **kwargs):
|
||||||
@ -572,14 +574,14 @@ def print_status(self, *specs, **kwargs):
|
|||||||
|
|
||||||
for s, v in zip(specs, in_view):
|
for s, v in zip(specs, in_view):
|
||||||
if not v:
|
if not v:
|
||||||
tty.error(self._croot + "Package not linked: %s" % s.name)
|
tty.error(f"{self._croot}Package not linked: {s.name}")
|
||||||
elif s != v:
|
elif s != v:
|
||||||
self.print_conflict(v, s, level="warn")
|
self.print_conflict(v, s, level="warn")
|
||||||
|
|
||||||
in_view = list(filter(None, in_view))
|
in_view = list(filter(None, in_view))
|
||||||
|
|
||||||
if len(specs) > 0:
|
if len(specs) > 0:
|
||||||
tty.msg("Packages linked in %s:" % self._croot[:-1])
|
tty.msg(f"Packages linked in {self._croot[:-1]}:")
|
||||||
|
|
||||||
# Make a dict with specs keyed by architecture and compiler.
|
# Make a dict with specs keyed by architecture and compiler.
|
||||||
index = index_by(specs, ("architecture", "compiler"))
|
index = index_by(specs, ("architecture", "compiler"))
|
||||||
@ -589,20 +591,19 @@ def print_status(self, *specs, **kwargs):
|
|||||||
if i > 0:
|
if i > 0:
|
||||||
print()
|
print()
|
||||||
|
|
||||||
header = "%s{%s} / %s{%s}" % (
|
header = (
|
||||||
spack.spec.ARCHITECTURE_COLOR,
|
f"{spack.spec.ARCHITECTURE_COLOR}{{{architecture}}} "
|
||||||
architecture,
|
f"/ {spack.spec.COMPILER_COLOR}{{{compiler}}}"
|
||||||
spack.spec.COMPILER_COLOR,
|
|
||||||
compiler,
|
|
||||||
)
|
)
|
||||||
tty.hline(colorize(header), char="-")
|
tty.hline(colorize(header), char="-")
|
||||||
|
|
||||||
specs = index[(architecture, compiler)]
|
specs = index[(architecture, compiler)]
|
||||||
specs.sort()
|
specs.sort()
|
||||||
|
|
||||||
format_string = "{name}{@version}"
|
abbreviated = [
|
||||||
format_string += "{%compiler}{compiler_flags}{variants}"
|
s.cformat("{name}{@version}{%compiler}{compiler_flags}{variants}")
|
||||||
abbreviated = [s.cformat(format_string) for s in specs]
|
for s in specs
|
||||||
|
]
|
||||||
|
|
||||||
# Print one spec per line along with prefix path
|
# Print one spec per line along with prefix path
|
||||||
width = max(len(s) for s in abbreviated)
|
width = max(len(s) for s in abbreviated)
|
||||||
@ -634,22 +635,19 @@ def unlink_meta_folder(self, spec):
|
|||||||
|
|
||||||
|
|
||||||
class SimpleFilesystemView(FilesystemView):
|
class SimpleFilesystemView(FilesystemView):
|
||||||
"""A simple and partial implementation of FilesystemView focused on
|
"""A simple and partial implementation of FilesystemView focused on performance and immutable
|
||||||
performance and immutable views, where specs cannot be removed after they
|
views, where specs cannot be removed after they were added."""
|
||||||
were added."""
|
|
||||||
|
|
||||||
def __init__(self, root, layout, **kwargs):
|
def __init__(self, root, layout, **kwargs):
|
||||||
super().__init__(root, layout, **kwargs)
|
super().__init__(root, layout, **kwargs)
|
||||||
|
|
||||||
def _sanity_check_view_projection(self, specs):
|
def _sanity_check_view_projection(self, specs):
|
||||||
"""A very common issue is that we end up with two specs of the same
|
"""A very common issue is that we end up with two specs of the same package, that project
|
||||||
package, that project to the same prefix. We want to catch that as
|
to the same prefix. We want to catch that as early as possible and give a sensible error to
|
||||||
early as possible and give a sensible error to the user. Here we use
|
the user. Here we use the metadata dir (.spack) projection as a quick test to see whether
|
||||||
the metadata dir (.spack) projection as a quick test to see whether
|
two specs in the view are going to clash. The metadata dir is used because it's always
|
||||||
two specs in the view are going to clash. The metadata dir is used
|
added by Spack with identical files, so a guaranteed clash that's easily verified."""
|
||||||
because it's always added by Spack with identical files, so a
|
seen = {}
|
||||||
guaranteed clash that's easily verified."""
|
|
||||||
seen = dict()
|
|
||||||
for current_spec in specs:
|
for current_spec in specs:
|
||||||
metadata_dir = self.relative_metadata_dir_for_spec(current_spec)
|
metadata_dir = self.relative_metadata_dir_for_spec(current_spec)
|
||||||
conflicting_spec = seen.get(metadata_dir)
|
conflicting_spec = seen.get(metadata_dir)
|
||||||
@ -695,13 +693,11 @@ def skip_list(file):
|
|||||||
# Inform about file-file conflicts.
|
# Inform about file-file conflicts.
|
||||||
if visitor.file_conflicts:
|
if visitor.file_conflicts:
|
||||||
if self.ignore_conflicts:
|
if self.ignore_conflicts:
|
||||||
tty.debug("{0} file conflicts".format(len(visitor.file_conflicts)))
|
tty.debug(f"{len(visitor.file_conflicts)} file conflicts")
|
||||||
else:
|
else:
|
||||||
raise MergeConflictSummary(visitor.file_conflicts)
|
raise MergeConflictSummary(visitor.file_conflicts)
|
||||||
|
|
||||||
tty.debug(
|
tty.debug(f"Creating {len(visitor.directories)} dirs and {len(visitor.files)} links")
|
||||||
"Creating {0} dirs and {1} links".format(len(visitor.directories), len(visitor.files))
|
|
||||||
)
|
|
||||||
|
|
||||||
# Make the directory structure
|
# Make the directory structure
|
||||||
for dst in visitor.directories:
|
for dst in visitor.directories:
|
||||||
|
@ -13,8 +13,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import llnl.util.filesystem as fs
|
import llnl.util.filesystem as fs
|
||||||
import llnl.util.symlink
|
from llnl.util.symlink import islink, symlink
|
||||||
from llnl.util.symlink import SymlinkError, _windows_can_symlink, islink, symlink
|
|
||||||
|
|
||||||
import spack.paths
|
import spack.paths
|
||||||
|
|
||||||
@ -754,93 +753,6 @@ def test_is_nonsymlink_exe_with_shebang(tmpdir):
|
|||||||
assert not fs.is_nonsymlink_exe_with_shebang("symlink_to_executable_script")
|
assert not fs.is_nonsymlink_exe_with_shebang("symlink_to_executable_script")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.platform == "win32", reason="Unix-only test.")
|
|
||||||
def test_lexists_islink_isdir(tmpdir):
|
|
||||||
root = str(tmpdir)
|
|
||||||
|
|
||||||
# Create a directory and a file, an a bunch of symlinks.
|
|
||||||
dir = os.path.join(root, "dir")
|
|
||||||
file = os.path.join(root, "file")
|
|
||||||
nonexistent = os.path.join(root, "does_not_exist")
|
|
||||||
symlink_to_dir = os.path.join(root, "symlink_to_dir")
|
|
||||||
symlink_to_file = os.path.join(root, "symlink_to_file")
|
|
||||||
dangling_symlink = os.path.join(root, "dangling_symlink")
|
|
||||||
symlink_to_dangling_symlink = os.path.join(root, "symlink_to_dangling_symlink")
|
|
||||||
symlink_to_symlink_to_dir = os.path.join(root, "symlink_to_symlink_to_dir")
|
|
||||||
symlink_to_symlink_to_file = os.path.join(root, "symlink_to_symlink_to_file")
|
|
||||||
|
|
||||||
os.mkdir(dir)
|
|
||||||
with open(file, "wb") as f:
|
|
||||||
f.write(b"file")
|
|
||||||
|
|
||||||
symlink("dir", symlink_to_dir)
|
|
||||||
symlink("file", symlink_to_file)
|
|
||||||
symlink("does_not_exist", dangling_symlink)
|
|
||||||
symlink("dangling_symlink", symlink_to_dangling_symlink)
|
|
||||||
symlink("symlink_to_dir", symlink_to_symlink_to_dir)
|
|
||||||
symlink("symlink_to_file", symlink_to_symlink_to_file)
|
|
||||||
|
|
||||||
assert fs.lexists_islink_isdir(dir) == (True, False, True)
|
|
||||||
assert fs.lexists_islink_isdir(file) == (True, False, False)
|
|
||||||
assert fs.lexists_islink_isdir(nonexistent) == (False, False, False)
|
|
||||||
assert fs.lexists_islink_isdir(symlink_to_dir) == (True, True, True)
|
|
||||||
assert fs.lexists_islink_isdir(symlink_to_file) == (True, True, False)
|
|
||||||
assert fs.lexists_islink_isdir(symlink_to_dangling_symlink) == (True, True, False)
|
|
||||||
assert fs.lexists_islink_isdir(symlink_to_symlink_to_dir) == (True, True, True)
|
|
||||||
assert fs.lexists_islink_isdir(symlink_to_symlink_to_file) == (True, True, False)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.platform != "win32", reason="For Windows Only")
|
|
||||||
@pytest.mark.parametrize("win_can_symlink", [True, False])
|
|
||||||
def test_lexists_islink_isdir_windows(tmpdir, monkeypatch, win_can_symlink):
|
|
||||||
"""Run on windows without elevated privileges to test junctions and hard links which have
|
|
||||||
different results from the lexists_islink_isdir method.
|
|
||||||
"""
|
|
||||||
if win_can_symlink and not _windows_can_symlink():
|
|
||||||
pytest.skip("Cannot test dev mode behavior without dev mode enabled.")
|
|
||||||
with tmpdir.as_cwd():
|
|
||||||
monkeypatch.setattr(llnl.util.symlink, "_windows_can_symlink", lambda: win_can_symlink)
|
|
||||||
dir = str(tmpdir.join("dir"))
|
|
||||||
file = str(tmpdir.join("file"))
|
|
||||||
nonexistent = str(tmpdir.join("does_not_exist"))
|
|
||||||
symlink_to_dir = str(tmpdir.join("symlink_to_dir"))
|
|
||||||
symlink_to_file = str(tmpdir.join("symlink_to_file"))
|
|
||||||
dangling_symlink = str(tmpdir.join("dangling_symlink"))
|
|
||||||
symlink_to_dangling_symlink = str(tmpdir.join("symlink_to_dangling_symlink"))
|
|
||||||
symlink_to_symlink_to_dir = str(tmpdir.join("symlink_to_symlink_to_dir"))
|
|
||||||
symlink_to_symlink_to_file = str(tmpdir.join("symlink_to_symlink_to_file"))
|
|
||||||
|
|
||||||
os.mkdir(dir)
|
|
||||||
assert fs.lexists_islink_isdir(dir) == (True, False, True)
|
|
||||||
|
|
||||||
symlink("dir", symlink_to_dir)
|
|
||||||
assert fs.lexists_islink_isdir(dir) == (True, False, True)
|
|
||||||
assert fs.lexists_islink_isdir(symlink_to_dir) == (True, True, True)
|
|
||||||
|
|
||||||
with open(file, "wb") as f:
|
|
||||||
f.write(b"file")
|
|
||||||
assert fs.lexists_islink_isdir(file) == (True, False, False)
|
|
||||||
|
|
||||||
symlink("file", symlink_to_file)
|
|
||||||
if win_can_symlink:
|
|
||||||
assert fs.lexists_islink_isdir(file) == (True, False, False)
|
|
||||||
else:
|
|
||||||
assert fs.lexists_islink_isdir(file) == (True, True, False)
|
|
||||||
assert fs.lexists_islink_isdir(symlink_to_file) == (True, True, False)
|
|
||||||
|
|
||||||
with pytest.raises(SymlinkError):
|
|
||||||
symlink("does_not_exist", dangling_symlink)
|
|
||||||
symlink("dangling_symlink", symlink_to_dangling_symlink)
|
|
||||||
|
|
||||||
symlink("symlink_to_dir", symlink_to_symlink_to_dir)
|
|
||||||
symlink("symlink_to_file", symlink_to_symlink_to_file)
|
|
||||||
|
|
||||||
assert fs.lexists_islink_isdir(nonexistent) == (False, False, False)
|
|
||||||
assert fs.lexists_islink_isdir(symlink_to_dangling_symlink) == (False, False, False)
|
|
||||||
assert fs.lexists_islink_isdir(symlink_to_symlink_to_dir) == (True, True, True)
|
|
||||||
assert fs.lexists_islink_isdir(symlink_to_symlink_to_file) == (True, True, False)
|
|
||||||
|
|
||||||
|
|
||||||
class RegisterVisitor(fs.BaseDirectoryVisitor):
|
class RegisterVisitor(fs.BaseDirectoryVisitor):
|
||||||
"""A directory visitor that keeps track of all visited paths"""
|
"""A directory visitor that keeps track of all visited paths"""
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user