import json import os import sys import subprocess import shutil import json5 import glob import fnmatch import hashlib from pathlib import Path from dataclasses import dataclass, fields, field, MISSING # 1. 使用 @dataclass 装饰器,Python 会自动为你生成 __init__, __repr__ 等方法 @dataclass class PackageType: type: str name: str encryption: str presets: list platform: list guid: str APPDATA: str OutputBaseFilename: str Shortcut: str MyAppName: str PRODUCT:str = "ENV_2025_V6_S0" hide:bool = False # 可选字段 (有默认值,必须放在最后) #Shortcut: str = "" # 假设你未来想加一个可选的数组字段,必须这样写: # ExtraPlugins: list = field(default_factory=list) def getInnosetupParam(self) -> str: sRel = "" # 1. 检查加密方式是否为 Trial if self.encryption == "Trial": sRel += "/dTRIAL_YEAR " # 2. 检查平台并附加对应的宏 if "ZWCAD" in self.platform: sRel += "/dZWCAD " if "ACAD" in self.platform: sRel += "/dACAD " if "ACAD" in self.platform and "ZWCAD" in self.platform: sRel += "/dMERGER_CAD" # 3. 遍历 presets 数组,添加元素宏 for preset in self.presets: sRel += f"/d{preset} " # 4. 将需要以键值对输出的变量集中处理 kv_vars = { "guid": self.guid, "APPDATA": self.APPDATA, "OutputBaseFilename": self.OutputBaseFilename, "Shortcut": self.Shortcut, "MyAppName": self.MyAppName, "PRODUCT": self.PRODUCT } for key, value in kv_vars.items(): # 只有当值不为空时才追加(可根据您的实际业务需求调整是否容许空值) if value: sRel += f"/d{key}=\"{value}\" " else: print("❌ {key} 的值为空!") # 返回结果,并使用 strip() 去掉字符串最末尾的一个多余空格,使其更规范 return sRel.strip() def display(self): print(f"=== {self.type} ===") # dataclass 仍然可以通过 __dict__ 遍历所有属性 for key, value in self.__dict__.items(): print(f" {key.ljust(20)}: {value}") class PackageParser: def __init__(self, raw_data: list): self.raw_dict = {item["type"]: item for item in raw_data} self.resolved_cache = {} # 提取所有定义的字段名(用于过滤 JSON 中多余的垃圾数据) self.all_fields = [f.name for f in fields(PackageType)] # 核心魔法:只提取没有默认值的字段作为必填校验项 self.required_fields = [ f.name for f in fields(PackageType) if f.default is MISSING and f.default_factory is MISSING ] def parse_all(self) -> list: instances = [] error_logs = [] for pkg_type in self.raw_dict.keys(): resolved_data = self._resolve_inheritance(pkg_type) # 只用 required_fields 进行缺失检查 missing_fields = [field for field in self.required_fields if field not in resolved_data] if missing_fields: error_logs.append(f"错误: 节点 [{pkg_type}] 缺失必填字段: {', '.join(missing_fields)}") else: # 使用 all_fields 进行过滤,防止把 JSON 里的无关字段塞进 dataclass 报错 filtered_data = {k: v for k, v in resolved_data.items() if k in self.all_fields} # 解包传入。如果 JSON 里没有可选字段,这里的 filtered_data 就不包含它, # dataclass 的 __init__ 会自动使用我们在类里定义的默认值。 instances.append(PackageType(**filtered_data)) if error_logs: print("❌ 配置文件解析失败,发现以下必填字段缺失:") for error in error_logs: print(" -", error) return [] return instances def _resolve_inheritance(self, pkg_type: str) -> dict: # (这里的继承逻辑完全保持不变,省略以突出重点) if pkg_type in self.resolved_cache: return self.resolved_cache[pkg_type] if pkg_type not in self.raw_dict: raise ValueError(f"找不到定义的继承基类: {pkg_type}") raw_node = self.raw_dict[pkg_type] merged_data = {} if "inherits" in raw_node: for base_type in raw_node["inherits"]: base_resolved = self._resolve_inheritance(base_type) for k, v in base_resolved.items(): if k == "inherits": continue if isinstance(v, list): if k not in merged_data: merged_data[k] = [] for item in v: if item not in merged_data[k]: merged_data[k].append(item) else: merged_data[k] = v for k, v in raw_node.items(): if k == "inherits": continue if isinstance(v, list): if k not in merged_data: merged_data[k] = [] for item in v: if item not in merged_data[k]: merged_data[k].append(item) else: merged_data[k] = v self.resolved_cache[pkg_type] = merged_data return merged_data def get_all_presets_union(packages: list) -> list: """ 接收一个 PackageType 列表,返回所有 presets 的并集列表(自动去重并保持顺序) """ # 收集所有的 preset all_presets = [] for pkg in packages: all_presets.extend(pkg.presets) # 利用 dict.fromkeys() 去重,同时利用 Python 3.7+ 字典保持插入顺序的特性 union_presets = list(dict.fromkeys(all_presets)) return union_presets def select_packages(packages: list) -> list: """ 在控制台显示非隐藏的 PackageType 列表,并让用户通过输入索引进行多选。 """ # 1. 过滤掉 hide 为 True 的对象 visible_packages = [pkg for pkg in packages if getattr(pkg, 'hide', False) == False] if not visible_packages: print("没有可供显示的打包类型。") return [] # 2. 检查是否有命令行参数传入 # sys.argv[0] 是脚本名称本身,sys.argv[1:] 是传入的参数列表 cli_args = sys.argv[1:] if cli_args: unique_indices = [] for idx_str in cli_args: try: idx = int(idx_str) # 检查索引是否越界 if 0 <= idx < len(visible_packages): if idx not in unique_indices: unique_indices.append(idx) else: print(f"⚠️ 致命错误: 命令行传入的序号 '{idx}' 超出范围。") sys.exit(1) # 命令行参数错误时,直接退出程序 except ValueError: print(f"⚠️ 致命错误: 命令行参数 '{idx_str}' 不是有效的数字。") sys.exit(1) # 解析成功,直接返回结果 selected_packages = [visible_packages[i] for i in unique_indices] print("✅ 检测到命令行参数,已自动选择:") for p in selected_packages: print(f" -> {p.name}") return selected_packages # ========================================== # 3. 如果没有命令行参数,则进入交互模式 # ========================================== print("\n请选择要执行的打包任务(输入数字序号,多选请用空格隔开,例如: 0 2 3):") print("-" * 50) for i, pkg in enumerate(visible_packages): print(f"[{i}] {pkg.name} (Type: {pkg.type})") print("-" * 50) # 3. 循环等待并处理用户输入 while True: user_input = input("请输入您的选择: ").strip() if not user_input: print("输入不能为空,请重新输入。") continue selected_indices_str = user_input.split() unique_indices = [] # 用来存放去重后的合法数字索引 has_error = False for idx_str in selected_indices_str: try: idx = int(idx_str) # 检查索引是否越界 if 0 <= idx < len(visible_packages): # 在这里对索引进行去重,防止用户输入 "0 0 1" if idx not in unique_indices: unique_indices.append(idx) else: print(f"⚠️ 错误: 序号 '{idx}' 超出范围,请重新输入。") has_error = True break except ValueError: print(f"⚠️ 错误: '{idx_str}' 不是有效的数字,请重新输入。") has_error = True break # 如果遍历过程中没有发生任何错误,并且用户确实做出了选择 if not has_error and unique_indices: # 根据去重后的索引,生成对应的 PackageType 列表 selected_packages = [visible_packages[i] for i in unique_indices] print("\n✅ 您已选择:") for p in selected_packages: print(f" -> {p.name}") return selected_packages def load_config(config_path="cmake_batch_build.json"): if not os.path.exists(config_path): print(f"错误: 找不到配置文件 {config_path}") sys.exit(1) try: with open(config_path, "r", encoding="utf-8") as f: return json5.load(f) except Exception as e: print(f"解析配置文件失败,请检查语法: {e}") sys.exit(1) def resolve_variables(text, context_vars): """递归解析字符串中的 ${variable} 变量""" while True: replaced = text for k, v in context_vars.items(): if isinstance(v, str): replaced = replaced.replace(f"${{{k}}}", v) if replaced == text: break text = replaced return text def buildStartApp(startapp_action, definitions): #startapp_action = actions.get("compile-startApp", {}) ss = os.getcwd() if startapp_action: print("--- 正在编译 startApp 工程 ---") for cp in startapp_action.get("copy", []): form_list = cp.get("form", []) to_list = cp.get("to", []) for f_item in form_list: for t_item in to_list: f_resolved = resolve_variables(f_item, definitions) t_resolved = resolve_variables(t_item, definitions) print(f"执行预处理拷贝: 从 {f_resolved} 到 {t_resolved}") try: # 确保目标文件夹存在,否则 copy 会报错 os.makedirs(os.path.dirname(t_resolved), exist_ok=True) # 使用 shutil.copy2 进行复制 (相当于 copy /y,且会保留文件的修改时间等元数据) shutil.copy2(f_resolved, t_resolved) print(f"✅ 拷贝成功: {t_resolved}") except FileNotFoundError: print(f"❌ 拷贝失败: 找不到源文件 {f_resolved}") except Exception as e: print(f"❌ 拷贝失败: {e}") start_cmd = resolve_variables(startapp_action.get("cmd", ""), definitions) print(f"执行 MSBuild: {start_cmd}") build_res = subprocess.run(start_cmd, shell=True) if build_res.returncode == 0: print(f"✅ startAPP 编译成功。") if os.path.exists("startApp.log"): try: os.remove("startApp.log") except Exception: pass def svnUpdate(update_action): if update_action: update_cmd = update_action.get("cmd", "svn update") update_path = update_action.get("path", "../../") print(f"--- 正在更新代码: {update_cmd} {update_path} ---") subprocess.run(update_cmd + ' ' + update_path, shell=True) def buildPresets(presets, definitions): # 现在编译presets,先取出presets的目录,作为工作目录 work_dir = resolve_variables(definitions.get("work-dir", "."), definitions) script_dir = os.getcwd() failed_presets = [] print(f"\n--- 开始编译 CMake Presets,目标 Presets: {presets} ---") for preset in presets: print(f"\n>>> 正在处理 Preset: {preset}") print(f"==================================================") print(f" [1/2] 正在配置 CMake Preset: {preset}") print(f"==================================================") configure_cmd = f'cd /d "{work_dir}" && cmake --preset {preset}' res = subprocess.run(configure_cmd, shell=True) if res.returncode != 0: print(f"❌ 错误: Preset [{preset}] 的 Configure (配置) 阶段失败!整个脚本终止。") sys.exit(res.returncode) print(f"\n==================================================") print(f" [2/2] 正在编译 CMake Preset: {preset}") print(f"==================================================") log_filename = f"{preset}.log" log_path = os.path.join(script_dir, log_filename) build_cmd = f'cd /d "{work_dir}" && cmake --build --preset {preset} --config release -j 8 -- /flp1:logfile="{log_path}";errorsonly;Verbosity:quiet' build_res = subprocess.run(build_cmd, shell=True) if build_res.returncode == 0: print(f"✅ Preset [{preset}] 编译成功。") if os.path.exists(log_path): try: os.remove(log_path) except Exception: pass else: print(f"⚠️ 警告: Preset [{preset}] 的 Build (编译) 阶段出现错误 (返回码: {build_res.returncode})。") print(f"错误日志已保存至 {log_path},继续执行下一个 preset...") failed_presets.append(preset) if failed_presets: print("\n" + "="*50) print(f"❌ 致命错误: 以下 Preset 编译失败: {failed_presets}") print("由于存在未成功编译的工程,为了防止产出损坏的安装包,已终止后续的拷贝和打包操作。") print("请检查对应的 .log 日志文件修复代码,然后重新运行本脚本。") print("="*50 + "\n") sys.exit(1) def calculate_files_sha1(file_paths: list) -> str: """ 计算多个文件的整体 SHA1 值。 为保证每次计算结果一致,会对文件路径进行排序。 """ if not file_paths: return "" sha1 = hashlib.sha1() # 排序保证即使文件遍历顺序不同,计算出的哈希也一致 for file_path in sorted(file_paths): if not os.path.isfile(file_path): continue # 记录文件名本身的信息也可以防止“同内容但改了文件名”的情况, # 如果你只关心内容,可以把这行注释掉。 sha1.update(os.path.basename(file_path).encode('utf-8')) # 分块读取文件内容,防止大文件撑爆内存 with open(file_path, 'rb') as f: while chunk := f.read(8192 * 1024): # 每次读取 8MB sha1.update(chunk) return sha1.hexdigest() def process_compression_tasks(compress_tasks: list, definitions: dict): """ 处理 JSON 中定义的压缩任务 :param compress_tasks: json 中的 "compress_huge_files" 数组 :param definitions: 传递给 resolve_variables 的变量字典 """ for task in compress_tasks: # 1. 解析所有路径中的变量 cmd_7za = os.path.abspath(os.path.normpath(task.get("7za", "7za.exe"))) compress_file = os.path.abspath(os.path.normpath(resolve_variables(task["compress_file"], definitions))) sh1_flag_file = os.path.abspath(os.path.normpath(resolve_variables(task["sh1_flag_file"], definitions))) raw_zip_files = [resolve_variables(f, definitions) for f in task["zip_files"]] # 2. 使用 glob 展开通配符(比如 *.*),找到所有真实存在的文件 actual_files = [] for pattern in raw_zip_files: # os.path.normpath 会把路径标准化,glob 才能更好地在 Windows 下工作 pattern = os.path.normpath(pattern) matched = glob.glob(pattern) actual_files.extend([f for f in matched if os.path.isfile(f)]) # 去重(防止通配符和精确路径重复匹配同一文件) actual_files = list(set(actual_files)) # ========================================== # 核心修复:剔除输出文件,防止“自我包含” # 使用 os.path.abspath 和 normcase 确保在 Windows 下路径比对准确(忽略大小写和正反斜杠差异) # ========================================== abs_compress_file = os.path.normcase(os.path.abspath(compress_file)) abs_sh1_file = os.path.normcase(os.path.abspath(sh1_flag_file)) safe_actual_files = [] for f in actual_files: abs_f = os.path.normcase(os.path.abspath(f)) # 如果抓取到的文件是我们将要生成的 7z 或 txt,直接跳过 if abs_f == abs_compress_file or abs_f == abs_sh1_file: continue safe_actual_files.append(f) actual_files = safe_actual_files # ========================================== if not actual_files: print(f"⚠️ 警告: 没有找到任何文件可以压缩,匹配模式为: {raw_zip_files}") continue # ========================================== # 新增:严格校验所有的文件是否在同一个目录下 # 使用 os.path.abspath 防止相对路径和绝对路径混用导致判断失误 # ========================================== parent_dirs = set(os.path.dirname(os.path.abspath(f)) for f in actual_files) if len(parent_dirs) > 1: print(f"❌ 致命错误: 待压缩的文件分布在不同的目录下!") for d in parent_dirs: print(f" - 检测到目录: {d}") print(f"规则要求 zip_files 里的文件必须在同一个目录。当前任务终止。") # 这里建议直接抛出异常或者 sys.exit(1) 中断整个打包流程,因为这是一个配置错误 raise ValueError("压缩失败:被压缩的文件未处于同一目录。") # 校验通过,拿到这个唯一的基准目录 base_dir = list(parent_dirs)[0] # 3. 计算当前真实文件的整体 SHA1 print(f"\n正在计算 {len(actual_files)} 个文件的联合 SHA1...") current_sha1 = calculate_files_sha1(actual_files) print(f"当前 SHA1: {current_sha1}") # 4. 判断是否需要重新压缩 need_compress = False if not os.path.exists(compress_file): print("=> 压缩文件不存在,需要重新压缩。") need_compress = True elif not os.path.exists(sh1_flag_file): print("=> SHA1 标志文件不存在,需要重新压缩。") need_compress = True else: # 读取旧的 SHA1 值进行比对 try: with open(sh1_flag_file, "r", encoding="utf-8") as f: saved_sha1 = f.read().strip() if saved_sha1 != current_sha1: print(f"=> SHA1 值不匹配 (旧: {saved_sha1}),文件已发生变动,需要重新压缩。") need_compress = True else: print(f"✅ 文件未发生变动且压缩包已存在,跳过压缩: {compress_file}") except Exception as e: print(f"=> 读取标志文件失败 ({e}),安全起见进行重新压缩。") need_compress = True # 5. 执行压缩 if need_compress: # 为了防止 7za.exe a 模式下“把新旧文件混进同一个压缩包”,先删除旧包 if os.path.exists(compress_file): try: os.remove(compress_file) except OSError as e: print(f"删除旧压缩包失败: {e}") # 确保压缩文件和标志文件的输出目录存在 os.makedirs(os.path.dirname(compress_file), exist_ok=True) os.makedirs(os.path.dirname(sh1_flag_file), exist_ok=True) file_names_only = [os.path.basename(f) for f in actual_files] # 组装命令行: 7za.exe a "C:/.../hugeDb.7z" Aec3DMep.db shmodel.db command = [cmd_7za, "a", compress_file] + file_names_only print(f"正在执行压缩 (工作目录: {base_dir})...") # print("命令:", " ".join(command)) # 运行压缩 result = subprocess.run(command, cwd=base_dir, shell=False) if result.returncode == 0: # 6. 压缩成功,写入最新的 SHA1 标志 with open(sh1_flag_file, "w", encoding="utf-8") as f: f.write(current_sha1) print("✅ 压缩完成,已更新 SHA1 标志文件。") else: print(f"❌ 压缩失败,7za 返回错误码: {result.returncode}") # 压缩失败时清理残次品,防止下次误认为压缩成功 if os.path.exists(compress_file): os.remove(compress_file) def copy_preset(copy_preset_dict, presets, definitions): """ 处理针对不同编译 Preset 的批量分发拷贝操作 :param copy_preset_dict: json 中的 "copy_preset" 对象字典 :param presets: 类似于 ["R200", "R210"] 的预设数组 :param definitions: 环境变量字典,用于 resolve_variables 解析 """ form_list = copy_preset_dict.get("form", []) to_list = copy_preset_dict.get("to", []) if not form_list or not to_list or not presets: return print(f"\n🚀 开始执行 copy_preset 批量分发,目标平台: {presets}") # 外层循环:遍历每一个 Preset for preset in presets: # 遍历每一个目的地 (to) for t_item in to_list: # 解析目标根路径并拼接 preset t_resolved = resolve_variables(t_item, definitions) dst_dir = os.path.normpath(os.path.join(t_resolved, preset)) # 确保目标目录已存在 os.makedirs(dst_dir, exist_ok=True) # 遍历每一个来源 (form) for f_item in form_list: f_resolved = os.path.normpath(resolve_variables(f_item, definitions)) # 情况 1: form 元素是一个明确的目录 if os.path.isdir(f_resolved): # 行为:源目录也要加上 preset src_dir = os.path.join(f_resolved, 'Release-x64-' + preset) if os.path.isdir(src_dir): print(f"📦 [目录分发] {src_dir} -> {dst_dir}") # shutil.copytree 的 dirs_exist_ok=True (Python 3.8+) # 能够完美实现“把目录里的所有文件(包括子目录)强行覆盖合并到目标目录” shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True) else: print(f"⚠️ 警告: 找不到对应的 preset 源目录,跳过: {src_dir}") # 情况 2: form 元素是文件或通配符 (*.dll, *.* 等) else: # 行为:直接使用 glob 匹配,解析出所有具体文件,无需加上 preset matched_files = glob.glob(f_resolved) if not matched_files: print(f"⚠️ 警告: 通配符/文件未匹配到任何内容: {f_resolved}") continue for f_path in matched_files: if os.path.isfile(f_path): print(f"📄 [文件分发] {f_path} -> {dst_dir}") # 拷贝单文件到目标目录 (如果存在同名文件会自动覆盖) shutil.copy2(f_path, dst_dir) print("✅ copy_preset 批量分发完成。") def process_general_copy(copy_actions, definitions): """ 处理普通的批量 copy 行为 :param copy_actions: json 中的 "copy" 数组 :param definitions: 用于 resolve_variables 解析的环境变量字典 """ # --- 辅助匹配函数(支持相对路径匹配和层级匹配) --- def is_match(rel_path, patterns): if not patterns: return False rel_path_str = str(rel_path).replace('\\', '/') parts = rel_path.parts for pattern in patterns: pattern = pattern.replace('\\', '/') # 情况 A: 路径整体匹配 if fnmatch.fnmatch(rel_path_str, pattern): return True # 情况 B: 逐级文件名或目录名匹配 for part in parts: if fnmatch.fnmatch(part, pattern): return True return False print("\n🚀 开始执行普通 Copy 行为...") for action in copy_actions: form_list = action.get("form", []) to_list = action.get("to", []) # 注意 JSON 里键名是 "include" 和 "exclude" includes = action.get("include", []) excludes = action.get("exclude", []) # m 循环: 遍历源路径 for f_item in form_list: f_resolved = os.path.normpath(resolve_variables(f_item, definitions)) # 判断是以通配符结尾,还是纯目录 base_dir_str, file_pattern = os.path.split(f_resolved) is_wildcard = ('*' in file_pattern or '?' in file_pattern) if is_wildcard: src_dir = Path(base_dir_str).resolve() else: src_dir = Path(f_resolved).resolve() if not src_dir.exists() or not src_dir.is_dir(): print(f"⚠️ 源目录不存在或无效,跳过: {src_dir}") continue # n 循环: 遍历目标路径 for t_item in to_list: t_resolved = os.path.normpath(resolve_variables(t_item, definitions)) dst_dir = Path(t_resolved).resolve() # 根据是否为通配符,决定目标输出的基准路径 if is_wildcard: # 通配符:直接 copy 到 dst_dir,不需要再建根目录 target_base = dst_dir print(f"📂 [通配符拷贝] {src_dir} / {file_pattern} -> {target_base}") else: # 纯目录:在目标文件夹中创建一个和源目录同名的子目录 (例如 xxx/yyy/ddd) target_base = dst_dir / src_dir.name print(f"📦 [整个目录拷贝] {src_dir} -> {target_base}") copy_count = 0 # 开始遍历源目录最深处的所有文件 for root, dirs, files in os.walk(src_dir): for file_name in files: file_path = Path(root) / file_name rel_path = file_path.relative_to(src_dir) # 1. 如果是通配符模式,文件自身必须先符合这个通配符规则 # (例如 *.* 就是全部通过,*.dll 只有 dll 通过) if is_wildcard: if not fnmatch.fnmatch(file_name, file_pattern): continue # 2. 匹配 include / exclude 规则 matched_include = is_match(rel_path, includes) matched_exclude = is_match(rel_path, excludes) # 3. 核心冲突解决逻辑 (白名单优先原则) should_copy = True if includes: # 如果配置了白名单,只有在白名单里才 copy if matched_include: should_copy = True else: should_copy = False else: # 没有配置白名单,碰到黑名单就不 copy if matched_exclude: should_copy = False # 最终覆盖:如果既满足 include 又满足 exclude,以 include 为准! if matched_include: should_copy = True # 4. 执行文件拷贝 if should_copy: target_path = target_base / rel_path # 自动创建深层级的子目录 target_path.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(file_path, target_path) copy_count += 1 print(f" └─ 完成,共拷贝了 {copy_count} 个文件。") def package(innosetup, packages, definitions): """ 调用 Inno Setup 命令行编译器 (ISCC) 为每个选择的 PackageType 进行打包 :param innosetup: json 中的 "innosetup" 对象字典 :param packages: 用户选择的 PackageType 实例列表 :param definitions: 环境变量字典 (用于 resolve_variables) """ if not innosetup or not packages: print("⚠️ 没有提供打包配置或包列表为空,跳过打包。") return print("\n" + "="*50) print("🚀 开始执行 Inno Setup 终极打包流程") print("="*50) iscc = innosetup.get("iscc") iss = innosetup.get("iss") base_cmd = f"\"{iscc}\" \"{iss}\"" success_count = 0 # 3. 循环遍历用户选择的每一个 PackageType for pkg in packages: print(f"\n📦 正在编译安装包: {pkg.name} (Type: {pkg.type})") # 获取属于这个包特有的宏定义参数 # 注意:这里调用的方法名是我们在前面定义的 getInnosetpParam pkg.PRODUCT = definitions.get("product") params = pkg.getInnosetupParam() # 拼接最终执行的完整命令 final_cmd = f'{base_cmd} {params}' print(f"执行命令: {final_cmd}") # 执行命令 (ISCC 这种带双引号包裹路径的复杂命令行,使用 shell=True 兼容性最好) try: result = subprocess.run(final_cmd, shell=True) if result.returncode == 0: print(f"✅ [{pkg.name}] 打包成功!") success_count += 1 else: print(f"❌ [{pkg.name}] 打包失败,ISCC 编译器返回了错误码: {result.returncode}") # 根据你的业务需求,遇到失败可以选择 break 直接终止,或者 continue 继续打下一个包 except Exception as e: print(f"❌ 致命错误:启动 ISCC 失败,请检查路径是否正确: {e}") # 4. 打印最终结果统计 print("\n" + "="*50) print(f"🎉 所有的打包任务已结束!总计: {len(packages)} 个,成功: {success_count} 个。") print("="*50) def main(): # 设置当前脚本的路径为工作目录 script_dir = os.path.dirname(os.path.abspath(__file__)) os.chdir(script_dir) config = load_config("cmake_batch_build.json") definitions = config.get("definitions", {}) actions = config.get("actions", {}) # 解析出所有的PackateType parser = PackageParser(definitions.get("package-types")) all_packages = parser.parse_all() # 与用户交互,得到用户选择的packageType列表 g_packages = select_packages(all_packages) # 得到需要编译的preset列表 g_presets = get_all_presets_union(g_packages) if not g_presets: print("没有选择打包类型。") return # 更新代码 svnUpdate(actions.get("update-svn", {})) # 编译startApp工程 buildStartApp(actions.get("compile-startApp", {}), definitions) # 编译presets buildPresets(g_presets, definitions) # 压缩大文件 process_compression_tasks(actions.get("compress_huge_files"), definitions) #copy 打包文件 dest_dir = resolve_variables(definitions.get("dest_dir", ""), definitions) if os.path.exists(dest_dir): print(f"[*] 清空统一输出基础目录: {dest_dir}") shutil.rmtree(dest_dir, ignore_errors=True) # copy 编译生成物和依赖 copy_preset(actions.get("copy_preset"), g_presets, definitions) # copy 其他需要打包的文件 process_general_copy(actions.get("copy"), definitions) # 打包 package(actions.get("innosetup"), g_packages, definitions) return if __name__ == "__main__": main()