Files
interface_sdk_c/build-tools/parse_native_headers.py
T
咚隆东 d15de4105e fix: 修复systemapi裁剪脚本的7个Bug并增强鲁棒性
- 清理裁剪残留的孤立分号行(;、;;、; ;)
- 修复 Windows 中文系统 subprocess 编码问题(GBK → utf-8 手动解码)
- 不再清理空条件编译块,保留#ifdef/#endif空壳

Signed-off-by: 咚隆东 <renlian1@h-partners.com>
2026-05-19 16:11:11 +08:00

487 lines
18 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2025 Huawei Device Co., Ltd.
# 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.
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)
# 先匹配带@since的注释块
self.since_comment_pattern = re.compile(
r'(?P<comment>/\*\*[^\*]*(?:\*(?!/)[^\*]*)*@since\s+\S+[^\*]*(?:\*(?!/)[^\*]*)*\*/)' # 带@since的完整注释块
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)' # 排除typedef和含inline的声明
r'[\w\s\*]+?\b\w+\s*\(' # 匹配返回值+函数名+左括号(支持指针类型)
r'[^;]*?' # 匹配任意参数(包括__restrict、const等修饰符),直到遇到分号
r'\)\s*;' # 右括号+分号(确保是纯声明,非函数体)
, re.MULTILINE | re.DOTALL
)
# @since 支持:
# - 纯数字: 22
# - 三段点分式: 26.0.1
# - 三段+括号: 5.0.3(15)
self.since_pattern = re.compile(
r'@since\s+('
r'\d+\.\d+\.\d+\(\d+\)|' # x.y.z(n)
r'\d+\.\d+\.\d+|' # x.y.z
r'\d+' # x
r')(?=\s|$|[*/])' # 后面可以有空白、行尾、或注释符号
)
# @deprecated since仅支持数字格式
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
# 处理所有带@since注释块关联的字段/接口
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注释块关联的字段或接口"""
# 找到所有带@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 # 无@since版本,跳过
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:]
# 判断是否为extern字段
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)
# 若未添加成功(未匹配到保护宏),在第一个#include前插入
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(取括号内为主版本)
"""
# 1. 纯数字格式:如 "26"
if since_version.isdigit():
major, minor, patch = since_version, '0', '0'
# 2. 三段+括号格式:如 "5.0.3(15)"
elif (dist_match := re.fullmatch(r'(\d+)\.(\d+)\.(\d+)\((\d+)\)', since_version)):
# 使用括号内的版本作为主 API Level
major = dist_match.group(4)
minor = patch = '0'
# 3. 标准三段式:如 "26.0.0" 或 "26.0.1"
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)
# 4. 不支持的格式抛异常
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:
# 使用当前Python解释器运行脚本
result = subprocess.run([
sys.executable, # 使用当前Python解释器
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'
# 第一步:如果 sdk-build-public=true,就地删除 @systemapi 声明
if sdk_build_public:
run_systemapi_cleanup(args.input)
# 第二步:校验 API 版本号
check_api_version_method(args)
# 第三步:添加版本宏(就地修改)
add_files_to_process(args.input)
if __name__ == '__main__':
sys.exit(main())