version_generator.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 版本生成器
  5. 根据 .github/versions.json 文件生成不同版本的文档
  6. 支持多分支文档生成
  7. """
  8. import os
  9. import sys
  10. import shutil
  11. import subprocess
  12. import yaml
  13. import json
  14. from pathlib import Path
  15. def load_versions():
  16. """从 versions.json 文件加载版本列表"""
  17. # 尝试多个可能的路径
  18. possible_paths = [
  19. Path("../../.github/versions.json") # 从docs/source运行
  20. ]
  21. versions_file = None
  22. for path in possible_paths:
  23. if path.exists():
  24. versions_file = path
  25. break
  26. if not versions_file:
  27. print(f"错误: 版本文件不存在,尝试的路径:")
  28. for path in possible_paths:
  29. print(f" - {path.absolute()}")
  30. return []
  31. try:
  32. with open(versions_file, 'r', encoding='utf-8') as f:
  33. config = json.load(f)
  34. versions = config.get('versions', [])
  35. print(f"加载版本配置: {[v['name'] for v in versions]}")
  36. return versions
  37. except Exception as e:
  38. print(f"错误: 无法解析版本配置文件: {e}")
  39. return []
  40. def get_branch_name():
  41. """获取当前分支名称"""
  42. try:
  43. result = subprocess.run(
  44. ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
  45. capture_output=True, text=True, check=True
  46. )
  47. return result.stdout.strip()
  48. except subprocess.CalledProcessError:
  49. print("警告: 无法获取当前分支名称")
  50. return None
  51. def checkout_branch(branch_name):
  52. """切换到指定分支"""
  53. try:
  54. print(f"切换到分支: {branch_name}")
  55. subprocess.run(['git', 'checkout', branch_name], check=True)
  56. return True
  57. except subprocess.CalledProcessError as e:
  58. print(f"错误: 无法切换到分支 {branch_name}: {e}")
  59. return False
  60. def get_branch_versions():
  61. """获取当前分支对应的版本列表"""
  62. current_branch = get_branch_name()
  63. if not current_branch:
  64. return []
  65. versions = load_versions()
  66. branch_versions = []
  67. for version in versions:
  68. # 检查版本是否对应当前分支
  69. if version['branch'] == current_branch:
  70. branch_versions.append(version)
  71. elif version['branch'] == 'master' and current_branch == 'main':
  72. # 处理主分支名称差异
  73. branch_versions.append(version)
  74. print(f"当前分支 {current_branch} 对应的版本: {[v['name'] for v in branch_versions]}")
  75. return branch_versions
  76. def build_version_docs(version_config, branch_name=None):
  77. """为指定版本构建文档"""
  78. version_name = version_config['name']
  79. print(f"\n开始构建版本 {version_name} 的文档...")
  80. # 确定对应的分支名称
  81. if branch_name is None:
  82. branch_name = version_config['branch']
  83. # 创建版本输出目录
  84. output_dir = Path(f"_build/html/{version_config['url_path']}")
  85. # 清理输出目录
  86. if output_dir.exists():
  87. shutil.rmtree(output_dir)
  88. output_dir.mkdir(parents=True, exist_ok=True)
  89. try:
  90. # 检查是否在GitHub Actions环境中
  91. is_github_actions = os.environ.get('GITHUB_ACTIONS') == 'true'
  92. # 保存当前分支名称
  93. current_branch = get_branch_name()
  94. print(f"当前分支: {current_branch}")
  95. # 如果需要切换到不同分支
  96. if branch_name != current_branch:
  97. print(f"切换到分支: {branch_name}")
  98. # 在切换分支前,先stash本地更改
  99. try:
  100. # 检查是否有未提交的更改
  101. result = subprocess.run(['git', 'status', '--porcelain'],
  102. capture_output=True, text=True, check=True)
  103. if result.stdout and result.stdout.strip():
  104. print("检测到本地更改,先stash保存...")
  105. subprocess.run(['git', 'stash', 'push', '-m', f'构建前保存 - {current_branch}'], check=True)
  106. print("已保存本地更改到stash")
  107. else:
  108. print("没有未提交的更改")
  109. except subprocess.CalledProcessError as e:
  110. print(f"警告: 检查Git状态失败: {e}")
  111. # 清理可能冲突的文件
  112. js_file = Path("_static/version_menu.js")
  113. bak_file = js_file.with_suffix('.js.bak')
  114. # 删除备份文件,避免冲突
  115. if bak_file.exists():
  116. try:
  117. bak_file.unlink()
  118. print(f"已删除备份文件: {bak_file}")
  119. except Exception as e:
  120. print(f"警告: 无法删除备份文件 {bak_file}: {e}")
  121. # 恢复 version_menu.js 文件(如果存在备份)
  122. if js_file.exists():
  123. try:
  124. # 重置文件到Git状态
  125. subprocess.run(['git', 'checkout', '--', str(js_file)], check=True)
  126. print(f"已重置 {js_file} 到Git状态")
  127. except subprocess.CalledProcessError as e:
  128. print(f"警告: 无法重置 {js_file}: {e}")
  129. # 清理构建目录以避免冲突
  130. build_dir = Path("_build")
  131. if build_dir.exists():
  132. try:
  133. shutil.rmtree(build_dir)
  134. print(f"已清理构建目录: {build_dir}")
  135. except PermissionError as e:
  136. print(f"警告: 无法清理构建目录 {build_dir}: {e}")
  137. print("尝试使用 git clean 清理...")
  138. try:
  139. subprocess.run(['git', 'clean', '-fd'], check=True)
  140. print("已使用 git clean 清理未跟踪文件")
  141. except subprocess.CalledProcessError:
  142. print("警告: git clean 也失败了,继续执行...")
  143. # 清理其他可能冲突的目录
  144. for dir_path in ['basic', 'component', 'driver', 'protocol', 'start']:
  145. dir_obj = Path(dir_path)
  146. if dir_obj.exists():
  147. try:
  148. shutil.rmtree(dir_obj)
  149. print(f"已清理目录: {dir_path}")
  150. except Exception as e:
  151. print(f"警告: 无法清理目录 {dir_path}: {e}")
  152. # 切换到目标分支
  153. subprocess.run(['git', 'checkout', branch_name], check=True)
  154. # 拉取最新代码
  155. try:
  156. subprocess.run(['git', 'pull', 'origin', branch_name], check=True)
  157. print(f"已拉取分支 {branch_name} 的最新代码")
  158. except subprocess.CalledProcessError as e:
  159. print(f"警告: 拉取代码失败: {e}")
  160. else:
  161. print(f"已在目标分支: {branch_name}")
  162. # 运行文档生成脚本
  163. subprocess.run([
  164. sys.executable, 'doc_generator.py'
  165. ], cwd=".", check=True)
  166. # 嵌入版本配置(自动嵌入 .github/versions.json 到 version_menu.js)
  167. subprocess.run([
  168. sys.executable, 'utils/embed_version_config.py'
  169. ], cwd=".", check=True)
  170. # 构建HTML文档
  171. subprocess.run([
  172. sys.executable, '-m', 'sphinx.cmd.build',
  173. '-b', 'html',
  174. '.',
  175. str(output_dir)
  176. ], cwd=".", check=True)
  177. # 生成版本切换配置文件
  178. generate_version_config(output_dir, version_config)
  179. # 写出项目信息供 PDF 下载按钮读取
  180. write_project_info(output_dir)
  181. # 构建完成后恢复备份文件
  182. subprocess.run([
  183. sys.executable, 'utils/embed_version_config.py', '--restore-after'
  184. ], cwd=".", check=True)
  185. # 如果切换了分支,恢复到原始分支
  186. if branch_name != current_branch:
  187. print(f"恢复到原始分支: {current_branch}")
  188. # 恢复 version_menu.js 文件
  189. js_file = Path("_static/version_menu.js")
  190. bak_file = js_file.with_suffix('.js.bak')
  191. if bak_file.exists():
  192. shutil.copy2(bak_file, js_file)
  193. print(f"已恢复 {js_file} 为备份版本")
  194. # 清理生成的文件以避免冲突
  195. try:
  196. # 强制清理所有未跟踪和修改的文件
  197. subprocess.run(['git', 'clean', '-fdx'], check=True)
  198. print("已清理未跟踪文件")
  199. # 重置所有修改的文件
  200. subprocess.run(['git', 'reset', '--hard'], check=True)
  201. print("已重置所有修改的文件")
  202. except subprocess.CalledProcessError as e:
  203. print(f"警告: git 清理失败: {e}")
  204. print("尝试手动清理...")
  205. # 手动清理一些关键目录
  206. for dir_path in ['basic', 'component', 'driver', 'protocol', 'start', '_build']:
  207. dir_obj = Path(dir_path)
  208. if dir_obj.exists():
  209. try:
  210. shutil.rmtree(dir_obj)
  211. print(f"已清理目录: {dir_path}")
  212. except Exception as e:
  213. print(f"警告: 无法清理目录 {dir_path}: {e}")
  214. # 切换回原始分支
  215. subprocess.run(['git', 'checkout', current_branch], check=True)
  216. # 恢复之前stash的更改
  217. try:
  218. # 检查是否有stash
  219. result = subprocess.run(['git', 'stash', 'list'],
  220. capture_output=True, text=True, check=True)
  221. if result.stdout and result.stdout.strip():
  222. print("恢复之前保存的本地更改...")
  223. subprocess.run(['git', 'stash', 'pop'], check=True)
  224. print("已恢复本地更改")
  225. else:
  226. print("没有需要恢复的stash")
  227. except subprocess.CalledProcessError as e:
  228. print(f"警告: 恢复stash失败: {e}")
  229. print(f"✓ 版本 {version_name} 文档构建完成: {output_dir}")
  230. return True
  231. except subprocess.CalledProcessError as e:
  232. print(f"✗ 版本 {version_name} 文档构建失败: {e}")
  233. # 如果构建失败,尝试恢复到原始状态
  234. if branch_name != current_branch:
  235. try:
  236. print("尝试恢复到原始分支...")
  237. subprocess.run(['git', 'checkout', current_branch], check=True)
  238. # 恢复stash
  239. try:
  240. result = subprocess.run(['git', 'stash', 'list'],
  241. capture_output=True, text=True, check=True)
  242. if result.stdout.strip():
  243. subprocess.run(['git', 'stash', 'pop'], check=True)
  244. print("已恢复本地更改")
  245. except subprocess.CalledProcessError:
  246. pass
  247. except subprocess.CalledProcessError as restore_error:
  248. print(f"警告: 恢复原始状态失败: {restore_error}")
  249. return False
  250. def generate_version_config(output_dir, current_version_config):
  251. """生成版本切换配置文件"""
  252. print(f"生成版本切换配置...")
  253. # 加载所有版本信息
  254. all_versions = load_versions()
  255. # 创建版本配置对象
  256. version_config = {
  257. "current_version": current_version_config['name'],
  258. "versions": {}
  259. }
  260. # 为每个版本创建配置
  261. for version in all_versions:
  262. version_config["versions"][version['name']] = {
  263. "display_name": version['display_name'],
  264. "url_path": version['url_path'],
  265. "description": version.get('description', '')
  266. }
  267. # 写入配置文件
  268. config_file = output_dir / "_static" / "version_config.json"
  269. config_file.parent.mkdir(parents=True, exist_ok=True)
  270. with open(config_file, 'w', encoding='utf-8') as f:
  271. json.dump(version_config, f, ensure_ascii=False, indent=2)
  272. print(f"✓ 版本配置已生成: {config_file}")
  273. def write_project_info(output_dir):
  274. """写出项目信息,供下载PDF按钮在 file:// 与网页模式下均可读取。
  275. 输出到 <output_dir>/_static/project_info.json 与 project_info.js
  276. 名称来源 docs/source/config.yaml 的 project.name。
  277. """
  278. try:
  279. cfg_path = Path('config.yaml')
  280. project_name = 'SDK 文档'
  281. if cfg_path.exists():
  282. with open(cfg_path, 'r', encoding='utf-8') as f:
  283. cfg = yaml.safe_load(f) or {}
  284. project_name = (cfg.get('project', {}) or {}).get('name', project_name) or project_name
  285. info = {
  286. 'projectName': project_name,
  287. 'pdfFileName': f"{project_name}.pdf",
  288. }
  289. static_dir = Path(output_dir) / '_static'
  290. static_dir.mkdir(parents=True, exist_ok=True)
  291. # JSON
  292. with open(static_dir / 'project_info.json', 'w', encoding='utf-8') as f:
  293. json.dump(info, f, ensure_ascii=False)
  294. # JS(兼容 file:// 无法 fetch 的场景)
  295. try:
  296. with open(static_dir / 'project_info.js', 'w', encoding='utf-8') as fjs:
  297. fjs.write('window.projectInfo = ' + json.dumps(info, ensure_ascii=False) + ';\n')
  298. except Exception:
  299. pass
  300. print(f"✓ 项目信息已生成: {static_dir / 'project_info.json'}")
  301. except Exception as e:
  302. print(f"⚠️ 生成项目信息失败: {e}")
  303. def create_root_redirect():
  304. """创建根目录重定向页面"""
  305. print("\n创建根目录重定向页面...")
  306. # 加载版本配置
  307. versions = load_versions()
  308. default_version = None
  309. # 查找默认版本
  310. for version in versions:
  311. if version['name'] == 'master':
  312. default_version = version
  313. break
  314. if not default_version:
  315. print("警告: 未找到默认版本,使用第一个版本")
  316. default_version = versions[0] if versions else None
  317. if not default_version:
  318. print("错误: 没有可用的版本配置")
  319. return False
  320. # 创建根目录的 index.html,重定向到默认版本
  321. root_index = Path("_build/html/index.html")
  322. root_index.parent.mkdir(parents=True, exist_ok=True)
  323. redirect_html = f"""<!DOCTYPE html>
  324. <html lang="zh-CN">
  325. <head>
  326. <meta charset="UTF-8">
  327. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  328. <title>SDK 文档</title>
  329. <meta http-equiv="refresh" content="0; url=./{default_version['url_path']}/">
  330. <style>
  331. body {{
  332. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  333. display: flex;
  334. justify-content: center;
  335. align-items: center;
  336. height: 100vh;
  337. margin: 0;
  338. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  339. color: white;
  340. }}
  341. .container {{
  342. text-align: center;
  343. background: rgba(255, 255, 255, 0.1);
  344. padding: 40px;
  345. border-radius: 12px;
  346. backdrop-filter: blur(10px);
  347. }}
  348. .spinner {{
  349. border: 3px solid rgba(255, 255, 255, 0.3);
  350. border-top: 3px solid white;
  351. border-radius: 50%;
  352. width: 40px;
  353. height: 40px;
  354. animation: spin 1s linear infinite;
  355. margin: 0 auto 20px;
  356. }}
  357. @keyframes spin {{
  358. 0% {{ transform: rotate(0deg); }}
  359. 100% {{ transform: rotate(360deg); }}
  360. }}
  361. h1 {{
  362. margin: 0 0 10px 0;
  363. font-size: 24px;
  364. }}
  365. p {{
  366. margin: 0;
  367. opacity: 0.9;
  368. }}
  369. a {{
  370. color: white;
  371. text-decoration: underline;
  372. }}
  373. </style>
  374. </head>
  375. <body>
  376. <div class="container">
  377. <div class="spinner"></div>
  378. <h1>SDK 文档</h1>
  379. <p>正在跳转到{default_version['display_name']}...</p>
  380. <p><a href="./{default_version['url_path']}/">如果页面没有自动跳转,请点击这里</a></p>
  381. </div>
  382. </body>
  383. </html>"""
  384. with open(root_index, 'w', encoding='utf-8') as f:
  385. f.write(redirect_html)
  386. print(f"✓ 根目录重定向页面创建完成: {root_index}")
  387. return True
  388. def main():
  389. """主函数"""
  390. print("开始生成多版本文档...")
  391. # 检查是否在GitHub Actions环境中
  392. is_github_actions = os.environ.get('GITHUB_ACTIONS') == 'true'
  393. if is_github_actions:
  394. print("检测到GitHub Actions环境")
  395. # 在GitHub Actions中,为所有版本构建文档
  396. versions = load_versions()
  397. if not versions:
  398. print("错误: 没有找到有效的版本配置")
  399. return 1
  400. # 获取当前分支名称
  401. current_branch = get_branch_name()
  402. print(f"当前触发分支: {current_branch}")
  403. # 为每个版本构建文档
  404. results = {}
  405. for version_config in versions:
  406. # 检查分支是否存在
  407. try:
  408. subprocess.run(['git', 'ls-remote', '--heads', 'origin', version_config['branch']],
  409. capture_output=True, check=True)
  410. print(f"✓ 分支 {version_config['branch']} 存在,开始构建")
  411. success = build_version_docs(version_config, version_config['branch'])
  412. except subprocess.CalledProcessError:
  413. print(f"⚠️ 分支 {version_config['branch']} 不存在,跳过构建")
  414. success = False
  415. results[version_config['name']] = success
  416. else:
  417. print("本地构建环境")
  418. # 在本地环境中,可以选择构建所有版本或只构建当前分支对应的版本
  419. import sys
  420. build_all = len(sys.argv) > 1 and sys.argv[1] == '--all'
  421. if build_all:
  422. print("构建所有版本...")
  423. versions = load_versions()
  424. results = {}
  425. for version_config in versions:
  426. # 检查分支是否存在
  427. try:
  428. subprocess.run(['git', 'ls-remote', '--heads', 'origin', version_config['branch']],
  429. capture_output=True, check=True)
  430. print(f"✓ 分支 {version_config['branch']} 存在,开始构建")
  431. success = build_version_docs(version_config, version_config['branch'])
  432. except subprocess.CalledProcessError:
  433. print(f"⚠️ 分支 {version_config['branch']} 不存在,跳过构建")
  434. success = False
  435. results[version_config['name']] = success
  436. # 为每个成功版本生成项目信息(安全兜底)
  437. for version_config in versions:
  438. out_dir = Path(f"_build/html/{version_config['url_path']}")
  439. if out_dir.exists():
  440. write_project_info(out_dir)
  441. for version_config in versions:
  442. out_dir = Path(f"_build/html/{version_config['url_path']}")
  443. if out_dir.exists():
  444. write_project_info(out_dir)
  445. else:
  446. print("只构建当前分支对应的版本...")
  447. # 只构建当前分支对应的版本
  448. branch_versions = get_branch_versions()
  449. if not branch_versions:
  450. print("警告: 当前分支没有对应的版本配置")
  451. # 尝试构建默认版本
  452. versions = load_versions()
  453. branch_versions = [v for v in versions if v['name'] == 'master']
  454. if not branch_versions:
  455. branch_versions = versions[:1] if versions else []
  456. results = {}
  457. for version_config in branch_versions:
  458. success = build_version_docs(version_config)
  459. results[version_config['name']] = success
  460. for version_config in branch_versions:
  461. out_dir = Path(f"_build/html/{version_config['url_path']}")
  462. if out_dir.exists():
  463. write_project_info(out_dir)
  464. # 输出结果
  465. print("\n" + "="*50)
  466. print("版本生成结果:")
  467. for version, success in results.items():
  468. status = "✓ 成功" if success else "✗ 失败"
  469. print(f" {version}: {status}")
  470. success_count = sum(1 for success in results.values() if success)
  471. total_count = len(results)
  472. print(f"\n总计: {success_count}/{total_count} 个版本生成成功")
  473. # 在所有版本构建完成后创建根目录重定向页面
  474. if success_count > 0:
  475. create_root_redirect()
  476. if success_count == total_count:
  477. print("🎉 所有版本生成完成!")
  478. return 0
  479. else:
  480. print("⚠️ 部分版本生成失败,请检查错误信息")
  481. return 1
  482. if __name__ == "__main__":
  483. sys.exit(main())