Files
envi-code/autoCompile/WF2026/cmake_batch_build.py
T
2026-09-28 15:28:50 +08:00

817 lines
34 KiB
Python

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()