forked from OSchip/llvm-project
198 lines
6.9 KiB
Python
Executable File
198 lines
6.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#===----------------------------------------------------------------------===##
|
|
#
|
|
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
# See https://llvm.org/LICENSE.txt for license information.
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
#
|
|
#===----------------------------------------------------------------------===##
|
|
"""Script to bisect over files in an rsp file.
|
|
|
|
This is mostly used for detecting which file contains a miscompile between two
|
|
compiler revisions. It does this by bisecting over an rsp file. Between two
|
|
build directories, this script will make the rsp file reference the current
|
|
build directory's version of some set of the rsp's object files/libraries, and
|
|
reference the other build directory's version of the same files for the
|
|
remaining set of object files/libraries.
|
|
|
|
Build the target in two separate directories with the two compiler revisions,
|
|
keeping the rsp file around since ninja by default deletes the rsp file after
|
|
building.
|
|
$ ninja -d keeprsp mytarget
|
|
|
|
Create a script to build the target and run an interesting test. Get the
|
|
command to build the target via
|
|
$ ninja -t commands | grep mytarget
|
|
The command to build the target should reference the rsp file.
|
|
This script doesn't care if the test script returns 0 or 1 for specifically the
|
|
successful or failing test, just that the test script returns a different
|
|
return code for success vs failure.
|
|
Since the command that `ninja -t commands` is run from the build directory,
|
|
usually the test script cd's to the build directory.
|
|
|
|
$ rsp_bisect.py --test=path/to/test_script --rsp=path/to/build/target.rsp
|
|
--other_rel_path=../Other
|
|
where --other_rel_path is the relative path from the first build directory to
|
|
the other build directory. This is prepended to files in the rsp.
|
|
|
|
|
|
For a full example, if the foo target is suspected to contain a miscompile in
|
|
some file, have two different build directories, buildgood/ and buildbad/ and
|
|
run
|
|
$ ninja -d keeprsp foo
|
|
in both so we have two versions of all relevant object files that may contain a
|
|
miscompile, one built by a good compiler and one by a bad compiler.
|
|
|
|
In buildgood/, run
|
|
$ ninja -t commands | grep '-o .*foo'
|
|
to get the command to link the files together. It may look something like
|
|
clang -o foo @foo.rsp
|
|
|
|
Now create a test script that runs the link step and whatever test reproduces a
|
|
miscompile and returns a non-zero exit code when there is a miscompile. For
|
|
example
|
|
```
|
|
#!/bin/bash
|
|
# immediately bail out of script if any command returns a non-zero return code
|
|
set -e
|
|
clang -o foo @foo.rsp
|
|
./foo
|
|
```
|
|
|
|
With buildgood/ as the working directory, run
|
|
$ path/to/llvm-project/llvm/utils/rsp_bisect.py \
|
|
--test=path/to/test_script --rsp=./foo.rsp --other_rel_path=../buildbad/
|
|
If rsp_bisect is successful, it will print the first file in the rsp file that
|
|
when using the bad build directory's version causes the test script to return a
|
|
different return code. foo.rsp.0 and foo.rsp.1 will also be written. foo.rsp.0
|
|
will be a copy of foo.rsp with the relevant file using the version in
|
|
buildgood/, and foo.rsp.1 will be a copy of foo.rsp with the relevant file
|
|
using the version in buildbad/.
|
|
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
def is_path(s):
|
|
return '/' in s
|
|
|
|
|
|
def run_test(test):
|
|
"""Runs the test and returns whether it was successful or not."""
|
|
return subprocess.run([test], capture_output=True).returncode == 0
|
|
|
|
|
|
def modify_rsp(rsp_entries, other_rel_path, modify_after_num):
|
|
"""Create a modified rsp file for use in bisection.
|
|
|
|
Returns a new list from rsp.
|
|
For each file in rsp after the first modify_after_num files, prepend
|
|
other_rel_path.
|
|
"""
|
|
ret = []
|
|
for r in rsp_entries:
|
|
if is_path(r):
|
|
if modify_after_num == 0:
|
|
r = os.path.join(other_rel_path, r)
|
|
else:
|
|
modify_after_num -= 1
|
|
ret.append(r)
|
|
assert modify_after_num == 0
|
|
return ret
|
|
|
|
|
|
def test_modified_rsp(test, modified_rsp_entries, rsp_path):
|
|
"""Write the rsp file to disk and run the test."""
|
|
with open(rsp_path, 'w') as f:
|
|
f.write(' '.join(modified_rsp_entries))
|
|
return run_test(test)
|
|
|
|
|
|
def bisect(test, zero_result, rsp_entries, num_files_in_rsp, other_rel_path, rsp_path):
|
|
"""Bisect over rsp entries.
|
|
|
|
Args:
|
|
zero_result: the test result when modify_after_num is 0.
|
|
|
|
Returns:
|
|
The index of the file in the rsp file where the test result changes.
|
|
"""
|
|
lower = 0
|
|
upper = num_files_in_rsp
|
|
while lower != upper - 1:
|
|
assert lower < upper - 1
|
|
mid = int((lower + upper) / 2)
|
|
assert lower != mid and mid != upper
|
|
print('Trying {} ({}-{})'.format(mid, lower, upper))
|
|
result = test_modified_rsp(test, modify_rsp(rsp_entries, other_rel_path, mid),
|
|
rsp_path)
|
|
if zero_result == result:
|
|
lower = mid
|
|
else:
|
|
upper = mid
|
|
return upper
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--test',
|
|
help='Binary to test if current setup is good or bad',
|
|
required=True)
|
|
parser.add_argument('--rsp', help='rsp file', required=True)
|
|
parser.add_argument(
|
|
'--other-rel-path',
|
|
help='Relative path from current build directory to other build ' +
|
|
'directory, e.g. from "out/Default" to "out/Other" specify "../Other"',
|
|
required=True)
|
|
args = parser.parse_args()
|
|
|
|
with open(args.rsp, 'r') as f:
|
|
rsp_entries = f.read()
|
|
rsp_entries = rsp_entries.split()
|
|
num_files_in_rsp = sum(1 for a in rsp_entries if is_path(a))
|
|
if num_files_in_rsp == 0:
|
|
print('No files in rsp?')
|
|
return 1
|
|
print('{} files in rsp'.format(num_files_in_rsp))
|
|
|
|
try:
|
|
print('Initial testing')
|
|
test0 = test_modified_rsp(args.test, modify_rsp(rsp_entries, args.other_rel_path,
|
|
0), args.rsp)
|
|
test_all = test_modified_rsp(
|
|
args.test, modify_rsp(rsp_entries, args.other_rel_path, num_files_in_rsp),
|
|
args.rsp)
|
|
|
|
if test0 == test_all:
|
|
print('Test returned same exit code for both build directories')
|
|
return 1
|
|
|
|
print('First build directory returned ' + ('0' if test_all else '1'))
|
|
|
|
result = bisect(args.test, test0, rsp_entries, num_files_in_rsp,
|
|
args.other_rel_path, args.rsp)
|
|
print('First file change: {} ({})'.format(
|
|
list(filter(is_path, rsp_entries))[result - 1], result))
|
|
|
|
rsp_out_0 = args.rsp + '.0'
|
|
rsp_out_1 = args.rsp + '.1'
|
|
with open(rsp_out_0, 'w') as f:
|
|
f.write(' '.join(modify_rsp(rsp_entries, args.other_rel_path, result - 1)))
|
|
with open(rsp_out_1, 'w') as f:
|
|
f.write(' '.join(modify_rsp(rsp_entries, args.other_rel_path, result)))
|
|
print('Bisection point rsp files written to {} and {}'.format(
|
|
rsp_out_0, rsp_out_1))
|
|
finally:
|
|
# Always make sure to write the original rsp file contents back so it's
|
|
# less of a pain to rerun this script.
|
|
with open(args.rsp, 'w') as f:
|
|
f.write(' '.join(rsp_entries))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|