1#!/usr/bin/env -S python3 -B 2# 3# Copyright (C) 2018 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17"""Generate ICU stable C API wrapper source. 18 19 20This script parses all the header files specified by the ICU module names. For 21each function in the allowlist, it generates the NDK headers, and shim functions 22to shim.cpp, which in turn calls the real implementation at runtime. 23The tool relies on libclang to parse header files. 24 25Reference to ICU4C stable C APIs: 26http://icu-project.org/apiref/icu4c/files.html 27""" 28from __future__ import absolute_import 29from __future__ import print_function 30 31import logging 32import os 33import re 34import shutil 35import subprocess 36from pathlib import Path 37from typing import Dict 38 39from genutil import ( 40 android_path, 41 generate_shim, 42 get_jinja_env, 43 get_allowlisted_apis, 44 AllowlistedDeclarationFilter, 45 DeclaredFunctionsParser, 46 StableDeclarationFilter, 47 THIS_DIR, 48) 49 50# No suffix for ndk shim 51SYMBOL_SUFFIX = '' 52 53SECRET_PROCESSING_TOKEN = "@@@SECRET@@@" 54 55DOC_BLOCK_COMMENT = r"\/\*\*(?:\*(?!\/)|[^*])*\*\/[ ]*\n" 56TILL_CLOSE_PARENTHESIS = r"[^)^;]*\)" 57STABLE_MACRO = r"(?:U_STABLE|U_CAPI)" 58STABLE_FUNCTION_DECLARATION = r"^(" + DOC_BLOCK_COMMENT + STABLE_MACRO \ 59 + TILL_CLOSE_PARENTHESIS + ");$" 60NONSTABLE_FUNCTION_DECLARATION = r"^(" + DOC_BLOCK_COMMENT + r"(U_INTERNAL|U_DEPRECATED|U_DRAFT)" \ 61 + TILL_CLOSE_PARENTHESIS + ");$" 62 63REGEX_STABLE_FUNCTION_DECLARATION = re.compile(STABLE_FUNCTION_DECLARATION, re.MULTILINE) 64REGEX_NONSTABLE_FUNCTION_DECLARATION = re.compile(NONSTABLE_FUNCTION_DECLARATION, re.MULTILINE) 65 66API_LEVEL_MACRO_MAP = { 67 '31': '31', 68 'T': '__ANDROID_API_T__', 69} 70 71def get_allowlisted_regex_string(decl_names): 72 """Return a regex in string to capture the C function declarations in the decl_names list""" 73 tag = "|".join(decl_names) 74 return r"(" + DOC_BLOCK_COMMENT + STABLE_MACRO + r"[^(]*(?=" + tag + r")(" + tag + ")" \ 75 + r"\("+ TILL_CLOSE_PARENTHESIS +");$" 76 77def get_replacement_adding_api_level_macro(api_level: str): 78 """Return the replacement string adding the NDK C macro 79 guarding C function declaration by the api_level""" 80 return r"\1 __INTRODUCED_IN({0});\n\n".format(api_level) 81 82def modify_func_declarations(src_path: str, dst_path: str, 83 exported_decl_api_map: Dict[str, str]): 84 """Process the source file, 85 remove the C function declarations not in the decl_names, 86 add guard the functions listed in decl_names by the API level, 87 and output to the dst_path """ 88 decl_names = list(exported_decl_api_map.keys()) 89 allowlist_regex_string = get_allowlisted_regex_string(decl_names) 90 allowlist_decl_regex = re.compile('^' + allowlist_regex_string, re.MULTILINE) 91 with open(src_path, "r") as file: 92 src = file.read() 93 94 # Remove all non-stable function declarations 95 modified = REGEX_NONSTABLE_FUNCTION_DECLARATION.sub('', src) 96 97 # Insert intermediate token to all functions in the allowlist 98 if decl_names: 99 modified = allowlist_decl_regex.sub(SECRET_PROCESSING_TOKEN + r"\1;", modified) 100 # Remove all other stable declarations not in the allowlist 101 modified = REGEX_STABLE_FUNCTION_DECLARATION.sub('', modified) 102 103 api_levels = list(set(exported_decl_api_map.values())) 104 for api_level in api_levels: 105 exported_decl_at_this_level = {key: value for key, value in 106 exported_decl_api_map.items() 107 if value == api_level } 108 109 # Insert C macro and annotation to indicate the API level to each functions 110 macro = API_LEVEL_MACRO_MAP[api_level] 111 decl_name_regex_string = get_allowlisted_regex_string( 112 list(exported_decl_at_this_level.keys())) 113 secret_allowlist_decl_regex = re.compile( 114 '^' + SECRET_PROCESSING_TOKEN + decl_name_regex_string, 115 re.MULTILINE) 116 modified = secret_allowlist_decl_regex.sub( 117 get_replacement_adding_api_level_macro(macro), modified) 118 119 with open(dst_path, "w") as out: 120 out.write(modified) 121 122def remove_ignored_includes(file_path, include_list): 123 """ 124 Remove the included header, i.e. #include lines, listed in include_list from the file_path 125 header. 126 """ 127 128 # Do nothing if the list is empty 129 if not include_list: 130 return 131 132 tag = "|".join(include_list) 133 134 with open(file_path, "r") as file: 135 content = file.read() 136 137 regex = re.compile(r"^#include \"unicode\/(" + tag + ")\"\n", re.MULTILINE) 138 content = regex.sub('', content) 139 140 with open(file_path, "w") as out: 141 out.write(content) 142 143def copy_header_only_files(): 144 """Copy required header only files""" 145 base_src_path = android_path('external/icu/icu4c/source/') 146 base_dest_path = android_path('external/icu/libicu/ndk_headers/unicode/') 147 with open(android_path('external/icu/tools/icu4c_srcgen/libicu_required_header_only_files.txt'), 148 'r') as in_file: 149 header_only_files = [ 150 base_src_path + line.strip() for line in in_file.readlines() if not line.startswith('#') 151 ] 152 153 for src_path in header_only_files: 154 dest_path = base_dest_path + os.path.basename(src_path) 155 cmd = ['sed', 156 "s/U_SHOW_CPLUSPLUS_API/LIBICU_U_SHOW_CPLUSPLUS_API/g", 157 src_path 158 ] 159 160 with open(dest_path, "w") as destfile: 161 subprocess.check_call(cmd, stdout=destfile) 162 163def copy_cts_headers(): 164 """Copy headers from common/ and i18n/ to cts_headers/ for compiling cintltst as CTS.""" 165 dst_folder = android_path('external/icu/libicu/cts_headers') 166 if os.path.exists(dst_folder): 167 shutil.rmtree(dst_folder) 168 os.mkdir(dst_folder) 169 os.mkdir(os.path.join(dst_folder, 'unicode')) 170 171 shutil.copyfile(android_path('external/icu/android_icu4c/include/uconfig_local.h'), 172 android_path('external/icu/libicu/cts_headers/uconfig_local.h')) 173 174 header_subfolders = ( 175 'common', 176 'common/unicode', 177 'i18n', 178 'i18n/unicode', 179 ) 180 for subfolder in header_subfolders: 181 path = android_path('external/icu/icu4c/source', subfolder) 182 files = [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.h')] 183 184 for src_path in files: 185 base_header_name = os.path.basename(src_path) 186 dst_path = dst_folder 187 if subfolder.endswith('unicode'): 188 dst_path = os.path.join(dst_path, 'unicode') 189 dst_path = os.path.join(dst_path, base_header_name) 190 191 shutil.copyfile(src_path, dst_path) 192 193def get_rename_macro_regex(decl_names): 194 """Return a regex in string to capture the C macro defining the name in the decl_names list""" 195 tag = "|".join(decl_names) 196 return re.compile(r"^(#define (?:" + tag + r") .*)$", re.MULTILINE) 197 198def generate_cts_headers(decl_names): 199 """Generate headers for compiling cintltst as CTS.""" 200 copy_cts_headers() 201 202 # Disable all C macro renaming the NDK functions in order to test the functions in the CTS 203 urename_path = android_path('external/icu/libicu/cts_headers/unicode/urename.h') 204 with open(urename_path, "r") as file: 205 src = file.read() 206 207 regex = get_rename_macro_regex(decl_names) 208 modified = regex.sub(r"// \1", src) 209 210 with open(urename_path, "w") as out: 211 out.write(modified) 212 213IGNORED_INCLUDE_DEPENDENCY = { 214 "ubrk.h": ["parseerr.h", ], 215 "ucol.h": ["uiter.h", "unorm.h", "uset.h", ], 216 "ulocdata.h": ["ures.h", "uset.h", ], 217 "unorm2.h": ["uset.h", ], 218 "ustring.h": ["uiter.h", ], 219 "utrans.h": ["uset.h", ], 220} 221 222IGNORED_HEADER_FOR_DOXYGEN_GROUPING = set([ 223 "uconfig.h", # pre-defined config that NDK users shouldn't change 224 "platform.h", # pre-defined variable not to be changed by the NDK users 225 "utf_old.h", # deprecated UTF macros 226 "uvernum.h", # ICU version information not useful for version-independent usage in NDK 227 "urename.h" # Renaming symbols, but not used in NDK 228]) 229 230""" 231This map should mirror the mapping in external/icu/icu4c/source/Doxyfile.in. 232This is needed because NDK doesn't allow per-module Doxyfile, 233apart from the shared frameworks/native/docs/Doxyfile. 234""" 235DOXYGEN_ALIASES = { 236 "@memo": '\\par Note:\n', 237 "@draft": '\\xrefitem draft "Draft" "Draft List" This API may be changed in the future versions and was introduced in', 238 "@stable": '\\xrefitem stable "Stable" "Stable List"', 239 "@deprecated": '\\xrefitem deprecated "Deprecated" "Deprecated List"', 240 "@obsolete": '\\xrefitem obsolete "Obsolete" "Obsolete List"', 241 "@system": '\\xrefitem system "System" "System List" Do not use unless you know what you are doing.', 242 "@internal": '\\xrefitem internal "Internal" "Internal List" Do not use. This API is for internal use only.', 243} 244 245def add_ndk_required_doxygen_grouping(): 246 """Add @addtogroup annotation to the header files for NDK API docs""" 247 path = android_path('external/icu/libicu/ndk_headers/unicode') 248 files = Path(path).glob("*.h") 249 250 for src_path in files: 251 header_content = src_path.read_text() 252 253 for old, new in DOXYGEN_ALIASES.items(): 254 header_content = header_content.replace(old, new) 255 256 src_path.write_text(header_content) 257 258 if os.path.basename(src_path) in IGNORED_HEADER_FOR_DOXYGEN_GROUPING: 259 continue 260 261 cmd_add_addtogroup_annotation = ['sed', 262 '-i', 263 '0,/^\( *\)\(\* *\\\\file\)/s//\\1* @addtogroup ICU4C\\n\\1* @{\\n\\1\\2/', 264 src_path 265 ] 266 267 subprocess.check_call(cmd_add_addtogroup_annotation) 268 269 # Next iteration if the above sed regex doesn't add the text 270 if not has_string_in_file(src_path, 'addtogroup'): 271 basename = os.path.basename(src_path) 272 print(f'Warning: unicode/{basename} has no "\\file" annotation') 273 continue 274 275 # Add the closing bracket for @addtogroup 276 with open(src_path, 'a') as header_file: 277 header_file.write('\n/** @} */ // addtogroup\n') 278 279def has_string_in_file(path, s): 280 """Return True if the a string exists in the file""" 281 with open(path, 'r') as file: 282 return s in file.read() 283 284def get_exported_symbol_map(export_file : str) -> Dict[str, str]: 285 """Return a dictionary mapping from the symbol name to API level in the 286 export_file""" 287 result_map = {} 288 with open(os.path.join(THIS_DIR, export_file), 'r') as file: 289 for line in file: 290 line = line.strip() 291 if line and not line.startswith("#"): 292 splits = line.split(',') 293 if len(splits) < 2: 294 raise ValueError(f'line "{line}" has no , separator') 295 result_map[splits[0]] = splits[1] 296 297 return result_map 298 299 300def main(): 301 """Parse the ICU4C headers and generate the shim libicu.""" 302 logging.basicConfig(level=logging.DEBUG) 303 304 exported_symbol_map = get_exported_symbol_map('libicu_export.txt') 305 allowlisted_apis = set(exported_symbol_map.keys()) 306 decl_filters = [StableDeclarationFilter()] 307 decl_filters.append(AllowlistedDeclarationFilter(allowlisted_apis)) 308 parser = DeclaredFunctionsParser(decl_filters, []) 309 parser.set_ignored_include_dependency(IGNORED_INCLUDE_DEPENDENCY) 310 311 parser.parse() 312 313 includes = parser.header_includes 314 functions = parser.declared_functions 315 header_to_function_names = parser.header_to_function_names 316 317 # The shim has the allowlisted functions only 318 functions = [f for f in functions if f.name in allowlisted_apis] 319 320 headers_folder = android_path('external/icu/libicu/ndk_headers/unicode') 321 if os.path.exists(headers_folder): 322 shutil.rmtree(headers_folder) 323 os.mkdir(headers_folder) 324 325 with open(android_path('external/icu/libicu/src/shim.cpp'), 326 'w') as out_file: 327 out_file.write(generate_shim(functions, includes, SYMBOL_SUFFIX, 'libicu_shim.cpp.j2')) 328 329 with open(android_path('external/icu/libicu/libicu.map.txt'), 'w') as out_file: 330 data = { 331 'exported_symbol_map' : exported_symbol_map, 332 } 333 out_file.write(get_jinja_env().get_template('libicu.map.txt.j2').render(data)) 334 335 # Process the C headers and put them into the ndk folder. 336 for src_path in parser.header_paths_to_copy: 337 basename = os.path.basename(src_path) 338 dst_path = os.path.join(headers_folder, basename) 339 exported_symbol_map_this_header = { 340 key: value for key, value in exported_symbol_map.items() 341 if key in header_to_function_names[basename]} 342 modify_func_declarations(src_path, dst_path, exported_symbol_map_this_header) 343 # Remove #include lines from the header files. 344 if basename in IGNORED_INCLUDE_DEPENDENCY: 345 remove_ignored_includes(dst_path, IGNORED_INCLUDE_DEPENDENCY[basename]) 346 347 copy_header_only_files() 348 349 generate_cts_headers(allowlisted_apis) 350 351 add_ndk_required_doxygen_grouping() 352 353 # Apply documentation patches by the following shell script 354 subprocess.check_call( 355 [android_path('external/icu/tools/icu4c_srcgen/doc_patches/apply_patches.sh')]) 356 357 print("Done. See the generated headers at libicu/ndk_headers/.") 358 359if __name__ == '__main__': 360 main() 361