#!/usr/bin/env python3 # Copyright 2022 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Helper for quickly generating all known JS externs.""" import argparse import os import re import sys from compiler import GenerateSchema # APIs with generated externs. API_SOURCES = ( ('chrome', 'common', 'apps', 'platform_apps', 'api'), ('chrome', 'common', 'extensions', 'api'), ('extensions', 'common', 'api'), ) _EXTERNS_UPDATE_MESSAGE = """Please run one of: src/ $ tools/json_schema_compiler/generate_all_externs.py OR src/ $ tools/json_schema_compiler/compiler.py\ %(source)s --root=. --generator=externs > %(externs)s""" DIR = os.path.dirname(os.path.realpath(__file__)) REPO_ROOT = os.path.dirname(os.path.dirname(DIR)) # Import the helper module. sys.path.insert(0, os.path.join(REPO_ROOT, 'extensions', 'common', 'api')) from externs_checker import ExternsChecker sys.path.pop(0) class FakeChange: """Stand-in for PRESUBMIT input_api.change. Enough to make ExternsChecker happy. """ @staticmethod def RepositoryRoot(): return REPO_ROOT class FakeInputApi: """Stand in for PRESUBMIT input_api. Enough to make ExternsChecker happy. """ change = FakeChange() os_path = os.path re = re @staticmethod def PresubmitLocalPath(): return DIR @staticmethod def ReadFile(path): with open(path) as fp: return fp.read() class FakeOutputApi: """Stand in for PRESUBMIT input_api. Enough to make CheckExterns happy. """ class PresubmitResult: def __init__(self, msg, long_text=None): self.msg = msg self.long_text = long_text def Generate(input_api, output_api, force=False, dryrun=False): """(Re)generate all the externs.""" src_root = input_api.change.RepositoryRoot() join = input_api.os_path.join # Load the list of all generated externs. api_pairs = {} for api_source in API_SOURCES: api_root = join(src_root, *api_source) api_pairs.update( ExternsChecker.ParseApiFileList(input_api, api_root=api_root)) # Unfortunately, our generator is still a bit buggy, so ignore externs that # are known to be hand edited after the fact. We require people to add an # explicit TODO marker bound to a known bug. # TODO(vapier): Improve the toolchain enough to not require this. re_disabled = input_api.re.compile( r'^// TODO\(crbug\.com/[0-9]+\): ' r'Disable automatic extern generation until fixed\.$', flags=input_api.re.M) # Make sure each one is up-to-date with our toolchain. ret = [] msg_len = 0 for source, externs in sorted(api_pairs.items()): try: old_data = input_api.ReadFile(externs) except OSError: old_data = '' if not force and re_disabled.search(old_data): continue source_relpath = input_api.os_path.relpath(source, src_root) externs_relpath = input_api.os_path.relpath(externs, src_root) print('\r' + ' ' * msg_len, end='\r') msg = 'Checking %s ...' % (source_relpath,) msg_len = len(msg) print(msg, end='') sys.stdout.flush() try: new_data = GenerateSchema('externs', [source], src_root, None, '', '', None, []) + '\n' except Exception as e: if not dryrun: print('\n%s: %s' % (source_relpath, e)) ret.append( output_api.PresubmitResult( '%s: unable to generate' % (source_relpath,), long_text=str(e))) continue # Ignore the first line (copyright) to avoid yearly thrashing. if '\n' in old_data: copyright, old_data = old_data.split('\n', 1) assert 'Copyright' in copyright copyright, new_data = new_data.split('\n', 1) assert 'Copyright' in copyright if old_data != new_data: settings = { 'source': source_relpath, 'externs': externs_relpath, } ret.append( output_api.PresubmitResult( '%(source)s: file needs to be regenerated' % settings, long_text=_EXTERNS_UPDATE_MESSAGE % settings)) if not dryrun: print('\r' + ' ' * msg_len, end='\r') msg_len = 0 print('Updating %s' % (externs_relpath,)) with open(externs, 'w', encoding='utf-8') as fp: fp.write(copyright + '\n') fp.write(new_data) print('\r' + ' ' * msg_len, end='\r') return ret def get_parser(): """Get CLI parser.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('-n', '--dry-run', dest='dryrun', action='store_true', help="Don't make changes; only show changed files") parser.add_argument('-f', '--force', action='store_true', help='Regenerate files even if they have a TODO ' 'disabling generation') return parser def main(argv): """The main entry point for scripts.""" parser = get_parser() opts = parser.parse_args(argv) results = Generate(FakeInputApi(), FakeOutputApi(), force=opts.force, dryrun=opts.dryrun) if opts.dryrun and results: for result in results: print(result.msg + '\n' + result.long_text) print() else: print('Done') if __name__ == '__main__': sys.exit(main(sys.argv[1:]))