#!/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" # 应用程序信息(使用英文避免CI/CD环境乱码) self.app_name = "ShuiDi AI Assistant" self.app_name_en = "ShuiDi AI Assistant" # 从 config.py 读取版本号(自动同步) self.app_version = self._get_version_from_config() self.app_publisher = "Shuidrop Technology" self.app_url = "https://shuidrop.com/" self.exe_name = "main.exe" def _get_version_from_config(self): """从 config.py 读取版本号""" try: config_file = self.project_root / "config.py" if config_file.exists(): with open(config_file, 'r', encoding='utf-8') as f: content = f.read() import re match = re.search(r'APP_VERSION\s*=\s*["\']([^"\']+)["\']', content) if match: version = match.group(1) print(f"Read version from config.py: v{version}") return version except Exception as e: print(f"WARNING: Failed to read version: {e}, using default version") return "1.0.0" def check_prerequisites(self): """检查构建前提条件""" print("Checking build prerequisites...") # 检查NSIS安装 try: result = subprocess.run(['makensis', '/VERSION'], capture_output=True, text=True, check=True) nsis_version = result.stdout.strip() print(f"NSIS version: {nsis_version}") except (subprocess.CalledProcessError, FileNotFoundError): print("ERROR: NSIS or makensis command not found") print(" Please download and install NSIS from https://nsis.sourceforge.io/Download") print(" Make sure to add NSIS to system PATH") return False # 检查dist目录 if not self.dist_dir.exists(): print(f"ERROR: Build output directory not found: {self.dist_dir}") print(" Please run build_production.py first") return False # 检查主程序文件 exe_path = self.dist_dir / self.exe_name if not exe_path.exists(): print(f"ERROR: Main executable not found: {exe_path}") return False print(f"Found main executable: {exe_path}") return True def prepare_assets(self): """准备安装包资源文件""" print("Preparing asset files...") # 创建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"Converted main icon: {icon_source.name} -> icon.ico") icon_found = True break except ImportError: print("WARNING: PIL library not installed, skipping icon conversion") break except Exception as e: print(f"WARNING: Main icon conversion failed: {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"Converted uninstall icon: {uninstall_icon_source.name} -> uninstall_icon.ico") uninstall_icon_found = True except ImportError: print("WARNING: PIL library not installed, skipping uninstall icon conversion") except Exception as e: print(f"WARNING: Uninstall icon conversion failed: {e}") else: print("WARNING: Uninstall icon not found, will use main icon") if not icon_found: print("WARNING: Main icon not found, will use default icon") return { 'main_icon': icon_found, 'uninstall_icon': uninstall_icon_found } def generate_nsis_script(self, icon_info): """生成NSIS安装脚本""" print("Generating NSIS installer script...") # 标准化安装包命名(不带时间戳,便于固定下载地址) installer_name = f"{self.app_name_en}_Setup_v{self.app_version}.exe" # 示例: ShuiDi_AI_Assistant_Setup_v1.4.12.exe print(f"Installer name: {installer_name}") nsis_content = f'''# ShuiDi AI Assistant NSIS Installer Script # 自动生成于 {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" "ShuiDi AI Assistant" 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 script generated: {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 created: {license_file}") def build_installer(self): """构建安装包""" print("Building NSIS installer...") # 创建输出目录 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 compilation successful") if result.stdout: print("NSIS output:") print(result.stdout) return True except subprocess.CalledProcessError as e: print("ERROR: NSIS compilation failed") print(f"Error message: {e.stderr}") return False def run(self): """执行完整的构建流程""" print("=" * 60) print(f"Installer Build Tool for {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("Installer build completed successfully!") print(f"Installer location: {installer_path}") print(f"File size: {installer_size:.1f} MB") print("Ready to distribute to users") print("=" * 60) return True else: print("ERROR: Installer file not found") return False except Exception as e: print(f"ERROR: Build process error: {e}") return False def main(): """主函数""" installer = NSISInstaller() success = installer.run() if not success: sys.exit(1) if __name__ == "__main__": main()