1*4d6fc14bSjoerg#!/usr/bin/env python 2*4d6fc14bSjoerg#===----------------------------------------------------------------------===## 3*4d6fc14bSjoerg# 4*4d6fc14bSjoerg# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 5*4d6fc14bSjoerg# See https://llvm.org/LICENSE.txt for license information. 6*4d6fc14bSjoerg# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 7*4d6fc14bSjoerg# 8*4d6fc14bSjoerg#===----------------------------------------------------------------------===## 9*4d6fc14bSjoerg 10*4d6fc14bSjoergimport argparse 11*4d6fc14bSjoergimport os 12*4d6fc14bSjoergimport re 13*4d6fc14bSjoergimport sys 14*4d6fc14bSjoerg 15*4d6fc14bSjoerg 16*4d6fc14bSjoergdef is_config_header(h): 17*4d6fc14bSjoerg return os.path.basename(h) in ['__config', '__libcpp_version'] 18*4d6fc14bSjoerg 19*4d6fc14bSjoerg 20*4d6fc14bSjoergdef is_experimental_header(h): 21*4d6fc14bSjoerg return ('experimental/' in h) or ('ext/' in h) 22*4d6fc14bSjoerg 23*4d6fc14bSjoerg 24*4d6fc14bSjoergdef is_support_header(h): 25*4d6fc14bSjoerg return '__support/' in h 26*4d6fc14bSjoerg 27*4d6fc14bSjoerg 28*4d6fc14bSjoergclass FileEntry: 29*4d6fc14bSjoerg def __init__(self, includes, individual_linecount): 30*4d6fc14bSjoerg self.includes = includes 31*4d6fc14bSjoerg self.individual_linecount = individual_linecount 32*4d6fc14bSjoerg self.cumulative_linecount = None # documentation: this gets filled in later 33*4d6fc14bSjoerg self.is_graph_root = None # documentation: this gets filled in later 34*4d6fc14bSjoerg 35*4d6fc14bSjoerg 36*4d6fc14bSjoergdef list_all_roots_under(root): 37*4d6fc14bSjoerg result = [] 38*4d6fc14bSjoerg for root, _, files in os.walk(root): 39*4d6fc14bSjoerg for fname in files: 40*4d6fc14bSjoerg if '__support' in root: 41*4d6fc14bSjoerg pass 42*4d6fc14bSjoerg elif ('.' in fname and not fname.endswith('.h')): 43*4d6fc14bSjoerg pass 44*4d6fc14bSjoerg else: 45*4d6fc14bSjoerg result.append(root + '/' + fname) 46*4d6fc14bSjoerg return result 47*4d6fc14bSjoerg 48*4d6fc14bSjoerg 49*4d6fc14bSjoergdef build_file_entry(fname, options): 50*4d6fc14bSjoerg assert os.path.exists(fname) 51*4d6fc14bSjoerg 52*4d6fc14bSjoerg def locate_header_file(h, paths): 53*4d6fc14bSjoerg for p in paths: 54*4d6fc14bSjoerg fullname = p + '/' + h 55*4d6fc14bSjoerg if os.path.exists(fullname): 56*4d6fc14bSjoerg return fullname 57*4d6fc14bSjoerg if options.error_on_file_not_found: 58*4d6fc14bSjoerg raise RuntimeError('Header not found: %s, included by %s' % (h, fname)) 59*4d6fc14bSjoerg return None 60*4d6fc14bSjoerg 61*4d6fc14bSjoerg local_includes = [] 62*4d6fc14bSjoerg system_includes = [] 63*4d6fc14bSjoerg linecount = 0 64*4d6fc14bSjoerg with open(fname, 'r', encoding='utf-8') as f: 65*4d6fc14bSjoerg for line in f.readlines(): 66*4d6fc14bSjoerg linecount += 1 67*4d6fc14bSjoerg m = re.match(r'\s*#\s*include\s+"([^"]*)"', line) 68*4d6fc14bSjoerg if m is not None: 69*4d6fc14bSjoerg local_includes.append(m.group(1)) 70*4d6fc14bSjoerg m = re.match(r'\s*#\s*include\s+<([^>]*)>', line) 71*4d6fc14bSjoerg if m is not None: 72*4d6fc14bSjoerg system_includes.append(m.group(1)) 73*4d6fc14bSjoerg 74*4d6fc14bSjoerg fully_qualified_includes = [ 75*4d6fc14bSjoerg locate_header_file(h, options.search_dirs) 76*4d6fc14bSjoerg for h in system_includes 77*4d6fc14bSjoerg ] + [ 78*4d6fc14bSjoerg locate_header_file(h, os.path.dirname(fname)) 79*4d6fc14bSjoerg for h in local_includes 80*4d6fc14bSjoerg ] 81*4d6fc14bSjoerg 82*4d6fc14bSjoerg return FileEntry( 83*4d6fc14bSjoerg # If file-not-found wasn't an error, then skip non-found files 84*4d6fc14bSjoerg includes = [h for h in fully_qualified_includes if h is not None], 85*4d6fc14bSjoerg individual_linecount = linecount, 86*4d6fc14bSjoerg ) 87*4d6fc14bSjoerg 88*4d6fc14bSjoerg 89*4d6fc14bSjoergdef transitive_closure_of_includes(graph, h1): 90*4d6fc14bSjoerg visited = set() 91*4d6fc14bSjoerg def explore(graph, h1): 92*4d6fc14bSjoerg if h1 not in visited: 93*4d6fc14bSjoerg visited.add(h1) 94*4d6fc14bSjoerg for h2 in graph[h1].includes: 95*4d6fc14bSjoerg explore(graph, h2) 96*4d6fc14bSjoerg explore(graph, h1) 97*4d6fc14bSjoerg return visited 98*4d6fc14bSjoerg 99*4d6fc14bSjoerg 100*4d6fc14bSjoergdef transitively_includes(graph, h1, h2): 101*4d6fc14bSjoerg return (h1 != h2) and (h2 in transitive_closure_of_includes(graph, h1)) 102*4d6fc14bSjoerg 103*4d6fc14bSjoerg 104*4d6fc14bSjoergdef build_graph(roots, options): 105*4d6fc14bSjoerg original_roots = list(roots) 106*4d6fc14bSjoerg graph = {} 107*4d6fc14bSjoerg while roots: 108*4d6fc14bSjoerg frontier = roots 109*4d6fc14bSjoerg roots = [] 110*4d6fc14bSjoerg for fname in frontier: 111*4d6fc14bSjoerg if fname not in graph: 112*4d6fc14bSjoerg graph[fname] = build_file_entry(fname, options) 113*4d6fc14bSjoerg graph[fname].is_graph_root = (fname in original_roots) 114*4d6fc14bSjoerg roots += graph[fname].includes 115*4d6fc14bSjoerg for fname, entry in graph.items(): 116*4d6fc14bSjoerg entry.cumulative_linecount = sum(graph[h].individual_linecount for h in transitive_closure_of_includes(graph, fname)) 117*4d6fc14bSjoerg return graph 118*4d6fc14bSjoerg 119*4d6fc14bSjoerg 120*4d6fc14bSjoergdef get_friendly_id(fname): 121*4d6fc14bSjoerg i = fname.index('include/') 122*4d6fc14bSjoerg assert(i >= 0) 123*4d6fc14bSjoerg result = fname[i+8:] 124*4d6fc14bSjoerg return result 125*4d6fc14bSjoerg 126*4d6fc14bSjoerg 127*4d6fc14bSjoergdef get_graphviz(graph, options): 128*4d6fc14bSjoerg 129*4d6fc14bSjoerg def get_decorators(fname, entry): 130*4d6fc14bSjoerg result = '' 131*4d6fc14bSjoerg if entry.is_graph_root: 132*4d6fc14bSjoerg result += ' [style=bold]' 133*4d6fc14bSjoerg if options.show_individual_line_counts and options.show_cumulative_line_counts: 134*4d6fc14bSjoerg result += ' [label="%s\\n%d indiv, %d cumul"]' % ( 135*4d6fc14bSjoerg get_friendly_id(fname), entry.individual_linecount, entry.cumulative_linecount 136*4d6fc14bSjoerg ) 137*4d6fc14bSjoerg elif options.show_individual_line_counts: 138*4d6fc14bSjoerg result += ' [label="%s\\n%d indiv"]' % (get_friendly_id(fname), entry.individual_linecount) 139*4d6fc14bSjoerg elif options.show_cumulative_line_counts: 140*4d6fc14bSjoerg result += ' [label="%s\\n%d cumul"]' % (get_friendly_id(fname), entry.cumulative_linecount) 141*4d6fc14bSjoerg return result 142*4d6fc14bSjoerg 143*4d6fc14bSjoerg result = '' 144*4d6fc14bSjoerg result += 'strict digraph {\n' 145*4d6fc14bSjoerg result += ' rankdir=LR;\n' 146*4d6fc14bSjoerg result += ' layout=dot;\n\n' 147*4d6fc14bSjoerg for fname, entry in graph.items(): 148*4d6fc14bSjoerg result += ' "%s"%s;\n' % (get_friendly_id(fname), get_decorators(fname, entry)) 149*4d6fc14bSjoerg for h in entry.includes: 150*4d6fc14bSjoerg if any(transitively_includes(graph, i, h) for i in entry.includes) and not options.show_transitive_edges: 151*4d6fc14bSjoerg continue 152*4d6fc14bSjoerg result += ' "%s" -> "%s";\n' % (get_friendly_id(fname), get_friendly_id(h)) 153*4d6fc14bSjoerg result += '}\n' 154*4d6fc14bSjoerg return result 155*4d6fc14bSjoerg 156*4d6fc14bSjoerg 157*4d6fc14bSjoergif __name__ == '__main__': 158*4d6fc14bSjoerg parser = argparse.ArgumentParser(description='Produce a dependency graph of libc++ headers, in GraphViz dot format.') 159*4d6fc14bSjoerg parser.add_argument('--root', default=None, metavar='FILE', help='File or directory to be the root of the dependency graph') 160*4d6fc14bSjoerg parser.add_argument('-I', dest='search_dirs', default=[], action='append', metavar='DIR', help='Path(s) to search for local includes') 161*4d6fc14bSjoerg parser.add_argument('--show-transitive-edges', action='store_true', help='Show edges to headers that are transitively included anyway') 162*4d6fc14bSjoerg parser.add_argument('--show-config-headers', action='store_true', help='Show headers named __config') 163*4d6fc14bSjoerg parser.add_argument('--show-experimental-headers', action='store_true', help='Show headers in the experimental/ and ext/ directories') 164*4d6fc14bSjoerg parser.add_argument('--show-support-headers', action='store_true', help='Show headers in the __support/ directory') 165*4d6fc14bSjoerg parser.add_argument('--show-individual-line-counts', action='store_true', help='Include an individual line count in each node') 166*4d6fc14bSjoerg parser.add_argument('--show-cumulative-line-counts', action='store_true', help='Include a total line count in each node') 167*4d6fc14bSjoerg parser.add_argument('--error-on-file-not-found', action='store_true', help="Don't ignore failure to open an #included file") 168*4d6fc14bSjoerg 169*4d6fc14bSjoerg options = parser.parse_args() 170*4d6fc14bSjoerg 171*4d6fc14bSjoerg if options.root is None: 172*4d6fc14bSjoerg curr_dir = os.path.dirname(os.path.abspath(__file__)) 173*4d6fc14bSjoerg options.root = os.path.join(curr_dir, '../include') 174*4d6fc14bSjoerg 175*4d6fc14bSjoerg if options.search_dirs == [] and os.path.isdir(options.root): 176*4d6fc14bSjoerg options.search_dirs = [options.root] 177*4d6fc14bSjoerg 178*4d6fc14bSjoerg options.root = os.path.abspath(options.root) 179*4d6fc14bSjoerg options.search_dirs = [os.path.abspath(p) for p in options.search_dirs] 180*4d6fc14bSjoerg 181*4d6fc14bSjoerg if os.path.isdir(options.root): 182*4d6fc14bSjoerg roots = list_all_roots_under(options.root) 183*4d6fc14bSjoerg elif os.path.isfile(options.root): 184*4d6fc14bSjoerg roots = [options.root] 185*4d6fc14bSjoerg else: 186*4d6fc14bSjoerg raise RuntimeError('--root seems to be invalid') 187*4d6fc14bSjoerg 188*4d6fc14bSjoerg graph = build_graph(roots, options) 189*4d6fc14bSjoerg 190*4d6fc14bSjoerg # Eliminate certain kinds of "visual noise" headers, if asked for. 191*4d6fc14bSjoerg def should_keep(fname): 192*4d6fc14bSjoerg return all([ 193*4d6fc14bSjoerg options.show_config_headers or not is_config_header(fname), 194*4d6fc14bSjoerg options.show_experimental_headers or not is_experimental_header(fname), 195*4d6fc14bSjoerg options.show_support_headers or not is_support_header(fname), 196*4d6fc14bSjoerg ]) 197*4d6fc14bSjoerg 198*4d6fc14bSjoerg for fname in list(graph.keys()): 199*4d6fc14bSjoerg if should_keep(fname): 200*4d6fc14bSjoerg graph[fname].includes = [h for h in graph[fname].includes if should_keep(h)] 201*4d6fc14bSjoerg else: 202*4d6fc14bSjoerg del graph[fname] 203*4d6fc14bSjoerg 204*4d6fc14bSjoerg # Look for cycles. 205*4d6fc14bSjoerg no_cycles_detected = True 206*4d6fc14bSjoerg for fname, entry in graph.items(): 207*4d6fc14bSjoerg for h in entry.includes: 208*4d6fc14bSjoerg if transitively_includes(graph, h, fname): 209*4d6fc14bSjoerg sys.stderr.write('Cycle detected between %s and %s\n' % ( 210*4d6fc14bSjoerg get_friendly_id(fname), get_friendly_id(h) 211*4d6fc14bSjoerg )) 212*4d6fc14bSjoerg no_cycles_detected = False 213*4d6fc14bSjoerg assert no_cycles_detected 214*4d6fc14bSjoerg 215*4d6fc14bSjoerg print(get_graphviz(graph, options)) 216