blob: c66f300146dc715f08d73ea824469349d8275a52 [file] [log] [blame]
#
# Copyright (C) 2018 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Performs dependency tracking for ndk.builds modules."""
from typing import Dict, Iterable, List, Set
from ndk.builds import Module
import ndk.graph
class CyclicDependencyError(RuntimeError):
"""An error indicating a cyclic dependency in the module graph."""
def __init__(self, modules: Iterable[ndk.graph.Node]) -> None:
"""Initializes a CyclicDependencyError."""
super().__init__('Detected cyclic dependency: {}'.format(' -> '.join(
[m.name for m in modules])))
def prove_acyclic(modules: Iterable[Module]) -> None:
"""Proves that the graph is acyclic or raises an error.
Args:
modules: A list of all modules in the graph.
Raises:
CyclicDependencyError: A cycle was found in the module graph.
"""
nodes = {m.name: ndk.graph.Node(m.name, []) for m in modules}
for module in modules:
for dep in module.deps:
nodes[module.name].outs.append(nodes[dep])
graph = ndk.graph.Graph(nodes.values())
cycle = graph.find_cycle()
if cycle is not None:
raise CyclicDependencyError(cycle)
class DependencyManager:
"""Tracks module dependencies.
The DependencyManager computes module ordering based on the dependency
graph and exposes DependencyManager.get_buildable() as the list of modules
that are no longer waiting on their dependencies. This is updated whenever
the DependencyManager is informated of a module build being completed via
DependencyManager.complete().
"""
def __init__(self, all_modules: Iterable[Module]) -> None:
"""Initializes a DependencyManager."""
if not all_modules:
raise ValueError
prove_acyclic(all_modules)
self.buildable_modules = {m for m in all_modules if not m.deps}
# The values of this map are the modules that the key is still waiting
# for. When a build is complete, it is removed from all values in this
# dict. An empty value indicates that the module is now buildable.
self.blocked_modules = {m: set(m.deps) for m in all_modules if m.deps}
# Reverse map from a module to all of its dependents used to speed up
# lookups.
self.deps_to_modules: Dict[str, List[Module]] = {
m.name: []
for m in all_modules
}
for module in all_modules:
for dep in module.deps:
self.deps_to_modules[dep].append(module)
def get_buildable(self) -> Set[Module]:
"""Returns a set of modules that are ready to be built.
Retrieving the list of buildable modules removes them from the
buildable_modules set. This is done because it is assumed that the
caller will use the retrieved set to start those builds and the
buildable set should not include active builds.
"""
buildable = self.buildable_modules
self.buildable_modules = set()
return buildable
def complete(self, module: Module) -> None:
"""Signals that the given module has complete building.
Removes the module from the list of buildable modules and updates the
list of now buildable modules.
Args:
module: The module that has finished building.
"""
for dependent in self.deps_to_modules[module.name]:
self.blocked_modules[dependent].remove(module.name)
if self.blocked_modules[dependent]:
# Still blocked on other dependencies.
continue
del self.blocked_modules[dependent]
self.buildable_modules.add(dependent)