blob: 070cbd1fa3d099cb23a9275736a9ac339157bf7d [file]
#!/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()