已合并
fix: 修复genop重复添加算子分类CMake目录 #4194
fix: 修复genop重复添加算子分类CMake目录 #4194
已合并
陈思创建于 7月22日
1 个文件变更+326-277
@@ -1,277 +1,326 @@
1-# Copyright (c) 2025 Huawei Technologies Co., Ltd.1+# Copyright (c) 2025 Huawei Technologies Co., Ltd.
2-# This program is free software, you can redistribute it and/or modify it under the terms and conditions of2+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
3-# CANN Open Software License Agreement Version 2.0 (the "License").3+# CANN Open Software License Agreement Version 2.0 (the "License").
4-# Please refer to the License for details. You may not use this file except in compliance with the License.4+# Please refer to the License for details. You may not use this file except in compliance with the License.
5-# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,5+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
6-# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.6+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
7-# See LICENSE in the root of the software repository for the full text of the License.7+# See LICENSE in the root of the software repository for the full text of the License.
8- 8+ 
9-import argparse9+import argparse
10-import os10+import os
11-import shutil11+import shutil
12-import sys12+import sys
13-import re13+import re
14-import logging14+import logging
15- 15+ 
16- 16+ 
17-class OpGenerator:17+class OpGenerator:
18- """算子工程生成器"""18+ """算子工程生成器"""
19- 19+ 
20- def __init__(self, op_type, op_name, output_path, template_variant):20+ def __init__(self, op_type, op_name, output_path, template_variant):
21- self.op_type = op_type21+ self.op_type = op_type
22- self.op_name = op_name22+ self.op_name = op_name
23- self.output_path = output_path23+ self.output_path = output_path
24- self.template_name = "add_example"24+ self.template_name = "add_example"
25- 25+ 
26- self.script_dir = os.path.dirname(os.path.abspath(__file__))26+ self.script_dir = os.path.dirname(os.path.abspath(__file__))
27- if template_variant == "aicpu":27+ if template_variant == "aicpu":
28- self.template_dir = os.path.abspath(os.path.join(self.script_dir, 'template', 'add_example_aicpu'))28+ self.template_dir = os.path.abspath(
29- else:29+ os.path.join(self.script_dir, "template", "add_example_aicpu")
30- self.template_dir = os.path.abspath(os.path.join(self.script_dir, 'template', 'add_example'))30+ )
31- 31+ else:
32- self.dest_dir = os.path.abspath(os.path.join(self.output_path, self.op_type, self.op_name))32+ self.template_dir = os.path.abspath(
33- 33+ os.path.join(self.script_dir, "template", "add_example")
34- def run(self):34+ )
35- """执行生成流程"""35+ 
36- self._validate_inputs()36+ self.category_dir = os.path.abspath(
37- self._copy_template()37+ os.path.join(self.output_path, self.op_type)
38- self._rename_files()38+ )
39- self._replace_content()39+ self.category_existed = os.path.isdir(self.category_dir)
40- self._update_cmake_chain()40+ self.dest_dir = os.path.abspath(
41- logging.info(f"成功为 {self.op_type}/{self.op_name} 创建算子工程!")41+ os.path.join(self.output_path, self.op_type, self.op_name)
42- logging.info(f"工程路径: {self.dest_dir}")42+ )
43- 43+ 
44- def _validate_inputs(self):44+ def run(self):
45- """校验输入参数的有效性和安全性"""45+ """执行生成流程"""
46- if not self.op_type or not self.op_name:46+ self._validate_inputs()
47- raise ValueError("算子类型和算子名称均不能为空。")47+ self._copy_template()
48- 48+ self._rename_files()
49- if not re.match(r"^[a-zA-Z0-9_]+$", self.op_type):49+ self._replace_content()
50- raise ValueError(f"算子类型 '{self.op_type}' 包含无效字符。只允许字母、数字和下划线。")50+ self._update_cmake_chain()
51- 51+ logging.info(f"成功为 {self.op_type}/{self.op_name} 创建算子工程!")
52- if not re.match(r"^[a-zA-Z0-9_]+$", self.op_name):52+ logging.info(f"工程路径: {self.dest_dir}")
53- raise ValueError(f"算子名称 '{self.op_name}' 包含无效字符。只允许字母、数字和下划线。")53+ 
54- 54+ def _validate_inputs(self):
55- if os.path.exists(self.dest_dir):55+ """校验输入参数的有效性和安全性"""
56- raise FileExistsError(f"目标目录 '{self.dest_dir}' 已存在。")56+ if not self.op_type or not self.op_name:
57- 57+ raise ValueError("算子类型和算子名称均不能为空。")
58- def _copy_template(self):58+ 
59- """复制模板文件到目标目录"""59+ if not re.match(r"^[a-zA-Z0-9_]+$", self.op_type):
60- logging.info(f"使用模板在 '{self.dest_dir}' 创建算子工程...")60+ raise ValueError(
61- if not os.path.exists(self.template_dir):61+ f"算子类型 '{self.op_type}' 包含无效字符。只允许字母、数字和下划线。"
62- raise FileNotFoundError(f"找不到模板目录 '{self.template_dir}'。请确保模板目录存在。")62+ )
63- 63+ 
64- try:64+ if not re.match(r"^[a-zA-Z0-9_]+$", self.op_name):
65- shutil.copytree(self.template_dir, self.dest_dir)65+ raise ValueError(
66- except OSError as e:66+ f"算子名称 '{self.op_name}' 包含无效字符。只允许字母、数字和下划线。"
67- raise OSError(f"复制模板文件失败: {e}") from e67+ )
68- 68+ 
69- def _rename_files(self):69+ if os.path.exists(self.dest_dir):
70- """重命名文件和目录中的占位符"""70+ raise FileExistsError(f"目标目 '{self.dest_dir}' 已存在。")
71- for root, dirs, files in os.walk(self.dest_dir, topdown=False):71+ 
72- for name in files + dirs:72+ def _copy_template(self):
73- if self.template_name not in name:73+ """复制模板文件到目标目录"""
74- continue74+ logging.info(f"使用模板在 '{self.dest_dir}' 创建算子工程...")
75- 75+ if not os.path.exists(self.template_dir):
76- old_path = os.path.join(root, name)76+ raise FileNotFoundError(
77- new_name = name.replace(self.template_name, self.op_name)77+ f"找不到模板目录 '{self.template_dir}'。请确保模板目录存在。"
78- new_path = os.path.join(root, new_name)78+ )
79- try:79+ 
80- os.rename(old_path, new_path)80+ try:
81- except OSError as e:81+ shutil.copytree(self.template_dir, self.dest_dir)
82- raise OSError(f"重命名 '{old_path}' 到 '{new_path}' 失败: {e}") from e82+ except OSError as e:
83- 83+ raise OSError(f"复制模板文件失败: {e}") from e
84- @staticmethod84+ 
85- def _create_category_cmake(dir_path):85+ def _rename_files(self):
86- """为新的分类目录创建CMakeLists.txt,使用与math/conversion/random相同glob模式"""86+ """重命名文件和目录占位符"""
87- cmake_lines = [87+ for root, dirs, files in os.walk(self.dest_dir, topdown=False):
88- "file(GLOB SUBDIRECTORIES LIST_DIRECTORIES true",88+ for name in files + dirs:
89- " RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/*)",89+ if self.template_name not in name:
90- "foreach(SUBDIR ${SUBDIRECTORIES})",90+ continue
91- " if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}/CMakeLists.txt)",91+ 
92- " add_subdirectory(${SUBDIR})",92+ old_path = os.path.join(root, name)
93- " endif()",93+ new_name = name.replace(self.template_name, self.op_name)
94- "endforeach()",94+ new_path = os.path.join(root, new_name)
95- ]95+ try:
96- cmake_file = os.path.join(dir_path, "CMakeLists.txt")96+ os.rename(old_path, new_path)
97- if not os.path.exists(cmake_file):97+ except OSError as e:
98- with open(cmake_file, 'w', encoding='utf-8') as f:98+ raise OSError(
99- f.write("\n".join(cmake_lines) + "\n")99+ f"重命名 '{old_path}' 到 '{new_path}' 失败: {e}"
100- logging.info(f"Created CMakeLists.txt in {dir_path}")100+ ) from e
101- 101+ 
102- @staticmethod102+ @staticmethod
103- def _add_to_ops_category_list(category_list_file, child):103+ def _create_category_cmake(dir_path):
104- """新的分类添加到OPS_CATEGORY_LIST中"""104+ """新的分类目录创建CMakeLists.txt,使用与math/conversion/random相同的glob模式"""
105- with open(category_list_file, 'r', encoding='utf-8') as f:105+ cmake_lines = [
106- cat_content = f.read()106+ "file(GLOB SUBDIRECTORIES LIST_DIRECTORIES true",
107- updated = re.sub(107+ " RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/*)",
108- r'(set\s*\(\s*OPS_CATEGORY_LIST\s+[^)]*)',108+ "foreach(SUBDIR ${SUBDIRECTORIES})",
109- rf'\1\n "{child}"',109+ " if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIR}/CMakeLists.txt)",
110- cat_content110+ " add_subdirectory(${SUBDIR})",
111- )111+ " endif()",
112- with open(category_list_file, 'w', encoding='utf-8') as f:112+ "endforeach()",
113- f.write(updated)113+ ]
114- logging.info(f"Added '{child}' to OPS_CATEGORY_LIST in {category_list_file}")114+ cmake_file = os.path.join(dir_path, "CMakeLists.txt")
115- 115+ if not os.path.exists(cmake_file):
116- @staticmethod116+ with open(cmake_file, "w", encoding="utf-8") as f:
117- def _check_included_in_cmake(content, cmake_file, child):117+ f.write("\n".join(cmake_lines) + "\n")
118- """检查child是否已被CMakeLists.txt包含,返回(already_included, ops_category_file_path)"""118+ logging.info(f"Created CMakeLists.txt in {dir_path}")
119- already_included = bool(re.search(119+ 
120- rf'add_subdirectory\s*\(\s*{re.escape(child)}\s*[\s\)]', content120+ @staticmethod
121- ))121+ def _add_to_ops_category_list(category_list_file, child):
122- ops_category_file_path = None122+ """将新的分类添加到OPS_CATEGORY_LIST中"""
123- if already_included:123+ with open(category_list_file, "r", encoding="utf-8") as f:
124- return True, None124+ cat_content = f.read()
125- 125+ updated = re.sub(
126- cmake_dir = os.path.dirname(cmake_file)126+ r"(set\s*\(\s*OPS_CATEGORY_LIST\s+[^)]*)", rf'\1\n "{child}"', cat_content
127- files_to_check = [(content, None)]127+ )
128- for inc_match in re.finditer(r'include\s*\(\s*([^)\s]+)\s*\)', content):128+ with open(category_list_file, "w", encoding="utf-8") as f:
129- inc_path = inc_match.group(1)129+ f.write(updated)
130- if not os.path.isabs(inc_path):130+ logging.info(f"Added '{child}' to OPS_CATEGORY_LIST in {category_list_file}")
131- inc_path = os.path.join(cmake_dir, inc_path)131+ 
132- if os.path.exists(inc_path):132+ @staticmethod
133- try:133+ def _check_included_in_cmake(content, cmake_file, child):
134- with open(inc_path, 'r', encoding='utf-8') as inc_f:134+ """检查child是否已被CMakeLists.txt包含,返回(already_included, ops_category_file_path)"""
135- files_to_check.append((inc_f.read(), inc_path))135+ already_included = bool(
136- except (IOError, OSError):136+ re.search(rf"add_subdirectory\s*\(\s*{re.escape(child)}\s*[\s\)]", content)
137- pass137+ )
138- 138+ ops_category_file_path = None
139- for check_content, file_path in files_to_check:139+ if already_included:
140- match = re.search(r'set\s*\(\s*OPS_CATEGORY_LIST\s+([^)]+)\)', check_content)140+ return True, None
141- if not match:141+ 
142- continue142+ cmake_dir = os.path.dirname(cmake_file)
143- categories = [c.strip('"') for c in match.group(1).split()]143+ files_to_check = [(content, None)]
144- if child in categories:144+ for inc_match in re.finditer(r"include\s*\(\s*([^)\s]+)\s*\)", content):
145- return True, None145+ inc_path = inc_match.group(1)
146- if file_path is not None:146+ if not os.path.isabs(inc_path):
147- ops_category_file_path = file_path147+ inc_path = os.path.join(cmake_dir, inc_path)
148- 148+ if os.path.exists(inc_path):
149- return False, ops_category_file_path149+ try:
150- 150+ with open(inc_path, "r", encoding="utf-8") as inc_f:
151- @staticmethod151+ files_to_check.append((inc_f.read(), inc_path))
152- def _append_subdirectory(cmake_file, child):152+ except (IOError, OSError):
153- """在CMakeLists.txt末尾追加add_subdirectory(child)"""153+ pass
154- with open(cmake_file, 'a', encoding='utf-8') as f:154+ 
155- f.write(f"\nadd_subdirectory({child})\n")155+ for check_content, file_path in files_to_check:
156- logging.info(f"Added add_subdirectory({child}) to {cmake_file}")156+ match = re.search(
157- 157+ r"set\s*\(\s*OPS_CATEGORY_LIST\s+([^)]+)\)", check_content
158- def _update_cmake_chain(self):158+ )
159- """在父目录的CMakeLists.txt中添加add_subdirectory,确保构建系统能找到新目录"""159+ if not match:
160- current = self.dest_dir160+ continue
161- project_root = os.path.abspath(self.output_path)161+ categories = [c.strip('"') for c in match.group(1).split()]
162- 162+ if child in categories:
163- while True:163+ return True, None
164- parent = os.path.dirname(current)164+ if file_path is not None:
165- child = os.path.basename(current)165+ ops_category_file_path = file_path
166- parent_abs = os.path.abspath(parent)166+ 
167- 167+ return False, ops_category_file_path
168- cmake_file = os.path.join(parent, "CMakeLists.txt")168+ 
169- if os.path.exists(cmake_file):169+ @staticmethod
170- with open(cmake_file, 'r', encoding='utf-8') as f:170+ def _append_subdirectory(cmake_file, child):
171- content = f.read()171+ """在CMakeLists.txt末尾追加add_subdirectory(child)"""
172- 172+ with open(cmake_file, "a", encoding="utf-8") as f:
173- already_included, ops_category_file = self._check_included_in_cmake(173+ f.write(f"\nadd_subdirectory({child})\n")
174- content, cmake_file, child174+ logging.info(f"Added add_subdirectory({child}) to {cmake_file}")
175- )175+ 
176- uses_glob = bool(re.search(r'file\s*\(\s*GLOB', content))176+ def _update_cmake_chain(self):
177- 177+ """在父目录的CMakeLists.txt中添加add_subdirectory,确保构建系统能找到新目录"""
178- if not already_included and not uses_glob:178+ current = self.dest_dir
179- if ops_category_file:179+ project_root = os.path.abspath(self.output_path)
180- self._add_to_ops_category_list(ops_category_file, child)180+ 
181- else:181+ while True:
182- self._append_subdirectory(cmake_file, child)182+ parent = os.path.dirname(current)
183- else:183+ child = os.path.basename(current)
184- if parent_abs != project_root and os.path.isdir(parent):184+ parent_abs = os.path.abspath(parent)
185- self._create_category_cmake(parent)185+ 
186- 186+ cmake_file = os.path.join(parent, "CMakeLists.txt")
187- if parent_abs == project_root:187+ if os.path.exists(cmake_file):
188- break188+ with open(cmake_file, "r", encoding="utf-8") as f:
189- 189+ content = f.read()
190- current = parent190+ 
191- 191+ already_included, ops_category_file = self._check_included_in_cmake(
192- def _replace_content_in_file(self, file_path, replacements):192+ content, cmake_file, child
193- """Helper to replace content in a single file."""193+ )
194- try:194+ uses_glob = bool(re.search(r"file\s*\(\s*GLOB", content))
195- with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:195+ 
196- content = f.read()196+ if not already_included and not uses_glob:
197- except (IOError, OSError) as e:197+ if ops_category_file:
198- logging.warning(f"读取文件 '{file_path}' 失败: {e}")198+ self._add_to_ops_category_list(ops_category_file, child)
199- return199+ else:
200- 200+ self._append_subdirectory(cmake_file, child)
201- original_content = content201+ else:
202- for old, new in replacements.items():202+ if parent_abs != project_root and os.path.isdir(parent):
203- content = content.replace(old, new)203+ self._create_category_cmake(parent)
204- 204+ 
205- if content == original_content:205+ # An existing category was already connected to its ancestors before
206- return206+ # this operator was generated. Only its own CMake file may need an
207- 207+ # update; walking farther would risk adding the category twice when
208- try:208+ # an ancestor includes it indirectly (for example via a CMake list).
209- with open(file_path, 'w', encoding='utf-8') as f:209+ if self.category_existed and parent_abs == self.category_dir:
210- f.write(content)210+ break
211- except (IOError, OSError) as e:211+ 
212- logging.warning(f"写入文件 '{file_path}' 失败: {e}")212+ if parent_abs == project_root:
213- 213+ break
214- def _replace_content(self):214+ 
215- """替换文件内容中的占位符"""215+ current = parent
216- op_name_capitalized = ''.join(word.capitalize() for word in self.op_name.split('_'))216+ 
217- template_name_capitalized = ''.join(word.capitalize() for word in self.template_name.split('_'))217+ def _replace_content_in_file(self, file_path, replacements):
218- 218+ """Helper to replace content in a single file."""
219- replacements = {219+ try:
220- self.template_name: self.op_name,220+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
221- self.template_name.upper(): self.op_name.upper(),221+ content = f.read()
222- template_name_capitalized: op_name_capitalized,222+ except (IOError, OSError) as e:
223- "add_example": self.op_name,223+ logging.warning(f"读取文件 '{file_path}' 失败: {e}")
224- }224+ return
225- for root, _, files in os.walk(self.dest_dir):225+ 
226- for file in files:226+ original_content = content
227- if file.endswith(('.pyc', '.pyo')):227+ for old, new in replacements.items():
228- continue228+ content = content.replace(old, new)
229- 229+ 
230- file_path = os.path.join(root, file)230+ if content == original_content:
231- self._replace_content_in_file(file_path, replacements)231+ return
232- 232+ 
233- 233+ try:
234-def execute(args):234+ with open(file_path, "w", encoding="utf-8") as f:
235- """根据命令行参数执行算子生成"""235+ f.write(content)
236- generator = OpGenerator(236+ except (IOError, OSError) as e:
237- op_type=args.op_type,237+ logging.warning(f"写入文件 '{file_path}' 失败: {e}")
238- op_name=args.op_name,238+ 
239- output_path=args.output_path,239+ def _replace_content(self):
240- template_variant=args.template_variant240+ """替换文件内容中的占位符"""
241- )241+ op_name_capitalized = "".join(
242- generator.run()242+ word.capitalize() for word in self.op_name.split("_")
243- 243+ )
244- 244+ template_name_capitalized = "".join(
245-def register_parser(subparsers):245+ word.capitalize() for word in self.template_name.split("_")
246- """为 opgen 命令注册解析器。"""246+ )
247- parser_opgen = subparsers.add_parser('opgen', help='生成项目骨架')247+ 
248- parser_opgen.add_argument('--op_type', '-t', required=True, help='算子分类,例如 math')248+ replacements = {
249- parser_opgen.add_argument('--op_name', '-n', required=True, help='新算子的名称,例如 asinh')249+ self.template_name: self.op_name,
250- parser_opgen.add_argument('--output_path', '-p', default='.', help='生成工程的根路径')250+ self.template_name.upper(): self.op_name.upper(),
251- parser_opgen.add_argument(251+ template_name_capitalized: op_name_capitalized,
252- '--template_variant', '-v',252+ "add_example": self.op_name,
253- choices=['default', 'aicpu'], default='default', help='选择模板变种'253+ }
254- )254+ for root, _, files in os.walk(self.dest_dir):
255- parser_opgen.set_defaults(func=execute)255+ for file in files:
256- 256+ if file.endswith((".pyc", ".pyo")):
257- 257+ continue
258-def main():258+ 
259- """主函数,用于独立执行"""259+ file_path = os.path.join(root, file)
260- logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s', stream=sys.stdout)260+ self._replace_content_in_file(file_path, replacements)
261- parser = argparse.ArgumentParser(description="生成项目骨架")261+ 
262- 262+ 
263- parser.add_argument('--op_type', '-t', required=True, help='算子分类,例如 math')263+def execute(args):
264- parser.add_argument('--op_name', '-n', required=True, help='新算子的名称,例如 asinh')264+ """根据命令行参数执行算子生成"""
265- parser.add_argument('--output_path', '-p', default='.', help='生成工程的根路径')265+ generator = OpGenerator(
266- parser.add_argument('--template_variant', '-v', choices=['default', 'aicpu'], default='default', help='选择模板变种')266+ op_type=args.op_type,
267- 267+ op_name=args.op_name,
268- args = parser.parse_args()268+ output_path=args.output_path,
269- 269+ template_variant=args.template_variant,
270- try:270+ )
271- execute(args)271+ generator.run()
272- except Exception as e:272+ 
273- logging.error(f"发生非预期的错误,退出。错误信息: {e}")273+ 
274- sys.exit(1)274+def register_parser(subparsers):
275- 275+ """为 opgen 命令注册解析器。"""
276-if __name__ == "__main__":276+ parser_opgen = subparsers.add_parser("opgen", help="生成项目骨架")
277- main()277+ parser_opgen.add_argument(
278+ "--op_type", "-t", required=True, help="算子分类,例如 math"
279+ )
280+ parser_opgen.add_argument(
281+ "--op_name", "-n", required=True, help="新算子的名称,例如 asinh"
282+ )
283+ parser_opgen.add_argument(
284+ "--output_path", "-p", default=".", help="生成工程的根路径"
285+ )
286+ parser_opgen.add_argument(
287+ "--template_variant",
288+ "-v",
289+ choices=["default", "aicpu"],
290+ default="default",
291+ help="选择模板变种",
292+ )
293+ parser_opgen.set_defaults(func=execute)
294+ 
295+ 
296+def main():
297+ """主函数,用于独立执行"""
298+ logging.basicConfig(
299+ level=logging.INFO, format="%(levelname)s: %(message)s", stream=sys.stdout
300+ )
301+ parser = argparse.ArgumentParser(description="生成项目骨架")
302+ 
303+ parser.add_argument("--op_type", "-t", required=True, help="算子分类,例如 math")
304+ parser.add_argument(
305+ "--op_name", "-n", required=True, help="新算子的名称,例如 asinh"
306+ )
307+ parser.add_argument("--output_path", "-p", default=".", help="生成工程的根路径")
308+ parser.add_argument(
309+ "--template_variant",
310+ "-v",
311+ choices=["default", "aicpu"],
312+ default="default",
313+ help="选择模板变种",
314+ )
315+ 
316+ args = parser.parse_args()
317+ 
318+ try:
319+ execute(args)
320+ except Exception as e:
321+ logging.error(f"发生非预期的错误,退出。错误信息: {e}")
322+ sys.exit(1)
323+ 
324+ 
325+if __name__ == "__main__":
326+ main()