#!/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 # #===----------------------------------------------------------------------===## import argparse import os import re import sys def is_config_header(h): return os.path.basename(h) in ['__config', '__libcpp_version'] def is_experimental_header(h): return ('experimental/' in h) or ('ext/' in h) def is_support_header(h): return '__support/' in h class FileEntry: def __init__(self, includes, individual_linecount): self.includes = includes self.individual_linecount = individual_linecount self.cumulative_linecount = None # documentation: this gets filled in later self.is_graph_root = None # documentation: this gets filled in later def list_all_roots_under(root): result = [] for root, _, files in os.walk(root): for fname in files: if '__support' in root: pass elif ('.' in fname and not fname.endswith('.h')): pass else: result.append(root + '/' + fname) return result def build_file_entry(fname, options): assert os.path.exists(fname) def locate_header_file(h, paths): for p in paths: fullname = p + '/' + h if os.path.exists(fullname): return fullname if options.error_on_file_not_found: raise RuntimeError('Header not found: %s, included by %s' % (h, fname)) return None local_includes = [] system_includes = [] linecount = 0 with open(fname, 'r', encoding='utf-8') as f: for line in f.readlines(): linecount += 1 m = re.match(r'\s*#\s*include\s+"([^"]*)"', line) if m is not None: local_includes.append(m.group(1)) m = re.match(r'\s*#\s*include\s+<([^>]*)>', line) if m is not None: system_includes.append(m.group(1)) fully_qualified_includes = [ locate_header_file(h, options.search_dirs) for h in system_includes ] + [ locate_header_file(h, os.path.dirname(fname)) for h in local_includes ] return FileEntry( # If file-not-found wasn't an error, then skip non-found files includes = [h for h in fully_qualified_includes if h is not None], individual_linecount = linecount, ) def transitive_closure_of_includes(graph, h1): visited = set() def explore(graph, h1): if h1 not in visited: visited.add(h1) for h2 in graph[h1].includes: explore(graph, h2) explore(graph, h1) return visited def transitively_includes(graph, h1, h2): return (h1 != h2) and (h2 in transitive_closure_of_includes(graph, h1)) def build_graph(roots, options): original_roots = list(roots) graph = {} while roots: frontier = roots roots = [] for fname in frontier: if fname not in graph: graph[fname] = build_file_entry(fname, options) graph[fname].is_graph_root = (fname in original_roots) roots += graph[fname].includes for fname, entry in graph.items(): entry.cumulative_linecount = sum(graph[h].individual_linecount for h in transitive_closure_of_includes(graph, fname)) return graph def get_friendly_id(fname): i = fname.index('include/') assert(i >= 0) result = fname[i+8:] return result def get_graphviz(graph, options): def get_decorators(fname, entry): result = '' if entry.is_graph_root: result += ' [style=bold]' if options.show_individual_line_counts and options.show_cumulative_line_counts: result += ' [label="%s\\n%d indiv, %d cumul"]' % ( get_friendly_id(fname), entry.individual_linecount, entry.cumulative_linecount ) elif options.show_individual_line_counts: result += ' [label="%s\\n%d indiv"]' % (get_friendly_id(fname), entry.individual_linecount) elif options.show_cumulative_line_counts: result += ' [label="%s\\n%d cumul"]' % (get_friendly_id(fname), entry.cumulative_linecount) return result result = '' result += 'strict digraph {\n' result += ' rankdir=LR;\n' result += ' layout=dot;\n\n' for fname, entry in graph.items(): result += ' "%s"%s;\n' % (get_friendly_id(fname), get_decorators(fname, entry)) for h in entry.includes: if any(transitively_includes(graph, i, h) for i in entry.includes) and not options.show_transitive_edges: continue result += ' "%s" -> "%s";\n' % (get_friendly_id(fname), get_friendly_id(h)) result += '}\n' return result if __name__ == '__main__': parser = argparse.ArgumentParser(description='Produce a dependency graph of libc++ headers, in GraphViz dot format.') parser.add_argument('--root', default=None, metavar='FILE', help='File or directory to be the root of the dependency graph') parser.add_argument('-I', dest='search_dirs', default=[], action='append', metavar='DIR', help='Path(s) to search for local includes') parser.add_argument('--show-transitive-edges', action='store_true', help='Show edges to headers that are transitively included anyway') parser.add_argument('--show-config-headers', action='store_true', help='Show headers named __config') parser.add_argument('--show-experimental-headers', action='store_true', help='Show headers in the experimental/ and ext/ directories') parser.add_argument('--show-support-headers', action='store_true', help='Show headers in the __support/ directory') parser.add_argument('--show-individual-line-counts', action='store_true', help='Include an individual line count in each node') parser.add_argument('--show-cumulative-line-counts', action='store_true', help='Include a total line count in each node') parser.add_argument('--error-on-file-not-found', action='store_true', help="Don't ignore failure to open an #included file") options = parser.parse_args() if options.root is None: curr_dir = os.path.dirname(os.path.abspath(__file__)) options.root = os.path.join(curr_dir, '../include') if options.search_dirs == [] and os.path.isdir(options.root): options.search_dirs = [options.root] options.root = os.path.abspath(options.root) options.search_dirs = [os.path.abspath(p) for p in options.search_dirs] if os.path.isdir(options.root): roots = list_all_roots_under(options.root) elif os.path.isfile(options.root): roots = [options.root] else: raise RuntimeError('--root seems to be invalid') graph = build_graph(roots, options) # Eliminate certain kinds of "visual noise" headers, if asked for. def should_keep(fname): return all([ options.show_config_headers or not is_config_header(fname), options.show_experimental_headers or not is_experimental_header(fname), options.show_support_headers or not is_support_header(fname), ]) for fname in list(graph.keys()): if should_keep(fname): graph[fname].includes = [h for h in graph[fname].includes if should_keep(h)] else: del graph[fname] # Look for cycles. no_cycles_detected = True for fname, entry in graph.items(): for h in entry.includes: if transitively_includes(graph, h, fname): sys.stderr.write('Cycle detected between %s and %s\n' % ( get_friendly_id(fname), get_friendly_id(h) )) no_cycles_detected = False assert no_cycles_detected print(get_graphviz(graph, options))