Packaging of a .NET application on Windows

This post is about how to package .NET application for Windows into an MSI (MicroSoft Installer) package using Wix# tool in command line. Packaging process can be easily automated on continues integration server.

msi_package

Generally application building workflow is simple:

  • Assign a version number to an assembly
  • Build binaries
  • Copy binaries to package folder and build msi-package with the same version as  assembly

Build process will be controlled by MSBuild project file and can be started from a continues integrations server.

Sample solution is available for download here https://github.com/mchudinov/PackagingMSI. Solution is compatible with Visual Studio 2013.

Automated building and versioning processes were described in my previous posts:



I need to create a MSI Windows package from my .NET binaries. The result should be a package that does the following while installation:

  • Package version is the same as an assembly version of an application
  • Shows standard Windows graphical installation wizard
  • Installs binary by default into a predefined folder. It is typically %Program Files% folder.

I use Wix# that automates WiX toolset that automates creation of MSI packages. Wix# is a framework for building a complete MSI or WiX source code by using script files written with the C# syntax.

1. Prepare packaging environment

Since Wix# script is written in C# and will run in command line I need a C# scripting engine. I use CS-Script engine. CS-Script must be installed on the continues integration server and added to PATH in order to build packages there.

Installation process is simple:

  1. Download the CS-Script archive from official website
  2. Unarchive it to a folder without spaces. I use c:\lib\cs-script
  3. Add this folder to the PATH environment varible
  4. Follow instructions in readme.txt

Check that CS-Script engine works after installation and it was added to PATH. Run cscs command in command line:

cscs

 

2. Create packaging project

2.1 Add an empty C# project to the solution

Packaging script should be added to a separate project. I name it <MyProjectName>.WixSharp. It should be an empty C# project.

EmptyCSharp

2.2 Add Wix#

Add WixSharp NuGet package to this project.

2.3 Add packaging script

Add a public class Script in this new project.

Script.cs must have the method static public void Main(string[] args). This will be my packaging script. Add following using to the file:

using System;
using System.Diagnostics;
//css_ref %WIXSHARP_DIR%\wixsharp.dll;
using WixSharp;

Note //css_ref %WIXSHARP_DIR%\wixsharp.dll is commented out. This is how it must be. This is a directive for cs-script.

Keep method Main simple for a while:

using System;
using System.Diagnostics;
//css_ref %WIXSHARP_DIR%\wixsharp.dll;
using WixSharp;

public class Script
{
    static public void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

The new project should look like this:

SolutionsWixSharp

2.4 Test that CS-Script engine can run Script.cs

Write the following command in Windows command line:

cscs.exe <full path to project>\<project name>.WixSharp\Script.cs <project name> <full path to project>

You should see a Hello World! response from CS-Scritp running Script.cs

HelloCSScript

3. Create building script for the solution

Add simple MSBuild script to the project. Read more about MSBuild in my Building and Testing blogpost. This script will build and package application.

Build steps are the following:

  • Clean solution
  • Restore NuGet packages
  • Set version to assembles
  • Compile project

Note that versioning needs MSBuild.Extension.Pack and MSBuildTasks NuGet packages be installed.

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Run">   

    <PropertyGroup>
	    <Configuration>Release</Configuration>
        <SolutionName>PackagingMSI</SolutionName>
	</PropertyGroup>

    <Target Name="Run">
	 <CallTarget Targets="Clean" />
	 <CallTarget Targets="Restore" />
	 <CallTarget Targets="Version" />
	 <CallTarget Targets="Build" />
    </Target>
    
	<Target Name="Clean">
		<Message Text="Clean" />
		<ItemGroup>
			<FilesToDeleteInPackageFolder Include="*.msi"/>
			<FilesToDeleteInPackageFolder Include="$(SolutionName).WixSharp/*.msi"/>
		</ItemGroup>
		<Delete Files="@(FilesToDeleteInPackageFolder)"/>
	</Target>
    
    <Target Name="Restore">
	 <Message Text="Restore NuGet packages" />
     <Exec Command="nuget.exe restore" ContinueOnError="False"/>
    </Target>
  
  	<UsingTask AssemblyFile="packages/MSBuild.Extension.Pack.1.6.0/tools/net40/MSBuild.ExtensionPack.dll" TaskName="AssemblyInfo"/>
	<Target Name="Version">
		<Message Text="Versioning assemblies" />
	
		<ItemGroup>
		  <AssemblyInfoFiles Include="**\AssemblyInfo.cs" />
		</ItemGroup>
	
		<AssemblyInfo
			AssemblyInfoFiles="@(AssemblyInfoFiles)"
			
			AssemblyMajorVersion="$(MajorVersion)"
			AssemblyMinorVersion="$(MinorVersion)"
			AssemblyBuildNumberType="DateString"
			AssemblyBuildNumberFormat="MMdd"
			AssemblyRevisionType="AutoIncrement"
			AssemblyRevisionFormat="000"
		  
			AssemblyFileMajorVersion="$(MajorVersion)"
			AssemblyFileMinorVersion="$(MinorVersion)"
			AssemblyFileBuildNumberType="DateString"
			AssemblyFileBuildNumberFormat="MMdd"
			AssemblyFileRevisionType="AutoIncrement"
			AssemblyFileRevisionFormat="000"
		/>
	</Target>
	
    <Target Name="Build">
	  <Message Text="Build $(Configuration)" />
	  <MSBuild Projects="$(SolutionName)/$(SolutionName).csproj" Properties="Configuration=$(Configuration)" ContinueOnError="False"/>
    </Target>

    <Target Name="Pack">
        <Message Text="Pack application into MSI" />
        <ItemGroup>
			<FilesToDeleteInPackageFolder Include="*.msi"/>
            <FilesToDeleteInPackageFolder Include="$(SolutionName).WixSharp/*.msi"/>
        </ItemGroup>
        <Delete Files="@(FilesToDeleteInPackageFolder)"/>
        <Exec Command="cscs.exe $(SolutionName).WixSharp/Script.cs $(SolutionName) $(MSBuildProjectDirectory)" ContinueOnError="False"/>
    </Target>

</Project>

Test build in Visual Studio command line msbuild Build.proj:

MSBuildprocess

Target Pack will be started separately in command line msbuild /target:Pack Build.proj

packbuild

4. Write packaging script

using System;
using System.Diagnostics;
//css_ref %WIXSHARP_DIR%\wixsharp.dll;
using WixSharp;

public class Script
{
    static public void Main(string[] args)
    {
        string projectName = args[0];
        string projectNameExe = projectName + ".exe";
        string projectFolder = args[1];
        string binaryFolder = projectFolder + @"\" + projectName + @"\bin\Release\";
        string assemblyPath = binaryFolder + projectNameExe;
        FileVersionInfo assemblyInfo = FileVersionInfo.GetVersionInfo(assemblyPath);
        Version version = new Version(assemblyInfo.FileVersion);

        Console.WriteLine("Project name: " + projectName);
        Console.WriteLine("Project folder: " + projectFolder);
        Console.WriteLine("Binary folder: " + binaryFolder);
        Console.WriteLine("Assembly path: " + assemblyPath);
        Console.WriteLine("Version: " + version.ToString());
        Console.WriteLine("Manufacturer: " + assemblyInfo.CompanyName);

        Project project =
            new Project(projectName + "_" + version.ToString(),
                new Dir(new Id("INSTALL_DIR"), @"%ProgramFiles%\" + projectName,
                    new Files(binaryFolder + "*.exe"),
                    new Files(binaryFolder + "*.exe.config"),
                    new Files(binaryFolder + "*.dll"),

                    new Dir(@"%ProgramMenu%\" + projectName,
                        new ExeFileShortcut(projectName, "[INSTALL_DIR]" + projectNameExe, ""),
                        new ExeFileShortcut("Uninstall " + projectName, "[System64Folder]msiexec.exe", "/x [ProductCode]")
                    ),
                    new Dir(@"%Startup%\",
                        new ExeFileShortcut(projectName, "[INSTALL_DIR]" + projectNameExe, "")
                    )
                )
            );

        project.Version = version;
        project.GUID = new Guid("54f5a0b8-6f60-4a70-a095-43e2b93bc0fe");

        //project.SetNetFxPrerequisite("NETFRAMEWORK45 >='#378389'", "Please install .Net 4.5 first");
        //project.ControlPanelInfo.ProductIcon = projectFolder + @"\" + projectName + @"\Resources\trafficlights.ico";
        project.ControlPanelInfo.NoModify = true;
        project.ControlPanelInfo.Manufacturer = assemblyInfo.CompanyName;

        project.UI = WUI.WixUI_Common;
        var customUI = new CommomDialogsUI();
        var prevDialog = Dialogs.WelcomeDlg;
        var nextDialog = Dialogs.VerifyReadyDlg;
        customUI.UISequence.RemoveAll(x => (x.Dialog == prevDialog && x.Control == Buttons.Next) || (x.Dialog == nextDialog && x.Control == Buttons.Back));
        customUI.On(prevDialog, Buttons.Next, new ShowDialog(nextDialog));
        customUI.On(nextDialog, Buttons.Back, new ShowDialog(prevDialog));
        project.CustomUI = customUI;

        Compiler.BuildMsi(project);
    }
}

Do the following finale modifications:

Use same GUID in Script.cs as generated in AssemblyInfo.cs. (Or create a new GUID, but not use this one)

project.GUID = new Guid("54f5a0b8-6f60-4a70-a095-43e2b93bc0fe");

Then add AssemblyCompany info to assembly in AssemblyInfo.cs:

[assembly: AssemblyCompany("MyCompanyName")]

This is a requirement of MSI package.

Test msbuild /target:Pack Build.proj

packbuild2

Script will create a new MSI package named <project_name>.<version>.<daymonth>.msi in the solution folder.

Install this newly created package

PackMSI_gui

Then program will be available in Programs menu and in Control Panel

PackageMenu

package_control_panel