blob: f69e13857aeac6c66b316f2533814097f077a291 [file] [log] [blame]
#
# Copyright (C) 2020 Codethink Limited
#
# 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; either
# version 2 of the License, or (at your option) any later version.
#
# This library 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 GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
#
# Authors:
# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk>
from functools import cmp_to_key
from pyroaring import BitMap, FrozenBitMap # pylint: disable=no-name-in-module
from .._exceptions import LoadError
from ..exceptions import LoadErrorReason
from ..node cimport MappingNode, Node, ProvenanceInformation, ScalarNode, SequenceNode
from .types import Symbol
# Counter to get ids to LoadElements
cdef int _counter = 0
cdef int _next_synthetic_counter():
global _counter
_counter += 1
return _counter
# DependencyType
#
# A bitfield to represent dependency types
#
cpdef enum DependencyType:
# A build dependency
BUILD = 0x001
# A runtime dependency
RUNTIME = 0x002
# Both build and runtime dependencies
ALL = 0x003
# Some forward declared lists, avoid creating these lists repeatedly
#
cdef list _filename_allowed_types=[ScalarNode, SequenceNode]
cdef list _valid_dependency_keys = [Symbol.FILENAME, Symbol.TYPE, Symbol.JUNCTION, Symbol.STRICT, Symbol.CONFIG]
cdef list _valid_typed_dependency_keys = [Symbol.FILENAME, Symbol.JUNCTION, Symbol.STRICT, Symbol.CONFIG]
cdef list _valid_element_keys = [
'kind', 'depends', 'sources', 'sandbox', 'variables', 'environment', 'environment-nocache',
'config', 'public', 'description', 'build-depends', 'runtime-depends',
]
# Dependency():
#
# Early stage data model for dependencies objects, the LoadElement has
# Dependency objects which in turn refer to other LoadElements in the data
# model.
#
# The constructor is incomplete, normally dependencies are loaded
# via the Dependency.load() API below. The constructor arguments are
# only used as a convenience to create the dummy Dependency objects
# at the toplevel of the load sequence in the Loader.
#
# Args:
# element (LoadElement): a LoadElement on which there is a dependency
# dep_type (DependencyType): the type of dependency this dependency link is
#
cdef class Dependency:
cdef readonly LoadElement element # The resolved LoadElement
cdef readonly int dep_type # The dependency type (runtime or build or both)
cdef readonly str name # The project local dependency name
cdef readonly str junction # The junction path of the dependency name, if any
cdef readonly bint strict # Whether this is a strict dependency
cdef readonly list config_nodes # The custom config nodes for Element.configure_dependencies()
cdef readonly Node node # The original node of the dependency
def __cinit__(self, LoadElement element = None, int dep_type = DependencyType.ALL):
self.element = element
self.dep_type = dep_type
self.name = None
self.junction = None
self.strict = False
self.config_nodes = None
self.node = None
# path
#
# The path of the dependency represented as a single string,
# instead of junction and name being separate.
#
@property
def path(self):
if self.junction is not None:
return "{}:{}".format(self.junction, self.name)
return self.name
# set_element()
#
# Sets the resolved LoadElement
#
# When Dependencies are initially loaded, the `element` member
# will be None until later on when the Loader loads the LoadElement
# objects based on the Dependency `name` and `junction`, the Loader
# will then call this to resolve the `element` member.
#
# Args:
# element (LoadElement): The resolved LoadElement
#
cpdef set_element(self, element: LoadElement):
self.element = element
# load()
#
# Load dependency attributes from a Node, and validate it
#
# Args:
# dep (Node): A node to load the dependency from
# junction (str): The junction name, or None
# name (str): The element name
# default_dep_type (DependencyType): The default dependency type
#
cdef load(self, Node dep, str junction, str name, int default_dep_type):
cdef str parsed_type
cdef MappingNode config_node
cdef ProvenanceInformation provenance
self.junction = junction
self.name = name
self.node = dep
self.element = None
if type(dep) is ScalarNode:
self.dep_type = default_dep_type or DependencyType.ALL
elif type(dep) is MappingNode:
if default_dep_type:
(<MappingNode> dep).validate_keys(_valid_typed_dependency_keys)
self.dep_type = default_dep_type
else:
(<MappingNode> dep).validate_keys(_valid_dependency_keys)
# Resolve the DependencyType
parsed_type = (<MappingNode> dep).get_str(<str> Symbol.TYPE, None)
if parsed_type is None or parsed_type == <str> Symbol.ALL:
self.dep_type = DependencyType.ALL
elif parsed_type == <str> Symbol.BUILD:
self.dep_type = DependencyType.BUILD
elif parsed_type == <str> Symbol.RUNTIME:
self.dep_type = DependencyType.RUNTIME
else:
provenance = dep.get_scalar(Symbol.TYPE).get_provenance()
raise LoadError("{}: Dependency type '{}' is not 'build', 'runtime' or 'all'"
.format(provenance, parsed_type), LoadErrorReason.INVALID_DATA)
self.strict = (<MappingNode> dep).get_bool(<str> Symbol.STRICT, False)
config_node = (<MappingNode> dep).get_mapping(<str> Symbol.CONFIG, None)
if config_node:
if self.dep_type == DependencyType.RUNTIME:
raise LoadError("{}: Specifying 'config' for a runtime dependency is not allowed"
.format(config_node.get_provenance()), LoadErrorReason.INVALID_DATA)
self.config_nodes = [config_node]
# Here we disallow explicitly setting 'strict' to False.
#
# This is in order to keep the door open to allowing the project.conf
# set the default of dependency 'strict'-ness which might be useful
# for projects which use mostly static linking and the like, in which
# case we can later interpret explicitly non-strict dependencies
# as an override of the project default.
#
if self.strict == False and Symbol.STRICT in dep:
provenance = dep.get_scalar(Symbol.STRICT).get_provenance()
raise LoadError("{}: Setting 'strict' to False is unsupported"
.format(provenance), LoadErrorReason.INVALID_DATA)
else:
raise LoadError("{}: Dependency is not specified as a string or a dictionary".format(self.node.get_provenance()),
LoadErrorReason.INVALID_DATA)
# Only build dependencies are allowed to be strict
#
if self.strict and self.dep_type == DependencyType.RUNTIME:
raise LoadError("{}: Runtime dependency {} specified as `strict`.".format(self.node.get_provenance(), self.name),
LoadErrorReason.INVALID_DATA,
detail="Only dependencies required at build time may be declared `strict`.")
# merge()
#
# Merge the attributes of an existing dependency into this dependency
#
# Args:
# other (Dependency): The dependency to merge into this one
#
cdef merge(self, Dependency other):
self.dep_type = self.dep_type | other.dep_type
self.strict = self.strict or other.strict
if self.config_nodes and other.config_nodes:
self.config_nodes.extend(other.config_nodes)
else:
self.config_nodes = self.config_nodes or other.config_nodes
# LoadElement():
#
# A transient object breaking down what is loaded allowing us to
# do complex operations in multiple passes.
#
# Args:
# node (dict): A YAML loaded dictionary
# name (str): The element name
# loader (Loader): The Loader object for this element
#
cdef class LoadElement:
cdef readonly MappingNode node
cdef readonly str name
cdef readonly str full_name
cdef readonly str kind
cdef int node_id
cdef readonly bint first_pass
cdef readonly object _loader
cdef readonly ScalarNode link_target
# TODO: if/when pyroaring exports symbols, we could type this statically
cdef object _dep_cache
cdef readonly list dependencies
cdef readonly bint fully_loaded # This is True if dependencies were also loaded
def __cinit__(self, MappingNode node, str filename, object loader):
#
# Public members
#
self.kind = None # The Element kind
self.node = node # The YAML node
self.name = filename # The element name
self.full_name = None # The element full name (with associated junction)
self.node_id = _next_synthetic_counter()
self.link_target = None # The target of a link element (ScalarNode)
self.fully_loaded = False # Whether we entered the loop to load dependencies or not
#
# Private members
#
self._loader = loader # The Loader object
self._dep_cache = None # The dependency cache, to speed up depends()
#
# Initialization
#
if loader.project.junction:
# dependency is in subproject, qualify name
self.full_name = '{}:{}'.format(loader.project.junction._get_full_name(), self.name)
else:
# dependency is in top-level project
self.full_name = self.name
self.dependencies = []
# Ensure the root node is valid
self.node.validate_keys(_valid_element_keys)
self.kind = node.get_str(Symbol.KIND, default=None)
self.first_pass = self.kind in ("junction", "link")
#
# If this is a link, resolve it right away and just
# store the link target and provenance
#
if self.kind == 'link':
# Avoid cyclic import here
from ..element import Element
element = Element._new_from_load_element(self)
# Custom error for link dependencies, since we don't completely
# parse their dependencies we cannot rely on the built-in ElementError.
deps = extract_depends_from_node(self.node)
if deps:
raise LoadError(
"{}: Dependencies are forbidden for 'link' elements".format(element),
LoadErrorReason.LINK_FORBIDDEN_DEPENDENCIES
)
self.link_target = element.target_node
# We don't count progress for junction elements or link
# as they do not represent real elements in the build graph.
#
# We check for a `None` kind, to avoid reporting progress for
# the virtual toplevel element used to load the pipeline.
#
if self._loader.load_context.task and self.kind is not None and not self.first_pass:
self._loader.load_context.task.add_current_progress()
# project
#
# A property reporting the Project in which this element resides.
#
@property
def project(self):
return self._loader.project
# junction
#
# A property reporting the junction element accessing this
# element, if any.
#
@property
def junction(self):
return self._loader.project.junction
# depends():
#
# Checks if this element depends on another element, directly
# or indirectly.
#
# Args:
# other (LoadElement): Another LoadElement
#
# Returns:
# (bool): True if this LoadElement depends on 'other'
#
def depends(self, LoadElement other not None):
self._ensure_depends_cache()
return other.node_id in self._dep_cache
# mark_fully_loaded()
#
# Sets the fully loaded state on this load element
#
# This state bit is used by the Loader to distinguish
# between an element which has only been shallow loaded
# and an element which has entered the loop which loads
# it's dependencies.
#
# Args:
# element (LoadElement): The resolved LoadElement
#
def mark_fully_loaded(self):
self.fully_loaded = True
###########################################
# Private Methods #
###########################################
cdef void _ensure_depends_cache(self):
cdef Dependency dep
if self._dep_cache:
return
self._dep_cache = BitMap()
for dep in self.dependencies:
elt = dep.element
# Ensure the cache of the element we depend on
elt._ensure_depends_cache()
# We depend on this element
self._dep_cache.add(elt.node_id)
# And we depend on everything this element depends on
self._dep_cache.update(elt._dep_cache)
self._dep_cache = FrozenBitMap(self._dep_cache)
def _dependency_cmp(Dependency dep_a, Dependency dep_b):
cdef LoadElement element_a = dep_a.element
cdef LoadElement element_b = dep_b.element
# Sort on inter element dependency first
if element_a.depends(element_b):
return 1
elif element_b.depends(element_a):
return -1
# If there are no inter element dependencies, place
# runtime only dependencies last
if dep_a.dep_type != dep_b.dep_type:
if dep_a.dep_type == DependencyType.RUNTIME:
return 1
elif dep_b.dep_type == DependencyType.RUNTIME:
return -1
# All things being equal, string comparison.
if element_a.name > element_b.name:
return 1
elif element_a.name < element_b.name:
return -1
# Sort local elements before junction elements
# and use string comparison between junction elements
if element_a.junction and element_b.junction:
if element_a.junction > element_b.junction:
return 1
elif element_a.junction < element_b.junction:
return -1
elif element_a.junction:
return -1
elif element_b.junction:
return 1
# This wont ever happen
return 0
# sort_dependencies():
#
# Sort dependencies of each element by their dependencies,
# so that direct dependencies which depend on other direct
# dependencies (directly or indirectly) appear later in the
# list.
#
# This avoids the need for performing multiple topological
# sorts throughout the build process.
#
# Args:
# element (LoadElement): The element to sort
# visited (set): a list of elements that should not be treated because
# because they already have been treated.
# This is useful when wanting to sort dependencies of
# multiple top level elements that might have a common
# part.
#
def sort_dependencies(LoadElement element, set visited):
cdef list working_elements = [element]
cdef Dependency dep
if element in visited:
return
visited.add(element)
# Now dependency sort, we ensure that if any direct dependency
# directly or indirectly depends on another direct dependency,
# it is found later in the list.
while working_elements:
element = working_elements.pop()
for dep in element.dependencies:
if dep.element not in visited:
visited.add(dep.element)
working_elements.append(dep.element)
element.dependencies.sort(key=cmp_to_key(_dependency_cmp))
# _parse_dependency_filename():
#
# Parse the filename of a dependency with the already provided parsed junction
# name, if any.
#
# This will validate that the filename node does not contain `:` if
# the junction is already specified, and otherwise it will appropriately
# split the filename string and decompose it into a junction and filename.
#
# Args:
# node (ScalarNode): The ScalarNode of the filename
# junction (str): The already parsed junction, or None
#
# Returns:
# (str): The junction component of the dependency filename
# (str): The filename component of the dependency filename
#
cdef tuple _parse_dependency_filename(ScalarNode node, str junction):
cdef str name = node.as_str()
if junction is not None:
if ':' in name:
raise LoadError(
"{}: Dependency {} contains `:` in its name. "
"`:` characters are not allowed in filename when "
"junction attribute is specified.".format(node.get_provenance(), name),
LoadErrorReason.INVALID_DATA)
elif ':' in name:
junction, name = name.rsplit(':', maxsplit=1)
return junction, name
# _list_dependency_node_files():
#
# List the filename, junction tuples associated with a dependency node,
# this supports the `filename` attribute being expressed as a list, so
# that multiple dependencies can be expressed with the common attributes.
#
# Args:
# node (Node): A YAML loaded dictionary
#
# Returns:
# (list): A list of filenames for `node`
#
cdef list _list_dependency_node_files(Node node):
cdef list files = []
cdef str junction
cdef tuple parsed_filename
cdef Node filename_node
cdef Node filename_iter
cdef object filename_iter_object
# The node can be a single filename declaration
#
if type(node) is ScalarNode:
parsed_filename = _parse_dependency_filename(node, None)
files.append(parsed_filename)
# Otherwise it is a dictionary
#
elif type(node) is MappingNode:
junction = (<MappingNode> node).get_str(<str> Symbol.JUNCTION, None)
filename_node = (<MappingNode> node).get_node(<str> Symbol.FILENAME, allowed_types=_filename_allowed_types)
if type(filename_node) is ScalarNode:
parsed_filename = _parse_dependency_filename(filename_node, junction)
files.append(parsed_filename)
else:
# The filename attribute is a list, iterate here
for filename_iter_object in (<SequenceNode> filename_node).value:
filename_iter = <Node> filename_iter_object
if type(filename_iter_object) is not ScalarNode:
raise LoadError(
"{}: Expected string while parsing the filename list".format(filename_iter.get_provenance()),
LoadErrorReason.INVALID_DATA
)
parsed_filename = _parse_dependency_filename(<ScalarNode>filename_iter, junction)
files.append(parsed_filename)
else:
raise LoadError("{}: Dependency is not specified as a string or a dictionary".format(node.get_provenance()),
LoadErrorReason.INVALID_DATA)
return files
# _extract_depends_from_node():
#
# Helper for extract_depends_from_node to get dependencies of a particular type
#
# Adds to an array of Dependency objects from a given dict node 'node',
# allows both strings and dicts for expressing the dependency.
#
# After extracting depends, the symbol is deleted from the node
#
# Args:
# node (Node): A YAML loaded dictionary
# key (str): the key on the Node corresponding to the dependency type
# default_dep_type (DependencyType): type to give to the dependency
# acc (dict): a dict in which to add the loaded dependencies
#
cdef void _extract_depends_from_node(Node node, str key, int default_dep_type, dict acc) except *:
cdef SequenceNode depends = node.get_sequence(key, [])
cdef Dependency existing_dep
cdef object dep_node_object
cdef Node dep_node
cdef object deptup_object
cdef tuple deptup
cdef str junction
cdef str filename
for dep_node_object in depends.value:
dep_node = <Node> dep_node_object
for deptup_object in _list_dependency_node_files(dep_node):
deptup = <tuple> deptup_object
junction = <str> deptup[0]
filename = <str> deptup[1]
dependency = Dependency()
dependency.load(dep_node, junction, filename, default_dep_type)
# Accumulate dependencies, merging any matching elements along the way
existing_dep = <Dependency> acc.get(deptup, None)
if existing_dep is not None:
existing_dep.merge(dependency)
else:
acc[deptup] = dependency
# Now delete the field, we dont want it anymore
node.safe_del(key)
# extract_depends_from_node():
#
# Creates an array of Dependency objects from a given dict node 'node',
# allows both strings and dicts for expressing the dependency and
# throws a comprehensive LoadError in the case that the node is malformed.
#
# After extracting depends, the symbol is deleted from the node
#
# Args:
# node (Node): A YAML loaded dictionary
#
# Returns:
# (list): a list of Dependency objects
#
def extract_depends_from_node(Node node):
cdef dict acc = {}
_extract_depends_from_node(node, <str> Symbol.BUILD_DEPENDS, <int> DependencyType.BUILD, acc)
_extract_depends_from_node(node, <str> Symbol.RUNTIME_DEPENDS, <int> DependencyType.RUNTIME, acc)
_extract_depends_from_node(node, <str> Symbol.DEPENDS, <int> 0, acc)
return [dep for dep in acc.values()]