blob: 9742eae397ab4bcbc8dfff7ece6206f204771007 [file] [log] [blame]
#!/usr/bin/env python3.8
# Copyright 2021 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Determines the files that needed to be rebuilt by fx ninja.
The primary use of this script is to find which build targets were changed
by a particular CL or patch for use by the clang static analyzer.
The input will most likely be the compile_commands.json generated by either
an `fx set` or `fx gn gen --export-compile-commands=default <OUT_DIR>`.
The output is a json file containing the translation units (TUs) taken
from the input compile_commands.json of which the build targets were
modified by a fx ninja call.
The expectated workflow is:
1. Check out the base fuchsia commit against which to apply the patch.
1. Run `fx build`.
1. Apply the patch.
1. Run this script.
1. Run analyze-build against the output of this script.
"""
import argparse
import json
import os
import subprocess
import sys
import tempfile
import time
import helper as color
def main(input_args):
parser = argparse.ArgumentParser(
description=
'Runs `fx ninja build` on a set of targets for compile translation units '
'and determines which files were modified since that change.')
parser.add_argument(
'-i',
'--input',
required=True,
help='The path of a compile_commands.json generated '
'by either `fx set` or `fx gn gen '
'--export-compile-commands=default <OUT_DIR>`')
parser.add_argument(
'-o',
'--output',
required=True,
help=
'The path to write the output file of an array of paths for modified files in JSON format.'
)
parser.add_argument(
'-n',
'--ninja',
required=True,
help=
'Path to the prebuilt ninja compiler.'
)
args = parser.parse_args(input_args)
with open(args.input) as compile_commands:
res = ninja_build_tu(json.load(compile_commands), args.ninja)
if not res:
print(color.white('Did not find any changed files.'))
with open(args.output, 'w') as out:
json.dump(res, out, indent=2)
print(color.white('Wrote to output file'), color.yellow(args.output))
return 0
def ninja_build_tu(compdb, ninja_path):
"""Find build targets that were modified since last build.
Attemps to build all build targets specified, returning the subset
of those that require a rebuild since the last build. Leverages `fx
ninja` in order to determine which build targets needed rebuilding.
Args:
compdb: A json-style list containing dictionary-like entries
for each translation unit.
ninja_path: Path to the ninja executable.
Returns:
A subset of the input compdb, only keeping the translation units
that correspond to build targets that had been modified by the
`ninja` call.
"""
files = {}
tus = {}
# Find all TU targets
for tu in compdb:
directory = tu['directory']
command = tu['command']
command_entries = command.split()
# Gather all targets in this TU
for i in range(0, len(command_entries) - 1):
if command_entries[i] != '-o':
continue
target = command_entries[i + 1]
if directory not in files:
files[directory] = []
files[directory].append(target)
# Store TU for reverse lookup later
target_path = os.path.join(directory, target)
if target_path not in tus:
tus[target_path] = []
tus[target_path].append(tu)
modified_files = [process_tu_dir_xargs(directory, files[directory],
ninja_path) for directory in files]
# Find all TUs that match modified files
out_tus = []
for files in modified_files:
for target in files:
if target in tus:
out_tus.extend(tus[target])
else:
sys.exit('Expected %s to exist in reverse lookup dict.'%(target))
return out_tus
def process_tu_dir_xargs(directory, build_targets, ninja_path):
"""Find build targets that were rebuilt.
Takes an input directory (corresponding to the -C option in fx ninja) and
a list of build targets. Based on system modified time, returns the list
of build targets that have a new modified time.
Args:
directory: The string path name correpsonding to the build_targets.
build_targets: A list of build targets to attempt to build.
ninja_path: Path to the ninja executable.
Returns:
The subset of build_targets which were modified by `ninja`.
"""
# Write targets to a temp file
with tempfile.NamedTemporaryFile(mode='w') as fp:
for f in build_targets:
fp.write('%s\n' % (f))
fp.flush()
filename = os.path.realpath(fp.name)
# The argument list may exceed the input buffer, so we use xargs to
# call fx ninja multiple times, each with a subset of the arguments
# commands = ['xargs', '-a', filename, 'fx', 'ninja', '-C', directory]
commands = ['xargs', '-a', filename, ninja_path, '-C', directory]
# Record the time before running fx ninja
before = time.time()
print(color.white('Running command'), color.yellow(' '.join(commands)))
try:
subprocess.run(commands, capture_output=True, check=True)
except subprocess.CalledProcessError as err:
print(color.red('Failed with exit code'), color.white(err.returncode))
print(color.red(err.stderr))
sys.exit('Error in fx ninja.')
# Find all files that have a newer modified time than `before`
modified_targets = []
for file in build_targets:
file_path = os.path.join(directory, file)
if not os.path.isfile(file_path):
continue
mtime = os.path.getmtime(file_path)
if mtime > before:
modified_targets.append(file_path)
return modified_targets
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))