1#!/usr/bin/env python 2# 3# Copyright 2020 The Chromium Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6"""Helps generate enums.xml from ProductionSupportedFlagList. 7 8This is only a best-effort attempt to generate enums.xml values for the 9LoginCustomFlags enum. You need to verify this script picks the right string 10value for the new features and double check the hash value by running 11"AboutFlagsHistogramTest.*". 12""" 13 14from __future__ import print_function 15 16import argparse 17import os 18import re 19import hashlib 20import ctypes 21import xml.etree.ElementTree as ET 22import logging 23import sys 24 25_CHROMIUM_SRC = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) 26sys.path.append(os.path.join(_CHROMIUM_SRC, 'third_party', 'catapult', 'devil')) 27from devil.utils import logging_common # pylint: disable=wrong-import-position 28 29_FLAG_LIST_FILE = os.path.join(_CHROMIUM_SRC, 'android_webview', 'java', 'src', 30 'org', 'chromium', 'android_webview', 'common', 31 'ProductionSupportedFlagList.java') 32_ENUMS_XML_FILE = os.path.join(_CHROMIUM_SRC, 'tools', 'metrics', 'histograms', 33 'enums.xml') 34 35# This script tries to guess the commandline switch/base::Feature name from the 36# generated Java constant (assuming the constant name follows typical 37# conventions), but sometimes the script generates the incorrect name. 38# In this case, you can teach the 39# script the right name is by editing this dictionary. The perk of editing 40# here instead of fixing enums.xml by hand is this script *should* generate the 41# correct hash value once you add the right name, so you can just rerun the 42# script to get the correct set of enum entries. 43# 44# Keys are the names autogenerated by this script's logic, values are the 45# base::Feature/switch string names as they would appear in Java/C++ code. 46KNOWN_MISTAKES = { 47 # 'AutogeneratedName': 'CorrectName', 48 'WebViewAccelerateSmallCanvases': 'WebviewAccelerateSmallCanvases', 49 'EnableSharedImageForWebView': 'EnableSharedImageForWebview', 50} 51 52 53def GetSwitchId(label): 54 """Generate a hash consistent with flags_ui::GetSwitchUMAId().""" 55 digest = hashlib.md5(label).hexdigest() 56 first_eight_bytes = digest[:16] 57 long_value = int(first_eight_bytes, 16) 58 signed_32bit = ctypes.c_int(long_value).value 59 return signed_32bit 60 61 62def _Capitalize(value): 63 value = value[0].upper() + value[1:].lower() 64 if value == 'Webview': 65 value = 'WebView' 66 return value 67 68 69def FormatName(name, convert_to_pascal_case): 70 """Converts name to the correct format. 71 72 If name is shouty-case (ex. 'SOME_NAME') like a Java constant, then: 73 * it converts to pascal case (camel case, with the first letter capitalized) 74 if convert_to_pascal_case == True (ex. 'SomeName') 75 * it converts to hyphenates name and converts to lower case (ex. 76 'some-name') 77 raises 78 ValueError if name contains quotation marks like a Java literal (ex. 79 '"SomeName"') 80 """ 81 has_quotes_re = re.compile(r'".*"') 82 if has_quotes_re.match(name): 83 raise ValueError('String literals are not supported (got {})'.format(name)) 84 name = re.sub(r'^[^.]+\.', '', name) 85 sections = name.split('_') 86 87 if convert_to_pascal_case: 88 sections = [_Capitalize(section) for section in sections] 89 return ''.join(sections) 90 91 sections = [section.lower() for section in sections] 92 return '-'.join(sections) 93 94 95def ConvertNameIfNecessary(name): 96 """Fixes any names which are known to be autogenerated incorrectly.""" 97 if name in KNOWN_MISTAKES.keys(): 98 return KNOWN_MISTAKES.get(name) 99 return name 100 101 102class Flag(object): 103 """Simplified python equivalent of the Flag java class. 104 105 See //android_webview/java/src/org/chromium/android_webview/common/Flag.java 106 """ 107 108 def __init__(self, name, is_base_feature): 109 self.name = name 110 self.is_base_feature = is_base_feature 111 112 113class EnumValue(object): 114 def __init__(self, label): 115 self.label = label 116 self.value = GetSwitchId(label) 117 118 def ToXml(self): 119 return '<int value="{value}" label="{label}"/>'.format(value=self.value, 120 label=self.label) 121 122 123def _GetExistingFlagLabels(): 124 with open(_ENUMS_XML_FILE) as f: 125 root = ET.fromstring(f.read()) 126 all_enums = root.find('enums') 127 login_custom_flags = all_enums.find('enum[@name="LoginCustomFlags"]') 128 return [item.get('label') for item in login_custom_flags] 129 130 131def _RemoveDuplicates(enums, existing_labels): 132 return [enum for enum in enums if enum.label not in existing_labels] 133 134 135def ExtractFlagsFromJavaLines(lines): 136 flags = [] 137 138 hanging_name_re = re.compile( 139 r'(?:\s*Flag\.(?:baseFeature|commandLine)\()?(\S+),') 140 pending_feature = False 141 pending_commandline = False 142 143 for line in lines: 144 if 'baseFeature(' in line: 145 pending_feature = True 146 if 'commandLine(' in line: 147 pending_commandline = True 148 149 if pending_feature and pending_commandline: 150 raise RuntimeError('Somehow this is both a baseFeature and commandLine ' 151 'switch: ({})'.format(line)) 152 153 # This means we saw Flag.baseFeature() or Flag.commandLine() on this or a 154 # previous line but haven't found that flag's name yet. Check if we can 155 # find a name in this line. 156 if pending_feature or pending_commandline: 157 m = hanging_name_re.search(line) 158 if m: 159 name = m.group(1) 160 try: 161 formatted_name = FormatName(name, pending_feature) 162 formatted_name = ConvertNameIfNecessary(formatted_name) 163 flags.append(Flag(formatted_name, pending_feature)) 164 pending_feature = False 165 pending_commandline = False 166 except ValueError: 167 logging.warning('String literals are not supported, skipping %s', 168 name) 169 return flags 170 171 172def _GetMissingWebViewEnums(): 173 with open(_FLAG_LIST_FILE, 'r') as f: 174 lines = f.readlines() 175 flags = ExtractFlagsFromJavaLines(lines) 176 177 enums = [] 178 for flag in flags: 179 if flag.is_base_feature: 180 enums.append(EnumValue(flag.name + ':enabled')) 181 enums.append(EnumValue(flag.name + ':disabled')) 182 else: 183 enums.append(EnumValue(flag.name)) 184 185 existing_labels = set(_GetExistingFlagLabels()) 186 enums_to_add = _RemoveDuplicates(enums, existing_labels) 187 return enums_to_add 188 189 190def CheckMissingWebViewEnums(input_api, output_api): 191 """A presubmit check to find missing flag enums.""" 192 sources = input_api.AffectedSourceFiles( 193 lambda affected_file: input_api.FilterSourceFile( 194 affected_file, 195 files_to_check=(r'.*\bProductionSupportedFlagList\.java$', ))) 196 if not sources: 197 return [] 198 199 enums_to_add = _GetMissingWebViewEnums() 200 if not enums_to_add: 201 return [] 202 203 script_path = '//android_webview/tools/PRESUBMIT.py' 204 enums_path = '//tools/metrics/histograms/enums.xml' 205 xml_strs = sorted([' ' + enum.ToXml() for enum in enums_to_add]) 206 207 return [ 208 output_api.PresubmitPromptWarning(""" 209It looks like new flags have been added to ProductionSupportedFlagList but the 210labels still need to be added to LoginCustomFlags enum in {enums_path}. 211If you believe this 212warning is correct, please update enums.xml by pasting the following lines under 213LoginCustomFlags and running `git-cl format` to correctly sort the changes: 214 215{xml_strs} 216 217You can run this check again by running the {script_path} tool. 218 219If you believe this warning is a false positive, you can silence this warning by 220updating KNOWN_MISTAKES in {script_path}. 221""".format(xml_strs='\n'.join(xml_strs), 222 enums_path=enums_path, 223 script_path=script_path)) 224 ] 225 226 227def main(): 228 parser = argparse.ArgumentParser() 229 230 logging_common.AddLoggingArguments(parser) 231 args = parser.parse_args() 232 logging_common.InitializeLogging(args) 233 234 enums_to_add = _GetMissingWebViewEnums() 235 236 xml_strs = sorted([' ' + enum.ToXml() for enum in enums_to_add]) 237 if not xml_strs: 238 print('enums.xml is already up-to-date!') 239 return 240 241 message = """\ 242This is a best-effort attempt to generate missing enums.xml entries. Please 243double-check this picked the correct labels for your new features (labels are 244case-sensitive!), add these to enums.xml, run `git-cl format`, and then follow 245these steps as a final check: 246 247https://chromium.googlesource.com/chromium/src/+/master/tools/metrics/histograms/README.md#flag-histograms 248 249If any labels were generated incorrectly, please edit this script and change 250KNOWN_MISTAKES. 251""" 252 print(message) 253 254 for xml_str in xml_strs: 255 print(xml_str) 256 257 258if __name__ == '__main__': 259 main() 260