#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ NSIS 安装包构建脚本 用于创建 GUI 应用程序的Windows安装包 """ import os import sys import shutil import subprocess import datetime from pathlib import Path class NSISInstaller: def __init__(self): self.script_dir = Path(__file__).parent self.project_root = self.script_dir.parent self.dist_dir = self.project_root / "dist" / "MultiPlatformGUI" self.output_dir = self.script_dir / "output" self.assets_dir = self.script_dir / "assets" self.nsis_script = self.script_dir / "installer.nsi" # 应用程序信息 self.app_name = "水滴AI客服智能助手" self.app_name_en = "ShuiDi AI Assistant" self.app_version = "1.0.0" self.app_publisher = "水滴智能科技" self.app_url = "https://shuidrop.com/" self.exe_name = "main.exe" def check_prerequisites(self): """检查构建前提条件""" print("🔍 检查构建前提条件...") # 检查NSIS安装 try: result = subprocess.run(['makensis', '/VERSION'], capture_output=True, text=True, check=True) nsis_version = result.stdout.strip() print(f"✅ NSIS 版本: {nsis_version}") except (subprocess.CalledProcessError, FileNotFoundError): print("❌ 错误: 未找到NSIS或makensis命令") print(" 请从 https://nsis.sourceforge.io/Download 下载并安装NSIS") print(" 确保将NSIS添加到系统PATH环境变量") return False # 检查dist目录 if not self.dist_dir.exists(): print(f"❌ 错误: 未找到构建输出目录 {self.dist_dir}") print(" 请先运行 build_production.py 构建应用程序") return False # 检查主程序文件 exe_path = self.dist_dir / self.exe_name if not exe_path.exists(): print(f"❌ 错误: 未找到主程序文件 {exe_path}") return False print(f"✅ 找到主程序: {exe_path}") return True def prepare_assets(self): """准备安装包资源文件""" print("📁 准备资源文件...") # 创建assets目录 self.assets_dir.mkdir(exist_ok=True) # 准备主程序图标 icon_sources = [ self.project_root / "static" / "ai_assistant_icon_64.png", self.project_root / "static" / "ai_assistant_icon_32.png", self.project_root / "static" / "ai_assistant_icon_16.png" ] # 准备卸载程序专用图标 uninstall_icon_source = self.project_root / "static" / "ai_assistant_icon_uninstall.png" # 转换主程序图标 icon_found = False for icon_source in icon_sources: if icon_source.exists(): try: from PIL import Image img = Image.open(icon_source) ico_path = self.assets_dir / "icon.ico" img.save(ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)]) print(f"✅ 转换主程序图标: {icon_source.name} -> icon.ico") icon_found = True break except ImportError: print("⚠️ 未安装PIL库,跳过图标转换") break except Exception as e: print(f"⚠️ 主程序图标转换失败: {e}") continue # 转换卸载程序图标 uninstall_icon_found = False if uninstall_icon_source.exists(): try: from PIL import Image img = Image.open(uninstall_icon_source) uninstall_ico_path = self.assets_dir / "uninstall_icon.ico" img.save(uninstall_ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)]) print(f"✅ 转换卸载程序图标: {uninstall_icon_source.name} -> uninstall_icon.ico") uninstall_icon_found = True except ImportError: print("⚠️ 未安装PIL库,跳过卸载图标转换") except Exception as e: print(f"⚠️ 卸载程序图标转换失败: {e}") else: print("⚠️ 未找到卸载程序专用图标,将使用主程序图标") if not icon_found: print("⚠️ 未找到主程序图标文件,将使用默认图标") return { 'main_icon': icon_found, 'uninstall_icon': uninstall_icon_found } def generate_nsis_script(self, icon_info): """生成NSIS安装脚本""" print("📝 生成NSIS安装脚本...") timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") installer_name = f"{self.app_name_en}_Setup_{self.app_version}_{timestamp}.exe" nsis_content = f'''# 水滴AI客服智能助手 NSIS 安装脚本 # 自动生成于 {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} Unicode True # 定义应用程序信息 !define APP_NAME "{self.app_name}" !define APP_NAME_EN "{self.app_name_en}" !define APP_VERSION "{self.app_version}" !define APP_PUBLISHER "{self.app_publisher}" !define APP_URL "{self.app_url}" !define APP_EXE "{self.exe_name}" # 安装程序信息 Name "${{APP_NAME}} v${{APP_VERSION}}" OutFile "output\\{installer_name}" InstallDir "$PROGRAMFILES\\${{APP_NAME_EN}}" InstallDirRegKey HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}" "InstallPath" # 界面设置 !include "MUI2.nsh" !define MUI_ABORTWARNING {f'!define MUI_ICON "assets\\\\icon.ico"' if icon_info.get('main_icon', False) else ''} {f'!define MUI_UNICON "assets\\\\uninstall_icon.ico"' if icon_info.get('uninstall_icon', False) else f'!define MUI_UNICON "assets\\\\icon.ico"' if icon_info.get('main_icon', False) else ''} # 页面配置 !insertmacro MUI_PAGE_WELCOME !insertmacro MUI_PAGE_LICENSE "assets\\license.txt" !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_PAGE_FINISH !insertmacro MUI_UNPAGE_WELCOME !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES !insertmacro MUI_UNPAGE_FINISH # 语言 !insertmacro MUI_LANGUAGE "SimpChinese" # 版本信息 VIProductVersion "{self.app_version}.0" VIAddVersionKey /LANG=2052 "ProductName" "${{APP_NAME}}" VIAddVersionKey /LANG=2052 "Comments" "水滴AI客服智能助手" VIAddVersionKey /LANG=2052 "CompanyName" "${{APP_PUBLISHER}}" VIAddVersionKey /LANG=2052 "FileDescription" "${{APP_NAME}} 安装程序" VIAddVersionKey /LANG=2052 "FileVersion" "${{APP_VERSION}}" VIAddVersionKey /LANG=2052 "ProductVersion" "${{APP_VERSION}}" VIAddVersionKey /LANG=2052 "OriginalFilename" "{installer_name}" VIAddVersionKey /LANG=2052 "LegalCopyright" "© 2024 ${{APP_PUBLISHER}}" # 安装段 Section "MainSection" SEC01 SetOutPath "$INSTDIR" SetOverwrite on # 复制所有文件 File /r "..\\dist\\MultiPlatformGUI\\*.*" # 复制图标文件到安装目录 {f'File "assets\\\\icon.ico"' if icon_info.get('main_icon', False) else ''} {f'File "assets\\\\uninstall_icon.ico"' if icon_info.get('uninstall_icon', False) else ''} # 创建开始菜单快捷方式 CreateDirectory "$SMPROGRAMS\\${{APP_NAME}}" {f'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\${{APP_NAME}}.lnk" "$INSTDIR\\\\${{APP_EXE}}" "" "$INSTDIR\\\\icon.ico" 0' if icon_info.get('main_icon', False) else 'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\${{APP_NAME}}.lnk" "$INSTDIR\\\\${{APP_EXE}}"'} {f'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\卸载${{APP_NAME}}.lnk" "$INSTDIR\\\\uninstall.exe" "" "$INSTDIR\\\\uninstall_icon.ico" 0' if icon_info.get('uninstall_icon', False) else f'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\卸载${{APP_NAME}}.lnk" "$INSTDIR\\\\uninstall.exe" "" "$INSTDIR\\\\icon.ico" 0' if icon_info.get('main_icon', False) else 'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\卸载${{APP_NAME}}.lnk" "$INSTDIR\\\\uninstall.exe"'} # 创建桌面快捷方式 {f'CreateShortCut "$DESKTOP\\\\${{APP_NAME}}.lnk" "$INSTDIR\\\\${{APP_EXE}}" "" "$INSTDIR\\\\icon.ico" 0' if icon_info.get('main_icon', False) else 'CreateShortCut "$DESKTOP\\\\${{APP_NAME}}.lnk" "$INSTDIR\\\\${{APP_EXE}}"'} # 写入注册表 WriteRegStr HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}" "InstallPath" "$INSTDIR" WriteRegStr HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}" "Version" "${{APP_VERSION}}" # 写入卸载信息 WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "DisplayName" "${{APP_NAME}}" WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "UninstallString" "$INSTDIR\\uninstall.exe" WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "DisplayVersion" "${{APP_VERSION}}" WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "Publisher" "${{APP_PUBLISHER}}" WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "URLInfoAbout" "${{APP_URL}}" WriteRegDWORD HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "NoModify" 1 WriteRegDWORD HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "NoRepair" 1 # 创建卸载程序 WriteUninstaller "$INSTDIR\\uninstall.exe" SectionEnd # 卸载段 Section "Uninstall" # 删除快捷方式 Delete "$SMPROGRAMS\\${{APP_NAME}}\\${{APP_NAME}}.lnk" Delete "$SMPROGRAMS\\${{APP_NAME}}\\卸载${{APP_NAME}}.lnk" RMDir "$SMPROGRAMS\\${{APP_NAME}}" Delete "$DESKTOP\\${{APP_NAME}}.lnk" # 删除安装目录 RMDir /r "$INSTDIR" # 删除注册表项 DeleteRegKey HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" DeleteRegKey HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}" SectionEnd ''' # 写入NSIS脚本文件 # 使用UTF-8 BOM编码,NSIS可以正确识别 with open(self.nsis_script, 'w', encoding='utf-8-sig') as f: f.write(nsis_content) print(f"✅ NSIS脚本已生成: {self.nsis_script}") return installer_name def create_license_file(self): """创建许可证文件""" license_file = self.assets_dir / "license.txt" license_content = f"""软件许可协议 {self.app_name} v{self.app_version} 版权所有 © 2025 {self.app_publisher} 使用本软件即表示您同意以下条款: 1. 本软件仅供合法用途使用 2. 不得对软件进行反向工程、反编译或反汇编 3. 软件按"原样"提供,不提供任何明示或暗示的保证 4. 在任何情况下,软件提供者均不对因使用本软件而产生的任何损害承担责任 如有疑问,请联系:{self.app_url} """ with open(license_file, 'w', encoding='utf-8-sig') as f: f.write(license_content) print(f"✅ 许可证文件已创建: {license_file}") def build_installer(self): """构建安装包""" print("🚀 开始构建NSIS安装包...") # 创建输出目录 self.output_dir.mkdir(exist_ok=True) # 执行NSIS编译 try: cmd = ['makensis', str(self.nsis_script)] result = subprocess.run(cmd, cwd=str(self.script_dir), capture_output=True, text=True, check=True) print("✅ NSIS编译成功") if result.stdout: print("NSIS输出:") print(result.stdout) return True except subprocess.CalledProcessError as e: print("❌ NSIS编译失败") print(f"错误信息: {e.stderr}") return False def run(self): """执行完整的构建流程""" print("=" * 60) print(f"🔧 {self.app_name} 安装包构建工具") print("=" * 60) try: # 1. 检查前提条件 if not self.check_prerequisites(): return False # 2. 准备资源文件 icon_info = self.prepare_assets() # 3. 创建许可证文件 self.create_license_file() # 4. 生成NSIS脚本 installer_name = self.generate_nsis_script(icon_info) # 5. 构建安装包 if not self.build_installer(): return False # 6. 显示结果 installer_path = self.output_dir / installer_name if installer_path.exists(): installer_size = installer_path.stat().st_size / (1024 * 1024) print("\n" + "=" * 60) print("🎉 安装包构建成功!") print(f"📁 安装包位置: {installer_path}") print(f"📏 文件大小: {installer_size:.1f} MB") print("🚀 可以直接分发给用户使用") print("=" * 60) return True else: print("❌ 安装包文件未找到") return False except Exception as e: print(f"❌ 构建过程出错: {e}") return False def main(): """主函数""" installer = NSISInstaller() success = installer.run() if not success: sys.exit(1) if __name__ == "__main__": main()