mirror of
https://github.com/mongodb/mongo.git
synced 2024-12-01 09:32:32 +01:00
SERVER-52567 added basic functions for graph analyzer CLI tool and improved graph generation.
This commit is contained in:
parent
8e7ad370f7
commit
fa271cc17c
@ -4998,9 +4998,4 @@ for i, s in enumerate(BUILD_TARGETS):
|
||||
# SConscripts have been read but before building begins.
|
||||
if get_option('build-tools') == 'next':
|
||||
libdeps.LibdepLinter(env).final_checks()
|
||||
env.Command(
|
||||
target="${BUILD_DIR}/libdeps/libdeps.graphml",
|
||||
source=env.get('LIBDEPS_SYMBOL_DEP_FILES', []),
|
||||
action=SCons.Action.FunctionAction(
|
||||
libdeps.generate_graph,
|
||||
{"cmdstr": "Generating libdeps graph"}))
|
||||
libdeps.generate_libdeps_graph(env)
|
164
buildscripts/libdeps/gacli.py
Normal file
164
buildscripts/libdeps/gacli.py
Normal file
@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 2020 MongoDB Inc.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included
|
||||
# in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
"""
|
||||
Graph Analysis Command Line Interface.
|
||||
|
||||
A Command line interface to the graph analysis module.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import textwrap
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import networkx
|
||||
import graph_analyzer
|
||||
|
||||
|
||||
class LinterSplitArgs(argparse.Action):
|
||||
"""Custom argument action for checking multiple choice comma separated list."""
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
"""Create a multi choice comma separated list."""
|
||||
|
||||
selected_choices = [v for v in ''.join(values).split(',') if v]
|
||||
invalid_choices = [
|
||||
choice for choice in selected_choices if choice not in self.valid_choices
|
||||
]
|
||||
if invalid_choices:
|
||||
raise Exception(
|
||||
f"Invalid choices: {invalid_choices}\nMust use choices from {self.valid_choices}")
|
||||
if graph_analyzer.CountTypes.all.name in selected_choices or selected_choices == []:
|
||||
selected_choices = self.valid_choices
|
||||
setattr(namespace, self.dest, [opt.replace('-', '_') for opt in selected_choices])
|
||||
|
||||
|
||||
class CountSplitArgs(LinterSplitArgs):
|
||||
"""Special case of common custom arg action for Count types."""
|
||||
|
||||
valid_choices = [
|
||||
name[0].replace('_', '-') for name in graph_analyzer.CountTypes.__members__.items()
|
||||
]
|
||||
|
||||
|
||||
class CustomFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter):
|
||||
"""Custom arg help formatter for modifying the defaults printed for the custom list action."""
|
||||
|
||||
def _get_help_string(self, action):
|
||||
|
||||
if isinstance(action, CountSplitArgs):
|
||||
max_length = max(
|
||||
[len(name[0]) for name in graph_analyzer.CountTypes.__members__.items()])
|
||||
count_help = {}
|
||||
for name in graph_analyzer.CountTypes.__members__.items():
|
||||
count_help[name[0]] = name[0] + ('-' * (max_length - len(name[0]))) + ": "
|
||||
return textwrap.dedent(f"""\
|
||||
{action.help}
|
||||
default: all, choices:
|
||||
{count_help[graph_analyzer.CountTypes.all.name]}perform all counts
|
||||
{count_help[graph_analyzer.CountTypes.node.name]}count nodes
|
||||
{count_help[graph_analyzer.CountTypes.edge.name]}count edges
|
||||
{count_help[graph_analyzer.CountTypes.dir_edge.name]}count edges declared directly on a node
|
||||
{count_help[graph_analyzer.CountTypes.trans_edge.name]}count edges induced by direct public edges
|
||||
{count_help[graph_analyzer.CountTypes.dir_pub_edge.name]}count edges that are directly public
|
||||
{count_help[graph_analyzer.CountTypes.pub_edge.name]}count edges that are public
|
||||
{count_help[graph_analyzer.CountTypes.priv_edge.name]}count edges that are private
|
||||
{count_help[graph_analyzer.CountTypes.if_edge.name]}count edges that are interface
|
||||
""")
|
||||
return super()._get_help_string(action)
|
||||
|
||||
|
||||
def setup_args_parser():
|
||||
"""Add and parse the input args."""
|
||||
|
||||
parser = argparse.ArgumentParser(formatter_class=CustomFormatter)
|
||||
|
||||
parser.add_argument('--graph-file', type=str, action='store', help="The LIBDEPS graph to load.",
|
||||
default="build/opt/libdeps/libdeps.graphml")
|
||||
|
||||
parser.add_argument(
|
||||
'--build-dir', type=str, action='store', help=
|
||||
"The path where the generic build files live, corresponding to BUILD_DIR in the Sconscripts.",
|
||||
default=None)
|
||||
|
||||
parser.add_argument('--format', choices=['pretty', 'json'], default='pretty',
|
||||
help="The output format type.")
|
||||
|
||||
parser.add_argument('--counts', metavar='COUNT,', nargs='*', action=CountSplitArgs,
|
||||
help="Output various counts from the graph. Comma separated list.")
|
||||
|
||||
parser.add_argument('--direct-depends', action='append',
|
||||
help="Print the nodes which depends on a given node.")
|
||||
|
||||
parser.add_argument('--common-depends', nargs='+', action='append',
|
||||
help="Print the nodes which have a common dependency on all N nodes.")
|
||||
|
||||
parser.add_argument(
|
||||
'--exclude-depends', nargs='+', action='append', help=
|
||||
"Print nodes which depend on the first node of N nodes, but exclude all nodes listed there after."
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_graph_data(graph_file, output_format):
|
||||
"""Load a graphml file into a LibdepsGraph."""
|
||||
|
||||
if output_format == "pretty":
|
||||
sys.stdout.write("Loading graph data...")
|
||||
sys.stdout.flush()
|
||||
graph = graph = networkx.read_graphml(graph_file)
|
||||
if output_format == "pretty":
|
||||
sys.stdout.write("Loaded!\n\n")
|
||||
return graph
|
||||
|
||||
|
||||
def main():
|
||||
"""Perform graph analysis based on input args."""
|
||||
|
||||
args = setup_args_parser()
|
||||
if not args.build_dir:
|
||||
args.build_dir = str(Path(args.graph_file).parents[1])
|
||||
graph = load_graph_data(args.graph_file, args.format)
|
||||
|
||||
depends_reports = {
|
||||
graph_analyzer.DependsReportTypes.direct_depends.name: args.direct_depends,
|
||||
graph_analyzer.DependsReportTypes.common_depends.name: args.common_depends,
|
||||
graph_analyzer.DependsReportTypes.exclude_depends.name: args.exclude_depends,
|
||||
}
|
||||
libdeps = graph_analyzer.LibdepsGraph(graph)
|
||||
ga = graph_analyzer.LibdepsGraphAnalysis(libdeps, args.build_dir, args.counts, depends_reports)
|
||||
|
||||
if args.format == 'pretty':
|
||||
ga_printer = graph_analyzer.GaPrettyPrinter(ga)
|
||||
elif args.format == 'json':
|
||||
ga_printer = graph_analyzer.GaJsonPrinter(ga)
|
||||
else:
|
||||
return
|
||||
|
||||
ga_printer.print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
326
buildscripts/libdeps/graph_analyzer.py
Normal file
326
buildscripts/libdeps/graph_analyzer.py
Normal file
@ -0,0 +1,326 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 2020 MongoDB Inc.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included
|
||||
# in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
"""
|
||||
Libdeps Graph Analysis Tool.
|
||||
|
||||
This will perform various metric's gathering and linting on the
|
||||
graph generated from SCons generate-libdeps-graph target. The graph
|
||||
represents the dependency information between all binaries from the build.
|
||||
"""
|
||||
|
||||
from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
|
||||
import networkx
|
||||
|
||||
|
||||
class CountTypes(Enum):
|
||||
"""Enums for the different types of counts to perform on a graph."""
|
||||
|
||||
all = auto()
|
||||
node = auto()
|
||||
edge = auto()
|
||||
dir_edge = auto()
|
||||
trans_edge = auto()
|
||||
dir_pub_edge = auto()
|
||||
pub_edge = auto()
|
||||
priv_edge = auto()
|
||||
if_edge = auto()
|
||||
|
||||
|
||||
class DependsReportTypes(Enum):
|
||||
"""Enums for the different type of depends reports to perform on a graph."""
|
||||
|
||||
direct_depends = auto()
|
||||
common_depends = auto()
|
||||
exclude_depends = auto()
|
||||
|
||||
|
||||
class EdgeProps(Enum):
|
||||
"""Enums for edge properties."""
|
||||
|
||||
direct = auto()
|
||||
visibility = auto()
|
||||
|
||||
|
||||
class LibdepsGraph(networkx.DiGraph):
|
||||
"""Class for analyzing the graph."""
|
||||
|
||||
def __init__(self, graph=networkx.DiGraph()):
|
||||
"""Load the graph data."""
|
||||
|
||||
super().__init__(incoming_graph_data=graph)
|
||||
|
||||
# Load in the graph and store a reversed version as well for quick look ups
|
||||
# the in directions.
|
||||
self.rgraph = graph.reverse()
|
||||
|
||||
def number_of_edge_types(self, edge_type, value):
|
||||
"""Count the graphs edges based on type."""
|
||||
|
||||
return len([edge for edge in self.edges(data=True) if edge[2].get(edge_type) == value])
|
||||
|
||||
def node_count(self):
|
||||
"""Count the graphs nodes."""
|
||||
|
||||
return self.number_of_nodes()
|
||||
|
||||
def edge_count(self):
|
||||
"""Count the graphs edges."""
|
||||
|
||||
return self.number_of_edges()
|
||||
|
||||
def direct_edge_count(self):
|
||||
"""Count the graphs direct edges."""
|
||||
|
||||
return self.number_of_edge_types(EdgeProps.direct.name, 1)
|
||||
|
||||
def transitive_edge_count(self):
|
||||
"""Count the graphs transitive edges."""
|
||||
|
||||
return self.number_of_edge_types(EdgeProps.direct.name, 0)
|
||||
|
||||
def direct_public_edge_count(self):
|
||||
"""Count the graphs direct public edges."""
|
||||
|
||||
return len([
|
||||
edge for edge in self.edges(data=True) if edge[2].get(EdgeProps.direct.name) == 1
|
||||
and edge[2].get(EdgeProps.visibility.name) == 0
|
||||
])
|
||||
|
||||
def public_edge_count(self):
|
||||
"""Count the graphs public edges."""
|
||||
|
||||
return self.number_of_edge_types(EdgeProps.visibility.name, 0)
|
||||
|
||||
def private_edge_count(self):
|
||||
"""Count the graphs private edges."""
|
||||
|
||||
return self.number_of_edge_types(EdgeProps.visibility.name, 1)
|
||||
|
||||
def interface_edge_count(self):
|
||||
"""Count the graphs interface edges."""
|
||||
|
||||
return self.number_of_edge_types(EdgeProps.visibility.name, 2)
|
||||
|
||||
def direct_depends(self, node):
|
||||
"""For given nodes, report what nodes depend directly on that node."""
|
||||
|
||||
return [
|
||||
depender for depender in self[node]
|
||||
if self[node][depender].get(EdgeProps.direct.name) == 1
|
||||
]
|
||||
|
||||
def common_depends(self, nodes):
|
||||
"""For a given set of nodes, report what nodes depend on all nodes from that set."""
|
||||
|
||||
neighbor_sets = [set(self[node]) for node in nodes]
|
||||
return list(set.intersection(*neighbor_sets))
|
||||
|
||||
def exclude_depends(self, nodes):
|
||||
"""Find depends with exclusions.
|
||||
|
||||
Given a node, and a set of other nodes, find what nodes depend on the given
|
||||
node, but do not depend on the set of nodes.
|
||||
"""
|
||||
|
||||
valid_depender_nodes = []
|
||||
for depender_node in set(self[nodes[0]]):
|
||||
if all([
|
||||
bool(excludes_node not in set(self.rgraph[depender_node]))
|
||||
for excludes_node in nodes[1:]
|
||||
]):
|
||||
valid_depender_nodes.append(depender_node)
|
||||
return valid_depender_nodes
|
||||
|
||||
|
||||
class LibdepsGraphAnalysis:
|
||||
"""Runs the given analysis on the input graph."""
|
||||
|
||||
def __init__(self, libdeps_graph, build_dir='build/opt', counts='all', depends_reports=None):
|
||||
"""Perform analysis based off input args."""
|
||||
|
||||
self.build_dir = Path(build_dir)
|
||||
self.libdeps_graph = libdeps_graph
|
||||
|
||||
self.results = {}
|
||||
|
||||
self.count_types = {
|
||||
CountTypes.node.name: ("num_nodes", libdeps_graph.node_count),
|
||||
CountTypes.edge.name: ("num_edges", libdeps_graph.edge_count),
|
||||
CountTypes.dir_edge.name: ("num_direct_edges", libdeps_graph.direct_edge_count),
|
||||
CountTypes.trans_edge.name: ("num_trans_edges", libdeps_graph.transitive_edge_count),
|
||||
CountTypes.dir_pub_edge.name: ("num_direct_public_edges",
|
||||
libdeps_graph.direct_public_edge_count),
|
||||
CountTypes.pub_edge.name: ("num_public_edges", libdeps_graph.public_edge_count),
|
||||
CountTypes.priv_edge.name: ("num_private_edges", libdeps_graph.private_edge_count),
|
||||
CountTypes.if_edge.name: ("num_interface_edges", libdeps_graph.interface_edge_count),
|
||||
}
|
||||
|
||||
for name in DependsReportTypes.__members__.items():
|
||||
setattr(self, f'{name[0]}_key', name[0])
|
||||
|
||||
if counts:
|
||||
self.run_graph_counters(counts)
|
||||
if depends_reports:
|
||||
self.run_depend_reports(depends_reports)
|
||||
|
||||
def get_results(self):
|
||||
"""Return the results fo the analysis."""
|
||||
|
||||
return self.results
|
||||
|
||||
def _strip_build_dir(self, node):
|
||||
"""Small util function for making args match the graph paths."""
|
||||
|
||||
node = Path(node)
|
||||
if str(node.absolute()).startswith(str(self.build_dir.absolute())):
|
||||
return str(node.relative_to(self.build_dir))
|
||||
else:
|
||||
raise Exception(
|
||||
f"build path not in node path: node: {node} build_dir: {self.build_dir}")
|
||||
|
||||
def _strip_build_dirs(self, nodes):
|
||||
"""Small util function for making a list of nodes match graph paths."""
|
||||
|
||||
for node in nodes:
|
||||
yield self._strip_build_dir(node)
|
||||
|
||||
def run_graph_counters(self, counts):
|
||||
"""Run the various graph counters for nodes and edges."""
|
||||
|
||||
for count_type in CountTypes.__members__.items():
|
||||
if count_type[0] in self.count_types:
|
||||
dict_name, func = self.count_types[count_type[0]]
|
||||
|
||||
if count_type[0] in counts:
|
||||
self.results[dict_name] = func()
|
||||
|
||||
def run_depend_reports(self, depends_reports):
|
||||
"""Run the various dependency reports."""
|
||||
|
||||
if depends_reports.get(self.direct_depends_key):
|
||||
self.results[self.direct_depends_key] = {}
|
||||
for node in depends_reports[self.direct_depends_key]:
|
||||
self.results[self.direct_depends_key][node] = self.libdeps_graph.direct_depends(
|
||||
self._strip_build_dir(node))
|
||||
|
||||
if depends_reports.get(self.common_depends_key):
|
||||
self.results[self.common_depends_key] = {}
|
||||
for nodes in depends_reports[self.common_depends_key]:
|
||||
nodes = frozenset(self._strip_build_dirs(nodes))
|
||||
self.results[self.common_depends_key][nodes] = self.libdeps_graph.common_depends(
|
||||
nodes)
|
||||
|
||||
if depends_reports.get(self.exclude_depends_key):
|
||||
self.results[self.exclude_depends_key] = {}
|
||||
for nodes in depends_reports[self.exclude_depends_key]:
|
||||
nodes = tuple(self._strip_build_dirs(nodes))
|
||||
self.results[self.exclude_depends_key][nodes] = self.libdeps_graph.exclude_depends(
|
||||
nodes)
|
||||
|
||||
|
||||
class GaPrinter:
|
||||
"""Base class for printers of the graph analysis."""
|
||||
|
||||
def __init__(self, libdeps_graph_analysis):
|
||||
"""Store the graph analysis for use when printing."""
|
||||
|
||||
self.libdeps_graph_analysis = libdeps_graph_analysis
|
||||
|
||||
|
||||
class GaJsonPrinter(GaPrinter):
|
||||
"""Printer for json output."""
|
||||
|
||||
def serialize(self, dictionary):
|
||||
"""Serialize the k,v pairs in the dictionary."""
|
||||
|
||||
new = {}
|
||||
for key, value in dictionary.items():
|
||||
if isinstance(value, dict):
|
||||
value = self.serialize(value)
|
||||
new[str(key)] = value
|
||||
return new
|
||||
|
||||
def print(self):
|
||||
"""Print the result data."""
|
||||
|
||||
import json
|
||||
results = self.libdeps_graph_analysis.get_results()
|
||||
print(json.dumps(self.serialize(results)))
|
||||
|
||||
|
||||
class GaPrettyPrinter(GaPrinter):
|
||||
"""Printer for pretty console output."""
|
||||
|
||||
count_desc = {
|
||||
CountTypes.node.name: ("num_nodes", "Nodes in Graph: {}"),
|
||||
CountTypes.edge.name: ("num_edges", "Edges in Graph: {}"),
|
||||
CountTypes.dir_edge.name: ("num_direct_edges", "Direct Edges in Graph: {}"),
|
||||
CountTypes.trans_edge.name: ("num_trans_edges", "Transitive Edges in Graph: {}"),
|
||||
CountTypes.dir_pub_edge.name: ("num_direct_public_edges",
|
||||
"Direct Public Edges in Graph: {}"),
|
||||
CountTypes.pub_edge.name: ("num_public_edges", "Public Edges in Graph: {}"),
|
||||
CountTypes.priv_edge.name: ("num_private_edges", "Private Edges in Graph: {}"),
|
||||
CountTypes.if_edge.name: ("num_interface_edges", "Interface Edges in Graph: {}"),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _print_results_node_list(heading, nodes):
|
||||
"""Util function for printing a list of nodes for depend reports."""
|
||||
|
||||
print(heading)
|
||||
for i, depender in enumerate(nodes, start=1):
|
||||
print(f"\t{i}: {depender}")
|
||||
print("")
|
||||
|
||||
def print(self):
|
||||
"""Print the result data."""
|
||||
|
||||
results = self.libdeps_graph_analysis.get_results()
|
||||
for count_type in CountTypes.__members__.items():
|
||||
if count_type[0] in self.count_desc:
|
||||
dict_name, desc = self.count_desc[count_type[0]]
|
||||
if dict_name in results:
|
||||
print(desc.format(results[dict_name]))
|
||||
|
||||
if DependsReportTypes.direct_depends.name in results:
|
||||
print("\nNodes that directly depend on:")
|
||||
for node in results[DependsReportTypes.direct_depends.name]:
|
||||
self._print_results_node_list(f'=>depends on {node}:',
|
||||
results[DependsReportTypes.direct_depends.name][node])
|
||||
|
||||
if DependsReportTypes.common_depends.name in results:
|
||||
print("\nNodes that commonly depend on:")
|
||||
for nodes in results[DependsReportTypes.common_depends.name]:
|
||||
self._print_results_node_list(
|
||||
f'=>depends on {nodes}:',
|
||||
results[DependsReportTypes.common_depends.name][nodes])
|
||||
|
||||
if DependsReportTypes.exclude_depends.name in results:
|
||||
print("\nNodes that depend on a node, but exclude others:")
|
||||
for nodes in results[DependsReportTypes.exclude_depends.name]:
|
||||
self._print_results_node_list(
|
||||
f"=>depends: {nodes[0]}, exclude: {nodes[1:]}:",
|
||||
results[DependsReportTypes.exclude_depends.name][nodes])
|
@ -814,14 +814,6 @@ def _get_node_with_ixes(env, node, node_builder_type):
|
||||
|
||||
_get_node_with_ixes.node_type_ixes = dict()
|
||||
|
||||
def add_libdeps_node(env, target, libdeps):
|
||||
if str(target).endswith(env["SHLIBSUFFIX"]):
|
||||
t_str = _get_node_with_ixes(env, str(target.abspath), target.get_builder().get_name(env)).abspath
|
||||
env.GetLibdepsGraph().add_node(t_str)
|
||||
for libdep in libdeps:
|
||||
if str(libdep.target_node).endswith(env["SHLIBSUFFIX"]):
|
||||
env.GetLibdepsGraph().add_edge(str(libdep.target_node.abspath), t_str, visibility=libdep.dependency_type)
|
||||
|
||||
def make_libdeps_emitter(
|
||||
dependency_builder,
|
||||
dependency_map=dependency_visibility_ignored,
|
||||
@ -868,10 +860,6 @@ def make_libdeps_emitter(
|
||||
if not any("conftest" in str(t) for t in target):
|
||||
LibdepLinter(env, target).lint_libdeps(libdeps)
|
||||
|
||||
if env.get('SYMBOLDEPSSUFFIX', None):
|
||||
for t in target:
|
||||
add_libdeps_node(env, t, libdeps)
|
||||
|
||||
# We ignored the dependency_map until now because we needed to use
|
||||
# original dependency value for linting. Now go back through and
|
||||
# use the map to convert to the desired dependencies, for example
|
||||
@ -980,6 +968,67 @@ def expand_libdeps_with_flags(source, target, env, for_signature):
|
||||
|
||||
return libdeps_with_flags
|
||||
|
||||
def generate_libdeps_graph(env):
|
||||
if env.get('SYMBOLDEPSSUFFIX', None):
|
||||
import glob
|
||||
from buildscripts.libdeps.graph_analyzer import EdgeProps
|
||||
find_symbols = env.Dir("$BUILD_DIR").path + "/libdeps/find_symbols"
|
||||
symbol_deps = []
|
||||
for target, source in env.get('LIBDEPS_SYMBOL_DEP_FILES', []):
|
||||
direct_libdeps = []
|
||||
for direct_libdep in __get_sorted_direct_libdeps(source):
|
||||
env.GetLibdepsGraph().add_edges_from([(
|
||||
str(direct_libdep.target_node.abspath),
|
||||
str(source.abspath),
|
||||
{
|
||||
EdgeProps.direct.name: 1,
|
||||
EdgeProps.visibility.name: int(direct_libdep.dependency_type)
|
||||
})])
|
||||
direct_libdeps.append(direct_libdep.target_node.abspath)
|
||||
for libdep in __get_libdeps(source):
|
||||
if libdep.abspath not in direct_libdeps:
|
||||
env.GetLibdepsGraph().add_edges_from([(
|
||||
str(libdep.abspath),
|
||||
str(source.abspath),
|
||||
{
|
||||
EdgeProps.direct.name: 0,
|
||||
EdgeProps.visibility.name: 0
|
||||
})])
|
||||
|
||||
ld_path = ":".join([os.path.dirname(str(libdep)) for libdep in __get_libdeps(source)])
|
||||
symbol_deps.append(env.Command(
|
||||
target=target,
|
||||
source=source,
|
||||
action=SCons.Action.Action(
|
||||
f'{find_symbols} $SOURCE "{ld_path}" $TARGET',
|
||||
"Generating $SOURCE symbol dependencies")))
|
||||
|
||||
def write_graph_hash(env, target, source):
|
||||
import networkx
|
||||
import hashlib
|
||||
import json
|
||||
with open(target[0].path, 'w') as f:
|
||||
json_str = json.dumps(networkx.readwrite.json_graph.node_link_data(env.GetLibdepsGraph()), sort_keys=True).encode('utf-8')
|
||||
f.write(hashlib.sha256(json_str).hexdigest())
|
||||
|
||||
graph_hash = env.Command(target="$BUILD_DIR/libdeps/graph_hash.sha256",
|
||||
source=symbol_deps + [
|
||||
env.File("#SConstruct")] +
|
||||
glob.glob("**/SConscript", recursive=True) +
|
||||
[os.path.abspath(__file__)],
|
||||
action=SCons.Action.FunctionAction(
|
||||
write_graph_hash,
|
||||
{"cmdstr": None}))
|
||||
|
||||
graph_node = env.Command(
|
||||
target=env.get('LIBDEPS_GRAPH_FILE', None),
|
||||
source=symbol_deps,
|
||||
action=SCons.Action.FunctionAction(
|
||||
generate_graph,
|
||||
{"cmdstr": "Generating libdeps graph"}))
|
||||
|
||||
env.Depends(graph_node, graph_hash)
|
||||
|
||||
def get_typeinfo_link_command():
|
||||
if LibdepLinter.skip_linting:
|
||||
return "{ninjalink}"
|
||||
@ -1040,14 +1089,21 @@ def generate_graph(env, target, source):
|
||||
|
||||
for symbol_deps_file in source:
|
||||
with open(str(symbol_deps_file)) as f:
|
||||
symbols = {}
|
||||
for symbol, lib in json.load(f).items():
|
||||
# ignore symbols from external libraries,
|
||||
# they will just clutter the graph
|
||||
if lib.startswith(env.Dir("$BUILD_DIR").path):
|
||||
env.GetLibdepsGraph().add_edges_from([(
|
||||
os.path.abspath(lib).strip(),
|
||||
os.path.abspath(str(symbol_deps_file)[:-len(env['SYMBOLDEPSSUFFIX'])]),
|
||||
{symbol.strip(): "1"})])
|
||||
if lib not in symbols:
|
||||
symbols[lib] = []
|
||||
symbols[lib].append(symbol)
|
||||
|
||||
for lib in symbols:
|
||||
env.GetLibdepsGraph().add_edges_from([(
|
||||
os.path.abspath(lib).strip(),
|
||||
os.path.abspath(str(symbol_deps_file)[:-len(env['SYMBOLDEPSSUFFIX'])]),
|
||||
{"symbols": " ".join(symbols[lib]) })])
|
||||
|
||||
|
||||
libdeps_graph_file = f"{env.Dir('$BUILD_DIR').path}/libdeps/libdeps.graphml"
|
||||
networkx.write_graphml(env.GetLibdepsGraph(), libdeps_graph_file, named_key_ids=True)
|
||||
@ -1129,14 +1185,15 @@ def setup_environment(env, emitting_shared=False, linting='on', sanitize_typeinf
|
||||
find_symbols_env.VariantDir('${BUILD_DIR}/libdeps', 'buildscripts/libdeps', duplicate = 0)
|
||||
find_symbols_node = find_symbols_env.Program(
|
||||
target='${BUILD_DIR}/libdeps/find_symbols',
|
||||
source=['${BUILD_DIR}/libdeps/find_symbols.c'])
|
||||
source=['${BUILD_DIR}/libdeps/find_symbols.c'],
|
||||
CFLAGS=['-O3'])
|
||||
|
||||
# Here we are setting up some functions which will return single instance of the
|
||||
# network graph and symbol deps list. We also setup some environment variables
|
||||
# which are used along side the functions.
|
||||
symbol_deps = []
|
||||
def append_symbol_deps(env, symbol_deps_file):
|
||||
env.Depends("${BUILD_DIR}/libdeps/libdeps.graphml", symbol_deps_file)
|
||||
env.Depends(env['LIBDEPS_GRAPH_FILE'], symbol_deps_file[0])
|
||||
symbol_deps.append(symbol_deps_file)
|
||||
env.AddMethod(append_symbol_deps, "AppendSymbolDeps")
|
||||
|
||||
@ -1146,16 +1203,16 @@ def setup_environment(env, emitting_shared=False, linting='on', sanitize_typeinf
|
||||
env.AddMethod(get_libdeps_graph, "GetLibdepsGraph")
|
||||
|
||||
env['LIBDEPS_SYMBOL_DEP_FILES'] = symbol_deps
|
||||
env['LIBDEPS_GRAPH_FILE'] = env.File("${BUILD_DIR}/libdeps/libdeps.graphml")
|
||||
env["SYMBOLDEPSSUFFIX"] = '.symbol_deps'
|
||||
|
||||
# Now we will setup an emitter, and an additional action for several
|
||||
# of the builder involved with dynamic builds.
|
||||
def libdeps_graph_emitter(target, source, env):
|
||||
if "conftest" not in str(target[0]):
|
||||
symbol_deps_file = target[0].path + env['SYMBOLDEPSSUFFIX']
|
||||
env.Depends(target, '${BUILD_DIR}/libdeps/find_symbols')
|
||||
env.SideEffect(symbol_deps_file, target)
|
||||
env.AppendSymbolDeps(symbol_deps_file)
|
||||
symbol_deps_file = env.File(str(target[0]) + env['SYMBOLDEPSSUFFIX'])
|
||||
env.Depends(symbol_deps_file, '${BUILD_DIR}/libdeps/find_symbols')
|
||||
env.AppendSymbolDeps((symbol_deps_file,target[0]))
|
||||
|
||||
return target, source
|
||||
|
||||
@ -1165,15 +1222,6 @@ def setup_environment(env, emitting_shared=False, linting='on', sanitize_typeinf
|
||||
new_emitter = SCons.Builder.ListEmitter([base_emitter, libdeps_graph_emitter])
|
||||
builder.emitter = new_emitter
|
||||
|
||||
base_action = builder.action
|
||||
if not isinstance(base_action, SCons.Action.ListAction):
|
||||
base_action = SCons.Action.ListAction([base_action])
|
||||
find_symbols = env.Dir("$BUILD_DIR").path + "/libdeps/find_symbols"
|
||||
base_action.list.extend([
|
||||
SCons.Action.Action(f'if [ -e {find_symbols} ]; then {find_symbols} $TARGET "$_LIBDEPS_LD_PATH" ${{TARGET}}.symbol_deps; fi', None)
|
||||
])
|
||||
builder.action = base_action
|
||||
|
||||
# We need a way for environments to alter just which libdeps
|
||||
# emitter they want, without altering the overall program or
|
||||
# library emitter which may have important effects. The
|
||||
|
Loading…
Reference in New Issue
Block a user