A simple NSIS installer with user execution level

As I wrote last time, I recently switched OnTopReplica's installer from ClickOnce to a custom NSIS installer. In doing so I was voluntarily dropping a lot of nice features of ClickOnce (user-level installation, simple interface, automatic updating and Visual Studio integration) for a set of features I had to implement on my own. It worked out surprisingly well, so here's what I've done.

Taking care of UAC

Many people don't like the UAC you find in Windows Vista/7, but I actually find it one of the best features of Windows ever: finally forcing the user to run with limited “user-level” execution privileges was due for a long time. Unfortunately, even if the feature is great (and is pretty much equal to the access control in Mac OS X and most Linux distributions), most Windows applications and installers weren't written with UAC in mind.

Even most new applications still have trouble handling UAC: installers that run and then automatically launch the installed app using the Administrator account, for instance. This can get pretty painful sometimes. We can only hope that, given some time, programs will behave better and better. On the other hand, Microsoft has been pretty unhelpful on its side (why is there no user-level installation path like C:\Users\Programs folder?).

Google Chrome is an example of a very well behaved application that installs for the local user only (without any UAC prompt) and doesn't mess with the registry. I tried to mimic what Chrome's installer does for OnTopReplica. First of all, I told the NSIS installer that Administrator execution level access is not needed:

# INIT
Name "OnTopReplica"
RequestExecutionLevel user

This tells the NSIS compiler to write a manifest for the installer executable, which in turn will tell Windows to skip the UAC prompt. The installer will now run as a standard program launched by the user: this also means that references to common folders (like “%USERPROFILE%”) will redirect to the current user's folder under C:\Users (and not the Administrator ones).

This is exactly what I wanted in order to install OnTopReplica's executable files in the following folder:
C:\Users\USER\AppData\Local\OnTopReplica

Remember that AppData files are split in the following subfolders:

  • Local: this folder is for files local to the current machine. This works well for larger files and installed applications that reside on the machine itself.
  • Roaming: these files are synchronized with the user's profile (if you're on an Active Directory domain). This works best for settings and other small data files which are useful to be shared between more machines.
  • LocalLow: an area where “low integrity” applications can store data.

Here's how the “Local” folder is targeted in NSIS:

InstallDir "$LOCALAPPDATA\OnTopReplica"

As easy as that. Now all $INSTDIR references in the rest of the script will point to that folder, which always has full read/write access by the user of course.

The Start menu

In order for the user to be able to launch the application (which is the main goal of an installer, I guess), a shortcut to the app must be provided in the start menu. The menu is composed by Windows by attempting to pull together shortcuts from multiple sources. The only one of those sources which can be freely accessed by the user is:
C:\Users\USER\AppData\Roaming\Microsoft\Windows\Start Menu

The same folder can be opened by right-clicking on the “All programs” button in the menu and then on “Open”. In NSIS, a reference to that folder is returned by the $STARTMENU variable:

!define START_LINK_DIR "$STARTMENU\Programs\OnTopReplica"
!define START_LINK_RUN "$STARTMENU\Programs\OnTopReplica\OnTopReplica.lnk"
!define START_LINK_UNINSTALLER "$STARTMENU\Programs\OnTopReplica\Uninstall OnTopReplica.lnk"

# In your main installer section...
	SetShellVarContext current
	CreateDirectory "${START_LINK_DIR}"
	CreateShortCut "${START_LINK_RUN}" "$INSTDIR\OnTopReplica.exe"
	CreateShortCut "${START_LINK_UNINSTALLER}" "$INSTDIR\OnTopReplica-Uninstall.exe"

Notice the call to SetShellVarContext that tells NSIS I'm interested in the $STARTMENU reference of the current user (instead of the one of “all users”).

Registering the uninstaller

On Windows, uninstallers are traditionally registered inside the Control Panel (under “Programs” on Windows 7). The panel provides a simple list that gives an overview of all installed programs, their size and means to uninstall (or repair) them.

The OnTopReplica entry in the uninstaller control panel.

Registering an uninstaller is very easy and can be done by creating an own key at the following registry location:
Software\Microsoft\Windows\CurrentVersion\Uninstall\MY_APP_NAME

Of course, since the installer doesn't require UAC elevation, the installer's code cannot access the HKLM registry hive (i.e. “HKEY Local Machine”, the set of registry elements of the whole machine). It can however access the HKCU hive instead (“HKEY Current User”): fortunately the control panel retrieves info for its uninstaller list from both hives.

!define REG_UNINSTALL "Software\Microsoft\Windows\CurrentVersion\Uninstall\OnTopReplica"

WriteRegStr HKCU "${REG_UNINSTALL}" "DisplayName" "OnTopReplica"
WriteRegStr HKCU "${REG_UNINSTALL}" "DisplayIcon" "$\"$INSTDIR\OnTopReplica.exe$\""
WriteRegStr HKCU "${REG_UNINSTALL}" "Publisher" "Lorenz Cuno Klopfenstein"
WriteRegStr HKCU "${REG_UNINSTALL}" "DisplayVersion" "3.1.0.0"
WriteRegDWord HKCU "${REG_UNINSTALL}" "EstimatedSize" 800 ;KB
WriteRegStr HKCU "${REG_UNINSTALL}" "HelpLink" "${WEBSITE_LINK}"
WriteRegStr HKCU "${REG_UNINSTALL}" "URLInfoAbout" "${WEBSITE_LINK}"
WriteRegStr HKCU "${REG_UNINSTALL}" "InstallLocation" "$\"$INSTDIR$\""
WriteRegStr HKCU "${REG_UNINSTALL}" "InstallSource" "$\"$EXEDIR$\""
WriteRegDWord HKCU "${REG_UNINSTALL}" "NoModify" 1
WriteRegDWord HKCU "${REG_UNINSTALL}" "NoRepair" 1
WriteRegStr HKCU "${REG_UNINSTALL}" "UninstallString" "$\"$INSTDIR\${UNINSTALLER_NAME}$\""
WriteRegStr HKCU "${REG_UNINSTALL}" "Comments" "Uninstalls OnTopReplica."

Most elements are quite auto-explanatory (and they are well documented). Here's an overview:

  • DisplayName: the name that will appear in the list,
  • DisplayIcon: the icon to be shown (make sure you escape the path with quotes),
  • EstimatedSize: the app's size in Kilobytes. This value can be estimated by you manually or can be computed by NSIS itself (you need some additional scripts for that however),
  • HelpLink: a link to a website providing support,
  • URLInfoAbout: the application's main website,
  • InstallLocation: the root folder where the app has been installed,
  • InstallSource: path to the installer that installed the program and registered these values,
  • NoModify and NoRepair: when set to '1' they tell Windows that your uninstaller only uninstalls the program and does nothing else,
  • UninstallString: full path to the uninstaller (make sure you escape this path too with quotes),
  • Comments: a short string that appears on the “detail pane” when the program is selected in the control panel list.

That would be all! This kind of script should work perfectly well for the majority of applications that do not need to do some low-level stuff (like registering as a type handler, add file associations, add a hook inside Windows Explorer, etc). I can't think of any valid reason to require Administrator access level otherwise: if most apps installed this way, it would certainly improve the Windows application ecosystem (that is, until Microsoft starts providing some kind of application repository à la apt-get!).

So, go out there, spread the word and develop nicely behaved installers!  :)

By the way, you can check out OnTopReplica's installation script on its source code repository (under /Installer/script.nsi).