anvil-build/anvil/graph.py

211 lines
6.8 KiB
Python
Raw Normal View History

# Copyright 2012 Google Inc. All Rights Reserved.
"""Rule dependency graph.
A rule graph represents all of the rules in a project as they have been resolved
and tracked for dependencies. The graph can then be queried for various
information such as build rule sets/etc.
"""
__author__ = 'benvanik@google.com (Ben Vanik)'
import networkx as nx
import project
import util
class RuleGraph(object):
"""A graph of rule nodes.
"""
def __init__(self, project):
"""Initializes a rule graph.
Args:
project: Project to use for resolution.
"""
self.project = project
self.graph = nx.DiGraph()
# A map of rule paths to nodes, if they exist
self.rule_nodes = {}
def has_dependency(self, rule_path, predecessor_rule_path):
"""Checks to see if the given rule has a dependency on another rule.
Args:
rule_path: The name of the rule to check.
predecessor_rule_path: A potential predecessor rule.
Returns:
True if by any way rule_path depends on predecessor_rule_path.
Raises:
KeyError: One of the given rules was not found.
"""
rule_node = self.rule_nodes.get(rule_path, None)
if not rule_node:
raise KeyError('Rule "%s" not found' % (rule_path))
predecessor_rule_node = self.rule_nodes.get(predecessor_rule_path, None)
if not predecessor_rule_node:
raise KeyError('Rule "%s" not found' % (predecessor_rule_path))
return nx.has_path(self.graph, predecessor_rule_node, rule_node)
def _ensure_rules_present(self, rule_paths, requesting_module=None):
"""Ensures that the given list of rules are present in the graph, and if not
recursively loads them.
Args:
rule_paths: A list of target rule paths to add to the graph.
requesting_module: Module that is requesting the given rules or None if
all rule paths are absolute.
"""
# Add all of the rules listed
rules = []
for rule_path in rule_paths:
# Attempt to resolve the rule
rule = self.project.resolve_rule(rule_path,
requesting_module=requesting_module)
if not rule:
raise KeyError('Rule "%s" unable to be resolved' % (rule_path))
rules.append(rule)
# If already present, ignore (no need to recurse)
if self.rule_nodes.has_key(rule.path):
continue
# Wrap with our node and add it to the graph
rule_node = _RuleNode(rule)
self.rule_nodes[rule.path] = rule_node
self.graph.add_node(rule_node)
# Recursively resolve all dependent rules
dependent_rule_paths = []
for dep in rule.get_dependent_paths():
if util.is_rule_path(dep):
dependent_rule_paths.append(dep)
if len(dependent_rule_paths):
self._ensure_rules_present(dependent_rule_paths,
requesting_module=rule.parent_module)
# Add edges for all of the requested rules (at this point, all rules should
# be added to the graph)
for rule in rules:
rule_node = self.rule_nodes[rule.path]
for dep in rule_node.rule.get_dependent_paths():
if util.is_rule_path(dep):
dep_rule = self.project.resolve_rule(dep,
requesting_module=rule.parent_module)
dep_node = self.rule_nodes.get(dep_rule.path, None)
# Node should exist due to recursive addition above
assert dep_node
self.graph.add_edge(dep_node, rule_node)
# Ensure the graph is a DAG (no cycles)
if not nx.is_directed_acyclic_graph(self.graph):
# TODO(benvanik): use nx.simple_cycles() to print the cycles
raise ValueError('Cycle detected in the rule graph: %s' % (
nx.simple_cycles(self.graph)))
def add_rules_from_module(self, module):
"""Adds all rules (and their dependencies) from the given module.
Args:
module: A module with rules to add.
"""
rule_paths = []
for rule in module.rule_iter():
rule_paths.append(rule.path)
self._ensure_rules_present(rule_paths, requesting_module=module)
def has_rule(self, rule_path):
"""Whether the graph has the given rule loaded.
Args:
rule_path: Full rule path.
Returns:
True if the given rule has been resolved and added to the graph.
"""
return self.rule_nodes.get(rule_path, None) != None
def calculate_rule_sequence(self, target_rule_paths):
"""Calculates an ordered sequence of rules terminating with the given
target rules.
By passing multiple target names it's possible to build a combined sequence
that ensures all the given targets are included with no duplicate
dependencies.
Args:
target_rule_paths: A list of target rule paths to include in the
sequence, or a single target rule path.
Returns:
An ordered list of Rule instances including all of the given target rules
and their dependencies.
Raises:
KeyError: One of the given rules was not found.
"""
if isinstance(target_rule_paths, str):
target_rule_paths = [target_rule_paths]
# Ensure the graph has everything required - if things go south this will
# raise errors
self._ensure_rules_present(target_rule_paths)
# Reversed graph to make sorting possible
# If this gets expensive (or many sequences are calculated) it could be
# cached
reverse_graph = self.graph.reverse()
# Paths are added in reverse (from target to dependencies)
sequence_graph = nx.DiGraph()
def _add_rule_node_dependencies(rule_node):
if sequence_graph.has_node(rule_node):
# Already present in the sequence graph, no need to add again
return
# Add node
sequence_graph.add_node(rule_node)
# Recursively add all dependent children
for out_edge in reverse_graph.out_edges_iter(rule_node):
out_rule_node = out_edge[1]
if not sequence_graph.has_node(out_rule_node):
_add_rule_node_dependencies(out_rule_node)
sequence_graph.add_edge(rule_node, out_rule_node)
# Add all paths for targets
# Note that all nodes are present if we got this far, so no need to check
for rule_path in target_rule_paths:
rule = self.project.resolve_rule(rule_path)
assert rule
rule_node = self.rule_nodes.get(rule.path, None)
assert rule_node
_add_rule_node_dependencies(rule_node)
# Reverse the graph so that it's dependencies -> targets
reversed_sequence_graph = sequence_graph.reverse()
# Get the list of nodes in sorted order
rule_sequence = []
for rule_node in nx.topological_sort(reversed_sequence_graph):
rule_sequence.append(rule_node.rule)
return rule_sequence
class _RuleNode(object):
"""A node type that references a rule in the project."""
def __init__(self, rule):
"""Initializes a rule node.
Args:
rule: The rule this node describes.
"""
self.rule = rule
def __repr__(self):
return self.rule.path