|  | #!/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()) |