Search code examples
windowsinstallationnsis

Can I get NSIS to make a single installer that handles local deployment and system deployment?


Chrome has an installer that can be used to install system wide, or to a user's home directory if they are not an administrator. This is useful for when you're deploying in a corporate environment where you still want to allow your potential users to install even when they don't necessarily have permission to.

Can NSIS be used to create such an installer?


Solution

  • Turns out it can. The important parts are:

    • RequestExecutionLevel highest: This ensures that the installer gets the highest privilege available to the user's account. i.e. if they are in the Administrator group, the installer will ask for privilege escalation.
    • Determining if the user is an Administrator or not. This is achieved using the UserInfo plugin. It's straightforward enough.
    • SetShellVarContext all|current: This determines the value of special registry root key SHCTX. With all, it means the same as HKLM (system wide), for current it results in HKCU. SetShellVarContext also affects whether or not the value of $SMPROGRAMS refers to the system wide start menu or the just user's hierachy.

    Here's a skeleton for an installer that can deploy system wide or locally, depending on the user account's permissions. It uses C:\Windows\write.exe as its payload, and optionally installs start menu items and a desktop shortcut. It also puts a reference to the uninstaller in the registry such that it shows up in the Add/Remove Programs dialog. I used NSIS 3.0 (beta) to build this, but I don't see any obvious reason why it wouldn't work with a recent 2.x.

    !include "MUI2.nsh"
    
    !define PRODUCT_NAME "DummyProduct"
    !define VERSION "0.0.1"
    
    Var INSTDIR_BASE
    
    Name "${PRODUCT_NAME}"
    OutFile "${PRODUCT_NAME} Installer.exe"
    
    InstallDir ""
    
    ; Take the highest execution level available
    ; This means that if it's possible to, we become an administrator
    RequestExecutionLevel highest
    
    !macro ONINIT un
        Function ${un}.onInit
            ; The value of SetShellVarContext detetmines whether SHCTX is HKLM or HKCU
            ; and whether SMPROGRAMS refers to all users or just the current user
            UserInfo::GetAccountType
            Pop $0
            ${If} $0 == "Admin"
                ; If we're an admin, default to installing to C:\Program Files
                SetShellVarContext all
                StrCpy $INSTDIR_BASE "$PROGRAMFILES64"
            ${Else}
                ; If we're just a user, default to installing to ~\AppData\Local
                SetShellVarContext current
                StrCpy $INSTDIR_BASE "$LOCALAPPDATA"
            ${EndIf}
    
            ${If} $INSTDIR == ""
                ; This only happens in the installer, because the uninstaller already knows INSTDIR
                ReadRegStr $0 SHCTX "Software\${PRODUCT_NAME}" ""
    
                ${If} $0 != ""
                    ; If we're already installed, use the existing directory
                    StrCpy $INSTDIR "$0"
                ${Else}
                    StrCpy $INSTDIR "$INSTDIR_BASE\${PRODUCT_NAME}"
                ${Endif}
            ${Endif}
        FunctionEnd
    !macroend
    
    ; Define the function twice, once for the installer and again for the uninstaller
    !insertmacro ONINIT ""
    !insertmacro ONINIT "un"
    
    !define MUI_ABORTWARNING
    
    !define MUI_COMPONENTSPAGE_NODESC
    !insertmacro MUI_PAGE_COMPONENTS
    
    !insertmacro MUI_PAGE_DIRECTORY
    
    Var STARTMENU_FOLDER
    !define MUI_STARTMENUPAGE_REGISTRY_ROOT "SHCTX"
    !define MUI_STARTMENUPAGE_REGISTRY_KEY "Software\${PRODUCT_NAME}"
    !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "Start Menu Folder"
    !insertmacro MUI_PAGE_STARTMENU ${PRODUCT_NAME} $STARTMENU_FOLDER
    
    !insertmacro MUI_PAGE_INSTFILES
    !insertmacro MUI_UNPAGE_CONFIRM
    !insertmacro MUI_UNPAGE_INSTFILES
    
    !insertmacro MUI_LANGUAGE "English"
    
    Section "-Main Component"
        SetOutPath "$INSTDIR"
    
        File "C:\Windows\write.exe"
    
        WriteRegStr SHCTX "Software\${PRODUCT_NAME}" "" $INSTDIR
    
        ; These registry entries are necessary for the program to show up in the Add/Remove programs dialog
        WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME}"
        WriteRegStr SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString" '"$INSTDIR\Uninstall.exe"'
        WriteRegDWORD SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "NoModify" 1
        WriteRegDWORD SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "NoRepair" 1
    
        WriteUninstaller "$INSTDIR\Uninstall.exe"
    
        !insertmacro MUI_STARTMENU_WRITE_BEGIN ${PRODUCT_NAME}
            CreateDirectory "$SMPROGRAMS\$STARTMENU_FOLDER\"
            CreateShortCut "$SMPROGRAMS\$STARTMENU_FOLDER\${PRODUCT_NAME}.lnk" "$INSTDIR\write.exe"
        !insertmacro MUI_STARTMENU_WRITE_END
    SectionEnd
    
    Section "Desktop shortcut"
        CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\write.exe"
    SectionEnd
    
    Section "Uninstall"
        Delete "$INSTDIR\write.exe"
    
        Delete "$INSTDIR\Uninstall.exe"
    
        RMDir /r "$INSTDIR"
    
        !insertmacro MUI_STARTMENU_GETFOLDER ${PRODUCT_NAME} $STARTMENU_FOLDER
        Delete "$SMPROGRAMS\$STARTMENU_FOLDER\${PRODUCT_NAME}.lnk"
        RMDir /r "$SMPROGRAMS\$STARTMENU_FOLDER"
    
        Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
    
        DeleteRegKey /ifempty SHCTX "Software\${PRODUCT_NAME}"
    
        DeleteRegKey SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
    SectionEnd