tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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()