From fa271cc17cee33342432d2c080889bf00fbfb8d8 Mon Sep 17 00:00:00 2001 From: Daniel Moody Date: Mon, 23 Nov 2020 21:04:08 +0000 Subject: SERVER-52567 added basic functions for graph analyzer CLI tool and improved graph generation. --- SConstruct | 7 +- buildscripts/libdeps/gacli.py | 164 +++++++++++++++++ buildscripts/libdeps/graph_analyzer.py | 326 +++++++++++++++++++++++++++++++++ site_scons/libdeps_next.py | 110 +++++++---- 4 files changed, 570 insertions(+), 37 deletions(-) create mode 100644 buildscripts/libdeps/gacli.py create mode 100644 buildscripts/libdeps/graph_analyzer.py 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 -- cgit v1.2.1