blob: 3a39bcd3d22967a2dccb4d40edeeb8fd5864de43 [file] [log] [blame]
#!/usr/bin/env -S python3 -B
#
# Copyright (C) 2018 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Generate ICU stable C API wrapper source.
This script parses all the header files specified by the ICU module names. For
each function in the allowlist, it generates the NDK headers, and shim functions
to shim.cpp, which in turn calls the real implementation at runtime.
The tool relies on libclang to parse header files.
Reference to ICU4C stable C APIs:
http://icu-project.org/apiref/icu4c/files.html
"""
from __future__ import absolute_import
from __future__ import print_function
import logging
import os
import re
import shutil
import subprocess
from pathlib import Path
from typing import Dict
from genutil import (
android_path,
generate_shim,
get_jinja_env,
get_allowlisted_apis,
AllowlistedDeclarationFilter,
DeclaredFunctionsParser,
StableDeclarationFilter,
THIS_DIR,
)
# No suffix for ndk shim
SYMBOL_SUFFIX = ''
SECRET_PROCESSING_TOKEN = "@@@SECRET@@@"
DOC_BLOCK_COMMENT = r"\/\*\*(?:\*(?!\/)|[^*])*\*\/[ ]*\n"
TILL_CLOSE_PARENTHESIS = r"[^)^;]*\)"
STABLE_MACRO = r"(?:U_STABLE|U_CAPI)"
STABLE_FUNCTION_DECLARATION = r"^(" + DOC_BLOCK_COMMENT + STABLE_MACRO \
+ TILL_CLOSE_PARENTHESIS + ");$"
NONSTABLE_FUNCTION_DECLARATION = r"^(" + DOC_BLOCK_COMMENT + r"(U_INTERNAL|U_DEPRECATED|U_DRAFT)" \
+ TILL_CLOSE_PARENTHESIS + ");$"
REGEX_STABLE_FUNCTION_DECLARATION = re.compile(STABLE_FUNCTION_DECLARATION, re.MULTILINE)
REGEX_NONSTABLE_FUNCTION_DECLARATION = re.compile(NONSTABLE_FUNCTION_DECLARATION, re.MULTILINE)
API_LEVEL_MACRO_MAP = {
'31': '31',
'T': '__ANDROID_API_T__',
}
def get_allowlisted_regex_string(decl_names):
"""Return a regex in string to capture the C function declarations in the decl_names list"""
tag = "|".join(decl_names)
return r"(" + DOC_BLOCK_COMMENT + STABLE_MACRO + r"[^(]*(?=" + tag + r")(" + tag + ")" \
+ r"\("+ TILL_CLOSE_PARENTHESIS +");$"
def get_replacement_adding_api_level_macro(api_level: str):
"""Return the replacement string adding the NDK C macro
guarding C function declaration by the api_level"""
return r"\1 __INTRODUCED_IN({0});\n\n".format(api_level)
def modify_func_declarations(src_path: str, dst_path: str,
exported_decl_api_map: Dict[str, str]):
"""Process the source file,
remove the C function declarations not in the decl_names,
add guard the functions listed in decl_names by the API level,
and output to the dst_path """
decl_names = list(exported_decl_api_map.keys())
allowlist_regex_string = get_allowlisted_regex_string(decl_names)
allowlist_decl_regex = re.compile('^' + allowlist_regex_string, re.MULTILINE)
with open(src_path, "r") as file:
src = file.read()
# Remove all non-stable function declarations
modified = REGEX_NONSTABLE_FUNCTION_DECLARATION.sub('', src)
# Insert intermediate token to all functions in the allowlist
if decl_names:
modified = allowlist_decl_regex.sub(SECRET_PROCESSING_TOKEN + r"\1;", modified)
# Remove all other stable declarations not in the allowlist
modified = REGEX_STABLE_FUNCTION_DECLARATION.sub('', modified)
api_levels = list(set(exported_decl_api_map.values()))
for api_level in api_levels:
exported_decl_at_this_level = {key: value for key, value in
exported_decl_api_map.items()
if value == api_level }
# Insert C macro and annotation to indicate the API level to each functions
macro = API_LEVEL_MACRO_MAP[api_level]
decl_name_regex_string = get_allowlisted_regex_string(
list(exported_decl_at_this_level.keys()))
secret_allowlist_decl_regex = re.compile(
'^' + SECRET_PROCESSING_TOKEN + decl_name_regex_string,
re.MULTILINE)
modified = secret_allowlist_decl_regex.sub(
get_replacement_adding_api_level_macro(macro), modified)
with open(dst_path, "w") as out:
out.write(modified)
def remove_ignored_includes(file_path, include_list):
"""
Remove the included header, i.e. #include lines, listed in include_list from the file_path
header.
"""
# Do nothing if the list is empty
if not include_list:
return
tag = "|".join(include_list)
with open(file_path, "r") as file:
content = file.read()
regex = re.compile(r"^#include \"unicode\/(" + tag + ")\"\n", re.MULTILINE)
content = regex.sub('', content)
with open(file_path, "w") as out:
out.write(content)
def copy_header_only_files():
"""Copy required header only files"""
base_src_path = android_path('external/icu/icu4c/source/')
base_dest_path = android_path('external/icu/libicu/ndk_headers/unicode/')
with open(android_path('external/icu/tools/icu4c_srcgen/libicu_required_header_only_files.txt'),
'r') as in_file:
header_only_files = [
base_src_path + line.strip() for line in in_file.readlines() if not line.startswith('#')
]
for src_path in header_only_files:
dest_path = base_dest_path + os.path.basename(src_path)
cmd = ['sed',
"s/U_SHOW_CPLUSPLUS_API/LIBICU_U_SHOW_CPLUSPLUS_API/g",
src_path
]
with open(dest_path, "w") as destfile:
subprocess.check_call(cmd, stdout=destfile)
def copy_cts_headers():
"""Copy headers from common/ and i18n/ to cts_headers/ for compiling cintltst as CTS."""
dst_folder = android_path('external/icu/libicu/cts_headers')
if os.path.exists(dst_folder):
shutil.rmtree(dst_folder)
os.mkdir(dst_folder)
os.mkdir(os.path.join(dst_folder, 'unicode'))
shutil.copyfile(android_path('external/icu/android_icu4c/include/uconfig_local.h'),
android_path('external/icu/libicu/cts_headers/uconfig_local.h'))
header_subfolders = (
'common',
'common/unicode',
'i18n',
'i18n/unicode',
)
for subfolder in header_subfolders:
path = android_path('external/icu/icu4c/source', subfolder)
files = [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.h')]
for src_path in files:
base_header_name = os.path.basename(src_path)
dst_path = dst_folder
if subfolder.endswith('unicode'):
dst_path = os.path.join(dst_path, 'unicode')
dst_path = os.path.join(dst_path, base_header_name)
shutil.copyfile(src_path, dst_path)
def get_rename_macro_regex(decl_names):
"""Return a regex in string to capture the C macro defining the name in the decl_names list"""
tag = "|".join(decl_names)
return re.compile(r"^(#define (?:" + tag + r") .*)$", re.MULTILINE)
def generate_cts_headers(decl_names):
"""Generate headers for compiling cintltst as CTS."""
copy_cts_headers()
# Disable all C macro renaming the NDK functions in order to test the functions in the CTS
urename_path = android_path('external/icu/libicu/cts_headers/unicode/urename.h')
with open(urename_path, "r") as file:
src = file.read()
regex = get_rename_macro_regex(decl_names)
modified = regex.sub(r"// \1", src)
with open(urename_path, "w") as out:
out.write(modified)
IGNORED_INCLUDE_DEPENDENCY = {
"ubrk.h": ["parseerr.h", ],
"ucol.h": ["uiter.h", "unorm.h", "uset.h", ],
"ulocdata.h": ["ures.h", "uset.h", ],
"unorm2.h": ["uset.h", ],
"ustring.h": ["uiter.h", ],
"utrans.h": ["uset.h", ],
}
IGNORED_HEADER_FOR_DOXYGEN_GROUPING = set([
"ubidi.h", # ubidi.h has the @{ ... @} group block already.
"uconfig.h", # pre-defined config that NDK users shouldn't change
"platform.h", # pre-defined variable not to be changed by the NDK users
"utf_old.h", # deprecated UTF macros
"uvernum.h", # ICU version information not useful for version-independent usage in NDK
"urename.h" # Renaming symbols, but not used in NDK
])
"""
This map should mirror the mapping in external/icu/icu4c/source/Doxyfile.in.
This is needed because NDK doesn't allow per-module Doxyfile,
apart from the shared frameworks/native/docs/Doxyfile.
"""
DOXYGEN_ALIASES = {
"@memo": '\\par Note:\n',
"@draft": '\\xrefitem draft "Draft" "Draft List" This API may be changed in the future versions and was introduced in',
"@stable": '\\xrefitem stable "Stable" "Stable List"',
"@deprecated": '\\xrefitem deprecated "Deprecated" "Deprecated List"',
"@obsolete": '\\xrefitem obsolete "Obsolete" "Obsolete List"',
"@system": '\\xrefitem system "System" "System List" Do not use unless you know what you are doing.',
"@internal": '\\xrefitem internal "Internal" "Internal List" Do not use. This API is for internal use only.',
}
def add_ndk_required_doxygen_grouping():
"""Add @addtogroup annotation to the header files for NDK API docs"""
path = android_path('external/icu/libicu/ndk_headers/unicode')
files = Path(path).glob("*.h")
for src_path in files:
header_content = src_path.read_text()
for old, new in DOXYGEN_ALIASES.items():
header_content = header_content.replace(old, new)
src_path.write_text(header_content)
if os.path.basename(src_path) in IGNORED_HEADER_FOR_DOXYGEN_GROUPING:
continue
cmd_add_addtogroup_annotation = ['sed',
'-i',
'0,/^\( *\)\(\* *\\\\file\)/s//\\1* @addtogroup icu4c ICU4C\\n\\1* @{\\n\\1\\2/',
src_path
]
subprocess.check_call(cmd_add_addtogroup_annotation)
# Next iteration if the above sed regex doesn't add the text
if not has_string_in_file(src_path, 'addtogroup'):
basename = os.path.basename(src_path)
print(f'Warning: unicode/{basename} has no "\\file" annotation')
continue
# Add the closing bracket for @addtogroup
with open(src_path, 'a') as header_file:
header_file.write('\n/** @} */ // addtogroup\n')
def has_string_in_file(path, s):
"""Return True if the a string exists in the file"""
with open(path, 'r') as file:
return s in file.read()
def get_exported_symbol_map(export_file : str) -> Dict[str, str]:
"""Return a dictionary mapping from the symbol name to API level in the
export_file"""
result_map = {}
with open(os.path.join(THIS_DIR, export_file), 'r') as file:
for line in file:
line = line.strip()
if line and not line.startswith("#"):
splits = line.split(',')
if len(splits) < 2:
raise ValueError(f'line "{line}" has no , separator')
result_map[splits[0]] = splits[1]
return result_map
def main():
"""Parse the ICU4C headers and generate the shim libicu."""
logging.basicConfig(level=logging.DEBUG)
exported_symbol_map = get_exported_symbol_map('libicu_export.txt')
allowlisted_apis = set(exported_symbol_map.keys())
decl_filters = [StableDeclarationFilter()]
decl_filters.append(AllowlistedDeclarationFilter(allowlisted_apis))
parser = DeclaredFunctionsParser(decl_filters, [])
parser.set_ignored_include_dependency(IGNORED_INCLUDE_DEPENDENCY)
parser.parse()
includes = parser.header_includes
functions = parser.declared_functions
header_to_function_names = parser.header_to_function_names
# The shim has the allowlisted functions only
functions = [f for f in functions if f.name in allowlisted_apis]
headers_folder = android_path('external/icu/libicu/ndk_headers/unicode')
if os.path.exists(headers_folder):
shutil.rmtree(headers_folder)
os.mkdir(headers_folder)
with open(android_path('external/icu/libicu/src/shim.cpp'),
'w') as out_file:
out_file.write(generate_shim(functions, includes, SYMBOL_SUFFIX, 'libicu_shim.cpp.j2'))
with open(android_path('external/icu/libicu/libicu.map.txt'), 'w') as out_file:
data = {
'exported_symbol_map' : exported_symbol_map,
}
out_file.write(get_jinja_env().get_template('libicu.map.txt.j2').render(data))
# Process the C headers and put them into the ndk folder.
for src_path in parser.header_paths_to_copy:
basename = os.path.basename(src_path)
dst_path = os.path.join(headers_folder, basename)
exported_symbol_map_this_header = {
key: value for key, value in exported_symbol_map.items()
if key in header_to_function_names[basename]}
modify_func_declarations(src_path, dst_path, exported_symbol_map_this_header)
# Remove #include lines from the header files.
if basename in IGNORED_INCLUDE_DEPENDENCY:
remove_ignored_includes(dst_path, IGNORED_INCLUDE_DEPENDENCY[basename])
copy_header_only_files()
generate_cts_headers(allowlisted_apis)
add_ndk_required_doxygen_grouping()
# Apply documentation patches by the following shell script
subprocess.check_call(
[android_path('external/icu/tools/icu4c_srcgen/doc_patches/apply_patches.sh')])
print("Done. See the generated headers at libicu/ndk_headers/.")
if __name__ == '__main__':
main()