| #!/usr/bin/env python3 |
| # Copyright 2025 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # Script to use clang complier errors to enclose UNSAFE_TODO() regions. |
| |
| import argparse |
| import os |
| import re |
| import subprocess |
| import sys |
| import tempfile |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Rewrite UNSAFE_TODO regions using clang compiler errors.", |
| formatter_class=argparse.RawDescriptionHelpFormatter) |
| |
| parser.add_argument("-C", |
| dest="build_dir", |
| default="out/Debug", |
| help="Specify the build directory, defaults to out/Debug") |
| parser.add_argument("-f", |
| dest="force", |
| action="store_true", |
| help="skip conditional compilation checks.") |
| parser.add_argument("-v", |
| dest="verbose", |
| action="store_true", |
| help="Enable verbose logging") |
| parser.add_argument("directory", |
| help="Directory under src/ to checkout and modify.") |
| |
| args = parser.parse_args() |
| build_dir = args.build_dir |
| directory = args.directory |
| force = args.force |
| verbose = args.verbose |
| |
| print("Checking GN build arg configuration ...") |
| try: |
| dcheck_cmd = [ |
| "gn", "args", "-C", build_dir, "--short", "--list=dcheck_always_on" |
| ] |
| dcheck = subprocess.check_output(dcheck_cmd, text=True) |
| if "true" not in dcheck: |
| print("Set GN arg dcheck_always_on = true", file=sys.stderr) |
| sys.exit(1) |
| |
| diag_cmd = [ |
| "gn", "args", "-C", build_dir, "--short", |
| "--list=diagnostics_print_source_range_info" |
| ] |
| diag = subprocess.check_output(diag_cmd, text=True) |
| if "true" not in diag: |
| print("Set GN arg diagnostics_print_source_range_info = true", |
| file=sys.stderr) |
| sys.exit(1) |
| |
| warn_cmd = [ |
| "gn", "args", "-C", build_dir, "--short", |
| "--list=treat_warnings_as_errors" |
| ] |
| warn = subprocess.check_output(warn_cmd, text=True) |
| if "false" not in warn: |
| print("Set GN arg treat_warnings_as_errors = false", file=sys.stderr) |
| sys.exit(1) |
| |
| except subprocess.CalledProcessError as e: |
| print(f"Error checking GN args: {e.stderr}", file=sys.stderr) |
| sys.exit(1) |
| |
| tmpdir = tempfile.mkdtemp(None, "unsafe_pragma_rewriter.") |
| print(f"Temporary files will be written to {tmpdir}\n") |
| |
| try: |
| grep_cmd = ["git", "grep", "-l", "^#pragma allow_unsafe_", directory] |
| grep = subprocess.check_output(grep_cmd, text=True).strip() |
| except Exception as e: |
| print("No candidates found") |
| sys.exit(1) |
| |
| grep_lines = grep.splitlines() if grep else [] |
| source_files = [x for x in grep_lines if re.match(r".*\.cc$", x)] |
| if not source_files: |
| print("No files met criteria") |
| sys.exit(1) |
| |
| if verbose: |
| print("Files containing unsafe pragmas:") |
| print("\n".join(source_files), "\n") |
| |
| if not force: |
| iffy_cmd = ["grep", "-Pc", "^#if(?! DCHECK_IS_ON\\(\\))"] + source_files |
| iffy = subprocess.check_output(iffy_cmd, text=True).strip() |
| iffy_lines = iffy.splitlines() if iffy else [] |
| iffy_files = [x.split(":")[0] for x in iffy_lines if x.split(":")[1] != "1"] |
| if iffy_files: |
| if verbose: |
| print("Skipping conditionally-compiled files:") |
| print("\n".join(iffy_files), "\n") |
| source_files = [x for x in source_files if not x in set(iffy_files)] |
| |
| if not source_files: |
| print("No remaining files") |
| sys.exit(1) |
| |
| if verbose: |
| print("Remaining files after excluding #ifdefs:") |
| print("\n".join(source_files), "\n") |
| |
| # Starting with all files in the directory, find the ones that are |
| # able to be compiled on this platform/configurarion by asking ninja |
| # to build them all, and then removing the ones that aren't known. |
| obj_targets = ["../../" + x + "^" for x in source_files] |
| ninja_command = ["autoninja", "-C", build_dir] + obj_targets |
| ninja = subprocess.run(ninja_command, text=True, capture_output=True) |
| if ninja.stderr: |
| source_files = [ |
| x for x in source_files |
| if not 'unknown target "../../' + x in ninja.stderr |
| ] |
| |
| if verbose: |
| print("Remaining files after excluding unbuildable:") |
| print("\n".join(source_files), "\n") |
| |
| subprocess.run( |
| ["tools/clang/unsafe_pragma_rewriter/remove_unsafe_pragma.py"] + |
| source_files, |
| check=True) |
| |
| print("Compile to find unsafe errors ...") |
| targets = ["../../" + x + "^" for x in source_files] |
| buildlog0 = os.path.join(tmpdir, "buildlog0") |
| with open(buildlog0, "w") as f_log: |
| subprocess.run(["autoninja", "-k", "1000", "-C", build_dir, "-v"] + targets, |
| stdout=f_log, |
| stderr=subprocess.STDOUT) |
| |
| with open(buildlog0) as f_in: |
| compiled = subprocess.check_output( |
| ["tools/clang/unsafe_pragma_rewriter/extract_sources.py"], |
| stdin=f_in, |
| text=True).strip() |
| compiled_files = compiled.splitlines() if compiled else [] |
| |
| if not compiled_files: |
| print("No modified files were compiled") |
| sys.exit(1) |
| |
| if verbose: |
| print("Set of files compiled") |
| print("\n".join(compiled_files), "\n") |
| |
| source_files = [x for x in source_files if x in set(compiled_files)] |
| if verbose: |
| print("Set of modified files compiled") |
| print("\n".join(compiled_files), "\n") |
| |
| with open(buildlog0) as f_in: |
| fail = subprocess.check_output( |
| ["tools/clang/unsafe_pragma_rewriter/extract_failures.py"], |
| stdin=f_in, |
| text=True).strip() |
| fail_files = fail.splitlines() if fail else [] |
| |
| if verbose and fail_files: |
| print("Set of files with detected warnings") |
| print("\n".join(fail_files), "\n") |
| |
| print("Resetting to clean state ...") |
| subprocess.run(["git", "checkout", "--", directory], check=True) |
| subprocess.run( |
| ["tools/clang/unsafe_pragma_rewriter/remove_unsafe_pragma.py"] + |
| source_files, |
| check=True) |
| |
| print("Adding UNSAFE_TODO() ...") |
| with open(buildlog0) as f_in: |
| subprocess.run(["tools/clang/unsafe_pragma_rewriter/fix_unsafe.py"], |
| stdin=f_in, |
| check=True) |
| |
| if source_files: |
| try: |
| needs_header_cmd = ["git", "grep", "-l", "UNSAFE_TODO"] + source_files |
| needs_header = subprocess.check_output(needs_header_cmd, |
| text=True).strip() |
| needs_header_files = needs_header.splitlines() if needs_header else [] |
| except Exception as e: |
| needs_header_files = [] |
| |
| if needs_header_files: |
| subprocess.run( |
| ["tools/add_header.py", "--header", '"base/compiler_specific.h"'] + |
| needs_header_files, |
| check=True) |
| |
| for i in range(1, 5): |
| print(f"Compile to find bad rewrites (Pass {i}) ...") |
| buildlog_i = os.path.join(tmpdir, f"buildlog{i}") |
| with open(buildlog_i, "w") as f_log: |
| subprocess.run(["autoninja", "-k", "1000", "-C", build_dir] + targets, |
| stdout=f_log, |
| stderr=subprocess.STDOUT) |
| |
| with open(buildlog_i) as f_in: |
| fail = subprocess.check_output( |
| ["tools/clang/unsafe_pragma_rewriter/extract_failures.py"], |
| stdin=f_in, |
| text=True).strip() |
| failures = fail.splitlines() if fail else [] |
| |
| if not failures: |
| break |
| |
| if verbose: |
| print("Failed to compile, reverting:") |
| print("\n".join(failures), "\n") |
| |
| for failure in failures: |
| subprocess.run(["git", "checkout", "--", failure], check=True) |
| |
| print("Formatting changes") |
| subprocess.run(["git", "cl", "format"], check=True) |
| |
| print("Finished.") |
| |
| |
| if __name__ == "__main__": |
| main() |