June 2019

Volume 34 Number 6

[DevOps]

MSIX: The Modern Way to Deploy Desktop Apps on Windows

By Magnus Montin | June 2019 | Get the Code

MSIX is the new packaging format that was introduced with the October 2018 update of Windows 10. It aims to bring together the best of all previous installation technologies, such as MSI and ClickOnce, and will be the recommended way of installing applications on Windows going forward. This article shows you how to package a .NET desktop application and how to set up continuous integration (CI), continuous deployment (CD) and automatic updates of sideloaded MSIX packages using Azure Pipelines.

First, a bit of background. In Windows 8, Microsoft introduced an API and runtime called the Windows Runtime that mainly sought to provide a set of platform services to a new kind of application, which was originally referred to as “modern,” “Metro,” “immersive,” or just a “Windows Store” app. This kind of app was born of the mobile device revolution and typically targeted multiple device form factors, such as phones, tablets, and laptops, and was usually installed and updated from the central Microsoft Store.

This class of app has evolved quite a bit since then and is now known as a Universal Windows Platform (UWP) app. UWP apps run in a sandbox called an AppContainer that’s isolated from other processes. They explicitly declare capabilities to require the permissions needed to function properly, and it’s up to the user to decide whether these capabilities should be accepted. This is in contrast to a traditional desktop application that typically runs as a full-trust process with the current user’s full read and write permissions.

In the Anniversary Update of Windows 10, Microsoft introduced the Desktop Bridge (also known as the Centennial project). It let you package your traditional desktop application as a UWP app, but still run it as a full-trust process. A packaged application can be uploaded to the Microsoft Store or the Store for Business and benefit from the streamlined deployment and built-in licensing and automatic update facilities the store provides. Once you’ve packaged your application, you can also start using the new Windows 10 APIs and migrate your code to the UWP in order to reach customers across all devices.

Even if you’re not interested in the Store or the UWP, you may still want to package your line-of-business desktop applications to take advantage of the new app model that Windows 10 brings. It provides clean installs and uninstalls of apps by automatically redirecting all operations against the registry and some well-known system folders to a local folder of the installed application, where a virtual file system and registry are set up. You don’t have to do anything in your source code for this to happen—it’s taken care of for you automatically by Windows. The idea is that when a package is uninstalled, the entire local folder is removed, leaving no traces of the app left on the system.

MSIX is basically a successor to the Desktop Bridge, and the contents of an MSIX package—and the limitations that apply to packaged apps—are roughly the same as with the APPX format that the Desktop Bridge uses. The requirements are listed in the official docs at bit.ly/2OvCcVW and should be addressed before you decide whether to package your applications. Some of them apply only to apps that are being published to the Store.

MSIX adds a new feature called modification packages. It’s a concept similar to MST transformations that enables IT administrators to customize an app, typically from a third-party vendor, without having to repackage it from scratch each time a new feature or bug fix is released. The modification package is merged with the main application at run time and may disable some features of the app, by changing some registry settings, for example. From a developer’s point of view, this might not bring that much to the table, assuming you own both the source code and the build and release pipelines for your apps, but for large enterprises it may cut costs and prevent what’s known as package paralysis.

The definition of the MSIX format is open sourced on GitHub and Microsoft plans to provide an SDK that can be used to pack and unpack MSIX packages on all major OSes, including both Linux and macOS. MSIX was officially introduced with version 1809 of Windows 10 in October 2018, and Microsoft then added support for it to earlier versions—the April 2018 Update (version 1803) and the October 2017 Fall Creators Update (version 1709).

Packaging

If you have an existing installer, there’s an MSIX Packaging Tool available in the Store that lets you convert it to an MSIX. This enables administrators to package existing applications without even having access to the original source code. For developers, Visual Studio 2017 version 15.5 and higher provides a Windows Application Packaging Project that makes the process of packaging an existing application straightforward. You’ll find it under File | Add | New Project | Installed | Visual C# | Windows Universal. It includes an Application folder that you can right-click in the Solution Explorer and choose to add a reference to your Windows Presentation Foundation (WPF), Windows Forms (WinForms) or whatever desktop project you want to package. If you then right-click on the referenced application and choose Set as Entry Point, you’ll be able to build, run and debug your application just as you’re used to.

The difference between starting the original desktop process versus the packaging project is that the latter will run your application inside a modern app container. Behind the scenes, Visual Studio uses the MakeAppx and SignTool command-line tools from the Windows SDK to first create an .msix file, and then sign it with a certificate. This step isn’t optional. All MSIX packages must be signed with a certificate that chains to a trusted root authority on the machine where you intend to install and run the packaged app.

Digital Signing The packaging project includes a default password-protected personal information exchange (PFX) format file that you probably want to replace with your own. If your enterprise doesn’t provide you with a code-signing certificate, you can either buy one from a trusted authority or create a self-signed certificate. There’s a “Create test certificate” option and an import wizard in Visual Studio, which you’ll find if you open the Package.appxmanifest file in the default app manifest designer and look under the Packaging tab. If you’re not that into wizards and dialogs, you can use the New-SelfSignedCertificate PowerShell cmdlet to create a certificate:

> New-SelfSignedCertificate -Type CodeSigningCert -Subject "CN=MyCompany,
  O=MyCompany, L=Stockholm, S=N/A, C=Sweden" -KeyUsage DigitalSignature
    -FriendlyName MyCertificate -CertStoreLocation "Cert:\LocalMachine\My"
      -TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3',
        '2.5.29.19={text}Subject Type:End Entity')

The cmdlet outputs a thumbprint (like the A27…D9F here) that you can pass to another cmdlet, Move-Item, to move the certificate into the trusted root certification store:

>Move-Item Cert:\LocalMachine\My\A27A5DBF5C874016E1A0DEBF38A97061F6625D9F
  -Destination Cert:\LocalMachine\Root

Again, you need to install the certificate into this store on all computers where you intend to install and run the packaged app. You also need to enable sideloading of apps on these devices. On an unmanaged computer, this can be done under Update & Security | For Developers in the Settings app. On a device that’s managed by an organization, you can turn on sideloading by pushing a policy with a mobile device management (MDM) provider.

The thumbprint can also be used to export the certificate to a new PFX file using the Export-PfxCertificate cmdlet:

>$pwd = ConvertTo-SecureString -String secret -Force -AsPlainText
>Export-PfxCertificate -cert
  "Cert:\LocalMachine\Root\A27A5DBF5C874016E1A0DEBF38A97061F6625D9F"
    -FilePath "c:/<SolutionFolder>/Msix/certificate.pfx" -Password $pwd

Remember to tell Visual Studio to use the generated PFX file to sign the MSIX package by selecting it under the Packaging tab in the designer, or by manually editing the .wapproj project file and replacing the values of the <PackageCertificateKeyFile> and <PackageCertificateThumbprint> elements.

Package Manifest The Package.appxmanifest file is an XML-based template that the build process uses to generate a digitally signed AppxManifest.xml file that includes all information the OS needs to deploy, display and update the packaged app. This is where you specify the display name and logo of your app, as it will appear in the Windows shell after the app has been installed.

Make sure that the Subject property of the certificate you use to sign the MSIX package with exactly matches the value of the Publisher attribute of the Identity element. Because a packaged desktop application can run only on desktop devices, you should also remove the TargetDeviceFamily element with the name of Windows.Universal from the Dependencies element in the default template that Visual Studio generates.

The MinVersion and MaxVersionTested attributes, or Minimum Version and Target Version as they’re called in the dialog that shows up when you create a packaging project, is a UWP concept in which the former specifies the oldest version of the OS that your app is compatible with, and the latter is used to identify the set of APIs that are available when you compile the app. When packaging desktop applications that don’t call into any Windows 10 APIs, you should select the same version. Whenever you don’t, your code should include runtime API checks to avoid getting exceptions when running your app on devices that target the minimum version.

To generate the actual MSIX package, there’s a wizard available under Project | Store | Create App Packages in Visual Studio. An end user installs an MSIX package by simply double-clicking on the generated .msix file. This brings up a built-in, non-customizable dialog, shown in Figure 1, that guides you through the process of installing the app.

The App Installer and MSIX Installation Experience in Windows 10
Figure 1 The App Installer and MSIX Installation Experience in Windows 10

Continuous Integration

If you want to set up CI for your MSIX packages, Azure Pipelines has great support. It supports Configuration as Code (CAC) through the use of YAML files and provides a cloud-hosted build agent that comes with all the software required to create MSIX packages pre-installed.

Before building the packaging project the same way the wizard in Visual Studio does using the MSBuild command line, the build process can version the MSIX package that’s being produced by editing the Version attribute of the Package element in the Package.appxmanifest file. In Azure Pipelines, this can be achieved by using an expression for setting a counter variable that gets incremented for every build, and a PowerShell script that uses the System.Xml.Linq.XDocument class in .NET to change the value of the attribute. Figure 2 shows an example YAML file that versions and creates an MSIX package based on a packaging project before it copies it to a staging directory on the build agent.

Figure 2 The YAML File That Defines the MSIX Build Pipeline

pool: 
  vmImage: vs2017-win2016
variables:
  buildPlatform: 'x86'
  buildConfiguration: 'release'
  major: 1
  minor: 0
  build: 0
  revision: $[counter('rev', 0)]
steps:
- powershell: |
   [Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq")
   $path = "Msix/Package.appxmanifest"
   $doc = [System.Xml.Linq.XDocument]::Load($path)
   $xName =
     [System.Xml.Linq.XName]
       "{https://schemas.microsoft.com/appx/manifest/foundation/windows10}Identity"
   $doc.Root.Element($xName).Attribute("Version").Value =
     "$(major).$(minor).$(build).$(revision)";
   $doc.Save($path)
  displayName: 'Version Package Manifest'
- task: MSBuild@1
  inputs:
    solution: Msix/Msix.wapproj
    platform: $(buildPlatform)
    configuration: $(buildConfiguration)
    msbuildArguments: '/p:OutputPath=NonPackagedApp
     /p:UapAppxPackageBuildMode=SideLoadOnly  /p:AppxBundle=Never /p:AppxPackageOutput=$(Build.ArtifactStagingDirectory)\MsixDesktopApp.msix /p:AppxPackageSigningEnabled=false'
  displayName: 'Package the App'
- task: DownloadSecureFile@1
  inputs:
    secureFile: 'certificate.pfx'
  displayName: 'Download Secure PFX File'
- script: '"C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x86\signtool"
    sign /fd SHA256 /f $(Agent.TempDirectory)/certificate.pfx /p secret $(
    Build.ArtifactStagingDirectory)/MsixDesktopApp.msix'
  displayName: 'Sign MSIX Package'
- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact: drop'

The name of the hosted virtual machine that runs Visual Studio 2017 on Windows Server 2016 is vs2017-win2016. It has the required UWP and .NET development workloads installed, including SignTool, which is used to sign the MSIX package after it has been created by MSBuild. Note that the PFX file shouldn’t be added to the source control. It’s also ignored by Git by default. Instead, it should be uploaded to Azure Pipelines as a secret file under the Library tab in the Web portal. Because it includes the private key of the certificate that represents the digital signature and identity of your company, you don’t want to distribute it to more people than necessary.

In large enterprises where you release your software to multiple environments in different stages, it’s considered a best practice to sign the packages as part of the release process and let the build pipeline produce unsigned packages. This not only lets you sign with different certificates for different environments, but it also gives you the ability to upload your packages to the Store where they’ll be signed by a Microsoft certificate.

Also note that secrets, such as the password for the PFX file, shouldn’t be included in the YAML file. Unlike variables that specify the targeted processor architecture and the package version, they’re defined and set in the Web interface.

Figure 3shows the solution explorer for a WPF application that was created using the default project template in Visual Studio and packaged using a Windows Application Packaging project. The YAML file has been added to the packaging project and is checked in to a source code repository together with the rest of the code.

A Packaged WPF Application Ready to Be Pushed to the Source Control
Figure 3 A Packaged WPF Application Ready to Be Pushed to the Source Control

To set up the actual build pipeline, you browse to the Azure DevOps portal at dev.azure.com/<organization> and create a new project. If you don’t have an account, you can create one for free. Once you’ve signed in and created a project, you can either push the source code to the Git repository that’s set up for you at https://<organization>@dev.azure.com/<organization>/<project>/_git/<project>, or use any other provider, such as GitHub. You’ll get to choose the location of your repository when you create a new pipeline in the portal by clicking first on the “Pipelines” button and then on “New Pipeline.”

On the Configure screen that comes next, you should select the “Existing Azure Pipelines YAML file” option and select the path to the checked-in YAML file in your repository, as Figure 4 shows.

The Pipeline Configuration Web Interface
Figure 4 The Pipeline Configuration Web Interface

The MSIX package that’s produced by the build can be downloaded and double-click installed on any Windows 10-compatible computer with the required certificate installed, or you could set up a CD pipeline that copies the package to a Web site or a file share where your end users can download it. I’ll come back to this in just a bit.

Auto-updates While an MSIX package is able to extract itself and automatically replace any older version of the packaged app that may be present on the machine when you install it, the MSIX format doesn’t provide any built-in support for automatically updating an app that has already been installed from an .msix file when you open it.

However, starting with the April 2018 Update of Windows 10, there’s support for an app installer file that you can deploy along with your package to enable automatic updates. It contains a MainPackage element whose Uri attribute refers to the original or an updated MSIX package. Figure 5 shows an example of a minimal .appinstaller file. Note that the Uri attribute of the root element specifies a URL or a UNC path to a file share where the OS will look for the updated files. When the URI differs between a currently installed version and a new app installer file, the deployment operation will redirect to the “old” URI.

Figure 5 An .appinstaller File That Will Look for Updated Files on \\server\foo

<?xml version="1.0" encoding="utf-8"?>
<AppInstaller xmlns="https://schemas.microsoft.com/appx/appinstaller/2018"
              Version="1.0.0.0"
              Uri="\\server\foo\MsixDesktopApp.appinstaller">
  <MainPackage Name="MyCompany.MySampleApp"
               Publisher="CN=MyCompany, O=MyCompany, L=Stockholm, S=N/A, C=Sweden"
               Version="1.0.0.0"
               Uri="\\server\foo\MsixDesktopApp.msix"
               ProcessorArchitecture="x86"/>
  <UpdateSettings>
    <OnLaunch HoursBetweenUpdateChecks="0" />
  </UpdateSettings>
</AppInstaller>

The UpdateSettings element is used to tell the system when to check for updates and whether to force the user to update. The full schema reference, including the supported namespaces for each version of Windows 10, can be found in the docs at bit.ly/2TGWnCR.

If you add the .appinstaller file in Figure 5 to the packaging project and set its Package Action property to Content and the Copy to Output Directory property to Copy if newer, you can then add another PowerShell task to the YAML file that updates the Version attributes of the root and MainPackage elements and saves the updated file to the staging directory:

- powershell: |
  [Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq")
  $doc = [System.Xml.Linq.XDocument]::Load(
    "$(Build.SourcesDirectory)/Msix/Package.appinstaller")
  $version = "$(major).$(minor).$(build).$(revision)"
  $doc.Root.Attribute("Version").Value = $version;
  $xName =
    [System.Xml.Linq.XName]
      "{https://schemas.microsoft.com/appx/appinstaller/2018}MainPackage"
  $doc.Root.Element($xName).Attribute("Version").Value = $version;
  $doc.Save("$(Build.ArtifactStagingDirectory)/MsixDesktopApp.appinstaller")
displayName: 'Version App Installer File'

You’d then distribute the .appinstaller file to your end users and let them double-click on this one instead of the .msix file to install the packaged app.

Continuous Deployment

The app installer file itself is an uncompiled XML file that can be edited after the build, if required. This makes it easy to use when you deploy your software to multiple environments and when you want to separate the build pipeline from the release process.

If you create a release pipeline in the Azure Portal using the “Empty job” template and use the recently set up build pipeline as the source of the artifact to be deployed, as shown in Figure 6, you can then add the PowerShell task in Figure 7 to the release stage in order to dynamically change the values of the two Uri attributes in the .appinstaller file to reflect the location to which the app is published.

The Pipeline Tab in the Azure DevOps Portal
Figure 6 The Pipeline Tab in the Azure DevOps Portal

Figure 7 A Release Pipeline Task That Modifies the Uris in the .appinstaller File

- powershell: |
  [Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq")
  $fileShare = "\\filesharestorageccount.file.core.windows.net\myfileshare\"
  $localFilePath =
    "$(System.DefaultWorkingDirectory)\_MsixDesktopApp\drop\MsixDesktopApp.appinstaller"
  $doc = [System.Xml.Linq.XDocument]::Load("$localFilePath")
  $doc.Root.Attribute("Uri").Value = [string]::Format('{0}{1}', $fileShare,
    'MsixDesktopApp.appinstaller')
  $xName =
    [System.Xml.Linq.XName]"{https://schemas.microsoft.com/appx/appinstaller/2018}MainPackage"
  $doc.Root.Element($xName).Attribute("Uri").Value = [string]::Format('{0}{1}',
    $fileShare, 'MsixDesktopApp.appx')
  $doc.Save("$localFilePath")
displayName: 'Modify URIs in App Installer File'

In the task in Figure 7, the URI is set to the UNC path of an Azure file share. Because this is where the OS will look for the MSIX package when you install and update the app, I’ve also added another command-line script to the release pipeline that first maps the file share in the cloud to the local Z:\ drive on the build agent before it uses the xcopy command to copy the .appinstaller and .msix files there:

- script: |
  net use Z: \\filesharestorageccount.file.core.windows.net\myfileshare
    /u:AZURE\filesharestorageccount
    3PTYC+ociHIwNgCnyg7zsWoKBxRmkEc4Aew4FMzbpUl/
    dydo/3HVnl71XPe0uWxQcLddEUuq0fN8Ltcpc0LYeg==
  xcopy $(System.DefaultWorkingDirectory)\_MsixDesktopApp\drop Z:\ /Y
  displayName: 'Publish App Installer File and MSIX package'

If you host your own on-premises Azure DevOps Server, you may of course publish the files to your own internal network share.

Web Installs If you choose to publish to a Web server, you can tell MSBuild to generate a versioned .appinstaller file and an HTML page that contains a download link and some information about the packaged app by supplying a few additional arguments in the YAML file:

- task: MSBuild@1
  inputs:
    solution: Msix/Msix.wapproj
    platform: $(buildPlatform)
    configuration: $(buildConfiguration)
    msbuildArguments: '/p:OutputPath=NonPackagedApp /p:UapAppxPackageBuildMode=SideLoadOnly  /p:AppxBundle=Never /p:GenerateAppInstallerFile=True
/p:AppInstallerUri=https://yourwebsite.com/packages/ /p:AppInstallerCheckForUpdateFrequency=OnApplicationRun /p:AppInstallerUpdateFrequency=1 /p:AppxPackageDir=$(Build.ArtifactStagingDirectory)/'
  displayName: 'Package the App'

Figure 8 shows the contents of the staging directory on the build agent after running the previous command. It’s the same output you get from the wizard in Visual Studio if you choose to create packages for sideloading and check the “Enable automatic updates” checkbox. Using this approach, you can remove the manually created .appinstaller file at the possible expense of some flexibility regarding the configuration of the update behavior.

The Build Artifacts Explorer in the Azure DevOps Portal
Figure 8 The Build Artifacts Explorer in the Azure DevOps Portal

The generated HTML file includes a hyperlink prefixed with the browser-agnostic ms-appinstaller protocol activation scheme:

<a href="ms-appinstaller:?source=
  https://yourwebsite.com/packages/Msix_x86.appinstaller ">Install App</a>

If you set up a release pipeline that publishes the contents of the drop folder to your intranet or any other Web site, and the Web server supports byte-range requests and is configured properly, your end users can use this link to directly install the app without downloading the MSIX package first.

Wrapping Up

In this article you’ve seen how easy it is to package a .NET desktop application as an MSIX using Visual Studio. You also saw how to set up CI and CD pipelines in Azure Pipelines and how to configure automatic updates. MSIX is the modern way to deploy applications on Windows. It’s built to be safe, secure and reliable, and lets you and your customers take advantage of the new app model and the modern APIs that have been introduced in Windows 10, regardless of whether you intend to upload your apps to the Microsoft Store or sideload them onto computers in your enterprise. As long as all of your users have moved to Windows 10, you should be able to leverage MSIX to package most Windows desktop apps that exist out there.


Magnus Montin *is a Microsoft MVP who works as a self-employed software developer and consultant in Stockholm, Sweden. He specializes in .NET and the Microsoft stack and has over a decade of hands-on experience. You can read his blog at blog.magnusmontin.net.*​

Thanks to the following Microsoft technical expert for reviewing this article: Matteo Pagani (Matteo.Pagani@microsoft.com)
Matteo Pagani is a developer with a strong passion forclient development and the Windows platform. He’s a regular speaker in conferences around the world, he’s a book author and he regularly writes technical articles for tech websites and blogs. He has been a Microsoft MVP in the Windows Development category for 5 years, after which he joined Microsoft as an engineer in the Windows AppConsult team.


Discuss this article in the MSDN Magazine forum