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