Skip to content

Commit ddb6c2b

Browse files
committed
[libc++] Rewrites graph_header_deps.py.
The new version is a lot simpler and has less option which were not used. This uses the CSV files as generated by D133127 as input data. The current Python script has more features but uses a simple "grep" making the output less accurate: - Conditionally included header are always included. This is an issue since part of our includes are unneeded transitive includes. Based on the language version they may be omitted. The script however always includes them. - Includes in comments are processed as-if they are includes. This is an issue when comments explain how certain data is generated; of course there are digraphs which the script omits. This implementation uses Clang's --trace-includes to generate the includes per header. This means the input of the generation script always has the real list of includes. Libc++ is moving from large monolithic Standard headers to more fine grained headers. For example, algorithm includes every header in `__algorithm`. Adding all these detail headers in the graph makes the output unusable. Instead it only shows the Standard headers. The transitive includes of the detail headers are parsed and "attributed" to the Standard header including them. This gives an accurate include graph without the unneeded clutter. Note that this graph is still big. This changes fixes the cyclic dependency issue with the previous version of the tool so the markers and its documentation is removed. Since the input has no cycles the CI test is removed. Reviewed By: #libc, ldionne Differential Revision: https://reviews.llvm.org/D134188
1 parent dcc756d commit ddb6c2b

File tree

9 files changed

+34
-235
lines changed

9 files changed

+34
-235
lines changed

libcxx/docs/DesignDocs/HeaderRemovalPolicy.rst

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,6 @@ include transitive headers, regardless of the language version. This can be
4949
useful for users to aid the transition to a newer language version, or by users
5050
who simply want to make sure they include what they use in their code.
5151

52-
One of the issues for libc++ with transitive includes is that these includes
53-
may create dependency cycles and cause the validation script
54-
``libcxx/utils/graph_header_deps.py`` to have false positives. To ignore an
55-
include from the validation script, add a comment containing ``IGNORE-CYCLE``.
56-
This should only be used when there is a cycle and it has been verified it is a
57-
false positive.
58-
59-
.. code-block:: cpp
60-
61-
#if !defined(_LIBCPP_REMOVE_TRANSITIVE_INCLUDES) && _LIBCPP_STD_VER <= 17
62-
# include <chrono> // IGNORE-CYCLE due to <format>
63-
#endif
64-
6552

6653
Rationale
6754
---------

libcxx/include/algorithm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1910,7 +1910,7 @@ template <class BidirectionalIterator, class Compare>
19101910
#endif
19111911

19121912
#if !defined(_LIBCPP_REMOVE_TRANSITIVE_INCLUDES) && _LIBCPP_STD_VER <= 17
1913-
# include <chrono> // IGNORE-CYCLE due to <format>
1913+
# include <chrono>
19141914
#endif
19151915

19161916
#if !defined(_LIBCPP_REMOVE_TRANSITIVE_INCLUDES) && _LIBCPP_STD_VER <= 20

libcxx/include/any

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,7 @@ any_cast(any * __any) _NOEXCEPT
695695
_LIBCPP_END_NAMESPACE_STD
696696

697697
#if !defined(_LIBCPP_REMOVE_TRANSITIVE_INCLUDES) && _LIBCPP_STD_VER <= 17
698-
# include <chrono> // IGNORE-CYCLE due to <format>
698+
# include <chrono>
699699
#endif
700700

701701
#if !defined(_LIBCPP_REMOVE_TRANSITIVE_INCLUDES) && _LIBCPP_STD_VER <= 20

libcxx/include/atomic

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2656,7 +2656,7 @@ typedef atomic<__libcpp_unsigned_lock_free> atomic_unsigned_lock_free;
26562656
_LIBCPP_END_NAMESPACE_STD
26572657

26582658
#if !defined(_LIBCPP_REMOVE_TRANSITIVE_INCLUDES) && _LIBCPP_STD_VER <= 17
2659-
# include <chrono> // IGNORE-CYCLE due to <format>
2659+
# include <chrono>
26602660
#endif
26612661

26622662
#if !defined(_LIBCPP_REMOVE_TRANSITIVE_INCLUDES) && _LIBCPP_STD_VER <= 20

libcxx/include/future

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2433,7 +2433,7 @@ future<void>::share() _NOEXCEPT
24332433
_LIBCPP_END_NAMESPACE_STD
24342434

24352435
#if !defined(_LIBCPP_REMOVE_TRANSITIVE_INCLUDES) && _LIBCPP_STD_VER <= 17
2436-
# include <chrono> // IGNORE-CYCLE due to <format>
2436+
# include <chrono>
24372437
#endif
24382438

24392439
#endif // _LIBCPP_FUTURE

libcxx/include/optional

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1573,7 +1573,7 @@ _LIBCPP_END_NAMESPACE_STD
15731573
#endif // _LIBCPP_STD_VER > 14
15741574

15751575
#if !defined(_LIBCPP_REMOVE_TRANSITIVE_INCLUDES) && _LIBCPP_STD_VER <= 17
1576-
# include <chrono> // IGNORE-CYCLE due to <format>
1576+
# include <chrono>
15771577
#endif
15781578

15791579
#if !defined(_LIBCPP_REMOVE_TRANSITIVE_INCLUDES) && _LIBCPP_STD_VER <= 20

libcxx/include/thread

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ _LIBCPP_END_NAMESPACE_STD
409409
_LIBCPP_POP_MACROS
410410

411411
#if !defined(_LIBCPP_REMOVE_TRANSITIVE_INCLUDES) && _LIBCPP_STD_VER <= 17
412-
# include <chrono> // IGNORE-CYCLE due to <format>
412+
# include <chrono>
413413
#endif
414414

415415
#if !defined(_LIBCPP_REMOVE_TRANSITIVE_INCLUDES) && _LIBCPP_STD_VER <= 20

libcxx/utils/ci/run-buildbot

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,6 @@ check-generated-output)
198198

199199
# Reject code with trailing whitespace
200200
! grep -rn '[[:blank:]]$' libcxx/include libcxx/src libcxx/test libcxx/benchmarks || false
201-
202-
# Reject patches that introduce dependency cycles in the headers.
203-
python3 libcxx/utils/graph_header_deps.py >/dev/null
204201
;;
205202
generic-cxx03)
206203
clean

libcxx/utils/graph_header_deps.py

Lines changed: 28 additions & 213 deletions
Original file line numberDiff line numberDiff line change
@@ -1,232 +1,47 @@
11
#!/usr/bin/env python
2-
#===----------------------------------------------------------------------===##
2+
# ===----------------------------------------------------------------------===##
33
#
44
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
55
# See https://llvm.org/LICENSE.txt for license information.
66
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
77
#
8-
#===----------------------------------------------------------------------===##
8+
# ===----------------------------------------------------------------------===##
99

1010
import argparse
11-
import os
12-
import re
1311
import sys
1412

13+
if __name__ == "__main__":
14+
"""Converts a header dependency CSV file to Graphviz dot file.
1515
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+
"""
1819

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__':
16620
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+
""",
16925
formatter_class=argparse.RawDescriptionHelpFormatter,
17026
)
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+
)
18133
options = parser.parse_args()
18234

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
21544

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]}"')
23146

232-
print(get_graphviz(graph, options))
47+
print("}")

0 commit comments

Comments
 (0)