Deploying .NET projects with NSIS
Last updated 08 May 2011
Introduction
The Visual Studio Express IDE does have certain limitations compared to the professional version. One of these limitations is the lack of deployment options. Deploying an application is not a menial task - several things need taking care of, such as deciding what deployment software to use and determining the additional libraries your application needs to enable it to function. To complicate matters even more, different libraries will be available on different systems.
When the Visual Studio IDE is installed on the developer's system, it automatically installs all the libraries that .NET needs. So once that developer transports his executable to a system which hasn't that particular version of Visual Studio installed, chances are that the application will not execute and throw an error. This is because a .NET application requires a few extra library packages on top of the standard Windows libraries. In effect it needs 2 library packages:
- A .NET Framework. The particular version will depend on the version of Framework your project targets. This can be set in Visual Studio by going Project → Properties → Common Properties → targeted Framework
- A Visual Studio Redistributable package. The version of this depends on the Visual Studio version your project is designed on.
The deployment software needs to know if these packages reside on the user's computer, and if not, it will need to facilitate installment of these.
Both the packages can be found in the system's Control Panel → Programs and Features. You will find entries such as Microsoft Visual C++ 2008 Redistributable... and Microsoft .NET Framework... (if they are available). Usually various versions of each package exist.
I checked these packages on many Windows 7 computers during a recent stroll around a computer store, and they all seem to have the 2008 redistributable and the Framework 3.5 installed. So it pays to aim your development project to make use of these packages. I also found that the packages are installed on many XP and Vista systems.
Which Visual Studio Version?
At this time of writing I advocate using Visual Studio 2008. First and most importantly, Visual Studio 2010 does not have intellisense working, which to me is a serious drawback. Also, the Microsoft Visual 2010 Redistributable is not available as yet on many systems, so it will take that extra step to include it within your deployment.
The Express version is free and has all the functionalities needed to develop serious software.
The NSIS Deployment package
NSIS (Nullsoft Scriptable Install System) is a professional open source system that creates Windows installers. At first, its script language seems a bit tedius, but it is very powerful and there are many examples on the net. Tedious is a bit of an understatement, I do find the NSIS scripting actually quite perplexing, especially its use of stacks and labels - normally low level mechanisms - which is odd for a scripting language. Also, creating pages is quite confusing, but the potential is certainly there, as can be learned from the many sophisticated examples on the web.
The HM NIS Edit is a free NSIS editor and includes a GUI editor to create pages. This facility is (confusingly) located in the editor's File → New Install Options File
It is easy to get lost in the countless articles and webpages on NSIS. Reading the documentation that comes with the software and running its examples provides a good first step.
A few basic NSIS code snippets
These are just a few comments on some basic NSIS scripting before delving into the more serious stuff. You will need to read the NSIS Scripting reference for a detailed analysis of the language. The semi colon (;) denotes a comment.
ini files
Ini files are generated by the HM NIS Edit IO designer. To write code to the ini file:
!insertmacro INSTALLOPTIONS_WRITE "customPage.ini" "Field 2" "State" "Blah|Value2|Foo|Bar"
ReadINIStr
user_var(output) ini_filename section_name entry_name
Reads from entry_name in [section_name] of ini_filename and stores the value into user variable $x. The error flag will be set and $x will be assigned to an empty string if the entry is not found:
ReadINIStr $0 $INSTDIR\winamp.ini winamp outname
strcmp
str1 str2 jump_if_equal [jump_if_not_equal]
Compares (case insensitively) str1 to str2. If str1 and str2 are equal, Gotos jump_if_equal, otherwise Gotos jump_if_not_equal.
StrCmp $0 "a string" 0 +3 ;+3 means 'skip 3 steps' DetailPrint '$$0 == "a string"' Goto +2 DetailPrint '$$0 != "a string"'
You can use labels:
StrCmp $0 "aValue" continue wrong StrCmp $0 "anotherValue" continue wrong continue: wrong:
Using flow control
The LogicLib.nsh library enables one to use more familiar flow control. So instead of using StrCmp, you could use this:
${If} $0 == 'some value' MessageBox MB_OK '$$0 is some value' ${ElseIf} $0 == 'some other value' MessageBox MB_OK '$$0 is some other value' ${Else} MessageBox MB_OK '$$0 is "$0"' ${EndIf}
The Stack
The stack can be used to pass parameters to functions or plugins. It can also be used to extend the $0-$9 and $R0-$R9 values, by putting their current values on the stack, assign new values to them, doing something with it, and return the old values to the variables. Following shows what happens with an example:
Code Stack Push "Value 1" Push "Value 2" Value 2 Value 1 Pop $0 ;$0 contains: "Value 2" Value 1 Push $0 Push "Value 3" Push "Value 4" Value 4 Value 3 Value 2 Value 1
DetailPrint
Adds the string "user_message" to the details view of the installer.
DetailPrint "this message will show on the installation window"
The MessageBox
Use a messagebox as per standard windows coding:
Messagebox MB_OK|MB_ICONINFORMATION \ ; \ indicates new line "SetCustom"
Standard pre-formatted (wizard) page
To display a page, just state it
Page xxx ;xxx is a standard wizard page
Sections
Each section contains zero or more instructions. Sections are executed in order by the resulting installer. Instructions in sections are executed at runtime. Most used instructions are SetOutPath which tells installer where to extract the files to, and File which simply extracts files.
Section "xxx"
Functions
Functions are like sections but are called differently. There are 2 types:
- user functions: called by user from sections or other functions, using Call keyword
- callback functions: called by installer on certain events (e.g. .onInit)
NSIS Code excerpts
The following script only shows the code necessary to do the following things:
- Show Splash screen
- Add a serial number registration window
- Install proprietry drivers
- Check and install .NET
- Check and install the VC 2008 Redistributable
- Check for previously installed versions
!define TEMP1 $R0 ; Temp variable to store license key Var DotnetInstalled ; if OK no need to install ;///////// ; INCLUDES ;///////// !include LogicLib.nsh ; Use more familiar flow control !include "MUI.nsh" ; Modern GUI !include "x64.nsh" ; For using {RunningX64} !include "DotNetVer.nsh" ; for ${HasDotNet3.5} !include "nsDialogs.nsh" ; To create labels on a custom page ;Request application privileges for Windows Vista RequestExecutionLevel user Page custom Registration "" ": Registration" Page custom InstallDrivers "" ": Drivers" Page custom CheckAndInstallDotNet "" ": Dot Net Framework" page custom CheckAndInstallVcRedist "" ": VC++ Redistributable" Function .onInit InitPluginsDir File /oname=$PLUGINSDIR\splash.bmp "DeploymentResources\YourSplash.BMP" ; show x in milliseconds splash::show 2000 $PLUGINSDIR\splash ; insert dialog ini file in pluginsdirectory: File /oname=$PLUGINSDIR\serial.ini "serial.ini" Pop $0 Call RunUninstaller ; uninstall previous versions FunctionEnd Function RunUnInstaller ; uninstall previous versions ReadRegStr $R0 HKLM \ "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" \ "UninstallString" StrCmp $R0 "" done MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION \ "${PRODUCT_NAME} is already installed. $\n$\nClick `OK` to remove the \ previous version or `Cancel` to cancel this upgrade. \ IDOK uninst Abort ;Run the uninstaller: uninst: ClearErrors ExecWait '$R0 _?=$INSTDIR' IfErrors no_remove_uninstaller done no_remove_uninstaller: done: FunctionEnd Function Registration !insertmacro MUI_HEADER_TEXT "Registration" "" Push ${TEMP1} again: InstallOptions::dialog "$PLUGINSDIR\serial.ini" Pop ${TEMP1} StrCmp ${TEMP1} "success" 0 continue ; read from State in Field 1 of up.ini and store into $0 ReadINIStr $0 "$PLUGINSDIR\serial.ini" "Field 1" "State" ReadINIStr $1 "$PLUGINSDIR\serial.ini" "Field 2" "State" ; username and password can be one of 5: ; user1, user1 password OR user2, user2 password OR user3, user3 password, ... StrCmp $0 "user1" 0 +2 StrCmp $1 "user1 password" continue wrong StrCmp $0 "user2" 0 +2 StrCmp $1 "user2 password" continue wrong StrCmp $0 "user3" 0 +2 StrCmp $1 "user3 password" continue wrong StrCmp $0 "user4" 0 +2 StrCmp $1 "user4 password" continue wrong StrCmp $0 "user5" 0 +2 StrCmp $1 "user5 password" continue wrong wrong: IntOp $2 $2 + 1 StrCmp $2 5 kill MessageBox MB_OK|MB_ICONSTOP "Wrong user name and password, try again" Goto again continue: Pop ${TEMP1} Return kill: MessageBox MB_OK|MB_ICONSTOP "5 times wrong. Please contact YourCompanyName for assistance." Quit FunctionEnd Function InstallDrivers !insertmacro MUI_HEADER_TEXT "FTDI Drivers" "" nsDialogs::Create 1018 ; Create a Dialog Window ${NSD_CreateLabel} 0 10 100% 24u "Click next to install drivers ..." ; copy to temp location SetOutPath '$TEMP' SetOverwrite on File /r Drivers ${If} ${RunningX64} ExecWait '"$TEMP\Drivers\dpinst_amd64.exe" /c /PATH "$TEMP\Drivers"' ${NSD_CreateLabel} 0 10 100% 24u "X64 Drivers installed. Click 'Next'to continue." nsDialogs::Show RMDir /r "$TEMP\Drivers" ${Else} ExecWait '"$TEMP\Drivers\dpinst32.exe" /c /PATH "$TEMP\Drivers"' ${NSD_CreateLabel} 0 10 100% 24u "X32 Drivers installed. Click 'Next'to continue." nsDialogs::Show RMDir /r "$TEMP\Drivers" ${EndIf} FunctionEnd Function CheckAndInstallDotNet !insertmacro MUI_HEADER_TEXT "DOTNET 3.5 Framework" "" nsDialogs::Create 1018 ;Create a Dialog Window ${NSD_CreateLabel} 0 0 100% 10% "Checking for Dot NET Framework 3.5 on your system ..." ;Check the DotNet Version Call CheckDotNetFramework ; Install Dot net if not present ${If} $DotnetInstalled == "" ${NSD_CreateLabel} 0 40 100% 24u "Your System does not have Dot Net Framework 3.5 installed. \ Ensure you are connected tot the Internet and click 'Next' to install the Dot Net Framework ..." nsDialogs::Show ; show the dialog window Call InstallDotNet ${Else} ${NSD_CreateLabel} 0 40 100% 24u "Your System has the required Dot Net Framework already installed. Please click Next \ to continue this installation." nsDialogs::Show ${EndIf} FunctionEnd Function CheckDotNetFramework ${If} ${HasDotNet3.5} Push "Microsoft .NET Framework 3.5 installed." Pop $DotnetInstalled ${Else} Push "" Pop $DotnetInstalled ${EndIf} FunctionEnd Function InstallDotNet SetOutPath '$TEMP' SetOverwrite on ;file work UNDO to save time in debugging File 'DeploymentResources\dotnetfx35setup.exe' ExecWait '"$TEMP\dotnetfx35setup" /norestart' Delete "$TEMP\dotnetfx35setup.exe" FunctionEnd Function CheckAndInstallVcRedist !insertmacro MUI_HEADER_TEXT "VC++ 2008 Redistributable" "" nsDialogs::Create 1018 ;Create a Dialog Window ${NSD_CreateLabel} 0 0 100% 10% "Checking for VC 2008 C++ Redistributable ..." Call CheckVCRedist StrCmp $R0 "-1" 0 +4 ${NSD_CreateLabel} 0 40 100% 10% "Installing VC 2008 C++ Redistributable..." Call InstallVcRedist Goto +2 ${NSD_CreateLabel} 0 40 100% 10% "VC 2008 C++ Redistributable already installed. \ Please click next to continue installation" nsDialogs::Show ;show the dialog window FunctionEnd Function CheckVCRedist ; check for 2 possible keys in registry, if any one is good, ; the distributable has already been installed: ReadRegDword $R0 HKLM "SOFTWARE\Wow6432Node\Microsoft\DevDiv\VC\Servicing\9.0\RED\1033" "SPName" ${If} $R0 == "SP1" return ${Else} ReadRegDword $R0 HKLM "SOFTWARE\Microsoft\DevDiv\VC\Servicing\9.0\RED\1033" "SPName" ${If} $R0 == "SP1" return ${Else} StrCpy $R0 "-1" ${EndIf} ${EndIf} FunctionEnd ; install Visual Studio 2008 C++ redistributable Function InstallVcRedist SetOutPath '$TEMP' SetOverwrite on ;file work File 'DeploymentResources\vcredist_x86.exe' ExecWait '"$TEMP\vcredist_x86.exe" /norestart' Delete "$TEMP\vcredist_x86.exe" FunctionEnd
Debugging a 'Side-by-side configuration is incorrect' error
I encountered a Side-by-side Error when trying to execute one of my applications on a different system.
Here's a clear explanation of Side-bySide from Wikipedia:
A common issue in previous versions of Windows was that users frequently suffered from DLL hell, where more than one version of the same dynamically linked library (DLL) was installed on the computer. As software relies on DLLs, using the wrong version could result in non-functional applications, or worse. Windows XP solved this problem for native code by introducing side-by-side assemblies. The technology keeps multiple versions of a DLL in the WinSxS folder and runs them on demand to the appropriate application keeping applications isolated from each other and not using common dependencies.
From the Microsoft MSDN website:
A side-by-side assembly contains a collection of resources - a group of DLLs, Windows classes, COM servers, type libraries, or interfaces - that are always provided to applications together. These are described in the assembly manifest.
From XP systems onwards, the shared components of side-by-side applications are located in a huge (5 GB +) folder, named WinSxS (SxS being an acronym for Side-by-Side). It is the biggest systems folder on your computer.
When the system throws an error, a log file is automatically created. This file can be read by going to Control Panel → Administrative Tools → Event viewer → Windows Logs → Applications. Just clear the panel and start the executable that throws this error. Reopen the System Viewer and one or more errors should appear. Double click on an error message to view the details.
But you can do a more detailed analysis using a system tool called sxstrace:
- run in the command line: sxstrace trace -logfile:sxstrace.etl
- run your app and click OK in the Error Message
- stop tracing by pressing Return in the command line
- convert the binary error file to txt: sxstrace parse -logfile:sxstrace.etl -outfile:sxstrace.txt
Another tool that proved most useful of all is the Dependency Walker which can be freely downloaded from the dependency walker website. The application is called depends.exe. Use it as follows:
- start depends.exe
- drag your application into it
- profile
- save as dwl file and read
- right click the module and select full paths
See also Determining Which DLLs to Redistribute
As it turned out in my case, depends.exe allerted me to the fact it couldn't find msvcrtd.lib. I solved this by adding the following lines in Visual Studio's project command line properties (project → Properties → command line):
/NODEFAULTLIB:libcmt.lib /NODEFAULTLIB:libct.lib /NODEFAULTLIB:libcd.lib /NODEFAULTLIB:libcmtd.lib /NODEFAULTLIB:msvcrtd.lib