#!/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/\*\*[^\*]*(?:\*(?!/)[^\*]*)*@since\s+\S+[^\*]*(?:\*(?!/)[^\*]*)*\*/)' # 带@since的完整注释块 r'(?P[\s\n\r]*)' # 注释块后仅允许空格/制表符(无空行) r'(?P\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())