import argparse
import io
import json
import logging
import os
import re
import sys
sys.path.insert(1, os.path.join(os.path.dirname(__file__), '..'))
import action_helpers
_REGEX = re.compile(
r'^(?:subninja (.*)|build ([^:]+): [^\s]+ (.*)| rlibs = (.*))',
re.MULTILINE)
_IGNORED_SUFFIXES = (
'.build_metadata',
'.empty',
'_expected_outputs.txt',
'_grd_files.json',
'.modulemap',
'.rsp',
'typemap_config',
)
_MAX_UNMATCHED_TO_LOG = 1000
_MAX_UNMATCHED_TO_IGNORE = 200
_MAX_RECURSION = 200
class _SourceMapper:
def __init__(self, dep_map, parsed_files):
self._dep_map = dep_map
self.parsed_files = parsed_files
self._unmatched_paths = set()
def Lookup(self, path):
ret = self._dep_map.get(path)
if not ret:
if not path.endswith(_IGNORED_SUFFIXES):
if path not in self._unmatched_paths:
if self.unmatched_paths_count < _MAX_UNMATCHED_TO_LOG:
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 _ParseNinjaPathList(path_list):
ret = path_list.replace('\\ ', '\b')
return [
os.path.normpath(s.replace('\b', ' ')) for s in ret.split() if s[0] != '|'
]
def _ParseOneFile(data, dep_map, executable_path):
sub_ninjas = []
prev_inputs = None
for m in _REGEX.finditer(data):
groups = m.groups()
if groups[0]:
sub_ninjas.append(groups[0])
elif groups[3]:
prev_inputs.extend(_ParseNinjaPathList(groups[3]))
else:
outputs = _ParseNinjaPathList(groups[1])
input_paths = _ParseNinjaPathList(groups[2])
for output in outputs:
assert output not in dep_map, f'Duplicate output: {output}'
dep_map[output] = input_paths
prev_inputs = input_paths
return sub_ninjas
def _Parse(output_directory, executable_path):
to_parse = ['build.ninja']
seen_paths = set(to_parse)
dep_map = {}
while to_parse:
path = os.path.join(output_directory, to_parse.pop())
with open(path, encoding='utf-8', errors='ignore') as obj:
data = obj.read()
sub_ninjas = _ParseOneFile(data, dep_map, executable_path)
for subpath in sub_ninjas:
assert subpath not in seen_paths, 'Double include of ' + subpath
seen_paths.add(subpath)
to_parse.extend(sub_ninjas)
assert executable_path in dep_map, ('Failed to find rule that builds ' +
executable_path)
return _SourceMapper(dep_map, seen_paths)
def _CollectInputs(source_mapper, ret, output_path, stack, visited):
token = len(visited)
if visited.setdefault(output_path, token) != token:
return
if token % 10000 == 0:
logging.info('Resolved %d', token)
if output_path.startswith('..') or output_path.endswith(('.so', '.so.TOC')):
ret.add(output_path)
return
inputs = source_mapper.Lookup(output_path)
if not inputs:
return
stack.append(output_path)
if len(stack) > _MAX_RECURSION:
print('Input loop!')
print('\n'.join(enumerate(stack)))
sys.exit(1)
for p in inputs:
_CollectInputs(source_mapper, ret, p, stack, visited)
stack.pop()
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--executable', required=True)
parser.add_argument('--result-json', required=True)
parser.add_argument('--depfile')
args = parser.parse_args()
logs_io = io.StringIO()
logging.basicConfig(level=logging.DEBUG,
format='%(levelname).1s %(relativeCreated)6d %(message)s',
stream=logs_io)
logging.info('Parsing build.ninja')
executable_path = os.path.relpath(args.executable)
source_mapper = _Parse('.', executable_path)
object_paths = source_mapper.Lookup(executable_path)
logging.info('Found %d linker inputs', len(object_paths))
source_paths = set()
stack = []
visited = {}
for p in object_paths:
_CollectInputs(source_mapper, source_paths, p, stack, visited)
logging.info('Found %d source paths', len(source_paths))
num_unmatched = source_mapper.unmatched_paths_count
logging.warning('%d paths were missing sources', num_unmatched)
if num_unmatched > _MAX_UNMATCHED_TO_IGNORE:
raise Exception(f'{num_unmatched} unmapped files (of {len(object_paths)}).'
' Likely a bug in ninja_parser.py')
if args.depfile:
action_helpers.write_depfile(args.depfile, args.result_json,
source_mapper.parsed_files)
logging.warning('Writing %s source paths', len(source_paths))
with open(args.result_json, 'w', encoding='utf-8') as f:
json.dump(
{
'logs': logs_io.getvalue(),
'source_paths': sorted(source_paths),
}, f)
if __name__ == '__main__':
main()