How do I show an uninstaller window with CPack NSIS during re-installation?

Viewed 131

I have the following CMakeLists.txt (the only thing in the directory):

cmake_minimum_required(VERSION 3.0.0)

project(CPackUninstallerTest)

set(CPACK_GENERATOR NSIS)
set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)
set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS
"DetailPrint \\\"Sleeping...\\\"
Sleep 3000"
)

file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/test.txt" "Some output\n")
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/test.txt" TYPE DATA)

include(CPack)

Running cmake and cpack works fine, and .../build/CPackUninstallerTest-0.1.1-win64.exe is generated.

Running the installer works as expected:

enter image description here

And running the uninstaller (Uninstall.exe in the install directory) also works, where the sleep takes three seconds:

enter image description here

However, this uninstall window does not show up if I try to install on top of an existing installation. After clicking Yes here:

enter image description here

That window goes away for three seconds (as it uninstalls) before the new installer runs.

This is a terrible user experience and results in them re-running the installer while waiting for the hidden uninstaller, causing confusing results.

How do I configure NSIS or CMake/CPack to show the uninstaller progress bar when using CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL and re-installing?

2 Answers

Your issue could not be solved with NSIS interfaces of CMake.

I had a similar problem with CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL. The user could not install it again when uninstallation run manually.

The problem here is that manually running the uninstaller does not delete the install flag from windows registry keys.

My recommendation is that you can create your NSIS script by using NSIS template script of CMake. And you can add remove command easily to uninstaller section of new script.

And then you can give the new script as template to CMake.

As sad @hrn

For "normal" launch Uninstaller from installer (if apllication was installed) You can create own NSIS.template.in (for example get cmake standart template and modify it)

  1. Set new template as source CMake checks if files NSIS.template.in, NSIS.InstallOptions.ini.in are in the module path (module path in sample project was set with this command:
set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/resources/nsis ${CMAKE_MODULE_PATH}))

If those files are found, then they are used for generating the installer.

  1. There is an issue with sequential launch installer and uninstaller (not parallel) NSIS ExecWait always waits for the child process but it does not wait for grandchildren. So you need to use something more powerful, for example nsis plugin StdUtils with functions StdUtils.ExecShellWaitEx/StdUtils.WaitForProcEx

  2. There an issue with launching uninstaller: firstly it copied itself to some temprorary directory and launches from this space, and you dont know what you need to wait. To avoid it launch uninstaller with flag:

${StdUtils.ExecShellWaitEx} $0 $1 "$uninstallerFullPath" "open" "_?=$uninstallerDirectoryPath"
  1. If you cannot add plugin to NSIS directory (default C:\Program Files (x86)\NSIS), you need to set path to this plugin from CMake, add own variable CPACK_NSIS_INCLUDE_PLUGINS as script and you can use it in some place in NSIS.template.in

CMakeLists:

set(CPACK_NSIS_INCLUDE_PLUGINS "
!addincludedir \\\"${PROJECT_SOURCE_DIR}\\\\resources\\\\nsis\\\\Include\\\"
!addplugindir /x86-unicode \\\"${PROJECT_SOURCE_DIR}\\\\resources\\\\nsis\\\\Plugins\\\\x86-unicode\\\"
!include 'StdUtils.nsh'
")

NSIS.template.in:

; on the begining of file
@CPACK_NSIS_OWN_INCLUDE_PLUGINS@
Function .onInit
...
    Var /GLOBAL uninstallerDirectoryPath
    Var /GLOBAL uninstallerFullPath
    ; Get the address of uninstaller
    ReadRegStr $uninstallerFullPath HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@" "UninstallString"

    ; uninstallerFullPath has symbol " at the begin and symbol " at the end, we need to remove it
    StrCpy $uninstallerFullPath $uninstallerFullPath "" 1 ; Remove first symbol
    StrCpy $uninstallerFullPath $uninstallerFullPath -1 ; Remove last symbol

    StrLen $2 "\@CPACK_NSIS_UNINSTALL_NAME@.exe"
    StrCpy $uninstallerDirectoryPath $uninstallerFullPath -$2 # remove "\@CPACK_NSIS_UNINSTALL_NAME@.exe" from UninstallString to get path

    StrCmp $uninstallerFullPath "" inst
    MessageBox MB_YESNO|MB_ICONQUESTION \
    "@CPACK_NSIS_PACKAGE_NAME@ is already installed and must be removed before installation.$\n$\nDo you want to continue?" \
    IDYES uninst IDNO 0
    Quit

uninst:
    ${StdUtils.ExecShellWaitEx} $0 $1 "$uninstallerFullPath" "open" "_?=$uninstallerDirectoryPath"

    ; MessageBox MB_OK|MB_ICONSTOP "Result: $0 -> $1" ;returns "ok", "no_wait" or "error".
    StrCmp $0 "error" ExecFailed ;check if process failed to create
    StrCmp $0 "no_wait" WaitNotPossible ;check if process can be waited for - always check this!
    StrCmp $0 "ok" WaitForProc ;make sure process was created successfully
    MessageBox MB_OK|MB_ICONSTOP "error with StdUtils.ExecShellWaitEx"
    Abort

ExecFailed:
    MessageBox MB_OK|MB_ICONSTOP "Failed to create Uninstall process (error code: $1)"
    Abort

WaitNotPossible:
    MessageBox MB_OK|MB_ICONSTOP "Can not wait for Uninstall process."
    Abort
        
WaitForProc:
    ${StdUtils.WaitForProcEx} $2 $1
    IntCmp $2 0 uninst_successful
    IntCmp $2 1 uninst_canceled
    MessageBox MB_OK|MB_ICONSTOP "Uninstall process ends with error. (exit code: $2)"
    Abort

uninst_canceled:
    MessageBox MB_OK|MB_ICONINFORMATION "Uninstall canceled (exit code: $2)"
    Quit

uninst_successful:
    Delete "$uninstallerFullPath"
    RMDir "$uninstallerDirectoryPath"

inst:
Related