chromium/tools/android/dependency_analysis/target_dependency.py

# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Implementation of the graph module for a build target dependency graph."""

import class_dependency
import graph
import group_json_consts
import java_group

# Usually a class is in exactly one target, but due to jar_excluded_patterns and
# android_library_factory some are in two or three. If there is a class that is
# in more than 3 build targets, it will be removed from the build graph. Some
# examples include:
# - org.chromium.base.natives.GEN_JNI (in 100+ targets)
# - org.chromium.android_webview.ProductConfig (in 15+ targets)
# - org.chromium.content.R (in 60+ targets)
_MAX_CONCURRENT_BUILD_TARGETS = 3


class JavaTarget(java_group.JavaGroup):
    """A representation of a Java target."""


class JavaTargetDependencyGraph(graph.Graph[JavaTarget]):
    """A graph representation of the dependencies between Java build targets.

    A directed edge A -> B indicates that A depends on B.
    """

    def __init__(self, class_graph: class_dependency.JavaClassDependencyGraph):
        """Initializes a new target-level dependency graph
        by "collapsing" a class-level dependency graph into its targets.

        Args:
            class_graph: A class-level graph to collapse to a target-level one.
        """
        super().__init__()

        # Create list of all targets using class nodes
        # so we don't miss targets with no dependencies (edges).
        for class_node in class_graph.nodes:
            if len(class_node.build_targets) > _MAX_CONCURRENT_BUILD_TARGETS:
                continue
            for build_target in class_node.build_targets:
                self.add_node_if_new(build_target)

        for begin_class, end_class in class_graph.edges:
            if len(begin_class.build_targets) > _MAX_CONCURRENT_BUILD_TARGETS:
                continue
            if len(end_class.build_targets) > _MAX_CONCURRENT_BUILD_TARGETS:
                continue
            for begin_target in begin_class.build_targets:
                for end_target in end_class.build_targets:
                    # Avoid intra-target deps.
                    if begin_target == end_target:
                        continue

                    self.add_edge_if_new(begin_target, end_target)

                    begin_target_node = self.get_node_by_key(begin_target)
                    end_target_node = self.get_node_by_key(end_target)
                    assert begin_target_node is not None
                    assert end_target_node is not None
                    begin_target_node.add_class(begin_class)
                    end_target_node.add_class(end_class)
                    begin_target_node.add_class_dependency_edge(
                        end_target_node, begin_class, end_class)

    def create_node_from_key(self, key: str):
        """Create a JavaTarget node from the given key (target name)."""
        return JavaTarget(key)

    def get_edge_metadata(self, begin_node, end_node):
        """Generates JSON metadata for the current edge.

        The list of edges is sorted in order to help with testing.
        Structure:
        {
            'class_edges': [
                [begin_key, end_key], ...
            ],
        }
        """
        return {
            group_json_consts.CLASS_EDGES:
            sorted(
                [begin.name, end.name] for begin, end in
                begin_node.get_class_dependencies_in_outbound_edge(end_node)),
        }