0
0
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:
Daniel Moody 2020-11-23 21:04:08 +00:00 committed by Evergreen Agent
parent 8e7ad370f7
commit fa271cc17c
4 changed files with 570 additions and 37 deletions

View File

@ -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)

View 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()

View 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])

View File

@ -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