Using NSIS to make a setup

Posted by William Basics on Sunday, November 20, 2022

Using NSIS to make a setup

引言

NSIS是一款制作Windows应用安装包的软件。由于其插件繁多以及不错的可扩展性,所以到今天依旧是一个不错的选择。

准备

文中使用的NSIS是3.08版本。下载地址: https://nsis.sourceforge.io/Download

在VS Code中调试NSIS脚本十分便利,相当推荐。

安装VS Code插件,NSIS

nsis

安装后,需要指定下makensis目录。

config

Build

当然可以通过makensis命令来build。但为了调试方便,我直接用插件提供的build功能。

toolbar

脚本编写

Debug

NSIS的调试主要是靠两个手段: !echoDetailPrint

! 开头表示的是编译时的命令,也就是说 !echo 是在编译时被执行的,在最终安装时不会被执行。相对地,DetailPrint 就是安装时执行的指令。

需要说明的是,!echo 会把跟在后面的内容原模原样的输出到命令行终端。

比如:

; 注释以分号开头
Section FirstStep
    Var /GLOBAL greeting            ; 声明一个变量,变量都看成字符串类型
    StrCpy $greeting "hello world"  ; 给变量赋值
    !echo $greeting
    DetailPrint $greeting
SectionEnd

编译脚本得到

console

可以看出!echo 不会打印出任何变量中的内容。所以基本上没什么用。

DetailPrint 可以打印变量内容,但是由于它是安装时的指令,所以它打印的内容是在安装时打印的。我们修改上面的脚本,加入 DetailPrint

Section FirstStep
    Var /GLOBAL greeting            ; 声明一个变量,变量都看成字符串类型
    StrCpy $greeting "hello world"  ; 给变量赋值
    !echo $greeting
    DetailPrint $greeting
SectionEnd

编译然后build。因为我们这个脚本文件名叫example.nsi,所以同目录下产生了同名安装文件example.exe。

folder

双击运行example.exe。因为我们并没有任何实际上的安装,所以安装立即就完成了。

run

点击 “Show details”, 我们就看到了DetailPrint 打印的内容。

detail

脚本示例

这个脚本是打包.NET6应用的基本安装流程,包括

  1. Windows系统版本检查
  2. 多国语言安装界面
  3. 注册表读写
  4. .NET 6 runtime检查以及安装
  5. 卸载
Unicode True

!define PRODUCT_NAME "YourApplication"
!define PRODUCT_VERSION "0.0.1"
!define PRODUCT_PUBLISHER "You"
!define PRODUCT_CODE "YourApplication"
!define PRODUCT_DIR_REGKEY "Software\Microsoft\Windows\CurrentVersion\App Paths\${PRODUCT_NAME}.exe"
!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
!define PRODUCT_UNINST_ROOT_KEY "HKLM"
!define SETUP_FILENAME "YourApplication.Install.exe"
!define WIN_RUNTIME "windowsdesktop-runtime-6.0.11-win-x64"

!include "MUI2.nsh"
!include "x64.nsh"
!include "nsProcess.nsh"
!include "LogicLib.nsh"
!include "WinVer.nsh"
!include "DotNetChecker.nsh"

; MUI Settings
!define MUI_ABORTWARNING
!define MUI_ICON "${NSISDIR}\Contrib\Graphics\Icons\orange-install.ico"
!define MUI_UNICON "${NSISDIR}\Contrib\Graphics\Icons\orange-uninstall.ico"
!define MUI_WELCOMEFINISHPAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Wizard\orange.bmp"
!define MUI_HEADERIMAGE
!define MUI_HEADERIMAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Header\nsis.bmp"

; Welcome page
!insertmacro MUI_PAGE_WELCOME

;Directory page
!insertmacro MUI_PAGE_DIRECTORY

;Instfiles page
!insertmacro MUI_PAGE_INSTFILES
; Finish page
!insertmacro MUI_PAGE_FINISH
; Uninstaller pages
!insertmacro MUI_UNPAGE_INSTFILES
; Language files
!insertmacro MUI_LANGUAGE "English"
!insertmacro MUI_LANGUAGE "SimpChinese"

Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
OutFile "${SETUP_FILENAME}"
InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}"
InstallDirRegKey HKLM "${PRODUCT_DIR_REGKEY}" ""
ShowInstDetails show
ShowUnInstDetails show
RequestExecutionLevel admin
BrandingText "     ${PRODUCT_PUBLISHER} . "

!define BINARY_SOURCE_DIR "path\to\your\build\output\directory"

Section "Bin" SEC01

  SetOutPath "$INSTDIR\Bin"

  File "..\runtimes\${WIN_RUNTIME}.exe"
  Call DotNetRuntimeCheck

  File "${BINARY_SOURCE_DIR}\*.dll"
  File "${BINARY_SOURCE_DIR}\${PRODUCT_NAME}.exe"
  File "${BINARY_SOURCE_DIR}\${PRODUCT_NAME}.pdb"
  File "${BINARY_SOURCE_DIR}\${PRODUCT_NAME}.runtimeconfig.json"

  ;start menu shortcut
  SetOutPath "$INSTDIR\Bin"
  SetShellVarContext current
  CreateDirectory "$SMPROGRAMS\${PRODUCT_CODE} ${PRODUCT_NAME}"
  CreateShortCut "$SMPROGRAMS\${PRODUCT_CODE} ${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\Bin\${PRODUCT_NAME}.exe"
  CreateShortCut "$SMPROGRAMS\${PRODUCT_CODE} ${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\uninst.exe"

  SetShellVarContext all
  CreateDirectory "$SMPROGRAMS\${PRODUCT_CODE} ${PRODUCT_NAME}"
  CreateShortCut "$SMPROGRAMS\${PRODUCT_CODE} ${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\Bin\${PRODUCT_NAME}.exe"
  CreateShortCut "$SMPROGRAMS\${PRODUCT_CODE} ${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\uninst.exe"
  ;desktop shortcut
  CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\Bin\${PRODUCT_NAME}.exe"

SectionEnd

Section -Post
  WriteUninstaller "$INSTDIR\uninst.exe"
  WriteRegStr HKLM "${PRODUCT_DIR_REGKEY}" "" "$INSTDIR\Bin\${PRODUCT_NAME}.exe"
  WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "${PRODUCT_NAME}"
  WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\uninst.exe"
  WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\Bin\${PRODUCT_NAME}.exe"
  WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
  WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
  WriteRegStr HKLM "Software\${PRODUCT_CODE}\${PRODUCT_NAME}" "AppPath" "$INSTDIR\Bin"
  WriteRegStr HKLM "Software\${PRODUCT_CODE}\${PRODUCT_NAME}" "InstallPath" $INSTDIR
SectionEnd

Function .onInit
	;Language selection dialog

  !insertmacro MUI_LANGDLL_DISPLAY

  ${IF} ${AtLeastWin10}
  ${Else}
    MessageBox MB_OK|MB_ICONEXCLAMATION "This application can only be run on Windows 10"
    Abort
  ${EndIf}

  ${nsProcess::FindProcess} "${PRODUCT_NAME}.exe" $R0
  ${If} $R0 == 0
    MessageBox MB_OK|MB_ICONEXCLAMATION "${PRODUCT_NAME} is running. Please close it first !" IDOK
    Abort
  ${EndIf}

  ReadRegStr $R0 HKLM "Software\${PRODUCT_CODE}\${PRODUCT_NAME}" "InstallPath"
  StrCmp $R0 "" done
  StrCpy $INSTDIR $R0
  done:

FunctionEnd

Function un.onInit
  ${nsProcess::FindProcess} "${PRODUCT_NAME}.exe" $R0
  ${If} $R0 == 0
    MessageBox MB_OK|MB_ICONEXCLAMATION "${PRODUCT_NAME} is running. Please close it first !" IDOK
    Abort
  ${Else}
    MessageBox MB_ICONQUESTION|MB_YESNO "Are you sure you want to completely remove $(^Name) and all of its components ?" /SD IDYES IDNO 0 IDYES +2
    Abort
  ${EndIf}
FunctionEnd

Function un.onUninstSuccess
  HideWindow
  MessageBox MB_ICONINFORMATION|MB_OK "$(^Name) has been removed from your computer successfully !" IDOK
FunctionEnd

Function DotNetRuntimeCheck
  SetRegView 64
  ReadRegStr $R0 HKLM "SOFTWARE\WOW6432Node\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App" "6.0.11"
  ${If} $R0 == ""
    DetailPrint "${WIN_RUNTIME} hasn't been installed"
    ExecWait "$INSTDIR\Bin\${WIN_RUNTIME}.exe"
    Delete "$INSTDIR\Bin\${WIN_RUNTIME}.exe"
  ${Else}
    DetailPrint "${WIN_RUNTIME} has been installed"
  ${EndIf}
FunctionEnd

Section Uninstall
  SetOutPath "$TEMP"
  Delete "$INSTDIR\${PRODUCT_CODE}.url"
  Delete "$INSTDIR\uninst.exe"

  SetShellVarContext current
  RMDir /r "$SMPROGRAMS\${PRODUCT_CODE} ${PRODUCT_NAME}"

  SetShellVarContext all
  RMDir /r "$SMPROGRAMS\${PRODUCT_CODE} ${PRODUCT_NAME}"
  Delete "$DESKTOP\${PRODUCT_NAME}.lnk"

  RMDir /r "$INSTDIR"

  DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}"
  DeleteRegKey HKLM "Software\${PRODUCT_CODE}\${PRODUCT_NAME}\InstallPath"
  DeleteRegKey HKLM "Software\${PRODUCT_CODE}\${PRODUCT_NAME}\AppPath"
  DeleteRegKey HKLM "${PRODUCT_DIR_REGKEY}"
  DeleteRegKey HKLM "Software\${PRODUCT_CODE}\${PRODUCT_NAME}"
  DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Run"
  SetAutoClose true
SectionEnd

NSIS脚本编写涉及到非常多的内容,详细可以参考 官网文档: https://nsis.sourceforge.io/Docs/ 以及 https://documentation.help/NSIS/ ,后者排版相对好一些 😄。

🔚