| #!/usr/bin/env python3 |
| # Copyright 2026 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import argparse |
| import difflib |
| import os |
| from pathlib import Path |
| import re |
| import shlex |
| import shutil |
| import sys |
| |
| COLOR_GREEN = '\033[32m' |
| COLOR_RED = '\033[31m' |
| COLOR_CYAN = '\033[36m' |
| COLOR_RESET = '\033[0m' |
| |
| # These two lines are platform-dependent, since they contain the GN binary itself. |
| STRIP_REGEN_COMMAND = re.compile( |
| r'^(rule gn|build build.ninja.stamp.*)\n(?:[ \t]+.+\n)*', flags=re.MULTILINE |
| ) |
| |
| |
| def get_ninja_files(directory: Path) -> set[Path]: |
| ninja_files = set() |
| for root, _, files in os.walk(directory): |
| for f in files: |
| if f.endswith('.ninja'): |
| rel = os.path.relpath(os.path.join(root, f), directory) |
| ninja_files.add(Path(rel)) |
| return ninja_files |
| |
| |
| def compare_files(want: Path, got: Path, update) -> bool: |
| got_text = STRIP_REGEN_COMMAND.sub( |
| '', got.read_text(encoding='utf-8', errors='replace') |
| ) |
| want_text = want.read_text(encoding='utf-8', errors='replace') |
| |
| diff = list( |
| difflib.unified_diff( |
| want_text.splitlines(keepends=True), |
| got_text.splitlines(keepends=True), |
| fromfile=str(want), |
| tofile=str(got), |
| lineterm='\n', |
| ) |
| ) |
| |
| if not diff: |
| return False |
| |
| if update: |
| want.write_text(got_text) |
| return False |
| |
| for line in diff: |
| if line.startswith('+') and not line.startswith('+++'): |
| sys.stdout.write(COLOR_GREEN + line + COLOR_RESET) |
| elif line.startswith('-') and not line.startswith('---'): |
| sys.stdout.write(COLOR_RED + line + COLOR_RESET) |
| elif line.startswith('@@'): |
| sys.stdout.write(COLOR_CYAN + line + COLOR_RESET) |
| else: |
| sys.stdout.write(line) |
| return True |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description='Compare or update ninja output files.' |
| ) |
| parser.add_argument( |
| '--update', action='store_true', help='Update the golden files' |
| ) |
| parser.add_argument('generated_dir', help='Path to generated files directory') |
| parser.add_argument('golden_dir', help='Path to golden files directory') |
| args = parser.parse_args() |
| |
| want_dir = Path(args.golden_dir).resolve() |
| got_dir = Path(args.generated_dir).resolve() |
| |
| want_files = get_ninja_files(want_dir) |
| got_files = get_ninja_files(got_dir) |
| |
| return_code = 0 |
| all_files = want_files | got_files |
| |
| for f in sorted(all_files): |
| want = want_dir / f |
| got = got_dir / f |
| |
| if f not in got_files: |
| if args.update: |
| want.unlink() |
| else: |
| print(f'Error: Missing generated file: {f}', file=sys.stderr) |
| return_code = 1 |
| elif f not in want_files: |
| if args.update: |
| shutil.copy2(got, want) |
| else: |
| print(f'Error: Unexpected generated file: {f}', file=sys.stderr) |
| return_code = 1 |
| else: |
| if compare_files(want, got, args.update): |
| return_code = 1 |
| |
| if return_code: |
| cmd = ' '.join([ |
| shlex.quote(sys.executable), |
| shlex.quote(str(Path(__file__).resolve())), |
| shlex.quote(str(got_dir)), |
| shlex.quote(str(want_dir)), |
| '--update', |
| ]) |
| print( |
| f'Run `{cmd}` to update the golden files if the changes are' |
| ' intentional.' |
| ) |
| sys.exit(return_code) |
| |
| |
| if __name__ == '__main__': |
| main() |