374 lines
15 KiB
Python
374 lines
15 KiB
Python
#!/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...")
|
||
|
||
# 标准化安装包命名(不带时间戳和空格,便于固定下载地址)
|
||
# 将应用名称中的空格替换为下划线
|
||
app_name_safe = self.app_name_en.replace(' ', '_')
|
||
installer_name = f"{app_name_safe}_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
|
||
|
||
# 🔥 请求管理员权限(解决静默安装时的"Access denied"问题)
|
||
RequestExecutionLevel admin
|
||
|
||
# 定义应用程序信息
|
||
!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() |