summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Moody <daniel.moody@mongodb.com>2020-11-23 21:04:08 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2020-11-25 03:21:49 +0000
commitfa271cc17cee33342432d2c080889bf00fbfb8d8 (patch)
tree3976c7f859db24f52af573127ef72b207d3aebc5
parent8e7ad370f7d35501a9fdd563456b361cbb133d86 (diff)
downloadmongo-fa271cc17cee33342432d2c080889bf00fbfb8d8.tar.gz
SERVER-52567 added basic functions for graph analyzer CLI tool and improved graph generation.
-rw-r--r--SConstruct7
-rw-r--r--buildscripts/libdeps/gacli.py164
-rw-r--r--buildscripts/libdeps/graph_analyzer.py326
-rw-r--r--site_scons/libdeps_next.py110
4 files changed, 570 insertions, 37 deletions
diff --git a/SConstruct b/SConstruct
index 3ef6caa14d2..0fc772c2b75 100644
--- a/SConstruct
+++ b/SConstruct
@@ -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) \ No newline at end of file
diff --git a/buildscripts/libdeps/gacli.py b/buildscripts/libdeps/gacli.py
new file mode 100644
index 00000000000..a89f8d7346e
--- /dev/null
+++ b/buildscripts/libdeps/gacli.py
@@ -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()
diff --git a/buildscripts/libdeps/graph_analyzer.py b/buildscripts/libdeps/graph_analyzer.py
new file mode 100644
index 00000000000..ac3d7339b7f
--- /dev/null
+++ b/buildscripts/libdeps/graph_analyzer.py
@@ -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])
diff --git a/site_scons/libdeps_next.py b/site_scons/libdeps_next.py
index 6da1a232a97..195567eda75 100644
--- a/site_scons/libdeps_next.py
+++ b/site_scons/libdeps_next.py
@@ -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