# Copyright (c) 2024 The Chromium Embedded Framework Authors. All rights # reserved. Use of this source code is governed by a BSD-style license that # can be found in the LICENSE file. from __future__ import absolute_import from __future__ import print_function from typing import Dict from cef_api_hash import CefApiHasher from cef_version import VersionFormatter from date_util import get_date from file_util import read_file, read_json_file, write_file, write_json_file from git_util import exec_git_cmd import os import sys from translator import translate from version_util import * def get_next_api_revision(api_versions_file, major_version): """ Returns the next available API revision for |major_version|. """ json = read_json_file(api_versions_file) if not bool(json): return 0 assert 'last' in json, api_versions_file last_version, last_revision = version_parse(json['last']) if major_version < last_version: sys.stderr.write( 'ERROR: Cannot add new API versions on old branches/checkouts ' '(found %d, expected >= %d)\b' % (major_version, last_version)) return -1 if major_version == last_version: # Increment the revision for the current major version. new_revision = last_revision + 1 assert new_revision <= 99, new_revision return new_revision # Reset the revision for a new major version. return 0 def compute_api_hashes(api_version: str, hasher: CefApiHasher, next_allowed: bool) -> Dict[str, str]: """ Computes API hashes for the specified |api_version|. """ if not next_allowed: # Next usage is banned with explicit API versions. assert api_version not in UNTRACKED_VERSIONS, api_version added_defines = [ # Using CEF_API_VERSION_NEXT is an error. 'CEF_API_VERSION_NEXT="Please_specify_an_exact_CEF_version"', ] else: added_defines = [] hashes = hasher.calculate(api_version, added_defines) if hashes: if api_version in UNTRACKED_VERSIONS: label = version_label(api_version) label = label[0:1].upper() + label[1:] hashes['comment'] = f'{label} last updated {get_date()}.' else: hashes['comment'] = f'Added {get_date()}.' return hashes def same_api_hashes(hashes1, hashes2): return all(hashes1[key] == hashes2[key] for key in ['linux', 'mac', 'windows']) def compute_next_api_version(api_versions_file): """ Computes the next available API version number. """ major_version = int(VersionFormatter().get_chrome_major_version()) next_revision = get_next_api_revision(api_versions_file, major_version) if next_revision < 0: return None return version_make(major_version, next_revision) def git_grep_next(cef_dir): cmd = "grep --no-color -n -E (CEF_NEXT|CEF_NEXT)|=next -- :!include/cef_api_hash.h *.h *.cc" if sys.platform == 'win32': # Pass the pipe (|) character as a literal argument. cmd = cmd.replace('|', '^|') return exec_git_cmd(cmd, cef_dir) def find_next_usage(cpp_header_dir): cef_dir = os.path.abspath(os.path.join(cpp_header_dir, os.pardir)) result = git_grep_next(cef_dir) if result is None: return False sys.stderr.write('ERROR: NEXT usage found in CEF source files:\n\n' + result + '\n\nFix manually or run with --replace-next.\n') return True def replace_next_usage(file, linenums, as_variable, as_metadata): assert len(linenums) > 0 contents = read_file(file) if contents is None: sys.stderr.write('ERROR: Failed to read file %s\n' % file) return 0 lines = contents.split('\n') changect = 0 messages = [] for num in linenums: idx = num - 1 if idx < 0 or idx >= len(lines): sys.stderr.write('ERROR: Invalid line number %d in file %s\n' % (num, file)) return 0 line = lines[idx] replaced = False if line.find('CEF_NEXT') >= 0: line = line.replace('CEF_NEXT', as_variable) replaced = True if line.find('=next') >= 0: line = line.replace('=next', '=' + as_metadata) replaced = True if replaced: lines[idx] = line changect += 1 else: messages.append( 'WARNING: No NEXT instances found on line number %d' % num) if changect > 0 and write_file(file, '\n'.join(lines)): messages.append('Replaced %d of %d NEXT instances' % (changect, len(linenums))) else: changect = 0 if len(messages) > 0: print('For file %s:' % file) for msg in messages: print(' %s' % msg) print() return changect def find_replace_next_usage(cpp_header_dir, next_version): cef_dir = os.path.abspath(os.path.join(cpp_header_dir, os.pardir)) result = git_grep_next(cef_dir) if result is None: return 0 print('Attempting to replace NEXT usage with %s in CEF headers:\n' % next_version) print(result + '\n') as_variable = version_as_variable(next_version) as_metadata = version_as_metadata(next_version) files = {} # Parse values like: # include/test/cef_translator_test.h:879:#if CEF_API_ADDED(CEF_NEXT) # include/test/cef_translator_test.h:883: /*--cef(added=next)--*/ for line in result.split('\n'): parts = line.split(':', maxsplit=2) name = parts[0] linenum = int(parts[1]) if not name in files: files[name] = [linenum] else: files[name].append(linenum) for file, linenums in files.items(): if replace_next_usage( os.path.join(cef_dir, file), linenums, as_variable, as_metadata) != len(linenums): sys.stderr.write('ERROR: Failed to replace all NEXT usage in %s\n' % file) return 1 # Sanity-check that all instances were fixed. if find_next_usage(cpp_header_dir): return 1 print('All NEXT instances successfully replaced.') return 0 def exec_apply(api_versions_file, api_untracked_file, next_version, apply_next, hasher: CefApiHasher) -> int: """ Updates untracked API hashes if necessary. Saves the hash for the next API version if |apply_next| is true. """ json_versions, json_untracked, initialized = \ read_version_files(api_versions_file, api_untracked_file, True) if initialized: # Also need to generate hashes for the first version. apply_next = True json_versions['min'] = next_version untracked_changed = False for version in UNTRACKED_VERSIONS: label = version_label(version) hashes = compute_api_hashes(version, hasher, next_allowed=True) if not hashes: sys.stderr.write('ERROR: Failed to process %s\n' % label) return 1 if version in json_untracked['hashes'] and same_api_hashes( hashes, json_untracked['hashes'][version]): print('Hashes for %s are unchanged.' % label) else: untracked_changed = True print('Updating hashes for %s.' % label) json_untracked['hashes'][version] = hashes next_changed = apply_next if apply_next: next_label = version_label(next_version) hashes = compute_api_hashes(next_version, hasher, next_allowed=False) if not hashes: sys.stderr.write('ERROR: Failed to process %s\n' % next_label) return 1 last_version = json_versions.get('last', None) if not last_version is None and last_version in json_versions['hashes']: if same_api_hashes(hashes, json_versions['hashes'][last_version]): print('Hashes for last %s are unchanged.' % version_label(last_version)) next_changed = False if next_changed: print('Adding hashes for %s.' % next_label) json_versions['last'] = next_version json_versions['hashes'][next_version] = hashes if NEXT_VERSION in json_untracked['hashes'] and not \ same_api_hashes(hashes, json_untracked['hashes'][NEXT_VERSION]): print('NOTE: Additional versions are available to generate.') write_versions = next_changed or not os.path.isfile(api_versions_file) write_untracked = untracked_changed or not os.path.isfile(api_untracked_file) if not write_versions and not write_untracked: print('No hash updates required.') return -1 if write_versions and not write_json_file( api_versions_file, json_versions, quiet=False): return 1 if write_untracked and not write_json_file( api_untracked_file, json_untracked, quiet=False): return 1 return 0 def exec_check(api_versions_file, api_untracked_file, fast_check, force_update, skip_untracked, hasher: CefApiHasher) -> int: """ Checks existing API version hashes. Resaves all API hashes if |force_update| is true. Otherwise, hash changes are considered an error. """ assert not (fast_check and force_update) json_versions, json_untracked, initialized = read_version_files( api_versions_file, api_untracked_file, initialize=False) assert not initialized versions = [] len_versioned_existing = len_versioned_checked = len_versioned_failed = 0 len_untracked_existing = len_untracked_checked = len_untracked_failed = 0 if json_versions is not None: keys = json_versions['hashes'].keys() len_versioned_existing = len(keys) if len_versioned_existing > 0: if fast_check: # Only checking a subset of versions. for key in ['last', 'min']: if key in json_versions: version = json_versions[key] assert version in json_versions['hashes'], version versions.append(version) len_versioned_checked += 1 else: versions.extend(keys) len_versioned_checked = len_versioned_existing if json_untracked is not None: keys = json_untracked['hashes'].keys() len_untracked_existing = len(keys) if len_untracked_existing > 0 and not skip_untracked: versions.extend(keys) len_untracked_checked = len_untracked_existing if not versions: print('No hashes to check.') return 0 write_versions = False write_untracked = False for version in versions: untracked = version in UNTRACKED_VERSIONS if untracked: stored_hashes = json_untracked['hashes'][version] else: stored_hashes = json_versions['hashes'][version] label = version_label(version) computed_hashes = compute_api_hashes(version, hasher, next_allowed=True) if not bool(computed_hashes): sys.stderr.write('ERROR: Failed to process %s\n' % label) return 1 if not same_api_hashes(computed_hashes, stored_hashes): if force_update: print('Updating hashes for %s' % label) if untracked: json_untracked['hashes'][version] = computed_hashes write_untracked = True else: json_versions['hashes'][version] = computed_hashes write_versions = True else: sys.stderr.write('ERROR: Hashes for %s do not match!\n' % label) if untracked: len_untracked_failed += 1 else: len_versioned_failed += 1 len_failed = len_untracked_failed + len_versioned_failed if len_failed == 0: if write_versions and not write_json_file( api_versions_file, json_versions, quiet=False): return 1 if write_untracked and not write_json_file( api_untracked_file, json_untracked, quiet=False): return 1 if write_versions: print('WARNING: This change can break back/forward binary compatibility.') else: sys.stderr.write('ERROR: %d hashes checked and failed\n' % len_failed) sys.stderr.write( '\nFor debugging tips/tricks see\n' + 'https://github.com/chromiumembedded/cef/issues/3836#issuecomment-2587767028\n\n' ) print('%d hashes checked and match (%d/%d versioned, %d/%d untracked).' % (len(versions) - len_failed, len_versioned_checked - len_versioned_failed, len_versioned_existing, len_untracked_checked - len_untracked_failed, len_untracked_existing)) return 0 if len_failed == 0 else 1 if __name__ == "__main__": from optparse import OptionParser desc = """ This utility manages CEF API versions. """ epilog = """ Call this utility without arguments after modifying header files in the CEF include/ directory. Translated files will be updated if necessary. If translated files have changed, or when running with -u, unversioned API hashes (next and experimental) will be checked and potentially updated. If translated files have changed, or when running with -c, versioned and unversioned API hashes will be checked. Any changes to versioned API hashes can break back/forward binary compatibility and are considered an error. API under development will use placeholder values like CEF_NEXT, added=next, removed=next in CEF header files. This utility can replace those placeholders with an actual new version and generate the associated versioned API hashes. Run with -n to output the next available API version. Run with -a to apply the next available API version. For complete usage details see https://bitbucket.org/chromiumembedded/cef/wiki/ApiVersioning.md """ class CustomParser(OptionParser): def format_epilog(self, formatter): return self.epilog parser = CustomParser(description=desc, epilog=epilog) parser.add_option( '--debug-dir', dest='debug_dir', metavar='DIR', help='intermediate directory for easy debugging') parser.add_option( '-v', '--verbose', action='store_true', dest='verbose', default=False, help='output detailed status information') parser.add_option( '-u', '--update', action='store_true', dest='update', default=False, help='update next and unversioned API hashes') parser.add_option( '-n', '--next', action='store_true', dest='next', default=False, help='output the next available API version') parser.add_option( '-a', '--apply-next', action='store_true', dest='apply', default=False, help='add a hash for the next available API version') parser.add_option( '-c', '--check', action='store_true', dest='check', default=False, help='check hashes for existing API versions') parser.add_option( '--fast-check', action='store_true', dest='fastcheck', default=False, help= 'only check minimum, last, next and experimental API hashes (use with -u, -a or -c)' ) parser.add_option( '--replace-next', action='store_true', dest='replacenext', default=False, help='replace NEXT usage in CEF headers (use with -a)') parser.add_option( '--replace-next-version', dest='replacenextversion', metavar='VERSION', help='replace NEXT usage with this value (use with --replace-next)') parser.add_option( '--force-update', action='store_true', dest='force_update', default=False, help='force update all API hashes (use with -c)') (options, args) = parser.parse_args() script_dir = os.path.dirname(__file__) cef_dir = os.path.abspath(os.path.join(script_dir, os.pardir)) cpp_header_dir = os.path.join(cef_dir, 'include') if not os.path.isdir(cpp_header_dir): sys.stderr.write( 'ERROR: Missing %s directory is required\n' % cpp_header_dir) sys.exit(1) api_versions_file = os.path.join(cef_dir, VERSIONS_JSON_FILE) api_untracked_file = os.path.join(cef_dir, UNTRACKED_JSON_FILE) mode_ct = options.update + options.next + options.apply + options.check if mode_ct > 1: sys.stderr.write( 'ERROR: Choose a single execution mode (-u, -n, -a or -c)\n') parser.print_help(sys.stdout) sys.exit(1) next_version = compute_next_api_version(api_versions_file) if next_version is None: sys.exit(1) if options.next: print(next_version) sys.exit(0) will_apply_next = options.apply or not os.path.isfile(api_versions_file) if will_apply_next: if options.replacenext: replace_version = options.replacenextversion if replace_version is None: replace_version = next_version elif not version_valid_for_next(replace_version, next_version): sys.stderr.write('ERROR: Invalid value for --replace-next-version\n') sys.exit(1) result = find_replace_next_usage(cpp_header_dir, replace_version) if result != 0: sys.exit(result) elif find_next_usage(cpp_header_dir): sys.exit(1) changed = translate(cef_dir, verbose=options.verbose) > 0 skip_untracked = False hasher = CefApiHasher(cpp_header_dir, options.debug_dir, options.verbose) if options.update or will_apply_next or changed or not os.path.isfile( api_untracked_file): skip_untracked = True if exec_apply(api_versions_file, api_untracked_file, next_version, options.apply, hasher) > 0: # Apply failed. sys.exit(1) elif not options.check: print('Nothing to do.') sys.exit(0) sys.exit( exec_check(api_versions_file, api_untracked_file, options.fastcheck and not options.force_update, options.check and options.force_update, skip_untracked, hasher))