cb424d7448
verify-patch-sanity.py validates every active recipe .patch has internally- consistent hunk line counts — catching the 'malformed patch at line N' failure at commit/CI/preflight time instead of hours into a cook. This cycle hit that class three times (qtwaylandscanner, sddm, xwayland), each only discovered when cookbook tried to apply the patch. Running it across the repo found 29 latent malformed patches (validated against GNU patch: e.g. relibc/P3-sysv-ipc reproduces 'malformed patch at line 22'). They were harmless only because they sit in vendored recipes (baked, not re- applied) — but would fail on any version-bump re-derivation. --fix recounts the hunk headers (body untouched) and repaired all 29. Wired into build-preflight.sh (Phase 1.0D) and redbear-ci.yml, with a unit test (test-patch-sanity.sh). Skips archived/legacy trees and unvalidatable formats (empty placeholders, bare-@@ git hunks).
215 lines
7.3 KiB
Python
Executable File
215 lines
7.3 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())
|