| #!/usr/bin/env python |
| # Copyright 2017 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Extract source file information from .ninja files.""" |
| |
| from __future__ import print_function |
| |
| import argparse |
| import logging |
| import os |
| import re |
| |
| |
| # E.g.: |
| # build obj/.../foo.o: cxx gen/.../foo.cc || obj/.../foo.inputdeps.stamp |
| # build obj/.../libfoo.a: alink obj/.../a.o obj/.../b.o | |
| # build ./libchrome.so ./lib.unstripped/libchrome.so: solink a.o b.o ... |
| _REGEX = re.compile(r'build ([^:]+): \w+ (.*?)(?: \||\n|$)') |
| |
| |
| class _SourceMapper(object): |
| def __init__(self, dep_map, parsed_file_count): |
| self._dep_map = dep_map |
| self.parsed_file_count = parsed_file_count |
| self._unmatched_paths = set() |
| |
| def _FindSourceForPathInternal(self, path): |
| if not path.endswith(')'): |
| return self._dep_map.get(path) |
| |
| # foo/bar.a(baz.o) |
| start_idx = path.index('(') |
| lib_name = path[:start_idx] |
| obj_name = path[start_idx + 1:-1] |
| by_basename = self._dep_map.get(lib_name) |
| if not by_basename: |
| return None |
| obj_path = by_basename.get(obj_name) |
| if not obj_path: |
| # Found the library, but it doesn't list the .o file. |
| logging.warning('no obj basename for %s', path) |
| return None |
| return self._dep_map.get(obj_path) |
| |
| def FindSourceForPath(self, path): |
| """Returns the source path for the given object path (or None if not found). |
| |
| Paths for objects within archives should be in the format: foo/bar.a(baz.o) |
| """ |
| ret = self._FindSourceForPathInternal(path) |
| if not ret and path not in self._unmatched_paths: |
| if self.unmatched_paths_count < 10: |
| logging.warning('Could not find source path for %s', path) |
| self._unmatched_paths.add(path) |
| return ret |
| |
| @property |
| def unmatched_paths_count(self): |
| return len(self._unmatched_paths) |
| |
| def IterAllPaths(self): |
| return self._dep_map.iterkeys() |
| |
| |
| def _ParseNinjaPathList(path_list): |
| ret = path_list.replace('\\ ', '\b') |
| return [s.replace('\b', ' ') for s in ret.split(' ')] |
| |
| |
| def _ParseOneFile(lines, dep_map, elf_path): |
| sub_ninjas = [] |
| elf_inputs = None |
| for line in lines: |
| if line.startswith('subninja '): |
| sub_ninjas.append(line[9:-1]) |
| continue |
| m = _REGEX.match(line) |
| if m: |
| outputs, srcs = m.groups() |
| if len(outputs) > 2 and outputs[-2] == '.' and outputs[-1] in 'ao': |
| output = outputs.replace('\\ ', ' ') |
| assert output not in dep_map, 'Duplicate output: ' + output |
| if output[-1] == 'o': |
| dep_map[output] = srcs.replace('\\ ', ' ') |
| else: |
| obj_paths = _ParseNinjaPathList(srcs) |
| dep_map[output] = {os.path.basename(p): p for p in obj_paths} |
| elif elf_path and elf_path in outputs: |
| properly_parsed = [ |
| os.path.normpath(p) for p in _ParseNinjaPathList(outputs)] |
| if elf_path in properly_parsed: |
| elf_inputs = _ParseNinjaPathList(srcs) |
| return sub_ninjas, elf_inputs |
| |
| |
| def Parse(output_directory, elf_path): |
| """Parses build.ninja and subninjas. |
| |
| Args: |
| output_directory: Where to find the root build.ninja. |
| elf_path: Path to elf file to find inputs for. |
| |
| Returns: A tuple of (source_mapper, elf_inputs). |
| """ |
| if elf_path: |
| elf_path = os.path.relpath(elf_path, output_directory) |
| to_parse = ['build.ninja'] |
| seen_paths = set(to_parse) |
| dep_map = {} |
| elf_inputs = None |
| while to_parse: |
| path = os.path.join(output_directory, to_parse.pop()) |
| with open(path) as obj: |
| sub_ninjas, found_elf_inputs = _ParseOneFile(obj, dep_map, elf_path) |
| if found_elf_inputs: |
| assert not elf_inputs, 'Found multiple inputs for elf_path ' + elf_path |
| elf_inputs = found_elf_inputs |
| for subpath in sub_ninjas: |
| assert subpath not in seen_paths, 'Double include of ' + subpath |
| seen_paths.add(subpath) |
| to_parse.extend(sub_ninjas) |
| |
| return _SourceMapper(dep_map, len(seen_paths)), elf_inputs |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--output-directory', required=True) |
| parser.add_argument('--elf-path') |
| parser.add_argument('--show-inputs', action='store_true') |
| parser.add_argument('--show-mappings', action='store_true') |
| args = parser.parse_args() |
| logging.basicConfig(level=logging.DEBUG, |
| format='%(levelname).1s %(relativeCreated)6d %(message)s') |
| |
| source_mapper, elf_inputs = Parse(args.output_directory, args.elf_path) |
| if not elf_inputs: |
| elf_inputs = [] |
| |
| print('Found {} elf_inputs, and {} source mappings'.format( |
| len(elf_inputs), len(source_mapper._dep_map))) |
| if args.show_inputs: |
| print('elf_inputs:') |
| print('\n'.join(elf_inputs)) |
| if args.show_mappings: |
| print('object_path -> source_path:') |
| for path in source_mapper.IterAllPaths(): |
| print('{} -> {}'.format(path, source_mapper.FindSourceForPath(path))) |
| |
| |
| if __name__ == '__main__': |
| main() |