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"""Find header files missing in GN. 7 8This script gets all the header files from ninja_deps, which is from the true 9dependency generated by the compiler, and report if they don't exist in GN. 10""" 11 12import argparse 13import json 14import os 15import re 16import shutil 17import subprocess 18import sys 19import tempfile 20from multiprocessing import Process, Queue 21 22SRC_DIR = os.path.abspath( 23 os.path.join(os.path.abspath(os.path.dirname(__file__)), os.path.pardir)) 24DEPOT_TOOLS_DIR = os.path.join(SRC_DIR, 'third_party', 'depot_tools') 25 26 27def GetHeadersFromNinja(out_dir, skip_obj, q): 28 """Return all the header files from ninja_deps""" 29 30 def NinjaSource(): 31 cmd = [ 32 os.path.join(SRC_DIR, 'third_party', 'ninja', 'ninja'), '-C', out_dir, 33 '-t', 'deps' 34 ] 35 # A negative bufsize means to use the system default, which usually 36 # means fully buffered. 37 popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, bufsize=-1) 38 for line in iter(popen.stdout.readline, ''): 39 yield line.rstrip() 40 41 popen.stdout.close() 42 return_code = popen.wait() 43 if return_code: 44 raise subprocess.CalledProcessError(return_code, cmd) 45 46 ans, err = set(), None 47 try: 48 ans = ParseNinjaDepsOutput(NinjaSource(), out_dir, skip_obj) 49 except Exception as e: 50 err = str(e) 51 q.put((ans, err)) 52 53 54def ParseNinjaDepsOutput(ninja_out, out_dir, skip_obj): 55 """Parse ninja output and get the header files""" 56 all_headers = {} 57 58 # Ninja always uses "/", even on Windows. 59 prefix = '../../' 60 61 is_valid = False 62 obj_file = '' 63 for line in ninja_out: 64 if line.startswith(' '): 65 if not is_valid: 66 continue 67 if line.endswith('.h') or line.endswith('.hh'): 68 f = line.strip() 69 if f.startswith(prefix): 70 f = f[6:] # Remove the '../../' prefix 71 # build/ only contains build-specific files like build_config.h 72 # and buildflag.h, and system header files, so they should be 73 # skipped. 74 if f.startswith(out_dir) or f.startswith('out'): 75 continue 76 if not f.startswith('build'): 77 all_headers.setdefault(f, []) 78 if not skip_obj: 79 all_headers[f].append(obj_file) 80 else: 81 is_valid = line.endswith('(VALID)') 82 obj_file = line.split(':')[0] 83 84 return all_headers 85 86 87def GetHeadersFromGN(out_dir, q): 88 """Return all the header files from GN""" 89 90 tmp = None 91 ans, err = set(), None 92 try: 93 # Argument |dir| is needed to make sure it's on the same drive on Windows. 94 # dir='' means dir='.', but doesn't introduce an unneeded prefix. 95 tmp = tempfile.mkdtemp(dir='') 96 shutil.copy2(os.path.join(out_dir, 'args.gn'), 97 os.path.join(tmp, 'args.gn')) 98 # Do "gn gen" in a temp dir to prevent dirtying |out_dir|. 99 gn_exe = 'gn.bat' if sys.platform == 'win32' else 'gn' 100 subprocess.check_call([ 101 os.path.join(DEPOT_TOOLS_DIR, gn_exe), 'gen', tmp, '--ide=json', '-q']) 102 gn_json = json.load(open(os.path.join(tmp, 'project.json'))) 103 ans = ParseGNProjectJSON(gn_json, out_dir, tmp) 104 except Exception as e: 105 err = str(e) 106 finally: 107 if tmp: 108 shutil.rmtree(tmp) 109 q.put((ans, err)) 110 111 112def ParseGNProjectJSON(gn, out_dir, tmp_out): 113 """Parse GN output and get the header files""" 114 all_headers = set() 115 116 for _target, properties in gn['targets'].items(): 117 sources = properties.get('sources', []) 118 public = properties.get('public', []) 119 # Exclude '"public": "*"'. 120 if type(public) is list: 121 sources += public 122 for f in sources: 123 if f.endswith('.h') or f.endswith('.hh'): 124 if f.startswith('//'): 125 f = f[2:] # Strip the '//' prefix. 126 if f.startswith(tmp_out): 127 f = out_dir + f[len(tmp_out):] 128 all_headers.add(f) 129 130 return all_headers 131 132 133def GetDepsPrefixes(q): 134 """Return all the folders controlled by DEPS file""" 135 prefixes, err = set(), None 136 try: 137 gclient_exe = 'gclient.bat' if sys.platform == 'win32' else 'gclient' 138 gclient_out = subprocess.check_output([ 139 os.path.join(DEPOT_TOOLS_DIR, gclient_exe), 140 'recurse', '--no-progress', '-j1', 141 'python', '-c', 'import os;print os.environ["GCLIENT_DEP_PATH"]'], 142 universal_newlines=True) 143 for i in gclient_out.split('\n'): 144 if i.startswith('src/'): 145 i = i[4:] 146 prefixes.add(i) 147 except Exception as e: 148 err = str(e) 149 q.put((prefixes, err)) 150 151 152def IsBuildClean(out_dir): 153 cmd = [os.path.join(DEPOT_TOOLS_DIR, 'ninja'), '-C', out_dir, '-n'] 154 try: 155 out = subprocess.check_output(cmd) 156 return 'no work to do.' in out 157 except Exception as e: 158 print(e) 159 return False 160 161def ParseWhiteList(whitelist): 162 out = set() 163 for line in whitelist.split('\n'): 164 line = re.sub(r'#.*', '', line).strip() 165 if line: 166 out.add(line) 167 return out 168 169 170def FilterOutDepsedRepo(files, deps): 171 return {f for f in files if not any(f.startswith(d) for d in deps)} 172 173 174def GetNonExistingFiles(lst): 175 out = set() 176 for f in lst: 177 if not os.path.isfile(f): 178 out.add(f) 179 return out 180 181 182def main(): 183 184 def DumpJson(data): 185 if args.json: 186 with open(args.json, 'w') as f: 187 json.dump(data, f) 188 189 def PrintError(msg): 190 DumpJson([]) 191 parser.error(msg) 192 193 parser = argparse.ArgumentParser(description=''' 194 NOTE: Use ninja to build all targets in OUT_DIR before running 195 this script.''') 196 parser.add_argument('--out-dir', metavar='OUT_DIR', default='out/Release', 197 help='output directory of the build') 198 parser.add_argument('--json', 199 help='JSON output filename for missing headers') 200 parser.add_argument('--whitelist', help='file containing whitelist') 201 parser.add_argument('--skip-dirty-check', action='store_true', 202 help='skip checking whether the build is dirty') 203 parser.add_argument('--verbose', action='store_true', 204 help='print more diagnostic info') 205 206 args, _extras = parser.parse_known_args() 207 208 if not os.path.isdir(args.out_dir): 209 parser.error('OUT_DIR "%s" does not exist.' % args.out_dir) 210 211 if not args.skip_dirty_check and not IsBuildClean(args.out_dir): 212 dirty_msg = 'OUT_DIR looks dirty. You need to build all there.' 213 if args.json: 214 # Assume running on the bots. Silently skip this step. 215 # This is possible because "analyze" step can be wrong due to 216 # underspecified header files. See crbug.com/725877 217 print(dirty_msg) 218 DumpJson([]) 219 return 0 220 else: 221 # Assume running interactively. 222 parser.error(dirty_msg) 223 224 d_q = Queue() 225 d_p = Process(target=GetHeadersFromNinja, args=(args.out_dir, True, d_q,)) 226 d_p.start() 227 228 gn_q = Queue() 229 gn_p = Process(target=GetHeadersFromGN, args=(args.out_dir, gn_q,)) 230 gn_p.start() 231 232 deps_q = Queue() 233 deps_p = Process(target=GetDepsPrefixes, args=(deps_q,)) 234 deps_p.start() 235 236 d, d_err = d_q.get() 237 gn, gn_err = gn_q.get() 238 missing = set(d.keys()) - gn 239 nonexisting = GetNonExistingFiles(gn) 240 241 deps, deps_err = deps_q.get() 242 missing = FilterOutDepsedRepo(missing, deps) 243 nonexisting = FilterOutDepsedRepo(nonexisting, deps) 244 245 d_p.join() 246 gn_p.join() 247 deps_p.join() 248 249 if d_err: 250 PrintError(d_err) 251 if gn_err: 252 PrintError(gn_err) 253 if deps_err: 254 PrintError(deps_err) 255 if len(GetNonExistingFiles(d)) > 0: 256 print('Non-existing files in ninja deps:', GetNonExistingFiles(d)) 257 PrintError('Found non-existing files in ninja deps. You should ' + 258 'build all in OUT_DIR.') 259 if len(d) == 0: 260 PrintError('OUT_DIR looks empty. You should build all there.') 261 if any((('/gen/' in i) for i in nonexisting)): 262 PrintError('OUT_DIR looks wrong. You should build all there.') 263 264 if args.whitelist: 265 whitelist = ParseWhiteList(open(args.whitelist).read()) 266 missing -= whitelist 267 nonexisting -= whitelist 268 269 missing = sorted(missing) 270 nonexisting = sorted(nonexisting) 271 272 DumpJson(sorted(missing + nonexisting)) 273 274 if len(missing) == 0 and len(nonexisting) == 0: 275 return 0 276 277 if len(missing) > 0: 278 print('\nThe following files should be included in gn files:') 279 for i in missing: 280 print(i) 281 282 if len(nonexisting) > 0: 283 print('\nThe following non-existing files should be removed from gn files:') 284 for i in nonexisting: 285 print(i) 286 287 if args.verbose: 288 # Only get detailed obj dependency here since it is slower. 289 GetHeadersFromNinja(args.out_dir, False, d_q) 290 d, d_err = d_q.get() 291 print('\nDetailed dependency info:') 292 for f in missing: 293 print(f) 294 for cc in d[f]: 295 print(' ', cc) 296 297 print('\nMissing headers sorted by number of affected object files:') 298 count = {k: len(v) for (k, v) in d.items()} 299 for f in sorted(count, key=count.get, reverse=True): 300 if f in missing: 301 print(count[f], f) 302 303 if args.json: 304 # Assume running on the bots. Temporarily return 0 before 305 # https://crbug.com/937847 is fixed. 306 return 0 307 return 1 308 309 310if __name__ == '__main__': 311 sys.exit(main()) 312