If you follow my activities, like the articles I publish on this blog or the recent book I've published about MSIX, you'll know that one of the reasons why I like MSIX from a developer perspective is that it makes really easy to enable a CI/CD pipeline for Windows desktop applications. Thanks to the Windows Application Packaging Project, we can easily automate the creation of a MSIX package simply by adding it to our solution and running a build. And thanks to features like App Installer, we can easily deploy the generated MSIX package to a website and support automatic updates without changing the code or having to setup your own service.


However, in many cases you may already have an installer definition created with a 3rd party tool. Popular authoring tools, like Advanced Installer, InstallShield or Wix, are able to generate a MSIX package out from an installer project, making easier to reuse the work you may already have done to generate MSI installers in the past. Additionally, thanks to these tools, you have the opportunity to use the same project to generate at the same time a MSI and a MSIX, helping you to support customers who might not have migrated yet to Windows 10 and who are unable to use MSIX Core (for example, because it's a consumer application).


What about enabling a CI/CD pipeline in this scenario? In this blog post we're going to see an example on how to achieve this task with Advanced Installer. Why did I choose this product? Well, other than because it's a really good software, the Advanced Installer team has created a task for Azure DevOps which is able to build an Advanced Installer project stored on the repository. And, most of all, the task is able to download the most recent version of Advanced Installer and install it on the machine without requiring any user intervention. This means that we are not forced to create a self-hosted agent where to manually install the tool, but we can leverage the built-in agents provided by Azure DevOps.


Let's start!


The Advanced Installer project

As a starting point, I will use a simple Windows Forms application called MyEmployees, which I have used also in other posts. However, the main difference is that, this time, the solution doesn't have a Windows Application Packaging Project. We're going to use Advanced Installer to create a MSIX package, together with a MSI. In a real scenario probably you already have the setup created with Advanced Installer but, to understand better how it works, let's create a new one.


First you need to download and install the latest Advanced Installer version. It's a paid product, but it offers a 30 days trial and also a free tier. Once you have launched it, you will find in the Templates section one called Visual Studio Application, which is able to connect directly to a Visual Studio solution, like in our case.




First you will be asked the name and the publisher of your application, followed by the distribution type. For the moment choose MSI setup file. We're going to add the MSIX definition later. In the next step you will be asked where to save the project, using a file with .aip extension. You will need to add this file in a folder of your project, since we'll need to commit it to our repository, in order to use the Azure DevOps task. In my case, I've created a folder called Projects inside the solution. The project output folder will be automatically changed to point to the same location, but I suggest you to choose a different one, outside the repository. This folder, in fact, will contain the various artifacts (like the generated MSI or MSIX package) and we don't want to include them in the source code.


As next step, you must choose the Visual Studio solution which contains your project. In my case, I've chosen the MyEmployees.sln file. Advanced Installer will analyze the solution and it will ask you which configurations you want to import. Feel free to choose the ones that make sense for your project. In my case, for example, I want to distribute only the 64 bit version, built in Release mode:




In the next step you will be asked to select which files, among the ones that are created as build output, belong to the application and must be included in the installer. The section will be split in two categories:


  • Output files, which are the files created in the bin folder as part of the build process.
  • Reference files, which are the files that are referenced by your projects (for example, 3rd party libraries).

In my case, I'm going to include all the output files, plus the reference files coming from NuGet, like or System.Data.SQLite. I don't need to include the references related to the .NET Framework, since they are already installed on the machine.




In the next step, you will need to select the main executable of your application. Advanced Installer should be able to automatically detect it, especially if it's a scenario like mine where the application is composed by a single executable. This information will be used to configure the shortcuts on the desktop and/or the Start Menu.




All the other steps are optional and apply mostly to the MSI setup: you can choose if you want to launch the application after having it installed; you can customize the UI of the installer; you can choose the languages you support; you can add a license agreement that will be displayed during the setup. After you have finished the wizard, Advanced Installer will bring you to the main UI of the application, where you can further customize the project. The tool offers tons of options: you can add support to services or custom actions, you can customize the manifest for the MSIX version, etc.


We're going to keep it simple, so we won't add any special configuration. However, there's a setting that is very important. Move to the Files and Folders section, which displays what and where the installer will copy on the user's machine. You will see a folder called Application folder, which is the location where the files which compose the application will be copied. However, you might see that not all the files are properly included.




Additionally, you will notice that if you update the source code of your application and you produce a new build, it won't be automatically picked up. To solve this problem we need to setup a sync between the Visual Studio project and the Advanced Installer project, so that during the CI pipeline we can produce a MSI / MSIX which reflects the most recent version.


To achieve this goal we must enable the sync feature. Right click on the Application Folder in the tree and choose Properties. Then move to the Synchronize tab and click on Synchronize content with folder from disk. As source folder, you must specify the output of the build in the bin folder of your Visual Studio project. For example, since in my case I'm compiling the application in Release mode, I'm picking up the MyEmployees\bin\Release folder. Once you press Ok, you will be asked if you want to remove the files which are already included. Press Yes. You will notice now that all the files are correctly included:




Additionally, they're kept in sync with the folder. This means that, whenever you're going to launch a new Visual Studio build, the updated executable and DLLs will be copied over to the Advanced Installer project.


Test the MSI generation

Before working on the Azure DevOps side, let's see if the MSI is created successfully. Move to the Builds section, where you will find a definition called DefaultBuild. Right click on it and choose Build. If everything goes well, you will find in the Project Output folder you have set during the wizard the MSI. If you try to install it you will get some warnings because the file isn't signed with a valid certificate. Advanced Installer supports signing as part of the build process, but for the moment we're going to keep it unsigned. We'll perform the signing directly on Azure DevOps.


Create a MSIX package

Now that we have a project up & running, generating a MSIX package is really simple. We just need to move to the Builds section and choose MSIX / APPX Build. A new build definition will be created, called Build_MSIX_APPX.




From the page you will be able to set different options related to MSIX packaging, like the minimum Windows 10 version you want to target or the generation of an App Installer file as part of the process. If you want to try it, just right click on this build definition and choose Build. At the end of the process, you will get in the project output folder a MSIX package. However, in this case you will be completely blocked from installing it, since we haven't signed it. And, as you know, unsigned MSIX packages can't be installed. But don't worry, we're going to do this as well on Azure DevOps.


Putting everything under source control

Now our project should look like this:




The MyEmployees folder contains the Windows Forms project, while the Projects one includes the Advanced Installer setup we have just created. This is the folder you need to make sure to include in your repository on GitHub, Azure Repos or whatever source control provider you prefer. We need to keep the Advanced Installer project together with the source code of the project.


Setup the pipeline

Now that we have everything we need, we can start working on the pipeline. However, first, we need to install the task by Advanced Installer on our Azure DevOps account. This is the one you need, called Advanced Installer Build. The company provides also a task called Advanced Installer Tool Installer, which takes care of installing Advanced Installer on the hosted agent. However, it isn't really needed because the Build task can do it as well.

Once you have installed it, you can move to the Pipelines section of your Azure DevOps project and create a new pipeline. As usual, first you will have to choose the repository where your code is hosted, followed by the starting template. In my case it's a Windows Forms application, so I chose the .NET Desktop one, which looks like this:


# .NET Desktop
# Build and run tests for .NET Desktop or Windows classic desktop solutions.
# Add steps that publish symbols, save build artifacts, and more:

- master

  vmImage: 'windows-latest'

  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

- task: NuGetToolInstaller@1

- task: NuGetCommand@2
    restoreSolution: '$(solution)'

- task: VSBuild@1
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

It's a good starting point, since it already takes care of restoring the NuGet packages and building the code. The next step is to add the Advanced Installer tasks, so that the code can be converted into an installer. We're going to add two tasks: one to generate a MSI and one to generate a MSIX.

Let's see the first one:


- task: AdvancedInstaller@2
    advinstLicense: '$(AILicense)'
    aipPath: 'Projects\MyEmployees.aip'
    aipBuild: 'DefaultBuild'
    aipPackageName: 'MyEmployees-$(Build.BuildNumber).msi'
    aipOutputFolder: '$(Build.ArtifactStagingDirectory)\MSI'

The configuration is quite simple:


  • aiPath is the path, starting from the root of the repository, which contains the Advanced Installer project.
  • aipBuild is the name of the build definition we want to run. In this case we're creating the MSI first, so we use the DefaultBuild definition.
  • The aipPackageName parameter is optional. However, in my case, I prefer to add a reference to the build number in the file name generated by the tool, so that it's easier for me to understand the version number. As such, I have added the variable $(Build.BuildNumber) as suffix to the file name.
  • aipOutputFolder specifies where you want to copy the generated packages. In this case I use a folder called MSI, which is created under the default folder where artifacts are picked up. This way, it will be easier for me to publish the generated MSI as build artifact.

The advinstLicense parameter deserves a special mention. As anticipated in the beginning of the post, Advanced Installer is a paid product. As such, if you're building a project's type which isn't covered by the free license, you will need to provide your license key. Since YAML files are typically included in the repository, it isn't a good idea to store it in clear. As such, I have used the Variables panel to create a variable called AILicense with the key and then I've referenced it in the YAML using the $(AILicense) keyword.


Now that you have seen how to configure the task, it will be very easy to add a new one to generate the MSIX package:


- task: AdvancedInstaller@2
    advinstLicense: '$(AILicense)'
    aipPath: 'Projects\MyEmployees.aip'
    aipBuild: 'Build_MSIX_APPX'
    aipPackageName: 'MyEmployees-$(Build.BuildNumber).msix'
    aipOutputFolder: '$(Build.ArtifactStagingDirectory)\MSIX'

We change just the name of the build definition to run, the name of the package (this time the extension will be .msix and not .msi) and the output folder. To keep things clean, in fact, I create the MSIX package in a different folder, called MSIX.

Now the last step is to publish the build artifacts, by adding a publish task:


- task: PublishBuildArtifacts@1
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'drop'
    publishLocation: 'Container'

That's it! Now save the pipeline, which will automatically trigger a new build. Azure DevOps will first compile the Windows Forms application and then it will execute the Advanced Installer task twice, one for the MSI and one for the MSIX. The first task will take longer, because it will take care also of downloading the tool on the hosted agent. The second task, instead, will reuse the same one.

At the end of the process, if you explore the artifacts you should see something like this:




Create a release pipeline

Now that you have created a CI pipeline, it's time to create a CD pipeline to automate the deployment of these packages. Move to the Releases section on Azure DevOps and press the button to create a new pipeline. You will be prompted with a series of templates. We're going to start from an empty job, so click on the related link.


Let's start from the one to deploy the MSI, so let's call it Deploy MSI. But first, click on Add an artifact in the Artifacts section and choose the one produced by the CI pipeline you have just created.




Now you can click on the link under the task name to start adding tasks. The first step is to sign the installer, to avoid the warnings by Windows that are displayed when you tried to install an untrusted installer. The easiest way to achieve this goal is to use an extension created by Stefan Kert called Code Signing. If you haven't already installed it on your Azure DevOps account, go on and install it.


Then click on the + symbol near Agent job, look for the Code Signing task and add it. You'll need to have a .pfx file with the certificate that you're going to use to sign the installer. You can acquire one from a public certificate authority or get it from your internal one if the installer is meant for enterprise deployment; alternatively, if you're just doing some tests, you can generate a self-signed certificate.

Here are the field to configure:


  • Secure File is the path to the file which contains your certificate. The task uses the Secure Files feature from Azure DevOps, which means that we can safely upload it by clicking on the Settings button near the field. The file will be available only to be used in this pipeline; no one will be able to download it and reuse it, blocking malicious actors that could be interested in stealing our identity.
  • Secure File Password is the password you have used to protect your certificate. Since it's better to not store the password in clear, I have setup a variable called $(PfxPassword) in the Variables tab.
  • File(s) to sign is the path of the file to sign. In this case, we just set it to **/*.msi. We generically sign every file with .msi extension in the artifacts folder.




That's it. The next step is... well, it's up to you =) Now that the installer has been signed, you can add a task to deploy it in the place which makes more sense for you. For example, you can deploy it to a storage or a FTP, where the file will be linked by a web page. In my scenario, I'm using Azure Storage to store the file in a blob, so I'm using the task called Azure File Copy.


Once you've completed the task, you can click on Pipeline and go back to setup the MSIX package deployment. In the Stages section click on Add and choose New stage. Also in this case choose to create an empty job and give it a meaningful name, like Deploy MSIX.


This is how your pipeline should look like:




Click on the link under the new stage to start adding tasks. Also in this case the required one is the Code Signing tasks, which you need to sign the MSIX package. The configuration is exactly the same as we did for the MSI, as long as you're using the same certificate. The only difference is that the File(s) to sign field must be changed to **/*.msix, as in the following image:




Pay attention that, in order for the signing to complete successful, the subject of the certificate must match the publisher declared in the manifest. You can check this in Advanced Installer. Move to the Package Information tab under the Universal Windows section and look for Publisher section:



The value that must match the subject of your certificate is the one in the ID field.


Now you're ready for the deployment. Also in this case you can choose the task which fits best your scenario. For example, you can deploy the MSIX package to Azure Storage and leverage the App Installer technology to handle the installation and automatic updates. If you want to generate an App Installer file as part of the build process, you can leverage again Advanced Installer. Move to the Builds section of your project and, in the MSIX build definition, look for the section titled AppInstaller. There you will be able to provide all the relevant information, like the URL where you're going to deploy the package, the frequency check for automatic updates, etc.




Remember that some options might be disabled, based on the Windows 10 version you're targeting in the Target Platforms section. For example, the Show Prompt option is supported starting from Windows 10 1903, so if you're targeting a lower version the option will be disabled.


Remember to commit the updated .ai file to your repository every time you make any change to the Advanced Installer project. This way, you will trigger a new build and a new generation of the MSI and MSIX installers. As last step, if you truly want to enable a full CI/CD experience, remember to click, in the Release pipeline, on the lighting symbol in the Artifacts section and set to Enabled the Continuous Deployment Trigger.




This way, Azure DevOps will automatically trigger the Release pipeline every time the build pipeline completes successfully.


Wrapping up

In this blog post we have seen how, with Advanced Installer, we can enable a CI/CD pipeline on Azure DevOps that automatically generates both a MSI and a MSIX package out from the code of our application. This way, we can keep supporting customers who aren't using Windows 10 yet but, at the same time, provide a much better deployment experience for the ones who already doing it. Hopefully, over time, you will be able to stop creating a MSI and focus only on MSIX =)

There are many great 3rd party tools on the market that can help you in the process of creating installers and packages for your applications. Advanced Installer is one of them and one thing I have appreciated is the availability of an Azure DevOps task, which allows me to use one of the built-in hosted agents, without forcing me to maintain a dedicated agent with the required tools.


You can find the sample used in this project, together with the full pipeline definition, on GitHub.


Happy packaging!


Regular Visitor