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