#!/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:]))