xref: /aosp_15_r20/external/cronet/build/fix_gn_headers.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1#!/usr/bin/env python3
2# Copyright 2017 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Fix header files missing in GN.
7
8This script takes the missing header files from check_gn_headers.py, and
9try to fix them by adding them to the GN files.
10Manual cleaning up is likely required afterwards.
11"""
12
13
14import argparse
15import os
16import re
17import subprocess
18import sys
19
20
21def GitGrep(pattern):
22  p = subprocess.Popen(
23      ['git', 'grep', '-En', pattern, '--', '*.gn', '*.gni'],
24      stdout=subprocess.PIPE)
25  out, _ = p.communicate()
26  return out, p.returncode
27
28
29def ValidMatches(basename, cc, grep_lines):
30  """Filter out 'git grep' matches with header files already."""
31  matches = []
32  for line in grep_lines:
33    gnfile, linenr, contents = line.split(':')
34    linenr = int(linenr)
35    new = re.sub(cc, basename, contents)
36    lines = open(gnfile).read().splitlines()
37    assert contents in lines[linenr - 1]
38    # Skip if it's already there. It could be before or after the match.
39    if lines[linenr] == new:
40      continue
41    if lines[linenr - 2] == new:
42      continue
43    print('    ', gnfile, linenr, new)
44    matches.append((gnfile, linenr, new))
45  return matches
46
47
48def AddHeadersNextToCC(headers, skip_ambiguous=True):
49  """Add header files next to the corresponding .cc files in GN files.
50
51  When skip_ambiguous is True, skip if multiple .cc files are found.
52  Returns unhandled headers.
53
54  Manual cleaning up is likely required, especially if not skip_ambiguous.
55  """
56  edits = {}
57  unhandled = []
58  for filename in headers:
59    filename = filename.strip()
60    if not (filename.endswith('.h') or filename.endswith('.hh')):
61      continue
62    basename = os.path.basename(filename)
63    print(filename)
64    cc = r'\b' + os.path.splitext(basename)[0] + r'\.(cc|cpp|mm)\b'
65    out, returncode = GitGrep('(/|")' + cc + '"')
66    if returncode != 0 or not out:
67      unhandled.append(filename)
68      continue
69
70    matches = ValidMatches(basename, cc, out.splitlines())
71
72    if len(matches) == 0:
73      continue
74    if len(matches) > 1:
75      print('\n[WARNING] Ambiguous matching for', filename)
76      for i in enumerate(matches, 1):
77        print('%d: %s' % (i[0], i[1]))
78      print()
79      if skip_ambiguous:
80        continue
81
82      picked = raw_input('Pick the matches ("2,3" for multiple): ')
83      try:
84        matches = [matches[int(i) - 1] for i in picked.split(',')]
85      except (ValueError, IndexError):
86        continue
87
88    for match in matches:
89      gnfile, linenr, new = match
90      print('  ', gnfile, linenr, new)
91      edits.setdefault(gnfile, {})[linenr] = new
92
93  for gnfile in edits:
94    lines = open(gnfile).read().splitlines()
95    for l in sorted(edits[gnfile].keys(), reverse=True):
96      lines.insert(l, edits[gnfile][l])
97    open(gnfile, 'w').write('\n'.join(lines) + '\n')
98
99  return unhandled
100
101
102def AddHeadersToSources(headers, skip_ambiguous=True):
103  """Add header files to the sources list in the first GN file.
104
105  The target GN file is the first one up the parent directories.
106  This usually does the wrong thing for _test files if the test and the main
107  target are in the same .gn file.
108  When skip_ambiguous is True, skip if multiple sources arrays are found.
109
110  "git cl format" afterwards is required. Manually cleaning up duplicated items
111  is likely required.
112  """
113  for filename in headers:
114    filename = filename.strip()
115    print(filename)
116    dirname = os.path.dirname(filename)
117    while not os.path.exists(os.path.join(dirname, 'BUILD.gn')):
118      dirname = os.path.dirname(dirname)
119    rel = filename[len(dirname) + 1:]
120    gnfile = os.path.join(dirname, 'BUILD.gn')
121
122    lines = open(gnfile).read().splitlines()
123    matched = [i for i, l in enumerate(lines) if ' sources = [' in l]
124    if skip_ambiguous and len(matched) > 1:
125      print('[WARNING] Multiple sources in', gnfile)
126      continue
127
128    if len(matched) < 1:
129      continue
130    print('  ', gnfile, rel)
131    index = matched[0]
132    lines.insert(index + 1, '"%s",' % rel)
133    open(gnfile, 'w').write('\n'.join(lines) + '\n')
134
135
136def RemoveHeader(headers, skip_ambiguous=True):
137  """Remove non-existing headers in GN files.
138
139  When skip_ambiguous is True, skip if multiple matches are found.
140  """
141  edits = {}
142  unhandled = []
143  for filename in headers:
144    filename = filename.strip()
145    if not (filename.endswith('.h') or filename.endswith('.hh')):
146      continue
147    basename = os.path.basename(filename)
148    print(filename)
149    out, returncode = GitGrep('(/|")' + basename + '"')
150    if returncode != 0 or not out:
151      unhandled.append(filename)
152      print('  Not found')
153      continue
154
155    grep_lines = out.splitlines()
156    matches = []
157    for line in grep_lines:
158      gnfile, linenr, contents = line.split(':')
159      print('    ', gnfile, linenr, contents)
160      linenr = int(linenr)
161      lines = open(gnfile).read().splitlines()
162      assert contents in lines[linenr - 1]
163      matches.append((gnfile, linenr, contents))
164
165    if len(matches) == 0:
166      continue
167    if len(matches) > 1:
168      print('\n[WARNING] Ambiguous matching for', filename)
169      for i in enumerate(matches, 1):
170        print('%d: %s' % (i[0], i[1]))
171      print()
172      if skip_ambiguous:
173        continue
174
175      picked = raw_input('Pick the matches ("2,3" for multiple): ')
176      try:
177        matches = [matches[int(i) - 1] for i in picked.split(',')]
178      except (ValueError, IndexError):
179        continue
180
181    for match in matches:
182      gnfile, linenr, contents = match
183      print('  ', gnfile, linenr, contents)
184      edits.setdefault(gnfile, set()).add(linenr)
185
186  for gnfile in edits:
187    lines = open(gnfile).read().splitlines()
188    for l in sorted(edits[gnfile], reverse=True):
189      lines.pop(l - 1)
190    open(gnfile, 'w').write('\n'.join(lines) + '\n')
191
192  return unhandled
193
194
195def main():
196  parser = argparse.ArgumentParser()
197  parser.add_argument('input_file', help="missing or non-existing headers, "
198                      "output of check_gn_headers.py")
199  parser.add_argument('--prefix',
200                      help="only handle path name with this prefix")
201  parser.add_argument('--remove', action='store_true',
202                      help="treat input_file as non-existing headers")
203
204  args, _extras = parser.parse_known_args()
205
206  headers = open(args.input_file).readlines()
207
208  if args.prefix:
209    headers = [i for i in headers if i.startswith(args.prefix)]
210
211  if args.remove:
212    RemoveHeader(headers, False)
213  else:
214    unhandled = AddHeadersNextToCC(headers)
215    AddHeadersToSources(unhandled)
216
217
218if __name__ == '__main__':
219  sys.exit(main())
220