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