mirror of
https://github.com/openharmony/interface_sdk_c.git
synced 2026-08-24 14:32:50 -04:00
d15de4105e
- 清理裁剪残留的孤立分号行(;、;;、; ;) - 修复 Windows 中文系统 subprocess 编码问题(GBK → utf-8 手动解码) - 不再清理空条件编译块,保留#ifdef/#endif空壳 Signed-off-by: 咚隆东 <renlian1@h-partners.com>
487 lines
18 KiB
Python
Executable File
487 lines
18 KiB
Python
Executable File
#!/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())
|