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