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