summaryrefslogtreecommitdiff
path: root/src/buildstream/element.py
diff options
context:
space:
mode:
authorTristan van Berkom <tristan@codethink.co.uk>2020-08-30 15:57:13 +0900
committerTristan van Berkom <tristan@codethink.co.uk>2020-09-04 18:22:38 +0900
commit1a3e4e89fc86b29342c9ec62ca8915b6eae084d2 (patch)
treed0e88bcc0fe25618306e93a3354026776bb9587b /src/buildstream/element.py
parent97812cbb7d295cc3d270be9205cbc12313215028 (diff)
downloadbuildstream-1a3e4e89fc86b29342c9ec62ca8915b6eae084d2.tar.gz
element.py: Hide dependencies which are irrelevant to the Element
This is a large breaking change, a summary of the changes are that: * The Scope type is now private, since Element plugins do not have the choice to view any other scopes. * Element.dependencies() API change Now it accepts a "selection" (sequence) of dependency elements, so that Element.dependencies() can iterate over a collection of dependencies, ensuring that we iterate over every element only once even when we need to iterate over multiple element's dependencies. The old API is moved to Element._dependencies() and still used internally. * Element.stage_dependency_artifacts() API change This gets the same treatment as Element.dependencies(), and the old API is also preserved as Element._stage_dependency_artifacts(), so that the CLI can stage things for `bst artifact checkout` and such. * Element.search() API change The Scope argument is removed, and the old API is preserved as Element._search() temporarily, until we can remove this completely.
Diffstat (limited to 'src/buildstream/element.py')
-rw-r--r--src/buildstream/element.py359
1 files changed, 233 insertions, 126 deletions
diff --git a/src/buildstream/element.py b/src/buildstream/element.py
index ffbf8216e..8c8de614c 100644
--- a/src/buildstream/element.py
+++ b/src/buildstream/element.py
@@ -84,7 +84,7 @@ from contextlib import contextmanager, suppress
from functools import partial
from itertools import chain
import string
-from typing import cast, TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Set
+from typing import cast, TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Set, Sequence
from pyroaring import BitMap # pylint: disable=no-name-in-module
from ruamel import yaml
@@ -103,7 +103,7 @@ from .plugin import Plugin
from .sandbox import SandboxFlags, SandboxCommandError
from .sandbox._config import SandboxConfig
from .sandbox._sandboxremote import SandboxRemote
-from .types import CoreWarnings, Scope, _CacheBuildTrees, _KeyStrength
+from .types import CoreWarnings, _Scope, _CacheBuildTrees, _KeyStrength
from ._artifact import Artifact
from ._elementproxy import ElementProxy
from ._elementsources import ElementSources
@@ -418,28 +418,56 @@ class Element(Plugin):
"""
return self.__sources.sources()
- def dependencies(self, scope: Scope, *, recurse: bool = True, visited=None) -> Iterator["Element"]:
- """dependencies(scope, *, recurse=True)
+ def dependencies(self, selection: Sequence["Element"] = None, *, recurse: bool = True) -> Iterator["Element"]:
+ """A generator function which yields the build dependencies of the given element.
- A generator function which yields the dependencies of the given element.
+ This generator gives the Element access to all of the dependencies which it is has
+ access to at build time. As explained in :ref:`the dependency type documentation <format_dependencies_types>`,
+ this includes the direct build dependencies of the element being built, along with any
+ transient runtime dependencies of those build dependencies.
+
+ Subsets of the dependency graph can be selected using the `selection` argument,, which
+ must consist of dependencies of this element. If the `selection` argument is specified as
+ `None`, then the `self` element on which this is called is used as the `selection`.
If `recurse` is specified (the default), the full dependencies will be listed
- in deterministic staging order, starting with the basemost elements in the
- given `scope`. Otherwise, if `recurse` is not specified then only the direct
- dependencies in the given `scope` will be traversed, and the element itself
- will be omitted.
+ in deterministic staging order, starting with the basemost elements. Otherwise,
+ if `recurse` is not specified then only the direct dependencies will be traversed.
Args:
- scope: The scope to iterate in
- recurse: Whether to recurse
+ selection (Sequence[Element]): A list of dependencies to select, or None
+ recurse (bool): Whether to recurse
Yields:
- The dependencies in `scope`, in deterministic staging order
+ The dependencies of the selection, in deterministic staging order
"""
- for dep in self._dependencies(scope, recurse=recurse):
- yield cast("Element", ElementProxy(self, dep))
+ #
+ # In this public API, we ensure the invariant that an element can only
+ # ever see elements in it's own _Scope.BUILD scope.
+ #
+ # - Yield ElementProxy objects for every element except for the self element
+ # - When a selection is provided, ensure that we call the real _dependencies()
+ # method using _Scope.RUNTIME
+ # - When iterating over the self element, use _Scope.BUILD
+ #
+ visited = (BitMap(), BitMap())
+ if selection is None:
+ selection = [self]
+
+ for element in selection:
+ if element is self:
+ scope = _Scope.BUILD
+ else:
+ scope = _Scope.RUN
+
+ # Elements in the `selection` will actually be `ElementProxy` objects, but
+ # those calls will be forwarded to their actual internal `_dependencies()`
+ # methods.
+ #
+ for dep in element._dependencies(scope, recurse=recurse, visited=visited):
+ yield cast("Element", ElementProxy(self, dep))
- def search(self, scope: Scope, name: str) -> Optional["Element"]:
+ def search(self, name: str) -> Optional["Element"]:
"""search(scope, *, name)
Search for a dependency by name
@@ -451,8 +479,10 @@ class Element(Plugin):
Returns:
The dependency element, or None if not found.
"""
- search = self._search(scope, name)
- if search:
+ search = self._search(_Scope.BUILD, name)
+ if search is self:
+ return self
+ elif search:
return cast("Element", ElementProxy(self, search))
return None
@@ -591,7 +621,7 @@ class Element(Plugin):
.. code:: python
# Stage the dependencies for a build of 'self'
- for dep in self.dependencies(Scope.BUILD):
+ for dep in self.dependencies():
dep.stage_artifact(sandbox)
"""
@@ -624,7 +654,7 @@ class Element(Plugin):
def stage_dependency_artifacts(
self,
sandbox: "Sandbox",
- scope: Scope,
+ selection: Sequence["Element"] = None,
*,
path: str = None,
include: Optional[List[str]] = None,
@@ -635,70 +665,32 @@ class Element(Plugin):
This is primarily a convenience wrapper around
:func:`Element.stage_artifact() <buildstream.element.Element.stage_artifact>`
- which takes care of staging all the dependencies in `scope` and issueing the
+ which takes care of staging all the dependencies in staging order and issueing the
appropriate warnings.
+ The `selection` argument will behave in the same was as specified by
+ :func:`Element.dependencies() <buildstream.element.Element.dependencies>`,
+ If the `selection` argument is specified as `None`, then the `self` element on which this
+ is called is used as the `selection`.
+
Args:
sandbox: The build sandbox
- scope: The scope to stage dependencies in
+ selection (Sequence[Element]): A list of dependencies to select, or None
path An optional sandbox relative path
include: An optional list of domains to include files from
exclude: An optional list of domains to exclude files from
orphans: Whether to include files not spoken for by split domains
Raises:
- (:class:`.ElementError`): If any of the dependencies in `scope` have not
- yet produced artifacts, or if forbidden overlaps
- occur.
+ (:class:`.ElementError`): if forbidden overlaps occur.
"""
- ignored = {}
- overlaps = OrderedDict() # type: OrderedDict[str, List[str]]
- files_written = {} # type: Dict[str, List[str]]
+ overlaps = _OverlapCollector(self)
- for dep in self.dependencies(scope):
+ for dep in self.dependencies(selection):
result = dep.stage_artifact(sandbox, path=path, include=include, exclude=exclude, orphans=orphans)
- if result.overwritten:
- for overwrite in result.overwritten:
- # Completely new overwrite
- if overwrite not in overlaps:
- # Find the overwritten element by checking where we've
- # written the element before
- for elm, contents in files_written.items():
- if overwrite in contents:
- overlaps[overwrite] = [elm, dep.name]
- else:
- overlaps[overwrite].append(dep.name)
- files_written[dep.name] = result.files_written
+ overlaps.collect_stage_result(dep.name, result)
- if result.ignored:
- ignored[dep.name] = result.ignored
-
- if overlaps:
- overlap_warning = False
- warning_detail = "Staged files overwrite existing files in staging area:\n"
- for f, elements in overlaps.items():
- overlap_warning_elements = []
- # The bottom item overlaps nothing
- overlapping_elements = elements[1:]
- for elm in overlapping_elements:
- element = cast(Element, self._search(scope, elm))
- if not element.__file_is_whitelisted(f):
- overlap_warning_elements.append(elm)
- overlap_warning = True
-
- warning_detail += _overlap_error_detail(f, overlap_warning_elements, elements)
-
- if overlap_warning:
- self.warn(
- "Non-whitelisted overlaps detected", detail=warning_detail, warning_token=CoreWarnings.OVERLAPS
- )
-
- if ignored:
- detail = "Not staging files which would replace non-empty directories:\n"
- for key, value in ignored.items():
- detail += "\nFrom {}:\n".format(key)
- detail += " " + " ".join(["/" + f + "\n" for f in value])
- self.warn("Ignored files", detail=detail)
+ overlaps.overlap_warnings()
def integrate(self, sandbox: "Sandbox") -> None:
"""Integrate currently staged filesystem against this artifact.
@@ -837,25 +829,25 @@ class Element(Plugin):
# will be omitted.
#
# Args:
- # scope (Scope): The scope to iterate in
+ # scope (_Scope): The scope to iterate in
# recurse (bool): Whether to recurse
#
# Yields:
# (Element): The dependencies in `scope`, in deterministic staging order
#
- def _dependencies(self, scope: Scope, *, recurse: bool = True, visited=None) -> Iterator["Element"]:
+ def _dependencies(self, scope, *, recurse=True, visited=None):
# The format of visited is (BitMap(), BitMap()), with the first BitMap
- # containing element that have been visited for the `Scope.BUILD` case
- # and the second one relating to the `Scope.RUN` case.
+ # containing element that have been visited for the `_Scope.BUILD` case
+ # and the second one relating to the `_Scope.RUN` case.
if not recurse:
result: Set[Element] = set()
- if scope in (Scope.BUILD, Scope.ALL):
+ if scope in (_Scope.BUILD, _Scope.ALL):
for dep in self.__build_dependencies:
if dep not in result:
result.add(dep)
yield dep
- if scope in (Scope.RUN, Scope.ALL):
+ if scope in (_Scope.RUN, _Scope.ALL):
for dep in self.__runtime_dependencies:
if dep not in result:
result.add(dep)
@@ -863,41 +855,41 @@ class Element(Plugin):
else:
def visit(element, scope, visited):
- if scope == Scope.ALL:
+ if scope == _Scope.ALL:
visited[0].add(element._unique_id)
visited[1].add(element._unique_id)
for dep in chain(element.__build_dependencies, element.__runtime_dependencies):
if dep._unique_id not in visited[0] and dep._unique_id not in visited[1]:
- yield from visit(dep, Scope.ALL, visited)
+ yield from visit(dep, _Scope.ALL, visited)
yield element
- elif scope == Scope.BUILD:
+ elif scope == _Scope.BUILD:
visited[0].add(element._unique_id)
for dep in element.__build_dependencies:
if dep._unique_id not in visited[1]:
- yield from visit(dep, Scope.RUN, visited)
+ yield from visit(dep, _Scope.RUN, visited)
- elif scope == Scope.RUN:
+ elif scope == _Scope.RUN:
visited[1].add(element._unique_id)
for dep in element.__runtime_dependencies:
if dep._unique_id not in visited[1]:
- yield from visit(dep, Scope.RUN, visited)
+ yield from visit(dep, _Scope.RUN, visited)
yield element
else:
yield element
if visited is None:
- # Visited is of the form (Visited for Scope.BUILD, Visited for Scope.RUN)
+ # Visited is of the form (Visited for _Scope.BUILD, Visited for _Scope.RUN)
visited = (BitMap(), BitMap())
else:
# We have already a visited set passed. we might be able to short-circuit
- if scope in (Scope.BUILD, Scope.ALL) and self._unique_id in visited[0]:
+ if scope in (_Scope.BUILD, _Scope.ALL) and self._unique_id in visited[0]:
return
- if scope in (Scope.RUN, Scope.ALL) and self._unique_id in visited[1]:
+ if scope in (_Scope.RUN, _Scope.ALL) and self._unique_id in visited[1]:
return
yield from visit(self, scope, visited)
@@ -907,13 +899,13 @@ class Element(Plugin):
# Search for a dependency by name
#
# Args:
- # scope (Scope): The scope to search
+ # scope (_Scope): The scope to search
# name (str): The dependency to search for
#
# Returns:
# (Element): The dependency element, or None if not found.
#
- def _search(self, scope: Scope, name: str) -> Optional["Element"]:
+ def _search(self, scope, name):
for dep in self._dependencies(scope):
if dep.name == name:
@@ -921,6 +913,34 @@ class Element(Plugin):
return None
+ # _stage_dependency_artifacts()
+ #
+ # Stage element dependencies in scope, this is used for core
+ # functionality especially in the CLI which wants to stage specifically
+ # build or runtime dependencies.
+ #
+ # Args:
+ # sandbox: The build sandbox
+ # scope (_Scope): The scope of artifacts to stage
+ # path An optional sandbox relative path
+ # include: An optional list of domains to include files from
+ # exclude: An optional list of domains to exclude files from
+ # orphans: Whether to include files not spoken for by split domains
+ #
+ # Raises:
+ # (:class:`.ElementError`): If any of the dependencies in `scope` have not
+ # yet produced artifacts, or if forbidden overlaps
+ # occur.
+ #
+ def _stage_dependency_artifacts(self, sandbox, scope, *, path=None, include=None, exclude=None, orphans=True):
+ overlaps = _OverlapCollector(self)
+
+ for dep in self._dependencies(scope):
+ result = dep.stage_artifact(sandbox, path=path, include=include, exclude=exclude, orphans=orphans)
+ overlaps.collect_stage_result(dep.name, result)
+
+ overlaps.overlap_warnings()
+
# _new_from_load_element():
#
# Recursively instantiate a new Element instance, its sources
@@ -1296,18 +1316,18 @@ class Element(Plugin):
self.__configure_sandbox(sandbox)
# Stage what we need
- if shell and scope == Scope.BUILD:
+ if shell and scope == _Scope.BUILD:
self.stage(sandbox)
else:
# Stage deps in the sandbox root
with self.timed_activity("Staging dependencies", silent_nested=True):
- self.stage_dependency_artifacts(sandbox, scope)
+ self._stage_dependency_artifacts(sandbox, scope)
# Run any integration commands provided by the dependencies
# once they are all staged and ready
if integrate:
with self.timed_activity("Integrating sandbox"):
- for dep in self.dependencies(scope):
+ for dep in self._dependencies(scope):
dep.integrate(sandbox)
yield sandbox
@@ -1393,7 +1413,7 @@ class Element(Plugin):
self.__required = True
# Request artifacts of runtime dependencies
- for dep in self._dependencies(Scope.RUN, recurse=False):
+ for dep in self._dependencies(_Scope.RUN, recurse=False):
dep._set_required()
# When an element becomes required, it must be assembled for
@@ -1420,7 +1440,7 @@ class Element(Plugin):
# Mark artifact files for this element and its runtime dependencies as
# required in the local cache.
#
- def _set_artifact_files_required(self, scope=Scope.RUN):
+ def _set_artifact_files_required(self, scope=_Scope.RUN):
if self.__artifact_files_required:
# Already done
return
@@ -1481,7 +1501,7 @@ class Element(Plugin):
self.__assemble_scheduled = True
# Requests artifacts of build dependencies
- for dep in self._dependencies(Scope.BUILD, recurse=False):
+ for dep in self._dependencies(_Scope.BUILD, recurse=False):
dep._set_required()
# Once we schedule an element for assembly, we know that our
@@ -1843,7 +1863,7 @@ class Element(Plugin):
# environment
#
# Args:
- # scope (Scope): Either BUILD or RUN scopes are valid, or None
+ # scope (_Scope): Either BUILD or RUN scopes are valid, or None
# mounts (list): A list of (str, str) tuples, representing host/target paths to mount
# isolate (bool): Whether to isolate the environment like we do in builds
# prompt (str): A suitable prompt string for PS1
@@ -2249,6 +2269,33 @@ class Element(Plugin):
def _add_build_dependency(self, dependency):
self.__build_dependencies.append(dependency)
+ # _file_is_whitelisted()
+ #
+ # Checks if a file is whitelisted in the overlap whitelist
+ #
+ # This is only internal (one underscore) and not locally private
+ # because it needs to be proxied through ElementProxy.
+ #
+ # Args:
+ # path (str): The path to check
+ #
+ # Returns:
+ # (bool): True of the specified `path` is whitelisted
+ #
+ def _file_is_whitelisted(self, path):
+ # Considered storing the whitelist regex for re-use, but public data
+ # can be altered mid-build.
+ # Public data is not guaranteed to stay the same for the duration of
+ # the build, but I can think of no reason to change it mid-build.
+ # If this ever changes, things will go wrong unexpectedly.
+ if not self.__whitelist_regex:
+ bstdata = self.get_public_data("bst")
+ whitelist = bstdata.get_sequence("overlap-whitelist", default=[])
+ whitelist_expressions = [utils._glob2re(self.__variables.subst(node)) for node in whitelist]
+ expression = "^(?:" + "|".join(whitelist_expressions) + ")$"
+ self.__whitelist_regex = re.compile(expression)
+ return self.__whitelist_regex.match(os.path.join(os.sep, path))
+
#############################################################
# Private Local Methods #
#############################################################
@@ -2312,7 +2359,7 @@ class Element(Plugin):
# __get_dependency_artifact_names()
#
- # Retrieve the artifact names of all of the dependencies in Scope.BUILD
+ # Retrieve the artifact names of all of the dependencies in _Scope.BUILD
#
# Returns:
# (list [str]): A list of refs of all dependencies in staging order.
@@ -2320,7 +2367,7 @@ class Element(Plugin):
def __get_dependency_artifact_names(self):
return [
os.path.join(dep.project_name, _get_normal_name(dep.name), dep._get_cache_key())
- for dep in self._dependencies(Scope.BUILD)
+ for dep in self._dependencies(_Scope.BUILD)
]
# __get_last_build_artifact()
@@ -2384,8 +2431,8 @@ class Element(Plugin):
def __preflight(self):
if self.BST_FORBID_RDEPENDS and self.BST_FORBID_BDEPENDS:
- if any(self._dependencies(Scope.RUN, recurse=False)) or any(
- self._dependencies(Scope.BUILD, recurse=False)
+ if any(self._dependencies(_Scope.RUN, recurse=False)) or any(
+ self._dependencies(_Scope.BUILD, recurse=False)
):
raise ElementError(
"{}: Dependencies are forbidden for '{}' elements".format(self, self.get_kind()),
@@ -2393,14 +2440,14 @@ class Element(Plugin):
)
if self.BST_FORBID_RDEPENDS:
- if any(self._dependencies(Scope.RUN, recurse=False)):
+ if any(self._dependencies(_Scope.RUN, recurse=False)):
raise ElementError(
"{}: Runtime dependencies are forbidden for '{}' elements".format(self, self.get_kind()),
reason="element-forbidden-rdepends",
)
if self.BST_FORBID_BDEPENDS:
- if any(self._dependencies(Scope.BUILD, recurse=False)):
+ if any(self._dependencies(_Scope.BUILD, recurse=False)):
raise ElementError(
"{}: Build dependencies are forbidden for '{}' elements".format(self, self.get_kind()),
reason="element-forbidden-bdepends",
@@ -2832,20 +2879,6 @@ class Element(Plugin):
if filter_func(filename):
yield filename
- def __file_is_whitelisted(self, path):
- # Considered storing the whitelist regex for re-use, but public data
- # can be altered mid-build.
- # Public data is not guaranteed to stay the same for the duration of
- # the build, but I can think of no reason to change it mid-build.
- # If this ever changes, things will go wrong unexpectedly.
- if not self.__whitelist_regex:
- bstdata = self.get_public_data("bst")
- whitelist = bstdata.get_sequence("overlap-whitelist", default=[])
- whitelist_expressions = [utils._glob2re(self.__variables.subst(node)) for node in whitelist]
- expression = "^(?:" + "|".join(whitelist_expressions) + ")$"
- self.__whitelist_regex = re.compile(expression)
- return self.__whitelist_regex.match(os.path.join(os.sep, path))
-
# __load_public_data():
#
# Loads the public data from the cached artifact
@@ -2957,7 +2990,7 @@ class Element(Plugin):
[e.project_name, e.name, e._get_cache_key(strength=_KeyStrength.WEAK)]
if self.BST_STRICT_REBUILD or e in self.__strict_dependencies
else [e.project_name, e.name]
- for e in self._dependencies(Scope.BUILD)
+ for e in self._dependencies(_Scope.BUILD)
]
self.__weak_cache_key = self._calculate_cache_key(dependencies)
@@ -2970,7 +3003,7 @@ class Element(Plugin):
if self.__strict_cache_key is None:
dependencies = [
[e.project_name, e.name, e.__strict_cache_key] if e.__strict_cache_key is not None else None
- for e in self._dependencies(Scope.BUILD)
+ for e in self._dependencies(_Scope.BUILD)
]
self.__strict_cache_key = self._calculate_cache_key(dependencies)
@@ -3060,7 +3093,7 @@ class Element(Plugin):
self.__cache_key = strong_key
elif self.__assemble_scheduled or self.__assemble_done:
# Artifact will or has been built, not downloaded
- dependencies = [[e.project_name, e.name, e._get_cache_key()] for e in self._dependencies(Scope.BUILD)]
+ dependencies = [[e.project_name, e.name, e._get_cache_key()] for e in self._dependencies(_Scope.BUILD)]
self.__cache_key = self._calculate_cache_key(dependencies)
if self.__cache_key is None:
@@ -3164,16 +3197,90 @@ class Element(Plugin):
self._update_ready_for_runtime_and_cached()
-def _overlap_error_detail(f, forbidden_overlap_elements, elements):
- if forbidden_overlap_elements:
- return "/{}: {} {} not permitted to overlap other elements, order {} \n".format(
- f,
- " and ".join(forbidden_overlap_elements),
- "is" if len(forbidden_overlap_elements) == 1 else "are",
- " above ".join(reversed(elements)),
- )
- else:
- return ""
+# _OverlapCollector()
+#
+# Collects results of Element.stage_artifact() and saves
+# them in order to raise a proper overlap error at the end
+# of staging.
+#
+# Args:
+# element (Element): The element for which we are staging artifacts
+#
+class _OverlapCollector:
+ def __init__(self, element):
+ self.element = element
+ self.ignored = {}
+ self.overlaps = {} # type: Dict[str, List[str]]
+ self.files_written = {} # type: Dict[str, List[str]]
+
+ # collect_stage_result()
+ #
+ # Collect and accumulate results of Element.stage_artifact()
+ #
+ # Args:
+ # element_name (str): The name of the element staged
+ # result (FileListResult): The result of Element.stage_artifact()
+ #
+ def collect_stage_result(self, element_name: str, result: FileListResult):
+ if result.overwritten:
+ for overwrite in result.overwritten:
+ # Completely new overwrite
+ if overwrite not in self.overlaps:
+ # Find the overwritten element by checking where we've
+ # written the element before
+ for elm, contents in self.files_written.items():
+ if overwrite in contents:
+ self.overlaps[overwrite] = [elm, element_name]
+ else:
+ self.overlaps[overwrite].append(element_name)
+
+ self.files_written[element_name] = result.files_written
+ if result.ignored:
+ self.ignored[element_name] = result.ignored
+
+ # overlap_warnings()
+ #
+ # Issue any warnings as a batch as a result of staging artifacts,
+ # based on the results collected with collect_stage_result().
+ #
+ def overlap_warnings(self):
+ if self.overlaps:
+ overlap_warning = False
+ warning_detail = "Staged files overwrite existing files in staging area:\n"
+ for f, elements in self.overlaps.items():
+ overlap_warning_elements = []
+ # The bottom item overlaps nothing
+ overlapping_elements = elements[1:]
+ for elm in overlapping_elements:
+ element = cast(Element, self.element.search(elm))
+ if not element._file_is_whitelisted(f):
+ overlap_warning_elements.append(elm)
+ overlap_warning = True
+
+ warning_detail += self._overlap_error_detail(f, overlap_warning_elements, elements)
+
+ if overlap_warning:
+ self.element.warn(
+ "Non-whitelisted overlaps detected", detail=warning_detail, warning_token=CoreWarnings.OVERLAPS
+ )
+
+ if self.ignored:
+ detail = "Not staging files which would replace non-empty directories:\n"
+ for key, value in self.ignored.items():
+ detail += "\nFrom {}:\n".format(key)
+ detail += " " + " ".join(["/" + f + "\n" for f in value])
+ self.element.warn("Ignored files", detail=detail)
+
+ def _overlap_error_detail(self, f, forbidden_overlap_elements, elements):
+ if forbidden_overlap_elements:
+ return "/{}: {} {} not permitted to overlap other elements, order {} \n".format(
+ f,
+ " and ".join(forbidden_overlap_elements),
+ "is" if len(forbidden_overlap_elements) == 1 else "are",
+ " above ".join(reversed(elements)),
+ )
+ else:
+ return ""
# _get_normal_name():