artifacts.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. """
  2. artifacts.py - 构建产物管理:日志、快照、QAC、FTP准备、发布基线。
  3. 对应原 Shell 脚本的 Collecting commit log, qac report, ftp upload,
  4. commit_snapshot_manifest 等部分。
  5. """
  6. from __future__ import annotations # 允许延迟类型注解求值
  7. import os
  8. import shutil
  9. import subprocess
  10. import logging
  11. import tempfile
  12. import xml.etree.ElementTree as ET
  13. from pathlib import Path
  14. from typing import Tuple, List
  15. from gerrit import _run_ssh # 直接使用 gerrit 提供的 SSH 工具
  16. logger = logging.getLogger(__name__)
  17. class ArtifactError(Exception):
  18. """产物处理异常"""
  19. pass
  20. # ---------------------------------------------------------------------------
  21. # 1. 提交日志收集
  22. # ---------------------------------------------------------------------------
  23. def collect_logs(repo_dir: str, output_csv: str) -> None:
  24. """收集近 7 天的提交日志,保存为 CSV 格式"""
  25. repo_dir = Path(repo_dir)
  26. if not (repo_dir / '.repo').is_dir():
  27. logger.warning("No .repo dir in %s, skip collect_logs", repo_dir)
  28. return
  29. script_content = '''#!/bin/sh
  30. git log --no-merges --after="$(date +"%Y-%m-%d %H:%M:%S" -d "-7 day")" \
  31. --date=format:'%Y-%m-%d %H:%M:%S' \
  32. --pretty=format:'"'$(basename $(pwd))'","%H","%an","%ae","%ad","%cd","%s"%n'
  33. '''
  34. _run_repo_forall_with_script(repo_dir, script_content, output_csv)
  35. def collect_newest_one_commit(repo_dir: str, output_csv: str) -> None:
  36. """收集每个项目的最新一次提交,保存为 CSV 格式"""
  37. repo_dir = Path(repo_dir)
  38. if not (repo_dir / '.repo').is_dir():
  39. logger.warning("No .repo dir in %s, skip collect_newest_one_commit", repo_dir)
  40. return
  41. script_content = '''#!/bin/sh
  42. git log --no-merges -n 1 --date=format:'%Y-%m-%d %H:%M:%S' \
  43. --pretty=format:'"'$(basename $(pwd))'","%H","%an","%ae","%ad","%cd","%s"%n'
  44. '''
  45. _run_repo_forall_with_script(repo_dir, script_content, output_csv)
  46. def _run_repo_forall_with_script(repo_dir: Path, script: str, output_csv: str) -> None:
  47. """创建临时脚本,用 repo forall -c 执行,结果追加到指定 CSV 文件"""
  48. with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
  49. f.write(script)
  50. f.flush()
  51. script_path = f.name
  52. try:
  53. cmd = ['repo', 'forall', '-c', f'sh {script_path}']
  54. with open(output_csv, 'a') as out:
  55. subprocess.run(cmd, cwd=str(repo_dir), stdout=out, check=True)
  56. finally:
  57. os.unlink(script_path)
  58. # ---------------------------------------------------------------------------
  59. # 2. 审查记录 & 快照
  60. # ---------------------------------------------------------------------------
  61. def collect_review_records(repo_dir: str, output_dir: str) -> None:
  62. """
  63. 从 Gerrit 获取已合并的变更历史,并生成当前代码快照。
  64. 对应原 Shell 的 get_repo_history_from_gerrit + create_snapshot。
  65. """
  66. repo_dir = Path(repo_dir)
  67. repo_name = repo_dir.name
  68. output_dir = Path(output_dir)
  69. output_dir.mkdir(parents=True, exist_ok=True)
  70. # 获取所有 project 名称和分支
  71. manifest = repo_dir / '.repo' / 'manifests' / 'default.xml'
  72. if not manifest.is_file():
  73. logger.warning("Manifest not found in %s, skipping review records", repo_dir)
  74. return
  75. projects, branch = _parse_manifest(manifest)
  76. # Gerrit 连接信息 —— 直接从环境变量读取(与 config.py 保持一致的默认值)
  77. host = os.environ.get('GERRIT_HOST', '10.2.90.253')
  78. port = int(os.environ.get('GERRIT_PORT', '29418'))
  79. user = os.environ.get('GERRIT_NAME', 'jenkins')
  80. # 生成 Gerrit 历史文件
  81. history_file = output_dir / f"{repo_name}_merged_history.txt"
  82. with open(history_file, 'w') as f:
  83. for project in projects:
  84. try:
  85. result = _get_history_to_str(host, port, user, project, branch)
  86. f.write(result)
  87. except Exception as e:
  88. logger.warning("Failed to get history for %s: %s", project, e)
  89. # 生成快照 XML
  90. snapshot_xml = output_dir / f"{repo_name}_snapshot.xml"
  91. subprocess.run(['repo', 'manifest', '-r', '-o', str(snapshot_xml)],
  92. cwd=str(repo_dir), check=True)
  93. def _parse_manifest(manifest_file: Path) -> Tuple[List[str], str]:
  94. """从 default.xml 提取所有 project name 和 default revision"""
  95. tree = ET.parse(manifest_file)
  96. root = tree.getroot()
  97. projects = [elem.get('name') for elem in root.findall('project')]
  98. branch = root.find('default').get('revision', 'master')
  99. return projects, branch
  100. def _get_history_to_str(host: str, port: int, user: str, project: str, branch: str) -> str:
  101. """获取 Gerrit 历史并返回字符串(内部使用)"""
  102. query = f"query --format=TEXT status:merged project:{project} branch:{branch} after:2020-01-01"
  103. return _run_ssh(host, port, user, query)
  104. # ---------------------------------------------------------------------------
  105. # 3. QAC 报告收集
  106. # ---------------------------------------------------------------------------
  107. def collect_qac_report(mcu_sdk_dir: str, output_dir: str) -> None:
  108. """复制 MCU SDK 中的 QAC 报告到输出目录"""
  109. qac_source = Path(mcu_sdk_dir) / '..' / 'qac_report' / 'qac_output' / 'soc' / 'sdk_la'
  110. if not qac_source.is_dir():
  111. logger.info("No QAC report found at %s, skipping", qac_source)
  112. return
  113. output_dir = Path(output_dir)
  114. if output_dir.exists():
  115. shutil.rmtree(output_dir)
  116. output_dir.mkdir(parents=True)
  117. # 复制所有 html 和 xlsx 文件
  118. for root, dirs, files in os.walk(qac_source):
  119. for file in files:
  120. if any(file.lower().endswith(ext) for ext in ['.html', '.xlsx']):
  121. src_path = Path(root) / file
  122. rel_path = src_path.relative_to(qac_source)
  123. dest_path = output_dir / rel_path
  124. dest_path.parent.mkdir(parents=True, exist_ok=True)
  125. shutil.copy2(src_path, dest_path)
  126. logger.info("QAC report copied to %s", output_dir)
  127. # ---------------------------------------------------------------------------
  128. # 4. FTP 上传准备
  129. # ---------------------------------------------------------------------------
  130. def prepare_ftp_upload(cfg, collect_log_file, newest_log_file,
  131. review_records_dir, qac_report_dir) -> None:
  132. """
  133. 将所有构建产物移动到本地 FTP 目录,并生成 env.properties。
  134. 对应原 Shell 的 prepare_for_upload_ftp()。
  135. """
  136. # 确定 SOC 部署输出目录
  137. soc_dir = Path(cfg.soc_dir)
  138. deploy_root = soc_dir / 'out' / 'deploy'
  139. if not deploy_root.is_dir():
  140. raise ArtifactError("No deploy directory found in SOC output")
  141. deploy_dirs = list(deploy_root.iterdir())
  142. if not deploy_dirs:
  143. raise ArtifactError("No deploy subdirectories found")
  144. deploy_dir_name = deploy_dirs[0].name
  145. image_deploy_local = Path(cfg.deploy_image_local_dir) / cfg.repo_branch / deploy_dir_name
  146. image_deploy_local.mkdir(parents=True, exist_ok=True)
  147. # 移动整个 deploy 目录
  148. src = deploy_dirs[0]
  149. for item in os.listdir(src):
  150. src_item = src / item
  151. dst_item = image_deploy_local / item
  152. if src_item.is_dir():
  153. shutil.move(str(src_item), str(dst_item))
  154. else:
  155. shutil.move(str(src_item), str(dst_item))
  156. logger.info("Deploy files moved to %s", image_deploy_local)
  157. # 决定是否需要测试提示
  158. test_msg = "无需测试"
  159. commit_log_filename = "no_commit"
  160. if Path(collect_log_file).is_file():
  161. with open(collect_log_file, 'r') as f:
  162. lines = f.readlines()
  163. # 排除 auto commit 行
  164. non_auto = [line for line in lines if 'auto commit by server' not in line]
  165. if len(non_auto) > 1: # 至少有一行有效信息(不含表头)
  166. test_msg = "需要测试"
  167. commit_log_filename = Path(collect_log_file).name
  168. shutil.copy2(collect_log_file, image_deploy_local)
  169. # 复制其他文件
  170. for src_file, dst_name in [
  171. (newest_log_file, Path(newest_log_file).name) if Path(newest_log_file).is_file() else None,
  172. (review_records_dir, Path(review_records_dir).name) if Path(review_records_dir).is_dir() else None,
  173. (qac_report_dir, Path(qac_report_dir).name) if Path(qac_report_dir).is_dir() else None,
  174. ]:
  175. if src_file is None:
  176. continue
  177. src_path = Path(src_file)
  178. dst_path = image_deploy_local / dst_name
  179. if src_path.is_dir():
  180. shutil.copytree(src_path, dst_path, dirs_exist_ok=True)
  181. else:
  182. shutil.copy2(src_path, dst_path)
  183. # 构建时间戳
  184. if '[' in deploy_dir_name:
  185. image_build_time = deploy_dir_name.split('[')[1].split(']')[0].replace('-', '')
  186. else:
  187. from datetime import datetime
  188. image_build_time = datetime.now().strftime('%Y%m%d%H%M%S')
  189. cfg.image_build_time = image_build_time # 存回 config 备用
  190. # 生成 env.properties
  191. ftp_user = os.environ.get('ftpUser', 'ftpuser')
  192. ftp_password = os.environ.get('ftpPasword', '123456')
  193. ftp_addr = os.environ.get('ftpAddr', '10.2.90.252')
  194. ftp_port = os.environ.get('ftpPort', '21')
  195. ftp_root = f"DB-{datetime.now().year}/{cfg.ftp_root}"
  196. deploy_rel = f"{cfg.repo_branch}/{deploy_dir_name}"
  197. ftp_deploy_url = f"ftp://{ftp_user}:{ftp_password}@{ftp_addr}:{ftp_port}/{ftp_root}/{deploy_rel}"
  198. ftp_commit_log_url = f"{ftp_deploy_url}/{commit_log_filename}" if commit_log_filename != "no_commit" else ""
  199. env_content = f"""store.repo_branch={cfg.repo_branch}
  200. store.deploy_dir={ftp_deploy_url}
  201. store.commit_log={ftp_commit_log_url}
  202. store.test_msg={test_msg}
  203. """
  204. env_file = image_deploy_local / 'env.properties'
  205. with open(env_file, 'w') as f:
  206. f.write(env_content)
  207. logger.info("env.properties generated at %s", env_file)
  208. # ---------------------------------------------------------------------------
  209. # 5. 发布基线 manifest 提交
  210. # ---------------------------------------------------------------------------
  211. def commit_snapshot_manifest(repo_dir: str, version: str) -> None:
  212. """生成 release_<version>.xml 并提交到 manifest 仓库的相应分支"""
  213. if not version:
  214. logger.info("No release version specified, skip commit manifest")
  215. return
  216. repo_dir = Path(repo_dir)
  217. manifests_repo = repo_dir / '.repo' / 'manifests'
  218. if not (manifests_repo / '.git').is_dir():
  219. logger.warning("No manifests git repo in %s, skip commit manifest", repo_dir)
  220. return
  221. release_dir = manifests_repo / 'release'
  222. release_dir.mkdir(parents=True, exist_ok=True)
  223. # 生成快照 XML
  224. snapshot_file = release_dir / f'release_{version}.xml'
  225. subprocess.run(['repo', 'manifest', '-r', '-o', str(snapshot_file)],
  226. cwd=str(repo_dir), check=True)
  227. # 提交并推送
  228. current_branch = _get_current_branch(manifests_repo)
  229. subprocess.run(['git', 'add', f'release/release_{version}.xml'],
  230. cwd=str(manifests_repo), check=True)
  231. try:
  232. subprocess.run(['git', 'commit', '-m', f'Auto commit by server: add release_{version}.xml'],
  233. cwd=str(manifests_repo), check=True, capture_output=True)
  234. except subprocess.CalledProcessError as e:
  235. if 'nothing to commit' in e.stderr.decode().lower():
  236. logger.info("Nothing to commit, skipping push for release manifest")
  237. return
  238. raise
  239. # 推送到当前分支(通常为 REPO_BRANCH 或默认分支)
  240. push_branch = os.environ.get('REPO_BRANCH', current_branch)
  241. subprocess.run(['git', 'push', 'origin', f'HEAD:{push_branch}'],
  242. cwd=str(manifests_repo), check=True)
  243. logger.info("Release manifest %s pushed to %s", snapshot_file.name, push_branch)
  244. def _get_current_branch(repo_dir: Path) -> str:
  245. """获取仓库当前分支"""
  246. result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
  247. cwd=str(repo_dir), capture_output=True, text=True, check=True)
  248. return result.stdout.strip()