import argparse
import re
import shutil
import sys
import os
import subprocess
import tempfile
from typing import Optional, Tuple, Pattern, List
_DIR_NOT_PROCESS = [
"aarch64-linux-ohos",
"arm-linux-ohos",
"asm-generic",
"asm-loongarch",
"asm-mips",
"asm-riscv",
"EGL",
"GLES2",
"GLES3",
"i686-linux-ohos",
"linux",
"x86_64-linux-ohos",
"TEEKit"
]
class HeaderProcessor:
MAX_LINE_LENGTH = 120
def __init__(self):
self.guard_pattern = re.compile(r'^#define\s+([A-Za-z0-9_]+_H(_)?)\s*$', re.MULTILINE)
self.since_comment_pattern = re.compile(
r'(?P<comment>/\*\*[^\*]*(?:\*(?!/)[^\*]*)*@since\s+\S+[^\*]*(?:\*(?!/)[^\*]*)*\*/)'
r'(?P<whitespace>[\s\n\r]*)'
r'(?P<code_start>\w+|extern)',
re.MULTILINE | re.DOTALL
)
self.extern_field_start_pattern = re.compile(r'^extern\s+(?!\"C\"\s*\{)[^(){}\[\]]*?;', re.DOTALL)
self.func_interface_start_pattern = re.compile(
r'^(?!typedef|.*inline)'
r'[\w\s\*]+?\b\w+\s*\('
r'[^;]*?'
r'\)\s*;'
, re.MULTILINE | re.DOTALL
)
self.since_pattern = re.compile(
r'@since\s+('
r'\d+\.\d+\.\d+\(\d+\)|'
r'\d+\.\d+\.\d+|'
r'\d+'
r')(?=\s|$|[*/])'
)
self.deprecated_pattern = re.compile(r'@deprecated since\s+(\d+)')
self.strict_comment_pattern = re.compile(r'^\s*\*/\s*$', re.MULTILINE)
def process_file(self, file_path: str) -> None:
"""处理单个头文件"""
try:
with open(file_path, 'r+', encoding='utf-8') as f:
content = f.read()
modified, has_changes = self._process_content(content)
if has_changes:
f.seek(0)
f.write(modified)
f.truncate()
print(f"modify success: {file_path}")
else:
print(f"No modifications are needed: {file_path}")
except Exception as e:
print(f"Processing failed. {file_path}: {str(e)}")
def _process_content(self, content: str) -> tuple[str, bool]:
"""处理文件内容逻辑:先处理注释块关联的字段/接口,最后添加版本引用"""
original_content = content
content = self._process_since_comment_related_code(content)
has_changes = content != original_content
if has_changes:
content = self._add_version_include(content)
return content, has_changes
def _process_since_comment_related_code(self, content: str) -> str:
"""处理带@since注释块关联的字段或接口"""
comment_matches = list(self.since_comment_pattern.finditer(content))
for match in reversed(comment_matches):
comment_block = match.group('comment')
whitespace = match.group('whitespace')
code_start_idx = match.start('code_start')
code_block, code_end_idx = self._extract_single_code_block(content, code_start_idx)
if not code_block:
continue
since_version = self._extract_tag(comment_block, self.since_pattern)
if not since_version:
continue
deprecated_version = self._extract_tag(comment_block, self.deprecated_pattern) or '0'
version_macro = self._get_version_macro(since_version, deprecated_version)
formatted_code = self._format_declaration(code_block.rstrip(';'), version_macro) + ';'
old_segment = comment_block + whitespace + content[code_start_idx:code_end_idx]
new_segment = comment_block + whitespace + formatted_code
content = content.replace(old_segment, new_segment, 1)
return content
def _extract_single_code_block(self, content: str, start_idx: int) -> Tuple[Optional[str], int]:
"""从start_idx提取第一个完整代码块(字段或接口),仅取紧接注释块的第一个"""
remaining_content = content[start_idx:]
field_match = self.extern_field_start_pattern.match(remaining_content)
if field_match:
field_content = field_match.group(0).strip()
end_idx = start_idx + field_match.end()
return (field_content, end_idx)
interface_match = self.func_interface_start_pattern.match(remaining_content)
if interface_match:
interface_content = interface_match.group(0).strip()
end_idx = start_idx + interface_match.end()
if interface_content.count('(') == interface_content.count(')'):
return (interface_content, end_idx)
return (None, -1)
def _add_version_include(self, content: str) -> str:
"""在头文件保护宏后添加#include "info/application_target_sdk_version.h",无保护宏则在第一个#include前添加"""
target_include = '#include "info/application_target_sdk_version.h"'
def replacer(match):
return f"{match.group(0)}\n{target_include}"
content_with_include = self.guard_pattern.sub(replacer, content, count=1)
if target_include not in content_with_include:
first_include_match = re.search(r'^\s*#include', content_with_include, re.MULTILINE)
if first_include_match:
insert_pos = first_include_match.start()
content_with_include = (
content_with_include[:insert_pos]
+ f"{target_include}\n"
+ content_with_include[insert_pos:]
)
return content_with_include
def _get_version_macro(self, since_version: str, deprecated_version: str) -> str:
"""
根据 @since 的值生成对应的 API 可用性宏。
支持格式:
- 纯数字:15 → introduced=15.0.0
- 三段点分式:15.0.1 → introduced=15.0.1
- 三段+括号:5.0.3(15) → introduced=15.0.0(取括号内为主版本)
"""
if since_version.isdigit():
major, minor, patch = since_version, '0', '0'
elif (dist_match := re.fullmatch(r'(\d+)\.(\d+)\.(\d+)\((\d+)\)', since_version)):
major = dist_match.group(4)
minor = patch = '0'
elif (triple_match := re.fullmatch(r'(\d+)\.(\d+)\.(\d+)', since_version)):
major = triple_match.group(1)
minor = triple_match.group(2)
patch = triple_match.group(3)
else:
raise ValueError(f"Invalid @since version format: '{since_version}'. "
f"Expected formats: X, X.Y.Z, or X.Y.Z(N).")
return f'__attribute__((__availability__(ohos, introduced={major}.{minor}.{patch})))'
def _format_declaration(self, decl: str, macro: str) -> str:
"""格式化声明:根据行长度决定单行或换行显示,保留原缩进"""
last_line = decl.split('\n')[-1]
indent_length = len(last_line) - len(last_line.lstrip())
indent = ' ' * indent_length
combined = f"{decl.rstrip()} {macro}"
if len(combined) <= self.MAX_LINE_LENGTH:
return combined
return f"{decl.rstrip()}\n{indent}{macro}"
def _extract_tag(self, text: str, pattern: re.Pattern) -> Optional[str]:
"""从文本中提取指定标签的值(如@since、@deprecated since)"""
tag_match = pattern.search(text)
return tag_match.group(1) if tag_match else None
def _is_blacklist_dir(rel_path: str) -> bool:
"""判断目录是否需要处理(仅检查第一级子目录)"""
first_level_dir = rel_path.split(os.sep)[0]
return first_level_dir in _DIR_NOT_PROCESS
def _process_root_files(root: str, processor: HeaderProcessor) -> None:
"""处理根目录下的文件"""
for file_name in os.listdir(root):
if file_name.endswith('.h'):
processor.process_file(os.path.join(root, file_name))
def _process_subdir_files(root: str, processor: HeaderProcessor) -> None:
"""处理子目录下的所有.h文件"""
for file_name in os.listdir(root):
if file_name.endswith('.h'):
processor.process_file(os.path.join(root, file_name))
def check_file_api_version(root: str, sdk_api_version: int) -> List[str]:
violation_files = []
since_pattern = re.compile(r'@since\s+([^\s\n\r]+)')
for file_name in os.listdir(root):
if not file_name.endswith('.h'):
continue
file_path = os.path.join(root, file_name)
file_results = process_single_file(file_path, since_pattern, sdk_api_version)
if file_results:
violation_files.append(file_path)
return violation_files
def process_single_file(file_path: str, since_pattern: Pattern, sdk_api_version: int) -> Optional[List[str]]:
"""处理单个文件,返回匹配的since值列表"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
print(f"读取文件失败")
return None
matches = since_pattern.findall(content)
if not matches:
return None
if _has_violation_version(matches, sdk_api_version):
return matches
return None
def _has_violation_version(matches: List[str], sdk_api_version: int) -> bool:
"""检查是否存在违规版本号"""
for since_value in matches:
if not since_value.isdigit():
continue
if int(since_value) > sdk_api_version:
return True
return False
def run_capi_parser(input_path, sdk_api_version):
"""
调用Python脚本并传递--input参数
Args:
script_path: 要调用的Python脚本路径
input_path: 输入文件/文件夹路径
"""
current_script_path = os.path.abspath(__file__)
script_path = os.path.join(os.path.dirname((current_script_path)), "parse_interfaces_since.py")
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
for path in input_path:
f.write(f"{path}\n")
input_list_filename = f.name
try:
result = subprocess.run([
sys.executable,
script_path,
'--input', str(input_list_filename),
'--sdk-api-version', str(sdk_api_version)
], capture_output=True, text=True, check=True)
if result.stdout:
print(f"API校验成功: {result.stdout}")
except subprocess.CalledProcessError as e:
err_msg = f"校验API版本失败,返回: {e.stderr}"
raise ValueError(err_msg)
except FileNotFoundError:
err_msg = f"文件未找到: {script_path}"
raise ValueError(err_msg)
except Exception as e:
err_msg = f"执行过程中发生错误: {e}"
raise ValueError(err_msg)
finally:
try:
if os.path.exists(input_list_filename):
os.unlink(input_list_filename)
print(f"临时文件已清理: {input_list_filename}")
except Exception as cleanup_error:
print(f"清理临时文件时出错: {cleanup_error}")
def _copy_processed_back(temp_dir, input_dir):
src = os.path.join(temp_dir, os.path.basename(input_dir.rstrip(os.sep)))
if not os.path.exists(src):
src = temp_dir
dst = input_dir
for item in os.listdir(src):
s = os.path.join(src, item)
d = os.path.join(dst, item)
if os.path.isdir(s):
if os.path.exists(d):
shutil.rmtree(d)
shutil.copytree(s, d)
else:
shutil.copy2(s, d)
def run_systemapi_cleanup(input_dir):
"""
调用 header_processor.py 删除带 @systemapi 标签的 API 声明。
采用安全策略:先复制到临时目录处理,成功后覆盖回原目录。
Args:
input_dir: 头文件目录(处理完后就地覆盖)
"""
current_script_path = os.path.abspath(__file__)
script_path = os.path.join(os.path.dirname(current_script_path), "header_processor.py")
if not os.path.exists(script_path):
raise FileNotFoundError(f"header_processor.py not found: {script_path}")
temp_dir = tempfile.mkdtemp(prefix='header_systemapi_')
try:
result = subprocess.run([
sys.executable,
script_path,
'-i', str(input_dir),
'-o', str(temp_dir),
], capture_output=True, text=True, encoding='utf-8', errors='replace')
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
if result.returncode != 0:
raise RuntimeError(f"systemapi cleanup failed (exit code {result.returncode})")
_copy_processed_back(temp_dir, input_dir)
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def add_files_to_process(header_path, recursive=True):
"""遍历目录并处理符合条件的头文件"""
abs_root_dir = os.path.abspath(header_path)
header_processor = HeaderProcessor()
for root, dirs, files in os.walk(abs_root_dir, topdown=True):
rel_dir_path = os.path.relpath(root, abs_root_dir)
if root == abs_root_dir:
_process_root_files(root, header_processor)
continue
if _is_blacklist_dir(rel_dir_path):
dirs[:] = []
print(f"the dir is in blacklists: {root}")
continue
_process_subdir_files(root, header_processor)
def check_api_version_method(options):
"""遍历目录并检查API LEVEL"""
abs_root_dir = os.path.abspath(options.input)
sdk_api_version = options.sdk_api_version
if not sdk_api_version.isdigit():
raise ValueError(f"api version must be digits!")
sdk_version_num = int(sdk_api_version)
if sdk_version_num == 0:
print("无需校验!")
return
violation_files = []
for root, dirs, files in os.walk(abs_root_dir, topdown=True):
violation_files.extend(check_file_api_version(root, sdk_version_num))
if violation_files:
run_capi_parser(violation_files, options.sdk_api_version)
def main():
parser = argparse.ArgumentParser(description="Process native header files to add version macros.")
parser.add_argument('--input', required=True, help="头文件目录路径(就地修改)")
parser.add_argument('--sdk-api-version', required=True, help="当前构建API版本")
parser.add_argument('--sdk-build-public', default='false', help="是否公共SDK构建(true则裁剪@systemapi)")
args = parser.parse_args()
if not os.path.exists(args.input):
print(f"header path does not exist -> {args.input}")
exit(1)
sdk_build_public = args.sdk_build_public.lower() == 'true'
if sdk_build_public:
run_systemapi_cleanup(args.input)
check_api_version_method(args)
add_files_to_process(args.input)
if __name__ == '__main__':
sys.exit(main())