使用NSIS制作便携软件:以PotPlayer Portable为例
正如上一篇文章所言,我推荐大家使用P.A.L来制作便携软件,重新制作轮子是没有必要的。关于便携软件的多个难点,例如多实例、进程控制、非正常退出恢复、临时复制到本地运行、环境变量、路径替换等等,在P.A.L中都有相当成熟的解决方案,用户只用填写一个ini,而不需要考虑便携软件是怎样运行的。省时省力、稳定可靠是其优点。但是,有的时候我们不得不编写一段自定义代码 (custom code)才能实现某些要求,或者你是个代码狂人,喜欢看到自己指尖的代码编译成屏幕上运行的程序,那么就必须接触P.A.L的母体——NSIS。
Nullsoft 脚本安装系统(Nullsoft Scriptable Install System)是一个开放源代码的安装程序制作工具,(Nullsoft 也是WinAmp的制作方)。使用一种制作安装程序的语言来制作一种从不需要安装的程序——PortableApps,再适合不过了。因为从本质来讲,Portableapps的运行原理,其实就是在程序启动时进行一次“安装”,在程序结束后进行一次“卸载”(当然这个时间一般非常快)。PortableApps Launcher的主要对象无非是注册表项与文件,而处理这些,正是NSIS的长项。NSIS语言简明易懂,特别擅长处理windows系统中的进程、注册表与文件。以下以PotPlayer Portable为例,浅谈使用NSIS制作便携软件的一般流程。
通过虚拟机观察得知,当PotPlayerMini.exe同目录存在PotPlayerMini.ini时,PotPlayer将配置保存在此ini中,否则,则将配置保存在注册表中。
因此,我们可以有两种选择:让PotPlayer保存配置在注册表,并在结束时导出为reg文件;或者保证其运行时目录下存在PotPlayerMini.ini,在结束后将PotPlayerMini.ini移至 Data 目录(程序与配置分离原则)。后一种选择的优点是,PotPlayer Portable 可以与系统中安装的PotPlayer 或其它位置的PotPlayer Portable 同时运行,不会互相干扰。因此我选择了保存配置进 ini 的方案。
另外,我希望PotPlayer Portable首次运行时显示XMP-Gray-Tab 这个皮肤,而不是默认的棒子文皮肤。因此,需要创建一个DefaultData。
创建 PotPlayerPortable 文件夹,创建 App\AppInfo ,App\DefaultData , App\PotPlayer ,Sources 。如右图。
将PotPlayer 程序文件放在 App\PotPlayer 目录中。
在 App\DefaultData 中创建 PotPlayerMini.ini ,写入:
1 2 |
[Settings] LastSkinName=[smilefly]XMP-Gray-Tab.dsf ;定义默认皮肤 |
提取 PotPlayer 图标,保存为:App\AppInfo\appicon.ico 。
在 Source\中创建 PotPlayerPortable.nsi ,开始敲代码吧。以下分为3段讲解这个nsi文件。
定义变量:
将固定的文件、注册表项以变量的形式在文件头部声明,不但可以少敲些文字,也有利于减少笔误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
; David Pi ; PortableAppC.com ; ************************************************************************** ; === 定义变量 === ; ************************************************************************** !define VER "1.5.29599.0" ; launcher 的版本 !define APPNAME "PotPlayer" ; 程序全名 !define APP "PotPlayer" ; 程序短名称(不带空格) !define APPEXE "PotPlayerMini.exe" ; 主程序 !define APPDIR "App\PotPlayer" ; 主程序路径 !define APPSWITCH `` ; 默认运行参数 ;--- 声明注册表 --- !define REGKEY1 "HKEY_CURRENT_USER\Software\Daum" ;其实对于 PotPlayer Portable 的制作并不需要修改注册表,作为示例而加上。 ; ---声明目录 --- !define LOCALDIR1 "$EXEDIR\App\PotPlayer\Capture" !define PORTABLEDIR1 "$EXEDIR\Data\Capture" ; 截图目录,保存到Data\ !define LOCALDIR2 "$EXEDIR\App\PotPlayer\log" ; 日志目录,结束后清除 !define LOCALDIR3 "$EXEDIR\App\PotPlayer\Playlist" !define PORTABLEDIR3 "$EXEDIR\Data\Playlist" ; 播放列表,保存到Data\ ; ---声明文件 --- !define LOCALFILE1 "$EXEDIR\App\PotPlayer\PotPlayerMini.ini" ;运行时位置 !define PORTABLEFILE1 "$EXEDIR\Data\settings\PotPlayerMini.ini" ;关闭后保存位置 !define DEFAULTFILE1 "$EXEDIR\App\DefaultData\PotPlayerMini.ini" ;默认设置 !define STATUS "$EXEDIR\Data\settings\${APP}Portable-RunningStatus.ini" ;这个文件,是判断便携软件是否正在运行、以及上一次是否正常退出的依据。 ; ************************************************************************** ; === 压缩选项 === ; ************************************************************************** SetCompressor /SOLID lzma SetCompressorDictSize 32 ; ************************************************************************** ; === 基本信息 === ; ************************************************************************** Name "${APPNAME} Portable" OutFile "..\${APP}Portable.exe" Icon "..\App\Appinfo\AppIcon.ico" ; ************************************************************************** ; === 版本信息 === ; ************************************************************************** Caption "${APPNAME} Portable" VIProductVersion "${VER}" VIAddVersionKey ProductName "${APPNAME} Portable" VIAddVersionKey Comments "Allows ${APPNAME} to be run from a removable drive." VIAddVersionKey CompanyName "PortableAppC.com" VIAddVersionKey LegalCopyright "David Pi" VIAddVersionKey FileDescription "${APPNAME} Portable" VIAddVersionKey FileVersion "${VER}" VIAddVersionKey ProductVersion "${VER}" VIAddVersionKey InternalName "${APPNAME} Portable" VIAddVersionKey LegalTrademarks "" VIAddVersionKey OriginalFilename "${APP}Portable.exe" ; ************************************************************************** ; === 头文件 === ; ************************************************************************** !include "LogicLib.nsh" !include "Registry.nsh" !include "TextFunc.nsh" !insertmacro GetParameters ; ************************************************************************** ; === 其它运行选项 === ; ************************************************************************** WindowIcon Off SilentInstall Silent AutoCloseWindow True RequestExecutionLevel user |
运行阶段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
; ************************************************************************** ; ==== 运行 ==== ; ************************************************************************** Section "Main" ;CheckDirExe: IfFileExists "$EXEDIR\${APPDIR}\${APPEXE}" +3 MessageBox MB_OK|MB_ICONEXCLAMATION `${APPNAME} Portable无法启动,请重新安装${APPNAME} Portable。$\n错误:找不到$EXEDIR\${APPDIR}\${APPEXE}。` Abort ;CheckRunExe: FindProcDLL::FindProc "${APPEXE}" ${If} $R0 == "0" Goto CheckGoodExit ;无第二实例,正常启动 ${Else} MessageBox MB_OK|MB_IconStop "另一个${APPNAME}正在运行,请在执行${APPNAME} Portable前关闭${APPNAME}!" Abort ${Endif} CheckGoodExit: IfFileExists "${STATUS}" 0 SplashLogo ;假如正常退出,此文件不应该存在 MessageBox MB_OK|MB_ICONEXCLAMATION `上一次${APPNAME} Portable在本机结束时未能执行数据恢复,现在将执行恢复并重新启动。` Call Restore SplashLogo: ReadINIStr $0 "$EXEDIR\${APP}Portable.ini" "${APP}Portable" "DisableSplashScreen" StrCmp $0 "true" Backup WriteINIStr "$EXEDIR\${APP}Portable.ini" "${APP}Portable" "DisableSplashScreen" "false" InitPluginsDir File /oname=$PLUGINSDIR\splash.bmp "Splash.bmp" newadvsplash::show /NOUNLOAD 1000 300 200 1 /L $PLUGINSDIR\splash.bmp Backup: CreateDirectory "$EXEDIR\Data\settings" WriteINIStr "${STATUS}" "${APP}Portable" "DeadInPeace" "false" ;写入状态文件 ;备份本地注册表: {registry::DeleteKey} "${REGKEY1}-BackupBy${APP}Portable" $R0 ${registry::MoveKey} "${REGKEY1}" "${REGKEY1}-BackupBy${APP}Portable" $R0 Sleep 50 ;恢复便携软件注册表: ${registry::RestoreKey} "$EXEDIR\Data\settings\${APP}.reg" $R0 Sleep 50 ;清扫工作,假如用户直接运行了App\PotPlayer中的程序,就会生成这些文件,影响后面的工作: RMDir /r ${LOCALDIR1} RMDir /r ${LOCALDIR2} RMDir /r ${LOCALDIR3} Delete ${LOCALFILE1} ;恢复本地文件: IfFileExists "${PORTABLEFILE1}" +2 CopyFiles /silent "${DEFAULTFILE1}" "$EXEDIR\Data\settings\" ;首次运行时,复制默认配置 Rename "${PORTABLEDIR1}" "${LOCALDIR1}" Rename "${PORTABLEDIR3}" "${LOCALDIR3}" Rename "${PORTABLEFILE1}" "${LOCALFILE1}" ;将用户配置移动到程序目录中去 ;启动主程序: SetOutPath "$EXEDIR\${APPDIR}" ${GetParameters} $R0 ExecWait `"$EXEDIR\${APPDIR}\${APPEXE}"${APPSWITCH} $R0` ;等待程序结束 ;程序结束,开始恢复 Call Restore End: ${registry::Unload} newadvsplash::stop /WAIT SectionEnd |
Restore 函数:之所以不将 Restore 写入 Section ,而是写成 Function ,是为了方便“CheckGoodExit”段的调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
Function Restore ;导出注册表: Delete "$EXEDIR\Data\settings\${APP}.reg" CreateDirectory "$EXEDIR\Data\settings" {registry::SaveKey} "${REGKEY1}" "$EXEDIR\Data\settings\${APP}.reg" "/A=1" $R0 ;恢复注册表: ${registry::DeleteKey} "${REGKEY1}" $R0 ${registry::MoveKey} "${REGKEY1}-BackupBy${APP}Portable" "${REGKEY1}" $R0 ${registry::DeleteKeyEmpty} "${REGKEY1}" $R0 ;备份用户配置: RMDir /r "${PORTABLEDIR1}" Rename "${LOCALDIR1}" "${PORTABLEDIR1}" RMDir /r "${PORTABLEDIR3}" Rename "${LOCALDIR3}" "${PORTABLEDIR3}" RMDir /r "${PORTABLEFILE1}" Rename "${LOCALFILE1}" "${PORTABLEFILE1}" RMDir /r "${LOCALDIR2}" ;清除Log文件 Delete "${STATUS}" ;正常退出,删除状态文件 FunctionEnd |
就是这么简单。打开NSIS,选择 Compile NSI scripts ,把保存的nsi文件拖进去,编译成功后,在 PotPlayerPortable 根目录则会生成 PotPlayerPortable.exe。
如果你希望制作的便携软件更好地在PortableApps.com 软件平台运行,可以参考上一篇文章编写appinfo.ini。
请注意,以上的代码只是最基础的示例,如果你希望制作更加强大、完善的便携软件,以下材料也许有帮助。
- NSIS 官方文档
- NSIS 插件列表
- Winamp论坛NSIS讨论区
- PortableAppZ提供的示例模板
- Windows Live Writer的示例模板
- PortableApps.com提供的大部分开源软件。阅读其 Other\Source 中的源代码将会非常有帮助。
发表评论
要发表评论,您必须先登录。