Files
shuidrop_gui/installer/build_installer.py

374 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()