ninja_parser.py (7447B)
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 """Extract source file information from .ninja files.""" 6 7 # Copied from //tools/binary_size/libsupersize 8 9 import argparse 10 import io 11 import json 12 import logging 13 import os 14 import re 15 import sys 16 17 sys.path.insert(1, os.path.join(os.path.dirname(__file__), '..')) 18 import action_helpers 19 20 # E.g.: 21 # build obj/.../foo.o: cxx gen/.../foo.cc || obj/.../foo.inputdeps.stamp 22 # build obj/.../libfoo.a: alink obj/.../a.o obj/.../b.o | 23 # build ./libchrome.so ./lib.unstripped/libchrome.so: solink a.o b.o ... 24 # build libmonochrome.so: __chrome_android_libmonochrome___rule | ... 25 _REGEX = re.compile(r'build ([^:]+): \w+ (.*?)(?: *\||\n|$)') 26 27 _RLIBS_REGEX = re.compile(r' rlibs = (.*?)(?:\n|$)') 28 29 # Unmatches seems to happen for empty source_sets(). E.g.: 30 # obj/chrome/browser/ui/libui_public_dependencies.a 31 _MAX_UNMATCHED_TO_LOG = 20 32 _MAX_UNMATCHED_TO_IGNORE = 200 33 34 35 class _SourceMapper: 36 37 def __init__(self, dep_map, parsed_files): 38 self._dep_map = dep_map 39 self.parsed_files = parsed_files 40 self._unmatched_paths = set() 41 42 def _FindSourceForPathInternal(self, path): 43 if not path.endswith(')'): 44 if path.startswith('..'): 45 return path 46 return self._dep_map.get(path) 47 48 # foo/bar.a(baz.o) 49 start_idx = path.index('(') 50 lib_name = path[:start_idx] 51 obj_name = path[start_idx + 1:-1] 52 by_basename = self._dep_map.get(lib_name) 53 if not by_basename: 54 if lib_name.endswith('rlib') and 'std/' in lib_name: 55 # Currently we use binary prebuilt static libraries of the Rust 56 # stdlib so we can't get source paths. That may change in future. 57 return '(Rust stdlib)/%s' % lib_name 58 return None 59 if lib_name.endswith('.rlib'): 60 # Rust doesn't really have the concept of an object file because 61 # the compilation unit is the whole 'crate'. Return whichever 62 # filename was the crate root. 63 return next(iter(by_basename.values())) 64 obj_path = by_basename.get(obj_name) 65 if not obj_path: 66 # Found the library, but it doesn't list the .o file. 67 logging.warning('no obj basename for %s %s', path, obj_name) 68 return None 69 return self._dep_map.get(obj_path) 70 71 def FindSourceForPath(self, path): 72 """Returns the source path for the given object path (or None if not found). 73 74 Paths for objects within archives should be in the format: foo/bar.a(baz.o) 75 """ 76 ret = self._FindSourceForPathInternal(path) 77 if not ret and path not in self._unmatched_paths: 78 if self.unmatched_paths_count < _MAX_UNMATCHED_TO_LOG: 79 logging.warning('Could not find source path for %s (empty source_set?)', 80 path) 81 self._unmatched_paths.add(path) 82 return ret 83 84 @property 85 def unmatched_paths_count(self): 86 return len(self._unmatched_paths) 87 88 89 def _ParseNinjaPathList(path_list): 90 ret = path_list.replace('\\ ', '\b') 91 return [s.replace('\b', ' ') for s in ret.split()] 92 93 94 def _OutputsAreObject(outputs): 95 return (outputs.endswith('.a') or outputs.endswith('.o') 96 or outputs.endswith('.rlib')) 97 98 99 def _ParseOneFile(lines, dep_map, executable_path): 100 sub_ninjas = [] 101 executable_inputs = None 102 last_executable_paths = [] 103 for line in lines: 104 if line.startswith('subninja '): 105 sub_ninjas.append(line[9:-1]) 106 # Rust .rlibs are listed as implicit dependencies of the main 107 # target linking rule, then are given as an extra 108 # rlibs = 109 # variable on a subsequent line. Watch out for that line. 110 elif m := _RLIBS_REGEX.match(line): 111 if executable_path in last_executable_paths: 112 executable_inputs.extend(_ParseNinjaPathList(m.group(1))) 113 elif m := _REGEX.match(line): 114 outputs, srcs = m.groups() 115 if _OutputsAreObject(outputs): 116 output = outputs.replace('\\ ', ' ') 117 assert output not in dep_map, 'Duplicate output: ' + output 118 if output[-1] == 'o': 119 dep_map[output] = srcs.replace('\\ ', ' ') 120 else: 121 obj_paths = _ParseNinjaPathList(srcs) 122 dep_map[output] = {os.path.basename(p): p for p in obj_paths} 123 elif executable_path: 124 last_executable_paths = [ 125 os.path.normpath(p) for p in _ParseNinjaPathList(outputs) 126 ] 127 if executable_path in last_executable_paths: 128 executable_inputs = _ParseNinjaPathList(srcs) 129 130 return sub_ninjas, executable_inputs 131 132 133 def _Parse(output_directory, executable_path): 134 """Parses build.ninja and subninjas. 135 136 Args: 137 output_directory: Where to find the root build.ninja. 138 executable_path: Path to binary to find inputs for. 139 140 Returns: A tuple of (source_mapper, executable_inputs). 141 source_mapper: _SourceMapper instance. 142 executable_inputs: List of paths of linker inputs. 143 """ 144 if executable_path: 145 executable_path = os.path.relpath(executable_path, output_directory) 146 to_parse = ['build.ninja'] 147 seen_paths = set(to_parse) 148 dep_map = {} 149 executable_inputs = None 150 while to_parse: 151 path = os.path.join(output_directory, to_parse.pop()) 152 with open(path, encoding='utf-8', errors='ignore') as obj: 153 sub_ninjas, found_executable_inputs = _ParseOneFile( 154 obj, dep_map, executable_path) 155 if found_executable_inputs: 156 assert not executable_inputs, ( 157 'Found multiple inputs for executable_path ' + executable_path) 158 executable_inputs = found_executable_inputs 159 for subpath in sub_ninjas: 160 assert subpath not in seen_paths, 'Double include of ' + subpath 161 seen_paths.add(subpath) 162 to_parse.extend(sub_ninjas) 163 164 assert executable_inputs, 'Failed to find rule that builds ' + executable_path 165 return _SourceMapper(dep_map, seen_paths), executable_inputs 166 167 168 def main(): 169 parser = argparse.ArgumentParser() 170 parser.add_argument('--executable', required=True) 171 parser.add_argument('--result-json', required=True) 172 parser.add_argument('--depfile') 173 args = parser.parse_args() 174 logs_io = io.StringIO() 175 logging.basicConfig(level=logging.DEBUG, 176 format='%(levelname).1s %(relativeCreated)6d %(message)s', 177 stream=logs_io) 178 179 source_mapper, object_paths = _Parse('.', args.executable) 180 logging.info('Found %d linker inputs', len(object_paths)) 181 source_paths = [] 182 for obj_path in object_paths: 183 result = source_mapper.FindSourceForPath(obj_path) or obj_path 184 # Need to recurse on .a files. 185 if isinstance(result, dict): 186 source_paths.extend( 187 source_mapper.FindSourceForPath(v) or v for v in result.values()) 188 else: 189 source_paths.append(result) 190 logging.info('Found %d source paths', len(source_paths)) 191 192 num_unmatched = source_mapper.unmatched_paths_count 193 if num_unmatched > _MAX_UNMATCHED_TO_LOG: 194 logging.warning('%d paths were missing sources (showed the first %d)', 195 num_unmatched, _MAX_UNMATCHED_TO_LOG) 196 if num_unmatched > _MAX_UNMATCHED_TO_IGNORE: 197 raise Exception('Too many unmapped files. Likely a bug in ninja_parser.py') 198 199 if args.depfile: 200 action_helpers.write_depfile(args.depfile, args.result_json, 201 source_mapper.parsed_files) 202 203 with open(args.result_json, 'w', encoding='utf-8') as f: 204 json.dump({ 205 'logs': logs_io.getvalue(), 206 'source_paths': source_paths, 207 }, f) 208 209 210 if __name__ == '__main__': 211 main()