xref: /openbsd-src/gnu/llvm/llvm/utils/rsp_bisect.py (revision d415bd752c734aee168c4ee86ff32e8cc249eb16)
1#!/usr/bin/env python3
2#===----------------------------------------------------------------------===##
3#
4# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5# See https://llvm.org/LICENSE.txt for license information.
6# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7#
8#===----------------------------------------------------------------------===##
9"""Script to bisect over files in an rsp file.
10
11This is mostly used for detecting which file contains a miscompile between two
12compiler revisions. It does this by bisecting over an rsp file. Between two
13build directories, this script will make the rsp file reference the current
14build directory's version of some set of the rsp's object files/libraries, and
15reference the other build directory's version of the same files for the
16remaining set of object files/libraries.
17
18Build the target in two separate directories with the two compiler revisions,
19keeping the rsp file around since ninja by default deletes the rsp file after
20building.
21$ ninja -d keeprsp mytarget
22
23Create a script to build the target and run an interesting test. Get the
24command to build the target via
25$ ninja -t commands | grep mytarget
26The command to build the target should reference the rsp file.
27This script doesn't care if the test script returns 0 or 1 for specifically the
28successful or failing test, just that the test script returns a different
29return code for success vs failure.
30Since the command that `ninja -t commands` is run from the build directory,
31usually the test script cd's to the build directory.
32
33$ rsp_bisect.py --test=path/to/test_script --rsp=path/to/build/target.rsp
34    --other_rel_path=../Other
35where --other_rel_path is the relative path from the first build directory to
36the other build directory. This is prepended to files in the rsp.
37
38
39For a full example, if the foo target is suspected to contain a miscompile in
40some file, have two different build directories, buildgood/ and buildbad/ and
41run
42$ ninja -d keeprsp foo
43in both so we have two versions of all relevant object files that may contain a
44miscompile, one built by a good compiler and one by a bad compiler.
45
46In buildgood/, run
47$ ninja -t commands | grep '-o .*foo'
48to get the command to link the files together. It may look something like
49  clang -o foo @foo.rsp
50
51Now create a test script that runs the link step and whatever test reproduces a
52miscompile and returns a non-zero exit code when there is a miscompile. For
53example
54```
55  #!/bin/bash
56  # immediately bail out of script if any command returns a non-zero return code
57  set -e
58  clang -o foo @foo.rsp
59  ./foo
60```
61
62With buildgood/ as the working directory, run
63$ path/to/llvm-project/llvm/utils/rsp_bisect.py \
64    --test=path/to/test_script --rsp=./foo.rsp --other_rel_path=../buildbad/
65If rsp_bisect is successful, it will print the first file in the rsp file that
66when using the bad build directory's version causes the test script to return a
67different return code. foo.rsp.0 and foo.rsp.1 will also be written. foo.rsp.0
68will be a copy of foo.rsp with the relevant file using the version in
69buildgood/, and foo.rsp.1 will be a copy of foo.rsp with the relevant file
70using the version in buildbad/.
71
72"""
73
74import argparse
75import os
76import subprocess
77import sys
78
79
80def is_path(s):
81  return '/' in s
82
83
84def run_test(test):
85  """Runs the test and returns whether it was successful or not."""
86  return subprocess.run([test], capture_output=True).returncode == 0
87
88
89def modify_rsp(rsp_entries, other_rel_path, modify_after_num):
90  """Create a modified rsp file for use in bisection.
91
92  Returns a new list from rsp.
93  For each file in rsp after the first modify_after_num files, prepend
94  other_rel_path.
95  """
96  ret = []
97  for r in rsp_entries:
98    if is_path(r):
99      if modify_after_num == 0:
100        r = os.path.join(other_rel_path, r)
101      else:
102        modify_after_num -= 1
103    ret.append(r)
104  assert modify_after_num == 0
105  return ret
106
107
108def test_modified_rsp(test, modified_rsp_entries, rsp_path):
109  """Write the rsp file to disk and run the test."""
110  with open(rsp_path, 'w') as f:
111    f.write(' '.join(modified_rsp_entries))
112  return run_test(test)
113
114
115def bisect(test, zero_result, rsp_entries, num_files_in_rsp, other_rel_path, rsp_path):
116  """Bisect over rsp entries.
117
118  Args:
119      zero_result: the test result when modify_after_num is 0.
120
121  Returns:
122      The index of the file in the rsp file where the test result changes.
123  """
124  lower = 0
125  upper = num_files_in_rsp
126  while lower != upper - 1:
127    assert lower < upper - 1
128    mid = int((lower + upper) / 2)
129    assert lower != mid and mid != upper
130    print('Trying {} ({}-{})'.format(mid, lower, upper))
131    result = test_modified_rsp(test, modify_rsp(rsp_entries, other_rel_path, mid),
132                               rsp_path)
133    if zero_result == result:
134      lower = mid
135    else:
136      upper = mid
137  return upper
138
139
140def main():
141  parser = argparse.ArgumentParser()
142  parser.add_argument('--test',
143                      help='Binary to test if current setup is good or bad',
144                      required=True)
145  parser.add_argument('--rsp', help='rsp file', required=True)
146  parser.add_argument(
147      '--other-rel-path',
148      help='Relative path from current build directory to other build ' +
149      'directory, e.g. from "out/Default" to "out/Other" specify "../Other"',
150      required=True)
151  args = parser.parse_args()
152
153  with open(args.rsp, 'r') as f:
154    rsp_entries = f.read()
155  rsp_entries = rsp_entries.split()
156  num_files_in_rsp = sum(1 for a in rsp_entries if is_path(a))
157  if num_files_in_rsp == 0:
158    print('No files in rsp?')
159    return 1
160  print('{} files in rsp'.format(num_files_in_rsp))
161
162  try:
163    print('Initial testing')
164    test0 = test_modified_rsp(args.test, modify_rsp(rsp_entries, args.other_rel_path,
165                                                    0), args.rsp)
166    test_all = test_modified_rsp(
167        args.test, modify_rsp(rsp_entries, args.other_rel_path, num_files_in_rsp),
168        args.rsp)
169
170    if test0 == test_all:
171      print('Test returned same exit code for both build directories')
172      return 1
173
174    print('First build directory returned ' + ('0' if test_all else '1'))
175
176    result = bisect(args.test, test0, rsp_entries, num_files_in_rsp,
177                    args.other_rel_path, args.rsp)
178    print('First file change: {} ({})'.format(
179        list(filter(is_path, rsp_entries))[result - 1], result))
180
181    rsp_out_0 = args.rsp + '.0'
182    rsp_out_1 = args.rsp + '.1'
183    with open(rsp_out_0, 'w') as f:
184      f.write(' '.join(modify_rsp(rsp_entries, args.other_rel_path, result - 1)))
185    with open(rsp_out_1, 'w') as f:
186      f.write(' '.join(modify_rsp(rsp_entries, args.other_rel_path, result)))
187    print('Bisection point rsp files written to {} and {}'.format(
188        rsp_out_0, rsp_out_1))
189  finally:
190    # Always make sure to write the original rsp file contents back so it's
191    # less of a pain to rerun this script.
192    with open(args.rsp, 'w') as f:
193      f.write(' '.join(rsp_entries))
194
195
196if __name__ == '__main__':
197  sys.exit(main())
198