Files
envi-code/autoCompile/WF2026/cmake_batch_build.py
T

817 lines
34 KiB
Python
Raw Normal View History

2026-09-28 15:28:50 +08:00
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()