1#!/usr/bin/env python3 2 3import argparse 4import os 5import re 6import shlex 7import subprocess 8import sys 9from tempfile import NamedTemporaryFile 10 11DESC = """ 12 13Determine whether `bindgen` can successfully process a C or C++ input header. 14 15First, `bindgen` is run on the input header. Then the emitted bindings are 16compiled with `rustc`. Finally, the compiled bindings' layout tests are run. 17 18By default, this script will exit zero if all of the above steps are successful, 19and non-zero if any of them fail. This is appropriate for determining if some 20test case (perhaps generated with `csmith` or another fuzzer) uncovers any bugs 21in `bindgen`. 22 23However, this script can also be used when reducing (perhaps with `creduce`) a 24known-bad test case into a new, smaller test case that exhibits the same bad 25behavior. In this mode, you might expect that the emitted bindings fail to 26compile with `rustc`, and want to exit non-zero early if that is not the 27case. See the "reducing arguments" for details and what knobs are available. 28 29""" 30 31parser = argparse.ArgumentParser( 32 formatter_class=argparse.RawDescriptionHelpFormatter, 33 description=DESC.strip()) 34 35BINDGEN_ARGS = "--with-derive-partialeq \ 36--with-derive-eq \ 37--with-derive-partialord \ 38--with-derive-ord \ 39--with-derive-hash \ 40--with-derive-default" 41 42parser.add_argument( 43 "--bindgen-args", 44 type=str, 45 default=BINDGEN_ARGS, 46 help="An argument string that `bindgen` should be invoked with. By default, all traits are derived. Note that the input header and output bindings file will automatically be provided by this script, and you should not manually specify them.") 47 48parser.add_argument( 49 "--save-temp-files", 50 action="store_true", 51 help="Do not delete temporary files.") 52 53parser.add_argument( 54 "input", 55 type=str, 56 default="input.h", 57 help="The input header file. Defaults to 'input.h'.") 58 59REDUCING_DESC = """ 60 61Arguments that are useful when reducing known-bad test cases into 62equivalent-but-smaller test cases that exhibit the same bug with `creduce`. 63 64""" 65 66reducing = parser.add_argument_group("reducing arguments", REDUCING_DESC.strip()) 67 68reducing.add_argument( 69 "--release", 70 action="store_true", 71 help="Use a release instead of a debug build.") 72reducing.add_argument( 73 "--expect-bindgen-fail", 74 action="store_true", 75 help="Exit non-zero if `bindgen` successfully emits bindings.") 76reducing.add_argument( 77 "--bindgen-grep", 78 type=str, 79 help="Exit non-zero if the given regexp pattern is not found in `bindgen`'s output.") 80reducing.add_argument( 81 "--bindings-grep", 82 type=str, 83 nargs='*', 84 help="Exit non-zero if the given regexp pattern is not found in the emitted bindings.") 85 86reducing.add_argument( 87 "--no-compile-bindings", 88 action="store_false", 89 dest="rustc", 90 help="Do not attempt to compile the emitted bindings with `rustc`.") 91reducing.add_argument( 92 "--extra-compile-file", 93 type=str, 94 help="Append the content of this extra file to the end of the emitted bindings just before compiling it.") 95reducing.add_argument( 96 "--expect-compile-fail", 97 action="store_true", 98 help="Exit non-zero if `rustc` successfully compiles the emitted bindings.") 99reducing.add_argument( 100 "--rustc-grep", 101 type=str, 102 help="Exit non-zero if the output from compiling the bindings with `rustc` does not contain the given regexp pattern") 103 104reducing.add_argument( 105 "--no-layout-tests", 106 action="store_false", 107 dest="layout_tests", 108 help="Do not run the compiled bindings' layout tests.") 109reducing.add_argument( 110 "--expect-layout-tests-fail", 111 action="store_true", 112 help="Exit non-zero if the compiled bindings' layout tests pass.") 113reducing.add_argument( 114 "--layout-tests-grep", 115 type=str, 116 help="Exit non-zero if the output of running the compiled bindings' layout tests does not contain the given regexp pattern.") 117 118################################################################################ 119 120class ExitOne(Exception): 121 pass 122 123def exit_1(msg, child=None): 124 print(msg) 125 126 if child: 127 stdout = decode(child.stdout) 128 for line in stdout.splitlines(): 129 sys.stdout.write("+") 130 sys.stdout.write(line) 131 sys.stdout.write("\n") 132 stderr = decode(child.stderr) 133 for line in stderr.splitlines(): 134 sys.stderr.write("+") 135 sys.stderr.write(line) 136 sys.stderr.write("\n") 137 138 raise ExitOne() 139 140def main(): 141 args = parser.parse_args() 142 os.environ["RUST_BACKTRACE"] = "full" 143 144 exit_code = 0 145 try: 146 bindings = new_temp_file(prefix="bindings-", suffix=".rs") 147 run_bindgen(args, bindings) 148 149 if args.rustc and not args.expect_bindgen_fail: 150 test_exe = new_temp_file(prefix="layout-tests-") 151 run_rustc(args, bindings, test_exe) 152 153 if args.layout_tests and not args.expect_compile_fail: 154 run_layout_tests(args, test_exe) 155 except ExitOne: 156 exit_code = 1 157 except Exception as e: 158 exit_code = 2 159 print("Unexpected exception:", e) 160 161 if not args.save_temp_files: 162 for path in TEMP_FILES: 163 try: 164 os.remove(path) 165 except Exception as e: 166 print("Unexpected exception:", e) 167 168 sys.exit(exit_code) 169 170def run(cmd, **kwargs): 171 print("Running:", cmd) 172 return subprocess.run(cmd, **kwargs) 173 174def decode(f): 175 return f.decode(encoding="utf-8", errors="ignore") 176 177TEMP_FILES = [] 178 179def new_temp_file(prefix, suffix=None): 180 temp = NamedTemporaryFile(delete=False, prefix=prefix, suffix=suffix) 181 temp.close() 182 TEMP_FILES.append(temp.name) 183 return temp.name 184 185def contains(pattern, lines): 186 for line in lines: 187 if re.match(pattern, line): 188 return True 189 return False 190 191def regexp(pattern): 192 if not pattern.startswith("^"): 193 pattern = ".*" + pattern 194 if not pattern.endswith("$"): 195 pattern = pattern + ".*" 196 return re.compile(pattern) 197 198def run_bindgen(args, bindings): 199 manifest_path = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), 200 "..", 201 "Cargo.toml")) 202 command = ["cargo", "run", "--manifest-path", manifest_path] 203 if args.release: 204 command += ["--release"] 205 command += ["--", args.input, "-o", bindings] 206 command += shlex.split(args.bindgen_args) 207 208 child = run(command, stderr=subprocess.PIPE, stdout=subprocess.PIPE) 209 210 if args.bindgen_grep: 211 pattern = regexp(args.bindgen_grep) 212 if not (contains(pattern, decode(child.stdout).splitlines()) or 213 contains(pattern, decode(child.stderr).splitlines())): 214 exit_1("Error: did not find '{}' in `bindgen`'s output".format(args.bindgen_grep), child) 215 216 if args.expect_bindgen_fail and child.returncode == 0: 217 exit_1("Error: expected running `bindgen` to fail, but it didn't", child) 218 219 if not args.expect_bindgen_fail and child.returncode != 0: 220 exit_1("Error: running `bindgen` failed", child) 221 222 for arg in args.bindings_grep or []: 223 pattern = regexp(arg) 224 with open(bindings, mode="r") as f: 225 if not contains(pattern, f): 226 print("Error: expected the emitted bindings to contain '{}', but they didn't".format(arg)) 227 print("---------- {} ----------------------------------------------".format(bindings)) 228 f.seek(0) 229 print(f.read()) 230 raise ExitOne() 231 232def run_rustc(args, bindings, test_exe): 233 if args.extra_compile_file: 234 with open(bindings, mode="a") as outfile: 235 with open(args.extra_compile_file, mode="r") as infile: 236 outfile.write(infile.read()) 237 child = run( 238 ["rustc", "--crate-type", "lib", "--test", "-o", test_exe, bindings], 239 stdout=subprocess.PIPE, 240 stderr=subprocess.PIPE) 241 242 if args.rustc_grep: 243 pattern = regexp(args.rustc_grep) 244 if not (contains(pattern, decode(child.stdout).splitlines()) or 245 contains(pattern, decode(child.stderr).splitlines())): 246 exit_1("Error: did not find '{}' in `rustc`'s output".format(args.rustc_grep), child) 247 248 if args.expect_compile_fail and child.returncode == 0: 249 exit_1("Error: expected running `rustc` on the emitted bindings to fail, but it didn't", child) 250 251 if not args.expect_compile_fail and child.returncode != 0: 252 exit_1("Error: running `rustc` on the emitted bindings failed", child) 253 254def run_layout_tests(args, test_exe): 255 child = run( 256 [test_exe], 257 stdout=subprocess.PIPE, 258 stderr=subprocess.PIPE) 259 260 if args.layout_tests_grep: 261 pattern = regexp(args.layout_tests_grep) 262 if not (contains(pattern, decode(child.stdout).splitlines()) or 263 contains(pattern, decode(child.stderr).splitlines())): 264 exit_1("Error: did not find '{}' in the compiled bindings' layout tests' output".format(args.layout_tests_grep), child) 265 266 if args.expect_layout_tests_fail and child.returncode == 0: 267 exit_1("Error: expected running the compiled bindings' layout tests to fail, but it didn't", child) 268 269 if not args.expect_layout_tests_fail and child.returncode != 0: 270 exit_1("Error: running the compiled bindings' layout tests failed", child) 271 272if __name__ == "__main__": 273 main() 274