2019 年 6 月

第 34 卷,第 6 期

[DevOps]

MSIX:在 Windows 上部署桌面应用的新式方法

作者:Magnus Montin | 2019 年 6 月 | 获取代码

MSIX 是随 Windows 10 的 2018 年 10 月更新引入的新打包格式。它旨在将先前的最佳安装技术(例如 MSI 和 ClickOnce)结合在一起,并且将是在 Windows 后续版本上安装应用程序的推荐方法。本文介绍了如何打包 .NET 桌面应用程序以及如何使用 Azure 管道设置持续集成 (CI)、持续部署 (CD) 和旁加载 MSIX 包的自动更新。

首先,需要介绍一点背景知识。在 Windows 8 中,Microsoft 引入了 API 和称为 Windows 运行时的运行时,主要致力于为新型应用程序(最初称为“新型”、“Metro”、“沉浸式”或只是“Microsoft Store”应用)提供一系列平台服务。此类应用源于移动设备革命,通常针对多种设备外观造型(如手机、平板电脑和笔记本电脑),并且通常从 Microsoft Store 中心进行安装和更新。

此类应用从那时起已发生了很多变化,现在称为通用 Windows 平台 (UWP) 应用。UWP 应用在与其他进程隔离的称为 AppContainer 的沙盒中运行。它们显式声明需要正常运行所需权限的功能,并且由用户决定是否应接受这些功能。这与通常使用当前用户的完全读写权限作为完全信任进程运行的传统桌面应用程序形成对照。

在 Windows 10 的周年更新中,Microsoft 引入了桌面桥(也称为 Centennial 项目)。它允许你将传统桌面应用程序打包为 UWP 应用,但仍将其作为完全信任进程运行。打包的应用程序可以上传到 Microsoft Store 或适用于企业的 Microsoft Store,并从简化的部署和内置许可及 Microsoft Store 提供的自动更新工具中获益。打包应用程序后,还可以开始使用新的 Windows 10 API 并将代码迁移到 UWP,以便覆盖所有设备上的客户。

即使你对 Microsoft Store 或 UWP 不感兴趣,但可能仍需要打包业务线桌面应用程序来充分利用 Windows 10 带来的新应用模型。通过将针对注册表和某些众所周知的系统文件夹的所有操作重定向到已安装应用程序的本地文件夹(其中已设置虚拟文件系统和注册表),可以提供应用的全新安装和卸载。你不必对源代码执行任何操作即可确保其发生,这些工作都是由 Windows 自动完成的。原理是:当卸载包时,将删除整个本地文件夹,从而不在系统上留下任何应用痕迹。

MSIX 基本上是桌面桥的后续版本,MSIX 包的内容以及适用于打包应用的限制大致与桌面桥使用的 APPX 格式相同。要求已在 bit.ly/2OvCcVW 的官方文档中列出,并且应在决定是否打包应用程序之前予以满足。其中一些要求仅适用于正在发布到 Microsoft Store 的应用。

MSIX 添加了名为修改包的新功能。它是类似于 MST 转换的概念,让 IT 管理员能够自定义应用(通常从第三方供应商),每当发布新功能或错误修复时无需从头开始重新打包。修改包在运行时与主应用程序合并,并且可能会禁用应用的某些功能(例如,通过更改某些注册表设置)。从开发人员的角度来看,提供的功能可能不多(假设你同时拥有应用的源代码和生成和发布管道),但对于大型企业,可能会降低成本并防止发生所谓的包瘫痪。

MSIX 格式的定义是在 GitHub 和 Microsoft 计划中用开放源代码编写的,提供了可用于在所有主要操作系统(包括 Linux 和 macOS)上打包和解压缩 MSIX 包的 SDK。MSIX 已于 2018 年 10 月随 Windows 10 版本 1809 正式引入,然后 Microsoft 向早期版本(2018 年 4 月更新(版本 1803)和 2017 年 10 月 Fall Creators Update (版本 1709))添加了对它的支持。

打包

如果已有安装程序,则 Microsoft Store 中提供的 MSIX 打包工具可将其转换为 MSIX。这使管理员能够打包现有应用程序,甚至无需访问原始源代码。对于开发人员,Visual Studio 2017 版本 15.5 及更高版本提供了 Windows 应用打包项目,可使打包现有应用程序的过程简单明了。可以在“文件”|“添加”|“新建项目”|“已安装”|“Visual C#”|“Windows 通用”下找到它。其中包括可在解决方案资源管理器中右键单击并选择添加对 Windows Presentation Foundation (WPF)、Windows 窗体 (WinForms) 或要打包的任何桌面项目的引用的应用程序文件夹。如果随后右键单击引用的应用程序并选择“设置为入口点”,则可以生成、运行和调试应用程序,正如你过去常常做的。

启动原始桌面进程与打包项目之间的区别是,后者将在现代应用容器内运行应用程序。在后台,Visual Studio 使用 Windows SDK 中的 MakeAppx 和 SignTool 命令行工具来先创建 .msix 文件,然后使用证书对其进行签名。此步骤不是可选步骤。所有 MSIX 包必须使用链接到想要安装并运行打包应用的计算机上的受信任根证书颁发机构的证书进行签名。

数字签名 打包项目包括可能想要替换为自己的格式文件的受默认密码保护的个人信息交换 (PFX) 格式文件。如果你的企业不向你提供代码签名证书,则可以从受信任的颁发机构购买一个证书或创建自签名证书。Visual Studio 中有“创建测试证书”选项和导入向导,可通过在默认应用清单设计器中打开 Package.appxmanifest 文件并在“打包”选项卡下查找来获取。如果不想使用向导和对话框,可以使用 New-SelfSignedCertificate PowerShell cmdlet 来创建证书:

> 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')

Cmdlet 输出可传递到另一个 cmdlet (Move-Item) 以将证书移动到受信任的根证书存储的指纹(例如此处的 A27…D9F):

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

同样,需要将证书安装到想要安装并运行打包应用的所有计算机上的此存储区中。此外还需要在这些设备上实现应用的旁加载。在非托管计算机上,这可以在“设置”应用中的“更新和安全”|“面向开发人员”下完成。在由组织管理的设备上,可以通过使用移动设备管理 (MDM) 提供程序推送策略来启用旁加载。

指纹也可用于使用 Export-PfxCertificate cmdlet 将证书导出到新的 PFX 文件:

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

请记得告诉 Visual Studio 使用生成的 PFX 文件对 MSIX 包进行签名,方法是通过在设计器中的“打包”选项卡下选择该包或通过手动编辑 .wapproj 项目文件并替换 <PackageCertificateKeyFile> 和 <PackageCertificateThumbprint> 元素的值。

包清单 Package.appxmanifest 文件是基于 XML 的模板,生成过程使用该模板可生成数字签名的 AppxManifest.xml 文件(其中包括操作系统部署、显示和更新打包应用所需的所有信息)。可以在此处指定应用的显示名称和徽标,因为在安装应用后会在 Windows Shell 中显示该文件。

确保用于对 MSIX 包进行签名的证书的 Subject 属性完全匹配 Identity 元素的 Publisher 属性的值。由于打包桌面应用程序只能在桌面设备上运行,因此还应从 Visual Studio 生成的默认模板中的依赖项元素中删除具有 Windows.Universal 名称的 TargetDeviceFamily 元素。

创建打包项目时显示的 MinVersion 和 MaxVersionTested 属性或最低版本和目标版本(在对话框中的名称)是一种 UWP 概念,前者指定与你的应用兼容的操作系统的最早版本,后者用于标识编译应用时提供的 API 集。打包未调用到任何 Windows 10 API 的桌面应用程序时,应选择相同的版本。不这样做时,你的代码应包括运行时 API 检查,以避免在以最低版本为目标的设备上运行应用时出现异常。

若要生成实际的 MSIX 包,可以使用“项目”|“Microsoft Store”|“在 Visual Studio 中创建应用包”下的向导。最终用户只需双击生成的 .msix 文件即可安装 MSIX 包。这时会打开一个不可自定义的内置对话框(如图 1 所示),指导你完成安装应用的过程。

Windows 10 中的应用安装程序和 MSIX 安装体验
图 1 Windows 10 中的应用安装程序和 MSIX 安装体验

持续集成

如果要为 MSIX 包设置 CI,Azure 管道具有强大支持。它通过使用 YAML 文件支持配置为代码 (CAC),并提供预安装了创建 MSIX 包所需的所有软件的云托管生成代理。

在使用与 Visual Studio 中的向导使用 MSBuild 命令行生成打包项目相同的方式来生成打包项目之前,生成过程可以对通过编辑 Package.appxmanifest 文件中的 Package 元素的 Version 属性生成的 MSIX 包进行版本控制。在 Azure 管道中,要达到此目的,可以使用用于设置对每个生成递增的计数器变量的表达式,以及使用 .NET 中的 System.Xml.Linq.XDocument 类更改属性值。图 2 显示了 YAML 文件示例,可在将打包项目复制到生成代理上的临时目录之前基于该打包项目创建 MSIX 包并对其进行版本控制。

图 2 定义 MSIX 生成管道的 YAML 文件

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'

在 Windows Server 2016 上运行 Visual Studio 2017 的托管虚拟机的名称是 vs2017-win2016。已安装所需的 UWP 和 .NET 开发工作负荷,包括用于在由 MSBuild 创建 MSIX 包之后对其进行签名的 SignTool。请注意,不应将 PFX 文件添加到源代码管理。默认情况下,它还会被 Git 忽略。相反,应将其作为 Web 门户中的“库”选项卡下的机密文件上传到 Azure 管道。由于它包含表示贵公司的数字签名和标识的证书的私钥,因此你不希望将其分发给超过必要数量的人员。

在分不同阶段将软件发布到多个环境的大型企业中,在发布过程中对包进行签名并允许生成管道生成未签名的包被视为最佳做法。这样,不仅可以使用不同环境的不同证书进行签名,还可以将包上传到通过 Microsoft 证书签名的 Microsoft Store。

另请注意,机密(例如,PFX 文件的密码)不应包含在 YAML 文件中。与指定目标处理器体系结构和包版本的变量不同,它们在 Web 界面中定义和设置。

图 3 显示了使用 Visual Studio 中的默认项目模板创建并使用 Windows 应用程序打包项目打包的 WPF 应用程序的解决方案资源管理器。YAML 文件已添加到打包项目,并与其余代码一起签入到源代码存储库中。

打包的 WPF 应用程序已准备好推送到源代码管理
图 3 打包的 WPF 应用程序已准备好推送到源代码管理

若要设置实际生成管道,浏览到dev.azure.com/<organization> 中的 Azure DevOps 门户并创建新项目。如果你没有帐户,则可以免费创建一个。登录并创建项目后,可以将源代码推送到在 https://<organization>@dev.azure.com/<organization>/<project>/_git/<project> 中为你设置的 Git 存储库,或使用任何其他提供程序(例如 GitHub)。当通过依次单击“管道”按钮和“新建管道”在门户中创建新管道时,你需要选择存储库的位置。

在接下来显示的“配置”屏幕中,应选择“现有 Azure Pipelines YAML 文件”选项并选择存储库中已签入的 YAML 文件的路径,如图 4 所示。

管道配置 Web 界面
图 4 管道配置 Web 界面

可以在安装了所需证书且与 Windows 10 兼容的任何计算机上下载并双击安装生成所得到的 MSIX 包,也可以设置将包复制到最终用户可从中下载该包的网站或文件共享的 CD 管道。我们稍后再讨论此内容。

自动更新 虽然 MSIX 包能够自解压缩并在安装该包后自动替换系统上可能存在的任何较早的打包应用版本,但 MSIX 格式不会为打开该包后自动更新已从 .msix 文件安装的应用提供内置支持。

但是,从 Windows 10 的 2018 年 4 月更新开始,支持可与包一起部署以实现自动更新的应用安装程序文件。其中包含其 Uri 属性指的是原始或已更新的 MSIX 包的 MainPackage 元素。图 5 显示了最小 .appinstaller 文件的示例。请注意,根元素的 Uri 属性指定 URL 或操作系统将在其中查找更新后文件的文件共享的 UNC 路径。当前安装的版本和新的应用安装程序文件之间的 URI 不同时,部署操作将重定向到“旧”URI。

图 5 将在 \\server\foo 上查找更新后文件的 .appinstaller 文件

<?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>

UpdateSettings 元素用于告知系统何时检查更新以及是否强制用户更新。可以在 bit.ly/2TGWnCR 上的文档中找到完整的架构引用(包括每个 Windows 10 版本支持的命名空间)。

如果将图 5 中的 .appinstaller 文件添加到打包项目并将其“包操作”属性设置为“内容”,将“复制到输出目录”属性设置为“如果较新则复制”,则可以将另一个 PowerShell 任务添加到更新根元素和 MainPackage 元素的版本属性并将更新后的文件保存到临时目录的 YAML 文件:

- 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'

然后将 .appinstaller 文件分发给最终用户,并让他们双击此文件(而不是 .msix 文件)以安装打包应用。

连续部署

应用安装程序文件本身是可在生成后编辑的未编译 XML 文件(如果需要)。这样就可以在将软件部署到多个环境且想要将生成管道与发布过程分开时轻松使用。

如果在 Azure 门户中使用“空作业”模板创建发布管道并将最近设置的生成管道用作要部署的项目的源(如图 6 所示),则可以将图 7 中的 PowerShell 任务添加到发布阶段,以便动态更改 .appinstaller 文件中的两个 Uri 属性的值以反映应用的发布位置。

Azure DevOps 门户中的“管道”选项卡
图 6 Azure DevOps 门户中的“管道”选项卡

图 7 修改 .appinstaller 文件中的 Uri 的发布管道任务

- 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'

在图 7 的任务中,URI 设置为 Azure 文件共享的 UNC 路径。由于这是在安装和更新应用时操作系统将查找 MSIX 包的位置,因此我还将另一个命令行脚本添加到了发布管道,此管道先将云中的文件共享映射到生成代理上的本地 Z:\ 驱动器,再使用 xcopy 命令将 .appinstaller 和 .msix 文件复制到此处:

- 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'

如果你托管自己的本地 Azure DevOps Server,则当然可以将文件发布到自己的内部网络共享。

Web 安装 如果你选择发布到 Web 服务器,则可以指示 MSBuild 通过提供 YAML 文件中的一些其他参数来生成版本控制的 .appinstaller 文件和包含下载链接的 HTML 页,以及有关打包应用的一些信息:

- 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=http://yourwebsite.com/packages/ /p:AppInstallerCheckForUpdateFrequency=OnApplicationRun /p:AppInstallerUpdateFrequency=1 /p:AppxPackageDir=$(Build.ArtifactStagingDirectory)/'
  displayName: 'Package the App'

图 8 显示运行上一个命令后生成代理上的临时目录的内容。如果选择创建用于旁加载的包并选中“启用自动更新”复选框,则上述结果与从 Visual Studio 的向导中获取的输出相同。使用此方法,可以删除手动创建的 .appinstaller 文件,但可能会降低有关更新行为配置的一些灵活性。

Azure DevOps 门户中的生成项目资源管理器
图 8 Azure DevOps 门户中的生成项目资源管理器

生成的 HTML 文件包含前缀为与浏览器无关的 ms-appinstaller 协议激活方案的超链接:

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

如果设置将放置文件夹的内容发布到 Intranet 或任何其他网站的发布管道,并且 Web 服务器支持字节范围请求且已正确配置,则最终用户可以使用此链接直接安装应用,而无需先下载 MSIX 包。

总结

在本文中,你已了解使用 Visual Studio 将 .NET 桌面应用程序打包为 MSIX 是多么简单的事。你还了解到如何设置 Azure 管道中的 CI 和 CD 管道以及如何配置自动更新。MSIX 是在 Windows 上部署应用程序的最新方法。它安全且可靠,并允许你和你的客户充分利用已在 Windows 10 中引入的新应用模型和新型 API,无论是否想要将应用上载到 Microsoft Store 或将其旁加载到企业中的计算机上。只要你的所有用户已移动到 Windows 10,就应能够利用 MSIX 打包大多数现有 Windows 桌面应用。


Magnus Montin是在瑞典斯德哥尔摩工作的一名个体私营软件开发人员和顾问,被评为 Microsoft MVP。他专门从事 .NET 和 Microsoft 堆栈工作,拥有十多年的亲身体验。你可以阅读他的博客 (blog.magnusmontin.net)。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Matteo Pagani (Matteo.Pagani@microsoft.com)
Matteo Pagani 是一名热衷于客户端开发和 Windows 平台的开发人员。他经常在世界各地的会议中发表演讲,是一名书籍作者,定期为技术网站和博客撰写技术文章。他已连续五年在 Windows 开发类别中被评为 Microsoft MVP,此后加入了 Microsoft,担任 Windows AppConsult 团队中的工程师。


在 MSDN 杂志论坛讨论这篇文章