Creating bootstrapper installers with dotNetInstaller
January 29, 2011 1 Comment
Currently we are using WIX integrated into our TFS build process to create our installation packages. This has been proved to be a fast and cheap way to create professional setup packages.Actually there was the need to create combined setup packages consisting of several MSI packages generated by WIX.After looking at several packages (including WIX 3.6 burn, and others) we decided to start over with dotNetInstaller. Using a XML file you are able to generate .EXE bootstrappers. After playing around with this tool we decided to incorporate this directly into our build process which wasn’t that easy as the integration needed to support the following “features”:
- MSBuild (the integration is done in the .wixproj file)
- Support for dynamic filenames (including the automatically generated version number)
- Support for multiple languages
The following sections describe the steps to include this into our software:
1. Create dotNetInstaller template
First we created a template that would suite our needs. This has been done with the dotNetInstaller editor provided together with dotNetInstaller. The following source code example is used by us to create an installer which automatically installs .NET 3.5 SP1 (or installs the appropriate feature on Windows 7/Microsoft Server 2008 R2):
<?xml version="1.0" encoding="utf-8"?>
<configurations lcid_type="UserExe" show_language_selector="False" language_selector_title="" language_selector_ok="OK" language_selector_cancel="Cancel" configuration_no_match_message="Your System doesn't match the system requirements, please contact Tideum for further assistance" ui_level="full" fileversion="1.3.1.1" productversion="1.3.1" log_enabled="True" log_file="#TEMPPATH\dotNetInstallerLog.txt">
<schema version="1.10.1525.0" generator="dotNetInstaller InstallerEditor" />
<fileattributes>
<fileattribute name="FileDescription" value="Product Setup" />
<fileattribute name="LegalCopyright" value="(c) 2011 Tideum Deutschland AG" />
<fileattribute name="ProductName" value="Product" />
<fileattribute name="CompanyName" value="Tideum Deutschland AG" />
</fileattributes>
<configuration dialog_caption="Product Installer Preferences" dialog_message="In order to install Product you must first install these components:" dialog_message_uninstall="" dialog_bitmap="#APPPATH\banner.bmp" skip_caption="Skip" install_caption="Install" uninstall_caption="Uninstall" cancel_caption="Close" status_installed=" (Installed)" status_notinstalled="" failed_exec_command_continue="Failed to install %s. Continue with others components?" installation_completed="" uninstallation_completed="VIPE Instance Manager uninstalled successfully!" installation_none="" uninstallation_none="VIPE Instance Manager is not installed!" installing_component_wait="Installing %s. Wait, this operation could take some time ..." uninstalling_component_wait="Uninstalling %s. Wait, this operation could take some time ..." reboot_required="To continue the installation you must restart your computer. Restart now?" must_reboot_required="False" dialog_otherinfo_caption="" dialog_otherinfo_link="" complete_command="" complete_command_silent="" complete_command_basic="" wait_for_complete_command="True" auto_close_if_installed="True" auto_close_on_error="False" reload_on_error="True" dialog_show_installed="True" dialog_show_uninstalled="True" dialog_show_required="True" cab_dialog_message="%s" cab_cancelled_message="" cab_dialog_caption="" cab_path="#TEMPPATH\#GUID" cab_path_autodelete="True" dialog_default_button="cancel" dialog_position="" dialog_components_list_position="" dialog_message_position="" dialog_bitmap_position="" dialog_otherinfo_link_position="" dialog_osinfo_position="" dialog_install_button_position="" dialog_cancel_button_position="" dialog_skip_button_position="" auto_start="False" auto_continue_on_reboot="True" reboot_cmd="" show_progress_dialog="True" show_cab_dialog="True" type="install" lcid_filter="" language_id="" language="" os_filter="" os_filter_min="winXPsp3" os_filter_max="" processor_architecture_filter="" supports_install="True" supports_uninstall="False">
<component command=""#TEMPPATH\dotNET351\dotnet35_en.exe"" command_silent="" command_basic="" uninstall_command="" uninstall_command_silent="" uninstall_command_basic="" returncodes_success="" returncodes_reboot="" id=".NET Framework 3.5 Service pack 1 (English)" display_name="Microsoft .NET Framework 3.5 Service pack 1 (English)" uninstall_display_name="" os_filter="" os_filter_min="winXP" os_filter_max="winServer2008" os_filter_lcid="1033" type="cmd" installcompletemessage="" uninstallcompletemessage="" mustreboot="False" reboot_required="" must_reboot_required="False" failed_exec_command_continue="" allow_continue_on_error="False" default_continue_on_error="False" required_install="True" required_uninstall="True" selected_install="True" selected_uninstall="True" note="" processor_architecture_filter="" status_installed="" status_notinstalled="" supports_install="True" supports_uninstall="False" show_progress_dialog="True" show_cab_dialog="True">
<downloaddialog dialog_caption="Microsoft .NET Framework 3.5 Service pack 1 (English) download" dialog_message="Please press "Continue" to start download and installing Microsoft .NET Framework 3.5 Service pack 1." dialog_message_downloading="Download in progress..." dialog_message_copying="Copying ..." dialog_message_connecting="Connecting ..." dialog_message_sendingrequest="Sending request ..." autostartdownload="False" buttonstart_caption="Continue" buttoncancel_caption="Cancel">
<download componentname="Microsoft .NET Framework 3.5 Service pack 1 (English)" sourceurl="https://www.tideum.com/updates/path/dotnetfx35_en.exe" sourcepath="" destinationpath="#TEMPPATH\dotNET351" destinationfilename="" alwaysdownload="True" clear_cache="False" />
</downloaddialog>
<installedcheck path="SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5" fieldname="SP" fieldvalue="1" defaultvalue="False" fieldtype="REG_DWORD" comparison="match" rootkey="HKEY_LOCAL_MACHINE" wowoption="NONE" type="check_registry_value" description="Installed Check" />
</component>
<component command=""#TEMPPATH\dotNET351\dotnet35_de.exe"" command_silent="" command_basic="" uninstall_command="" uninstall_command_silent="" uninstall_command_basic="" returncodes_success="" returncodes_reboot="" id=".NET Framework 3.5 Service pack 1 (German)" display_name="Microsoft .NET Framework 3.5 Service pack 1 (German)" uninstall_display_name="" os_filter="" os_filter_min="winXP" os_filter_max="winServer2008" os_filter_lcid="1031" type="cmd" installcompletemessage="" uninstallcompletemessage="" mustreboot="False" reboot_required="" must_reboot_required="False" failed_exec_command_continue="" allow_continue_on_error="False" default_continue_on_error="False" required_install="True" required_uninstall="True" selected_install="True" selected_uninstall="True" note="" processor_architecture_filter="" status_installed="" status_notinstalled="" supports_install="True" supports_uninstall="False" show_progress_dialog="True" show_cab_dialog="True">
<downloaddialog dialog_caption="Microsoft .NET Framework 3.5 Service pack 1 (German) download" dialog_message="Please press "Continue" to start download and installing Microsoft .NET Framework 3.5 Service pack 1." dialog_message_downloading="Download in progress..." dialog_message_copying="Copying ..." dialog_message_connecting="Connecting ..." dialog_message_sendingrequest="Sending request ..." autostartdownload="False" buttonstart_caption="Continue" buttoncancel_caption="Cancel">
<download componentname="Microsoft .NET Framework 3.5 Service pack 1 (German)" sourceurl="https://www.tideum.com/updates/path/dotnetfx35_de.exe" sourcepath="" destinationpath="#TEMPPATH\dotNET351" destinationfilename="" alwaysdownload="True" clear_cache="False" />
</downloaddialog>
<installedcheck path="SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5" fieldname="SP" fieldvalue="1" defaultvalue="False" fieldtype="REG_DWORD" comparison="match" rootkey="HKEY_LOCAL_MACHINE" wowoption="NONE" type="check_registry_value" description="Installed Check" />
</component>
<component package="#CABPATH\TideumLicenseTools.msi" cmdparameters="" cmdparameters_silent="/qn" cmdparameters_basic="/qb-" uninstall_package="" uninstall_cmdparameters="/qb-" uninstall_cmdparameters_silent="/qn" uninstall_cmdparameters_basic="/qb-" id="Tideum License Tools" display_name="Tideum License Tools" uninstall_display_name="" os_filter="" os_filter_min="winXP" os_filter_max="" os_filter_lcid="" type="msi" installcompletemessage="" uninstallcompletemessage="" mustreboot="False" reboot_required="" must_reboot_required="False" failed_exec_command_continue="" allow_continue_on_error="False" default_continue_on_error="False" required_install="True" required_uninstall="False" selected_install="True" selected_uninstall="False" note="" processor_architecture_filter="" status_installed="" status_notinstalled="" supports_install="True" supports_uninstall="False" show_progress_dialog="True" show_cab_dialog="True">
<embedfile sourcefilepath="ExternalFiles\TideumLicenseTools.msi" targetfilepath="TideumLicenseTools.msi" />
<installedcheck path="SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{GUID}" fieldname="DisplayVersion" fieldvalue="3.1.0.0" defaultvalue="False" fieldtype="REG_SZ" comparison="version_ge" rootkey="HKEY_LOCAL_MACHINE" wowoption="NONE" type="check_registry_value" description="Installed Check" />
</component>
<component command="cmd.exe /C "start /w %SystemRoot%\system32\ocsetup.exe NetFx3"" command_silent="" command_basic="" uninstall_command="" uninstall_command_silent="" uninstall_command_basic="" returncodes_success="" returncodes_reboot="" id=".NET Framework 3.5 Service pack 1 - Feature" display_name="Microsoft .NET Framework 3.5 Service pack 1" uninstall_display_name="" os_filter="" os_filter_min="winServer2008R2" os_filter_max="" os_filter_lcid="" type="cmd" installcompletemessage="" uninstallcompletemessage="" mustreboot="False" reboot_required="" must_reboot_required="False" failed_exec_command_continue="" allow_continue_on_error="True" default_continue_on_error="False" required_install="True" required_uninstall="False" selected_install="True" selected_uninstall="False" note="" processor_architecture_filter="" status_installed="" status_notinstalled="" supports_install="True" supports_uninstall="True" show_progress_dialog="True" show_cab_dialog="True">
<installedcheck path="SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5" fieldname="SP" fieldvalue="1" defaultvalue="False" fieldtype="REG_DWORD" comparison="match" rootkey="HKEY_LOCAL_MACHINE" wowoption="NONE" type="check_registry_value" description="Installed Check" />
</component>
</configuration>
<configuration dialog_caption="Product Installer" dialog_message="In order to install Product you must first install these components:" dialog_message_uninstall="" dialog_bitmap="#APPPATH\banner.bmp" skip_caption="Skip" install_caption="Install" uninstall_caption="Uninstall" cancel_caption="Close" status_installed=" (Installed)" status_notinstalled="" failed_exec_command_continue="" installation_completed="Product installed successfully!" uninstallation_completed="Product uninstalled successfully!" installation_none="Product is already installed!" uninstallation_none="Product is not installed!" installing_component_wait="Installing %s. Wait, this operation could take some time ..." uninstalling_component_wait="Uninstalling %s. Wait, this operation could take some time ..." reboot_required="To continue the installation you must restart your computer. Restart now?" must_reboot_required="False" dialog_otherinfo_caption="" dialog_otherinfo_link="" complete_command="" complete_command_silent="" complete_command_basic="" wait_for_complete_command="True" auto_close_if_installed="True" auto_close_on_error="True" reload_on_error="False" dialog_show_installed="False" dialog_show_uninstalled="False" dialog_show_required="False" cab_dialog_message="%s" cab_cancelled_message="" cab_dialog_caption="" cab_path="#TEMPPATH\#GUID" cab_path_autodelete="True" dialog_default_button="cancel" dialog_position="" dialog_components_list_position="" dialog_message_position="" dialog_bitmap_position="" dialog_otherinfo_link_position="" dialog_osinfo_position="" dialog_install_button_position="" dialog_cancel_button_position="" dialog_skip_button_position="" auto_start="True" auto_continue_on_reboot="False" reboot_cmd="" show_progress_dialog="True" show_cab_dialog="True" type="install" lcid_filter="" language_id="" language="" os_filter="" os_filter_min="" os_filter_max="" processor_architecture_filter="" supports_install="True" supports_uninstall="False">
<component package="#CABPATH\VIMSetup.msi" cmdparameters="" cmdparameters_silent="/qn" cmdparameters_basic="/qb-" uninstall_package="" uninstall_cmdparameters="/qb-" uninstall_cmdparameters_silent="/qn" uninstall_cmdparameters_basic="/qb-" id="instVIM" display_name="Product" uninstall_display_name="" os_filter="" os_filter_min="winXPsp2" os_filter_max="" os_filter_lcid="" type="msi" installcompletemessage="" uninstallcompletemessage="" mustreboot="False" reboot_required="" must_reboot_required="False" failed_exec_command_continue="" allow_continue_on_error="False" default_continue_on_error="False" required_install="True" required_uninstall="True" selected_install="True" selected_uninstall="True" note="" processor_architecture_filter="" status_installed="" status_notinstalled="" supports_install="True" supports_uninstall="True" show_progress_dialog="True" show_cab_dialog="True">
<embedfile sourcefilepath="--FilePath--" targetfilepath="VIMSetup.msi" />
<installedcheck path="SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{GUID}" fieldname="DisplayVersion" fieldvalue="1.2.0.0" defaultvalue="False" fieldtype="REG_SZ" comparison="version_ge" rootkey="HKEY_LOCAL_MACHINE" wowoption="NONE" type="check_registry_value" description="Installed Check" />
</component>
</configuration>
</configurations>
This template has been inserted into version control in the product’s setup project.
2. Create logic to update the template during build
During the build process after the MSI packages have been generated for a configuration, the “AfterBuild” target now needs to build a specific dotNetInstaller XML template per language and then we need to run the dotNetInstaller linker to acutally create the correct file:
<Project>
<!-- standard MSBuild stuff ... -->
<PropertyGroup>
<TPath>$(MSBuildExtensionsPath)\ExtensionPack\MSBuild.ExtensionPack.tasks</TPath>
</PropertyGroup>
<Import Project="$(TPath)" />
<PropertyGroup Condition=" '$(ProgramFiles(x86))' == '' ">
<DotNetInstallerPath>$(ProgramFiles)\dotnetinstaller\bin\</DotNetInstallerPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(ProgramFiles(x86))' != '' ">
<DotNetInstallerPath>$(ProgramFiles(x86))\dotnetinstaller\bin\</DotNetInstallerPath>
</PropertyGroup>
<PropertyGroup>
<DotNetExeTemplate>$(DotNetInstallerPath)dotNetInstaller.exe</DotNetExeTemplate>
</PropertyGroup>
<Target Name="BuildSetupExe" Inputs="@(CultureGroup)" Outputs="$(TargetDir)%(CultureGroup.OutputFolder)VIM-Setup-$(TideumFileVersion)-$(Configuration)-$(Platform)-%(CultureGroup.Identity).exe">
<attrib Files="$(TargetDir)dotnetinstaller.xml" ReadOnly="true" Condition="Exists('$(TargetDir)dotnetinstaller.xml')" />
<Copy SourceFiles="dotnetinstaller.xml" DestinationFolder="$(TargetDir)" />
<attrib Files="$(TargetDir)dotnetinstaller.xml" ReadOnly="true" />
<MSBuild.ExtensionPack.Xml.XmlFile TaskAction="UpdateAttribute" File="$(TargetDir)dotnetinstaller.xml" XPath="/configurations" Key="fileversion" Value="$(TideumFileVersion)" />
<MSBuild.ExtensionPack.Xml.XmlFile TaskAction="UpdateAttribute" File="$(TargetDir)dotnetinstaller.xml" XPath="/configurations" Key="productversion" Value="$(TideumAssemblyVersion)" />
<MSBuild.ExtensionPack.Xml.XmlFile TaskAction="UpdateAttribute" File="$(TargetDir)dotnetinstaller.xml" XPath="/configurations/configuration[@dialog_caption='Product Installer']/component/embedfile" Key="sourcefilepath" Value="$(TargetDir)%(CultureGroup.OutputFolder)$(TargetName)$(TargetExt)" />
<MSBuild.ExtensionPack.Xml.XmlFile TaskAction="UpdateAttribute" File="$(TargetDir)dotnetinstaller.xml" XPath="/configurations/configuration[@dialog_caption='Product Installer']/component/installedcheck" Key="fieldvalue" Value="$(TideumFileVersion)" />
<Exec Command=""$(DotNetInstallerPath)InstallerLinker.exe" "/Configuration:$(TargetDir)dotnetinstaller.xml" "/Output:$(TargetDir)%(CultureGroup.OutputFolder)VIM-Setup-$(TideumFileVersion)-$(Configuration)-$(Platform)-%(CultureGroup.Identity).exe" "/Template:$(DotNetInstallerPath)dotnetinstaller.exe" /Icon:Icon.ico" />
<Exec Command=""$(WindowsSdkDir)bin\signtool.exe" sign /sha1 *hash* /t <a href="http://timestamp.verisign.com/scripts/timstamp.dll">http://timestamp.verisign.com/scripts/timstamp.dll</a> /d "Product" "$(TargetDir)%(CultureGroup.OutputFolder)VIM-Setup-$(TideumFileVersion)-$(Configuration)-$(Platform)-%(CultureGroup.Identity).exe"" Condition=" '$(OutDir)'!='$(OutputPath)' " />
</Target>
</Project>
The first challenge with dotNetInstaller was the path where the tool is installed. As dotNetInstaller is a 32 tool the path is different between 32bit and 64bit systems.
Afterwards we needed to patch the dotNetInstaller template file to include the correct version numbers and filename. This is done by using the task MSBuild.ExtensionPack.Xml.XmlFile from MSBuild extension pack. To not alter the template file it is copied to the output directory before changing the file.
As the last action (before signing the executable) the dotNetInstaller linker is called (here the clean command line version without XML hassling; Please note the quotes to handle spaces in pathnames and filenames):
"$(DotNetInstallerPath)InstallerLinker.exe" "/Configuration:$(TargetDir)dotnetinstaller.xml" "/Output:$(TargetDir)%(CultureGroup.OutputFolder)VIM-Setup-$(TideumFileVersion)-$(Configuration)-$(Platform)-%(CultureGroup.Identity).exe" "/Template:$(DotNetInstallerPath)dotnetinstaller.exe" /Icon:Icon.ico
One point: the “Input” and “Output” parameters in the task will generate one task execution per defined language. You can add the cultures to be build by using the project’s property screen.
After running the build the executables are placed at the same location as the corresponding MSI files.
Pingback: 32- and 64-Bit Installations, WiX 3.5 - Topics of the Week 5 - InstallSite Blog