run_pika.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. #!/usr/bin/env python3
  2. """run_pika.py
  3. 单一职责:临时替换入口 + (可选)注入一个已有根目录模块 (--module) + 构建 + 运行 + 恢复 + 清理。
  4. 无任何历史兼容或多模式逻辑;所有路径清晰:
  5. 1. 复制脚本 -> main.py
  6. 2. 若 --module 则复制 <module>.pyi 与 C -> 源树 & 强制刷新 cmake
  7. 3. 预构建 (存在工具才执行)
  8. 4. make
  9. 5. 运行 (可 --no-run 跳过)
  10. 6. 恢复 main.py & 删除临时注入文件
  11. """
  12. from __future__ import annotations
  13. import argparse
  14. import os
  15. import shutil
  16. import subprocess
  17. import sys
  18. import datetime
  19. from pathlib import Path
  20. # --- 工具函数:打印文件最后 N 行 ---
  21. def print_last_lines(file_path: Path, n: int):
  22. try:
  23. with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
  24. lines = f.readlines()
  25. for line in lines[-n:]:
  26. print(line.rstrip('\n'))
  27. except Exception as e:
  28. print(f"[run_pika][WARN] 读取日志失败 {file_path}: {e}")
  29. PROJECT_ROOT = Path(__file__).parent.resolve()
  30. PIKA_LINUX_DIR = PROJECT_ROOT / "pikapython-linux"
  31. PIKA_DIR = PIKA_LINUX_DIR / "pikapython"
  32. TARGET_MAIN = PIKA_DIR / "main.py"
  33. LOG_ROOT = PROJECT_ROOT / "logs" / "run"
  34. ENV_VERBOSE = os.getenv("VERBOSE", "0") == "1"
  35. def log(msg: str):
  36. print(f"[run_pika] {msg}")
  37. def debug(msg: str):
  38. if ENV_VERBOSE:
  39. print(f"[run_pika][DEBUG] {msg}")
  40. class CommandError(RuntimeError):
  41. def __init__(self, cmd: list[str] | str, returncode: int):
  42. super().__init__(f"Command failed (exit {returncode}): {cmd}")
  43. self.cmd = cmd
  44. self.returncode = returncode
  45. def run_cmd(cmd, cwd: Path | None = None, log_file: Path | None = None, env: dict | None = None):
  46. """运行命令,将所有输出写入 log_file,不直接打印,失败时抛出异常。
  47. 返回:命令执行耗时(秒, float)。
  48. """
  49. if isinstance(cmd, (list, tuple)):
  50. display = " ".join(cmd)
  51. else:
  52. display = cmd
  53. debug(f"RUN: {display} (cwd={cwd})")
  54. start = datetime.datetime.now()
  55. with (open(log_file, "a", encoding="utf-8") if log_file else open(os.devnull, "w")) as lf:
  56. lf.write(f"\n[CMD] {display}\n")
  57. lf.flush()
  58. process = subprocess.Popen(
  59. cmd,
  60. cwd=str(cwd) if cwd else None,
  61. stdout=lf,
  62. stderr=subprocess.STDOUT,
  63. text=True,
  64. env=env or os.environ.copy(),
  65. )
  66. process.wait()
  67. dur = (datetime.datetime.now() - start).total_seconds()
  68. if process.returncode != 0:
  69. raise CommandError(display, process.returncode)
  70. return dur
  71. def prepare_logs_dir() -> Path:
  72. ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  73. run_dir = LOG_ROOT / ts
  74. run_dir.mkdir(parents=True, exist_ok=True)
  75. return run_dir
  76. def parse_args():
  77. parser = argparse.ArgumentParser(description="临时替换 main.py, 编译并运行 pikapython")
  78. parser.add_argument("script", help="要作为入口的 .py 文件 (相对或绝对路径)")
  79. parser.add_argument("--jobs", "-j", type=int, default=16, help="make 并行度 (默认 16)")
  80. parser.add_argument("--no-run", action="store_true", help="只编译不运行可执行文件")
  81. parser.add_argument("--tail-lines", type=int, default=5, help="终端仅显示编译/运行日志最后 N 行 (默认 5)")
  82. parser.add_argument("--fail-tail", type=int, default=20, help="失败时显示最后 N 行 (默认 20)")
  83. parser.add_argument("--module", dest="module", help="注入模块名 (目录名=模块名),仅复制(无自动生成)")
  84. parser.add_argument("--module-dir", dest="module_dir", help="模块目录的父目录(默认在项目根目录查找同名目录)")
  85. return parser.parse_args()
  86. # ---- 辅助输出格式化函数与失败路径 tail 处理 (需在 main 前定义) ----
  87. def print_section_header(title: str, lines: int):
  88. bar = '=' * 8
  89. print(f"\n{bar} {title} (last {lines}) {bar}\n")
  90. def print_section_footer():
  91. print("\n=============== END ===============")
  92. def tail_failure_logs(args, compile_log: Path, run_log: Path):
  93. tail_n = getattr(args, 'fail_tail', 20)
  94. log("失败摘要输出:")
  95. if compile_log.exists():
  96. print_section_header("COMPILE FAIL TAIL", tail_n)
  97. print_last_lines(compile_log, tail_n)
  98. print_section_footer()
  99. if run_log.exists():
  100. print_section_header("RUN FAIL TAIL", tail_n)
  101. print_last_lines(run_log, tail_n)
  102. print_section_footer()
  103. log("日志路径汇总 =>")
  104. if compile_log.exists():
  105. log(f"compile_log: {compile_log}")
  106. def main():
  107. args = parse_args()
  108. script_path = Path(args.script).resolve()
  109. if not script_path.exists() or script_path.suffix != '.py':
  110. log(f"[ERROR] 脚本不存在或不是 .py: {script_path}")
  111. return 2
  112. if not TARGET_MAIN.exists():
  113. log(f"[ERROR] 目标 main.py 不存在: {TARGET_MAIN}")
  114. return 2
  115. logs_dir = prepare_logs_dir()
  116. compile_log = logs_dir / 'compile.log'
  117. run_log = logs_dir / 'run.log'
  118. log(f"日志目录: {logs_dir}")
  119. backup_main = TARGET_MAIN.with_name(f"main.py.bak.{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}")
  120. log(f"备份 {TARGET_MAIN} -> {backup_main}")
  121. shutil.copy2(TARGET_MAIN, backup_main)
  122. log(f"复制 {script_path} -> {TARGET_MAIN}")
  123. shutil.copy2(script_path, TARGET_MAIN)
  124. injected = None
  125. if getattr(args, 'module', None):
  126. mod_name = args.module.strip()
  127. import re
  128. if not re.match(r'^[A-Za-z][A-Za-z0-9_]*$', mod_name):
  129. log(f"[ERROR] --module 名称非法: {mod_name}")
  130. return 2
  131. base_dir = PROJECT_ROOT if not getattr(args, 'module_dir', None) else Path(args.module_dir).resolve()
  132. template_dir = base_dir / mod_name
  133. pyi_src = template_dir / f"{mod_name}.pyi"
  134. c_src_list = list(template_dir.glob(f"{mod_name}_*.c"))
  135. if not template_dir.is_dir() or not pyi_src.exists() or not c_src_list:
  136. log(f"[ERROR] 根目录缺少合法模块目录: {template_dir} (需要 {mod_name}.pyi 与 {mod_name}_*.c)")
  137. return 2
  138. # 目标路径(临时)
  139. pyi_dst = PIKA_DIR / f"{mod_name}.pyi"
  140. c_dst_dir = PIKA_DIR / "pikascript-lib" / mod_name
  141. # 若之前遗留同名(可能上次异常未清理),先移除再复制,保持单路径
  142. if pyi_dst.exists():
  143. try:
  144. pyi_dst.unlink()
  145. log(f"[WARN] 移除遗留 {pyi_dst}")
  146. except Exception as e:
  147. log(f"[ERROR] 无法删除遗留 pyi: {e}")
  148. return 2
  149. if c_dst_dir.exists():
  150. try:
  151. shutil.rmtree(c_dst_dir)
  152. log(f"[WARN] 移除遗留目录 {c_dst_dir}")
  153. except Exception as e:
  154. log(f"[ERROR] 无法删除遗留目录: {e}")
  155. return 2
  156. try:
  157. shutil.copy2(pyi_src, pyi_dst)
  158. c_dst_dir.mkdir(parents=True, exist_ok=False)
  159. for cfile in c_src_list:
  160. shutil.copy2(cfile, c_dst_dir / cfile.name)
  161. # 确保 main.py 顶部 import
  162. main_code = TARGET_MAIN.read_text(encoding='utf-8')
  163. if f"import {mod_name}" not in main_code.splitlines()[:10]:
  164. TARGET_MAIN.write_text(f"import {mod_name}\n" + main_code, encoding='utf-8')
  165. injected = { 'mod_name': mod_name, 'pyi': pyi_dst, 'c_dir': c_dst_dir }
  166. log(f"已注入模块(复制): {mod_name}")
  167. except Exception as ie:
  168. log(f"[ERROR] 注入失败: {ie}")
  169. # 回滚已复制
  170. if pyi_dst.exists():
  171. try: pyi_dst.unlink()
  172. except Exception: pass
  173. if c_dst_dir.exists():
  174. try: shutil.rmtree(c_dst_dir)
  175. except Exception: pass
  176. return 2
  177. def restore():
  178. if backup_main.exists():
  179. shutil.move(str(backup_main), str(TARGET_MAIN))
  180. log("已恢复 main.py")
  181. if injected:
  182. # 清理复制的临时模块
  183. if injected['pyi'].exists():
  184. try: injected['pyi'].unlink()
  185. except Exception as e: log(f"[WARN] 删除临时 pyi 失败: {e}")
  186. if injected['c_dir'].exists():
  187. try: shutil.rmtree(injected['c_dir'])
  188. except Exception as e: log(f"[WARN] 删除临时模块目录失败: {e}")
  189. log(f"已清理注入模块 {injected['mod_name']}")
  190. try:
  191. build_dir = PIKA_LINUX_DIR / 'build'
  192. # 新策略:始终强制干净构建。删除已有 build/ 目录并重新 cmake。
  193. if build_dir.exists():
  194. log('检测到已有 build/ -> 强制删除以确保干净构建')
  195. try:
  196. shutil.rmtree(build_dir)
  197. log('已删除旧 build/ 目录')
  198. except Exception as e:
  199. log(f'[ERROR] 无法删除 build/ 目录: {e}')
  200. return 2
  201. # 同步清理自动生成的 pikascript-api/ 目录,确保绑定文件全量重新生成
  202. api_dir = PIKA_DIR / 'pikascript-api'
  203. if api_dir.exists():
  204. log('检测到已有 pikascript-api/ -> 强制删除以确保绑定干净')
  205. try:
  206. shutil.rmtree(api_dir)
  207. log('已删除旧 pikascript-api/ 目录')
  208. except Exception as e:
  209. log(f'[ERROR] 无法删除 pikascript-api/ 目录: {e}')
  210. return 2
  211. # 顺序调整:预构建工具需要在 cmake 之前生成/刷新绑定 & 源文件,再进行 cmake 配置。
  212. pre_tool = PIKA_DIR / 'rust-msc-latest-win10.exe'
  213. if pre_tool.exists():
  214. log('执行预构建 (cmake 前, 输出写入 compile.log)')
  215. run_cmd(['wine', pre_tool.name], cwd=PIKA_DIR, log_file=compile_log)
  216. else:
  217. log(f'[WARN] 预构建工具不存在: {pre_tool}')
  218. log('执行 cmake 初始化 (预构建后, 强制干净构建, 输出写入 compile.log)')
  219. build_dir.mkdir(parents=True, exist_ok=True)
  220. run_cmd(['cmake', '..'], cwd=build_dir, log_file=compile_log)
  221. log('执行 make (输出写入 compile.log)')
  222. run_cmd(['make', f'-j{args.jobs}'], cwd=build_dir, log_file=compile_log)
  223. exe = build_dir / 'pikapython'
  224. if not args.no_run:
  225. if exe.exists():
  226. log('运行 pikapython (输出写入 run.log)')
  227. run_cmd([str(exe)], cwd=PIKA_LINUX_DIR, log_file=run_log)
  228. else:
  229. log(f'[ERROR] 可执行文件不存在: {exe}')
  230. return 3
  231. except CommandError as e:
  232. log(f'[ERROR] {e}')
  233. tail_failure_logs(args, compile_log, run_log)
  234. return e.returncode if e.returncode != 0 else 1
  235. except Exception as e:
  236. log(f'[ERROR] 未处理异常: {e}')
  237. tail_failure_logs(args, compile_log, run_log)
  238. return 1
  239. finally:
  240. restore()
  241. log('执行完成,摘要输出:')
  242. print_section_header('COMPILE TAIL', args.tail_lines)
  243. print_last_lines(compile_log, args.tail_lines)
  244. print_section_footer()
  245. if not args.no_run and run_log.exists():
  246. print_section_header('RUN TAIL', args.tail_lines)
  247. print_last_lines(run_log, args.tail_lines)
  248. print_section_footer()
  249. log('日志路径汇总 =>')
  250. log(f'compile_log: {compile_log}')
  251. if run_log.exists() and not args.no_run:
  252. log(f'run_log: {run_log}')
  253. return 0
  254. if __name__ == '__main__':
  255. sys.exit(main())