forked from OSchip/llvm-project
209 lines
7.2 KiB
Python
Executable File
209 lines
7.2 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#===----------------------------------------------------------------------===##
|
|
#
|
|
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
# See https://llvm.org/LICENSE.txt for license information.
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
#
|
|
#===----------------------------------------------------------------------===##
|
|
|
|
from argparse import ArgumentParser
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import shlex
|
|
import json
|
|
import re
|
|
import libcxx.graph as dot
|
|
import libcxx.util
|
|
|
|
def print_and_exit(msg):
|
|
sys.stderr.write(msg + '\n')
|
|
sys.exit(1)
|
|
|
|
def libcxx_include_path():
|
|
curr_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
include_dir = os.path.join(curr_dir, 'include')
|
|
return include_dir
|
|
|
|
def get_libcxx_headers():
|
|
headers = []
|
|
include_dir = libcxx_include_path()
|
|
for fname in os.listdir(include_dir):
|
|
f = os.path.join(include_dir, fname)
|
|
if not os.path.isfile(f):
|
|
continue
|
|
base, ext = os.path.splitext(fname)
|
|
if (ext == '' or ext == '.h') and (not fname.startswith('__') or fname == '__config'):
|
|
headers += [f]
|
|
return headers
|
|
|
|
|
|
def rename_headers_and_remove_test_root(graph):
|
|
inc_root = libcxx_include_path()
|
|
to_remove = set()
|
|
for n in graph.nodes:
|
|
assert 'label' in n.attributes
|
|
l = n.attributes['label']
|
|
if not l.startswith('/') and os.path.exists(os.path.join('/', l)):
|
|
l = '/' + l
|
|
if l.endswith('.tmp.cpp'):
|
|
to_remove.add(n)
|
|
if l.startswith(inc_root):
|
|
l = l[len(inc_root):]
|
|
if l.startswith('/'):
|
|
l = l[1:]
|
|
n.attributes['label'] = l
|
|
for n in to_remove:
|
|
graph.removeNode(n)
|
|
|
|
def remove_non_std_headers(graph):
|
|
inc_root = libcxx_include_path()
|
|
to_remove = set()
|
|
for n in graph.nodes:
|
|
test_file = os.path.join(inc_root, n.attributes['label'])
|
|
if not test_file.startswith(inc_root):
|
|
to_remove.add(n)
|
|
for xn in to_remove:
|
|
graph.removeNode(xn)
|
|
|
|
class DependencyCommand(object):
|
|
def __init__(self, compile_commands, output_dir, new_std=None):
|
|
output_dir = os.path.abspath(output_dir)
|
|
if not os.path.isdir(output_dir):
|
|
print_and_exit('"%s" must point to a directory' % output_dir)
|
|
self.output_dir = output_dir
|
|
self.new_std = new_std
|
|
cwd,bcmd = self._get_base_command(compile_commands)
|
|
self.cwd = cwd
|
|
self.base_cmd = bcmd
|
|
|
|
def run_for_headers(self, header_list):
|
|
outputs = []
|
|
for header in header_list:
|
|
header_name = os.path.basename(header)
|
|
out = os.path.join(self.output_dir, ('%s.dot' % header_name))
|
|
outputs += [out]
|
|
cmd = self.base_cmd + ["-fsyntax-only", "-Xclang", "-dependency-dot", "-Xclang", "%s" % out, '-xc++', '-']
|
|
libcxx.util.executeCommandOrDie(cmd, cwd=self.cwd, input='#include <%s>\n\n' % header_name)
|
|
return outputs
|
|
|
|
def _get_base_command(self, command_file):
|
|
commands = None
|
|
with open(command_file, 'r') as f:
|
|
commands = json.load(f)
|
|
for compile_cmd in commands:
|
|
file = compile_cmd['file']
|
|
if not file.endswith('src/algorithm.cpp'):
|
|
continue
|
|
wd = compile_cmd['directory']
|
|
cmd_str = compile_cmd['command']
|
|
cmd = shlex.split(cmd_str)
|
|
out_arg = cmd.index('-o')
|
|
del cmd[out_arg]
|
|
del cmd[out_arg]
|
|
in_arg = cmd.index('-c')
|
|
del cmd[in_arg]
|
|
del cmd[in_arg]
|
|
if self.new_std is not None:
|
|
for f in cmd:
|
|
if f.startswith('-std='):
|
|
del cmd[cmd.index(f)]
|
|
cmd += [self.new_std]
|
|
break
|
|
return wd, cmd
|
|
print_and_exit("failed to find command to build algorithm.cpp")
|
|
|
|
def post_process_outputs(outputs, libcxx_only):
|
|
graphs = []
|
|
for dot_file in outputs:
|
|
g = dot.DirectedGraph.fromDotFile(dot_file)
|
|
rename_headers_and_remove_test_root(g)
|
|
if libcxx_only:
|
|
remove_non_std_headers(g)
|
|
graphs += [g]
|
|
g.toDotFile(dot_file)
|
|
return graphs
|
|
|
|
def build_canonical_names(graphs):
|
|
canonical_names = {}
|
|
next_idx = 0
|
|
for g in graphs:
|
|
for n in g.nodes:
|
|
if n.attributes['label'] not in canonical_names:
|
|
name = 'header_%d' % next_idx
|
|
next_idx += 1
|
|
canonical_names[n.attributes['label']] = name
|
|
return canonical_names
|
|
|
|
|
|
|
|
class CanonicalGraphBuilder(object):
|
|
def __init__(self, graphs):
|
|
self.graphs = list(graphs)
|
|
self.canonical_names = build_canonical_names(graphs)
|
|
|
|
def build(self):
|
|
self.canonical = dot.DirectedGraph('all_headers')
|
|
for k,v in self.canonical_names.iteritems():
|
|
n = dot.Node(v, edges=[], attributes={'shape': 'box', 'label': k})
|
|
self.canonical.addNode(n)
|
|
for g in self.graphs:
|
|
self._merge_graph(g)
|
|
return self.canonical
|
|
|
|
def _merge_graph(self, g):
|
|
for n in g.nodes:
|
|
new_name = self.canonical.getNodeByLabel(n.attributes['label']).id
|
|
for e in n.edges:
|
|
to_node = self.canonical.getNodeByLabel(e.attributes['label']).id
|
|
self.canonical.addEdge(new_name, to_node)
|
|
|
|
|
|
def main():
|
|
parser = ArgumentParser(
|
|
description="Generate a graph of libc++ header dependencies")
|
|
parser.add_argument(
|
|
'-v', '--verbose', dest='verbose', action='store_true', default=False)
|
|
parser.add_argument(
|
|
'-o', '--output', dest='output', required=True,
|
|
help='The output file. stdout is used if not given',
|
|
type=str, action='store')
|
|
parser.add_argument(
|
|
'--no-compile', dest='no_compile', action='store_true', default=False)
|
|
parser.add_argument(
|
|
'--libcxx-only', dest='libcxx_only', action='store_true', default=False)
|
|
parser.add_argument(
|
|
'compile_commands', metavar='compile-commands-file',
|
|
help='the compile commands database')
|
|
|
|
args = parser.parse_args()
|
|
builder = DependencyCommand(args.compile_commands, args.output, new_std='-std=c++2a')
|
|
if not args.no_compile:
|
|
outputs = builder.run_for_headers(get_libcxx_headers())
|
|
graphs = post_process_outputs(outputs, args.libcxx_only)
|
|
else:
|
|
outputs = [os.path.join(args.output, l) for l in os.listdir(args.output) if not l.endswith('all_headers.dot')]
|
|
graphs = [dot.DirectedGraph.fromDotFile(o) for o in outputs]
|
|
|
|
canon = CanonicalGraphBuilder(graphs).build()
|
|
canon.toDotFile(os.path.join(args.output, 'all_headers.dot'))
|
|
all_graphs = graphs + [canon]
|
|
|
|
found_cycles = False
|
|
for g in all_graphs:
|
|
cycle_finder = dot.CycleFinder(g)
|
|
all_cycles = cycle_finder.findCyclesInGraph()
|
|
if len(all_cycles):
|
|
found_cycles = True
|
|
print("cycle in graph %s" % g.name)
|
|
for start, path in all_cycles:
|
|
print("Cycle for %s = %s" % (start, path))
|
|
if not found_cycles:
|
|
print("No cycles found")
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|