openubmc-bingo 0.5.275__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of openubmc-bingo might be problematic. Click here for more details.

Files changed (56) hide show
  1. bmcgo/__init__.py +1 -1
  2. bmcgo/bmcgo_config.py +22 -10
  3. bmcgo/cli/cli.py +86 -39
  4. bmcgo/cli/config.conan2.yaml +9 -0
  5. bmcgo/codegen/lua/codegen.py +1 -1
  6. bmcgo/codegen/lua/script/gen_intf_rpc_json.py +15 -14
  7. bmcgo/component/analysis/analysis.py +8 -8
  8. bmcgo/component/analysis/intf_validation.py +23 -12
  9. bmcgo/component/build.py +76 -14
  10. bmcgo/component/component_dt_version_parse.py +3 -2
  11. bmcgo/component/component_helper.py +43 -15
  12. bmcgo/component/coverage/incremental_cov.py +2 -2
  13. bmcgo/component/deploy.py +68 -14
  14. bmcgo/component/gen.py +1 -1
  15. bmcgo/component/package_info.py +128 -38
  16. bmcgo/component/template_v2/conanbase.py.mako +352 -0
  17. bmcgo/component/template_v2/conanfile.deploy.py.mako +26 -0
  18. bmcgo/component/test.py +53 -20
  19. bmcgo/frame.py +7 -3
  20. bmcgo/functional/analysis.py +3 -2
  21. bmcgo/functional/check.py +10 -6
  22. bmcgo/functional/conan_index_build.py +79 -20
  23. bmcgo/functional/csr_build.py +11 -3
  24. bmcgo/functional/diff.py +1 -1
  25. bmcgo/functional/fetch.py +1 -1
  26. bmcgo/functional/full_component.py +32 -24
  27. bmcgo/functional/git_history.py +220 -0
  28. bmcgo/functional/maintain.py +55 -12
  29. bmcgo/functional/new.py +1 -1
  30. bmcgo/functional/schema_valid.py +2 -2
  31. bmcgo/logger.py +2 -3
  32. bmcgo/misc.py +130 -9
  33. bmcgo/tasks/conan/__init__.py +10 -0
  34. bmcgo/tasks/conan/conanfile.py +45 -0
  35. bmcgo/tasks/task.py +27 -4
  36. bmcgo/tasks/task_build_conan.py +433 -65
  37. bmcgo/tasks/task_buildgppbin.py +8 -2
  38. bmcgo/tasks/task_download_buildtools.py +76 -0
  39. bmcgo/tasks/task_download_dependency.py +1 -0
  40. bmcgo/tasks/task_hpm_envir_prepare.py +1 -1
  41. bmcgo/utils/build_conans.py +231 -0
  42. bmcgo/utils/component_post.py +6 -4
  43. bmcgo/utils/component_version_check.py +10 -5
  44. bmcgo/utils/config.py +76 -40
  45. bmcgo/utils/fetch_component_code.py +44 -25
  46. bmcgo/utils/installations/install_plans/qemu.yml +6 -0
  47. bmcgo/utils/installations/install_plans/studio.yml +6 -0
  48. bmcgo/utils/installations/install_workflow.py +28 -2
  49. bmcgo/utils/merge_csr.py +124 -0
  50. bmcgo/utils/tools.py +239 -117
  51. bmcgo/worker.py +2 -2
  52. {openubmc_bingo-0.5.275.dist-info → openubmc_bingo-0.6.0.dist-info}/METADATA +4 -2
  53. {openubmc_bingo-0.5.275.dist-info → openubmc_bingo-0.6.0.dist-info}/RECORD +56 -46
  54. {openubmc_bingo-0.5.275.dist-info → openubmc_bingo-0.6.0.dist-info}/WHEEL +0 -0
  55. {openubmc_bingo-0.5.275.dist-info → openubmc_bingo-0.6.0.dist-info}/entry_points.txt +0 -0
  56. {openubmc_bingo-0.5.275.dist-info → openubmc_bingo-0.6.0.dist-info}/top_level.txt +0 -0
@@ -11,11 +11,9 @@
11
11
  # MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
12
12
  # See the Mulan PSL v2 for more details.
13
13
  import os
14
- import pathlib
15
14
  import subprocess
16
- import shutil
17
- import stat
18
15
  import json
16
+ import re
19
17
  import yaml
20
18
 
21
19
  from bmcgo import errors
@@ -68,21 +66,31 @@ class BmcgoCommand:
68
66
  def __init__(self, bconfig: BmcgoConfig, *args):
69
67
  self.bconfig = bconfig
70
68
  parser = tool.create_common_parser("Conan Index Build")
69
+ parser.add_argument("--conan2", help="是否构建conan2.x版本的组件包", action=misc.STORE_TRUE)
71
70
  self.args, _ = parser.parse_known_args(*args)
71
+ self.conan2 = self.args.conan2
72
72
  self.path = ""
73
73
  self.version = ""
74
+ self.name = ""
75
+ self.user = ""
76
+ self.channel = ""
74
77
  self.conan_package = ""
75
78
  self.upload = False
76
79
  self.remote = None
77
80
  self.options = []
78
- self.stage = misc.StageEnum.STAGE_DEV.value
81
+ self.stage = self.args.stage
79
82
  self.enable_luajit = False
80
83
  self.from_source = False
81
84
  self.build_type = 'debug'
82
85
  self.asan = False
83
86
  self.profile = ''
87
+ self.recipe_folder = self.bconfig.conan_index.folder
88
+ if misc.conan_v2():
89
+ recipes2_folder = os.path.join(self.bconfig.conan_index.folder, "..", "recipes2")
90
+ recipes2_folder = os.path.realpath(recipes2_folder)
91
+ if os.path.isdir(recipes2_folder):
92
+ self.recipe_folder = recipes2_folder
84
93
  self.initialize()
85
- self.channel = f"@{misc.ConanUserEnum.CONAN_USER_DEV.value}/{self.stage}"
86
94
 
87
95
  @staticmethod
88
96
  def run_command(command, ignore_error=False, sudo=False, **kwargs):
@@ -109,13 +117,15 @@ class BmcgoCommand:
109
117
  os.environ["LUA_PATH"] = f"{conan_bin}/?.lua"
110
118
 
111
119
  def initialize(self):
120
+ if not self.args.conan_package:
121
+ msg = "构建参数错误,请指定有效的-cp参数,例:kmc/1.0.1 或 kmc/1.0.1@openubmc/stable"
122
+ raise errors.BmcGoException(msg)
112
123
  self.set_package(self.args.conan_package)
113
124
  self.upload = self.args.upload_package
114
125
  if self.args.remote:
115
126
  self.remote = self.args.remote
116
127
  if self.args.options:
117
128
  self.options = self.args.options
118
- self.stage = self.args.stage
119
129
  self.enable_luajit = self.args.enable_luajit
120
130
  self.from_source = self.args.from_source
121
131
  self.build_type = self.args.build_type
@@ -126,15 +136,25 @@ class BmcgoCommand:
126
136
 
127
137
  # 入参可以是huawei_secure_c/1.0.0样式
128
138
  def set_package(self, path: str):
129
- os.chdir(self.bconfig.conan_index.folder)
130
- split = path.split("/")
131
- if len(split) != 2:
132
- raise errors.BmcGoException(f"包名称({path})错误,例:kmc/1.0.1")
139
+ os.chdir(self.recipe_folder)
140
+ split = re.split('/|@', path)
141
+ if len(split) != 2 and len(split) != 4:
142
+ raise errors.BmcGoException(f"包名称({path})错误,例:kmc/1.0.1 或 kmc/1.0.1@openubmc/stable")
143
+ self.name = split[0].lower()
144
+ if len(split) == 2:
145
+ if self.stage == "dev":
146
+ self.user = misc.conan_user_dev()
147
+ else:
148
+ self.user = misc.conan_user()
149
+ self.channel = self.stage
150
+ else:
151
+ self.user = split[2]
152
+ self.channel = split[3]
133
153
 
134
154
  if not os.path.isdir(split[0]):
135
155
  raise errors.BmcGoException(f"包路径({split[0]})不存在,或不是文件夹")
136
156
 
137
- config_yaml = os.path.join(self.bconfig.conan_index.folder, split[0], "config.yml")
157
+ config_yaml = os.path.join(self.recipe_folder, split[0], "config.yml")
138
158
  with open(config_yaml) as f:
139
159
  config_data = yaml.safe_load(f)
140
160
  config_data = config_data.get('versions', None)
@@ -148,18 +168,47 @@ class BmcgoCommand:
148
168
  raise errors.BmcGoException(f"Unkown folder, config.yml path: {config_yaml}, version: {split[1]}")
149
169
  self.path = "{}/{}".format(split[0], folder)
150
170
  self.version = split[1]
171
+ if misc.conan_v2():
172
+ self.version = self.version.lower()
151
173
  if self.stage != "dev":
152
174
  self.tag_check()
153
175
 
154
- def run(self):
176
+ def run_v2(self):
177
+ log.info("Start build package")
178
+ if self.build_type == "debug":
179
+ setting = "-s build_type=Debug"
180
+ else:
181
+ setting = "-s build_type=Release"
182
+ options = " "
183
+ if self.asan:
184
+ options += f"-o {self.name}/*:asan=True"
185
+ for option in self.options:
186
+ options += f" -o {option}"
187
+
188
+ dt_stat = os.environ.get("BINGO_DT_RUN", "off")
189
+ show_log = True if dt_stat == "off" else False
190
+ pkg = self.name + "/" + self.version + "@" + self.user + "/" + self.channel
191
+ append_cmd = f"-r {self.remote}" if self.remote else ""
192
+ cmd = "conan create . --name={} --version={} -pr={} {} {} {}".format(
193
+ self.name, self.version, self.profile, setting, append_cmd, options
194
+ )
195
+
196
+ cmd += " --user={} --channel={}".format(self.user, self.channel)
197
+ if self.from_source:
198
+ cmd += " --build=*"
199
+ else:
200
+ cmd += f" --build={self.name}/* --build=missing"
201
+ self.run_command(cmd, show_log=show_log)
202
+
203
+ if not self.upload:
204
+ return
205
+
206
+ cmd = "conan upload {} -r {}".format(pkg, self.remote)
207
+ self.run_command(cmd)
208
+ log.info("===>>>Upload package successfully, pkg: {}".format(pkg))
209
+
210
+ def run_v1(self):
155
211
  self.check_luac()
156
- if self.path == "" or self.version == "":
157
- raise errors.BmcGoException(f"Path({self.path}) or version({self.version}) error")
158
- os.chdir(self.bconfig.conan_index.folder)
159
- os.chdir(self.path)
160
- if self.stage != misc.StageEnum.STAGE_DEV.value:
161
- # rc和stable模式
162
- self.channel = f"@{tool.conan_user}/{self.stage}"
163
212
  log.info("Start build package")
164
213
  if self.build_type == "dt":
165
214
  setting = "-s build_type=Dt"
@@ -177,7 +226,7 @@ class BmcgoCommand:
177
226
 
178
227
  dt_stat = os.environ.get("BMCGO_DT_RUN", "off")
179
228
  show_log = True if dt_stat == "off" else False
180
- pkg = packake_name + "/" + self.version + self.channel
229
+ pkg = self.name + "/" + self.version + "@" + self.user + "/" + self.channel
181
230
  append_cmd = f"-r {self.remote}" if self.remote else ""
182
231
  cmd = "conan create . {} -pr={} -pr:b profile.dt.ini {} {} -tf None {}".format(
183
232
  pkg, self.profile, setting, append_cmd, options
@@ -206,6 +255,16 @@ class BmcgoCommand:
206
255
  self.run_command(cmd)
207
256
  log.info("===>>>Upload package successfully, pkg: {}:{}".format(pkg, i["id"]))
208
257
 
258
+ def run(self):
259
+ if self.path == "" or self.version == "":
260
+ raise errors.BmcGoException(f"Path({self.path}) or version({self.version}) error")
261
+
262
+ os.chdir(self.path)
263
+ if misc.conan_v1():
264
+ self.run_v1()
265
+ else:
266
+ self.run_v2()
267
+
209
268
  def tag_check(self):
210
269
  yaml_file = f"{self.path}/conandata.yml"
211
270
  if os.path.exists(yaml_file) is False:
@@ -35,6 +35,7 @@ from bmcgo import errors
35
35
  from bmcgo.misc import CommandInfo
36
36
  from bmcgo.utils.tools import Tools
37
37
  from bmcgo.utils.buffer import Buffer
38
+ from bmcgo.utils.merge_csr import Merger
38
39
  from bmcgo.bmcgo_config import BmcgoConfig
39
40
  from bmcgo.functional.simple_sign import BmcgoCommand as SimpleSign
40
41
 
@@ -45,7 +46,7 @@ cwd = os.getcwd()
45
46
 
46
47
  SR_UPGRADE = "SRUpgrade"
47
48
  TIANCHI = "bmc.dev.Board.TianChi"
48
- HPM_PACK_PATH = "/usr/share/bmcgo/csr_packet"
49
+ HPM_PACK_PATH = "/usr/share/bingo/csr_packet"
49
50
  EEPROM_SIZE_LIMIT_CONFIG = "/usr/share/bmcgo/schema/eepromSizeLimit.json"
50
51
 
51
52
  JSON_DATA_FORMAT = 0x01
@@ -205,6 +206,8 @@ class BmcgoCommand:
205
206
  self.target_dir = None
206
207
  self.eeprom_sign_strategy = None
207
208
  self.hpm_sign_strategy = None
209
+ self.tmp_dir = None
210
+ self.merger = Merger(self.output_path)
208
211
 
209
212
  @staticmethod
210
213
  def get_oem_data(dir_path: str, comp_name: str):
@@ -294,6 +297,9 @@ class BmcgoCommand:
294
297
 
295
298
  def run(self):
296
299
  self.check_args()
300
+ tmp = tempfile.TemporaryDirectory()
301
+ self.tmp_dir = tmp.name
302
+ self.merger.update_tmp_dir(self.tmp_dir)
297
303
  if not os.path.exists(self.output_path):
298
304
  raise Exception(f"输出路径{self.output_path}不存在")
299
305
  sr_make_options_list = []
@@ -338,6 +344,8 @@ class BmcgoCommand:
338
344
  finally:
339
345
  if os.path.exists(self.work_dir):
340
346
  shutil.rmtree(self.work_dir)
347
+ if os.path.exists(self.tmp_dir):
348
+ shutil.rmtree(self.tmp_dir)
341
349
 
342
350
  def check_args(self):
343
351
  has_single_need_arg = self.bin or self.json or self.hpm or self.all
@@ -351,7 +359,7 @@ class BmcgoCommand:
351
359
 
352
360
  def get_sr_file_and_oem(self):
353
361
  sr_make_options_list = []
354
- csr_path = self.csr_path
362
+ csr_path = self.merger.get_single_csr(self.csr_path)
355
363
  oem_path = self.oem_path
356
364
  oem_data = bytearray()
357
365
  if not os.path.exists(csr_path):
@@ -382,7 +390,7 @@ class BmcgoCommand:
382
390
 
383
391
  def get_sr_files(self):
384
392
  dir_path = Path(self.csr_path)
385
- sr_files = [file for file in dir_path.glob("*.sr")]
393
+ sr_files = self.merger.get_multi_files(dir_path)
386
394
  sr_num = len(sr_files)
387
395
  if sr_num == 1:
388
396
  log.info("开始执行CSR出包任务...")
bmcgo/functional/diff.py CHANGED
@@ -105,7 +105,7 @@ class BmcgoCommand:
105
105
  tempfile = NamedTemporaryFile()
106
106
  # 过滤只包含scm的信息, 并将其生成为字典对象
107
107
  ret = tools.run_command(f"conan info {version} --json {tempfile.name} \
108
- -r {misc.CONAN_REPO}", ignore_error=True, command_echo=False, capture_output=True)
108
+ -r {misc.conan_remote()}", ignore_error=True, command_echo=False, capture_output=True)
109
109
  file_handler = open(tempfile.name, "r")
110
110
  conan_comps = json.load(file_handler)
111
111
  version_msg = ""
bmcgo/functional/fetch.py CHANGED
@@ -163,7 +163,7 @@ framework、bmc_core、security、hardware、ras、energy、om、interface、pro
163
163
  stage = self.stage
164
164
  if stage != misc.StageEnum.STAGE_STABLE.value:
165
165
  stage = misc.StageEnum.STAGE_RC.value
166
- user_channel = f"@{tools.conan_user}/{stage}"
166
+ user_channel = f"@{misc.conan_user()}/{stage}"
167
167
  com_package[index] += user_channel
168
168
 
169
169
  def __load_config_json(self, stage):
@@ -23,8 +23,6 @@ import functools
23
23
  import random
24
24
  from concurrent.futures import ProcessPoolExecutor, Future
25
25
  from typing import List
26
-
27
- from conans.model.manifest import FileTreeManifest
28
26
  import yaml
29
27
 
30
28
  from bmcgo import misc
@@ -71,7 +69,7 @@ class BuildComponent:
71
69
  build_type: str,
72
70
  options: dict,
73
71
  code_path: str,
74
- remote=misc.CONAN_REPO,
72
+ remote=misc.conan_remote(),
75
73
  service_json="mds/service.json",
76
74
  upload=False,
77
75
  ):
@@ -90,12 +88,15 @@ class BuildComponent:
90
88
  def run(self):
91
89
  os.chdir(os.path.join(self.code_path, self.comp_name))
92
90
  for key, value in self.options.items():
93
- self.option_cmd = self.option_cmd + f" -o {self.comp_name}:{key}={value}"
91
+ if misc.conan_v2():
92
+ self.option_cmd = self.option_cmd + f" -o {self.comp_name}/*:{key}={value}"
93
+ else:
94
+ self.option_cmd = self.option_cmd + f" -o {self.comp_name}:{key}={value}"
94
95
  command = (
95
96
  f"--remote {self.remote} -nc --stage {self.stage} --build_type {self.build_type.lower()}"
96
97
  f" --profile {self.profile} {self.option_cmd}"
97
98
  )
98
- log.info(f"执行构建命令bingo build {command}")
99
+ log.info(f"执行构建命令{misc.tool_name()} build {command}")
99
100
  args = command.split()
100
101
  build = BuildComp(self.bconfig, args, service_json=self.service_json)
101
102
  # 恢复工作区为clean状态, 保证conan export时scm信息准确
@@ -112,7 +113,7 @@ class BmcgoCommand:
112
113
  def __init__(self, bconfig: BmcgoConfig, *args):
113
114
  parser = argparse.ArgumentParser(description="Fetch component source code and build all binaries.")
114
115
  parser.add_argument("-comp", "--component", help="软件包名, 示例: oms/1.2.6", required=True)
115
- parser.add_argument("-r", "--remote", help="远端仓库名称", default=misc.CONAN_REPO)
116
+ parser.add_argument("-r", "--remote", help="远端仓库名称", default=misc.conan_remote())
116
117
  parser.add_argument(
117
118
  "-p",
118
119
  "--path",
@@ -136,9 +137,11 @@ class BmcgoCommand:
136
137
  self.upload = parsed_args.upload
137
138
  self.config_file = parsed_args.config_file
138
139
  self.skip_fetch = parsed_args.skip_fetch
139
- self.profile_list = ["profile.dt.ini", "profile.luajit.ini"]
140
- self.stage_list = [STAGE_STABLE]
141
- self.built_type_list = [BUILD_TYPE_DT, BUILD_TYPE_DEBUG, BUILD_TYPE_RELEASE]
140
+ self.profile_list = ["profile.dt.ini"]
141
+ if misc.conan_v1():
142
+ self.profile_list.append("profile.luajit.ini")
143
+ self.stage_list = [misc.StageEnum.STAGE_STABLE]
144
+ self.built_type_list = [misc.BuildTypeEnum.DEBUG, misc.BuildTypeEnum.RELEASE]
142
145
  if parsed_args.build_type:
143
146
  self.built_type_list = parsed_args.build_type
144
147
  self.options_dict = {}
@@ -185,7 +188,7 @@ class BmcgoCommand:
185
188
 
186
189
  # 添加module_symver选项
187
190
  module_symvers_path = os.path.join(SDK_PATH, MODULE_SYMVERS)
188
- module_symver_key, module_symver_value = tool.get_module_symver_option(module_symvers_path)
191
+ module_symver_key, module_symver_value = tool.get_171x_module_symver_option(module_symvers_path)
189
192
  if module_symver_key in options_dict:
190
193
  options_dict[module_symver_key] = [module_symver_value]
191
194
  if config_file:
@@ -237,24 +240,28 @@ class BmcgoCommand:
237
240
  @staticmethod
238
241
  def _replace_dev_version(temp_service_json: str):
239
242
  version_parse = ComponentDtVersionParse(serv_file=temp_service_json)
240
- for pkg in version_parse.conan_list:
241
- component = pkg[misc.CONAN]
242
- if "@" not in component:
243
- continue
244
- comp_version, user_channel = component.split("@")
245
- if user_channel.endswith(STAGE_DEV):
246
- pkg[misc.CONAN] = f"{comp_version}{ComponentHelper.get_user_channel(STAGE_DEV)}"
247
-
243
+ if misc.conan_v1():
244
+ for pkg in version_parse.conan_list:
245
+ component = pkg[misc.CONAN]
246
+ if "@" not in component:
247
+ continue
248
+ comp_version, user_channel = component.split("@")
249
+ if user_channel.endswith(STAGE_DEV):
250
+ pkg[misc.CONAN] = f"{comp_version}{ComponentHelper.get_user_channel(STAGE_DEV)}"
248
251
  version_parse.write_to_serv_file()
249
252
 
250
253
  @functools.cached_property
251
254
  def _full_reference(self) -> str:
252
- comp_pkg = self.comp if "@" in self.comp else f"{self.comp}@{CONAN_USER}.release/{STAGE_STABLE}"
255
+ if misc.conan_v1():
256
+ comp_pkg = self.comp if "@" in self.comp else f"{self.comp}@{CONAN_USER}.release/{STAGE_STABLE}"
257
+ else:
258
+ comp_pkg = self.comp if "@" in self.comp else f"{self.comp}@{misc.conan_user()}/{STAGE_STABLE}"
253
259
  return comp_pkg
254
260
 
255
261
  @functools.cached_property
256
262
  def _is_self_developed(self) -> bool:
257
- tool.run_command(f"conan download {self._full_reference} --recipe -r {self.remote}")
263
+ args = "--only-recipe" if misc.conan_v2() else "--recipe"
264
+ tool.run_command(f"conan download {self._full_reference} {args} -r {self.remote}")
258
265
  comp_package_path = os.path.join(tool.conan_data, self.comp_name)
259
266
  ret = tool.run_command(f"find {comp_package_path} -name 'conanbase.py'", capture_output=True)
260
267
  return bool(ret.stdout)
@@ -348,10 +355,11 @@ class BmcgoCommand:
348
355
  random.shuffle(all_build_params)
349
356
  for build_args in all_build_params:
350
357
  profile, stage, build_type, *option_values = build_args
351
- if profile == "profile.dt.ini" and build_type.lower() != BUILD_TYPE_DT.lower():
352
- continue
353
- if profile != "profile.dt.ini" and build_type.lower() == BUILD_TYPE_DT.lower():
354
- continue
358
+ if misc.conan_v1():
359
+ if profile == "profile.dt.ini" and build_type.lower() != BUILD_TYPE_DT.lower():
360
+ continue
361
+ if profile != "profile.dt.ini" and build_type.lower() == BUILD_TYPE_DT.lower():
362
+ continue
355
363
  build_options = dict(zip(options_dict.keys(), option_values))
356
364
  task = BuildComponent(
357
365
  self.comp,
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env python3
2
+ # coding: utf-8
3
+ # Copyright (c) 2025 Huawei Technologies Co., Ltd.
4
+ # openUBMC is licensed under Mulan PSL v2.
5
+ # You can use this software according to the terms and conditions of the Mulan PSL v2.
6
+ # You may obtain a copy of Mulan PSL v2 at:
7
+ # http://license.coscl.org.cn/MulanPSL2
8
+ # THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
9
+ # EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
10
+ # MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
11
+ # See the Mulan PSL v2 for more details.
12
+ import argparse
13
+ import datetime
14
+ import json
15
+ import os
16
+ import re
17
+ import sys
18
+ from datetime import timezone, timedelta
19
+ from typing import List, Tuple, Dict, Optional, Any
20
+ from git import Repo, Commit, Diff
21
+ from git.exc import InvalidGitRepositoryError, GitCommandError
22
+ from bmcgo import misc
23
+ from bmcgo.utils.tools import Tools
24
+ from bmcgo.bmcgo_config import BmcgoConfig
25
+ from bmcgo import errors
26
+
27
+
28
+ tools = Tools()
29
+ log = tools.log
30
+ beijing_timezone = timezone(timedelta(hours=8))
31
+
32
+
33
+ command_info: misc.CommandInfo = misc.CommandInfo(
34
+ group=misc.GRP_COMP,
35
+ name="git_history",
36
+ description=["获取指定开始时间与结束时间之间提交信息,并生成release note"],
37
+ hidden=False
38
+ )
39
+
40
+
41
+ def if_available(bconfig: BmcgoConfig):
42
+ if bconfig.component is None:
43
+ return False
44
+ return True
45
+
46
+
47
+ class BmcgoCommand:
48
+ def __init__(self, bconfig: BmcgoConfig, *args):
49
+ self.bconfig = bconfig
50
+ parser = argparse.ArgumentParser(prog="bmcgo git_history", description="生成Git仓库的Release Note", add_help=True,
51
+ formatter_class=argparse.RawTextHelpFormatter)
52
+ parser.add_argument("--start_date", "-s", help="起始日期YYYY-MM-DD", required=True)
53
+ parser.add_argument("--end_date", "-e", help="结束日期YYYY-MM-DD,默认为当前日期")
54
+ args, kwargs = parser.parse_known_args(*args)
55
+ self.start_date = args.start_date
56
+ self.end_date = args.end_date
57
+ self.output_file = f"./{self.start_date}_{self.end_date}_releaseNote.log"
58
+ self.service_json_path = "mds/service.json"
59
+ self.repo = Repo(".")
60
+
61
+ @staticmethod
62
+ def validate_date(date_str: str) -> datetime.datetime:
63
+ try:
64
+ date = datetime.datetime.strptime(date_str, "%Y-%m-%d")
65
+ except ValueError as e:
66
+ raise errors.BmcGoException(f"日期格式不正确: {date_str},请使用YYYY-MM-DD格式,错误信息{e}")
67
+ current_date = datetime.datetime.now(beijing_timezone)
68
+ if date.date() > current_date.date():
69
+ raise errors.BmcGoException(f"日期 {date_str} 超过当前日期")
70
+ return date
71
+
72
+ @staticmethod
73
+ def extract_version_from_diff(self, diff: Diff) -> Optional[str]:
74
+ try:
75
+ diff_text = diff.diff.decode('utf-8') if diff.diff else str(diff)
76
+ version_pattern = r'[-+]\s*"version"\s*:\s*"([^"]+)"'
77
+ matches = re.findall(version_pattern, diff_text)
78
+ if matches:
79
+ return matches[-1]
80
+ except (UnicodeDecodeError, AttributeError):
81
+ pass
82
+ return None
83
+
84
+ @staticmethod
85
+ def is_merge_into_main(s):
86
+ pattern = r'^merge .+ into main$'
87
+ # 使用re.match进行匹配,忽略大小写
88
+ match = re.match(pattern, s, re.IGNORECASE)
89
+ return match is not None
90
+
91
+ def get_current_version(self) -> str:
92
+ try:
93
+ with open(self.service_json_path, 'r') as f:
94
+ service_data = json.load(f)
95
+ return service_data.get('version', 'unknown')
96
+ except (FileNotFoundError, json.JSONDecodeError) as e:
97
+ raise errors.BmcGoException(f"无法读取或解析 {self.service_json_path}: {e}")
98
+
99
+ def get_commits_in_range(self, start_date: str, end_date: str) -> List[Commit]:
100
+ start_dt = self.validate_date(start_date)
101
+ if end_date is None:
102
+ end_dt = datetime.datetime.now(beijing_timezone)
103
+ else:
104
+ try:
105
+ end_dt = self.validate_date(end_date)
106
+ except errors.BmcGoException as e:
107
+ if "超过当前日期" in str(e):
108
+ log.warning(f"警告: {e},将使用当前日期作为结束日期")
109
+ end_dt = datetime.datetime.now(beijing_timezone).replace(tzinfo=None)
110
+ else:
111
+ raise
112
+ if end_dt < start_dt:
113
+ raise errors.BmcGoException("结束日期不能早于起始日期")
114
+ since_str = start_dt.strftime("%Y-%m-%d")
115
+ until_str = end_dt.strftime("%Y-%m-%d")
116
+ try:
117
+ commits = list(self.repo.iter_commits(
118
+ since=since_str,
119
+ until=until_str
120
+ ))
121
+ except GitCommandError as e:
122
+ raise errors.BmcGoException(f"获取commit记录时出错: {e}")
123
+ return commits
124
+
125
+ def get_version_from_commit(self, commit_hash: str) -> Optional[str]:
126
+ try:
127
+ commit = self.repo.commit(commit_hash)
128
+ try:
129
+ file_content = (commit.tree / self.service_json_path).data_stream.read().decode('utf-8')
130
+ data = json.loads(file_content)
131
+ version = data.get('version')
132
+ return version
133
+ except (KeyError, AttributeError):
134
+ log.error(f"在commit {commit_hash} 中找不到文件: {self.service_json_path}")
135
+ return None
136
+ except json.JSONDecodeError:
137
+ log.error(f"在commit {commit_hash} 中的 {self.service_json_path} 文件不是有效的JSON格式")
138
+ return None
139
+ except Exception as e:
140
+ log.error(f"获取commit {commit_hash} 的版本信息时出错: {e}")
141
+ return None
142
+
143
+ def get_version_info_for_commits(self, commits: List[Commit]) -> Dict[str, Any]:
144
+ version_info = {
145
+ 'current_version': self.get_current_version(),
146
+ 'commit_versions': {}
147
+ }
148
+ # 按时间顺序处理commit(从旧到新)
149
+ for commit in reversed(commits):
150
+ try:
151
+ if not self.is_merge_into_main(str(commit.summary)):
152
+ continue
153
+ modified_version = self.get_version_from_commit(commit.hexsha)
154
+ message = commit.message.strip()
155
+ lines = message.splitlines()
156
+ if len(lines) >= 3:
157
+ message = lines[2]
158
+ version_info['commit_versions'][commit.hexsha] = {
159
+ 'date': commit.committed_datetime.strftime("%Y-%m-%d"),
160
+ 'message': message,
161
+ 'version': modified_version if modified_version else version_info['current_version']
162
+ }
163
+ if modified_version:
164
+ version_info['current_version'] = modified_version
165
+ except Exception as e:
166
+ log.error(f"处理commit {commit.hexsha} 时出错: {e}")
167
+ message = commit.message.strip()
168
+ lines = message.splitlines()
169
+ if len(lines) >= 3:
170
+ message = lines[2]
171
+ version_info['commit_versions'][commit.hexsha] = {
172
+ 'date': commit.committed_datetime.strftime("%Y-%m-%d"),
173
+ 'message': message,
174
+ 'version': version_info['current_version']
175
+ }
176
+ return version_info
177
+
178
+ def generate_release_note(self,
179
+ start_date: str,
180
+ end_date: str,
181
+ output_file: str) -> str:
182
+ release_note = f"从{start_date}至{end_date}特性提交如下:\n"
183
+ commits = self.get_commits_in_range(start_date, end_date)
184
+ if not commits:
185
+ try:
186
+ release_note += f"无更新\n"
187
+ with open(output_file, 'w', encoding='utf-8') as f:
188
+ f.write(release_note)
189
+ log.info(f"Release Note已保存到: {output_file}")
190
+ except IOError as e:
191
+ log.error(f"保存文件时出错: {e}")
192
+ version_info = self.get_version_info_for_commits(commits)
193
+
194
+ for commit in commits:
195
+ commit_info = version_info['commit_versions'].get(commit.hexsha, {})
196
+ if len(commit_info) == 0:
197
+ continue
198
+ release_note += f"-commit ID: {commit.hexsha}\n"
199
+ release_note += f"-修改描述: {commit_info.get('message')}\n"
200
+ release_note += f"-版本号: {commit_info.get('version')}\n"
201
+ release_note += f"-发布日期:{commit_info.get('date')}\n\n"
202
+ if output_file:
203
+ try:
204
+ with open(output_file, 'w', encoding='utf-8') as f:
205
+ f.write(release_note)
206
+ log.info(release_note)
207
+ log.info(f"Release Note已保存到: {output_file}")
208
+ except IOError as e:
209
+ log.error(f"保存文件时出错: {e}")
210
+ return release_note
211
+
212
+ def run(self):
213
+ try:
214
+ _ = self.generate_release_note(
215
+ start_date=self.start_date,
216
+ end_date=self.end_date,
217
+ output_file=self.output_file
218
+ )
219
+ except (ValueError, RuntimeError) as e:
220
+ log.error(f"错误: {e}")