有关可运行的端到端示例(WPF 应用 + Inno Setup 安装程序),请参阅 sparse-app 示例。
使用、MSBuild、CMake 或任何其他工具链构建 dotnet build的标准桌面可执行文件没有 包标识。 如果没有标识,它无法使用许多新式Windows API(Toast 通知、后台任务、共享目标、启动任务、应用数据 API 等)。
稀疏打包可在不将应用的二进制文件移入 MSIX 的情况下,为应用赋予身份。 你会提供一个极小的仅含标识信息的包.msix(仅为清单),并通过外部位置将其与你按常规方式安装的应用一起注册。 您的 .exe 会准确地保留在安装程序放置它的位置。 这是 winapp create-debug-identity 对应的生产环境版本,而 winapp create-debug-identity 仅用于开发阶段调试。
本指南介绍三个 CLI 步骤,这些步骤对应于官方 向非打包应用授予标识 工作流的前三个步骤:
| Step | 命令 | Result |
|---|---|---|
| 1.创建标识清单 | winapp init --exe <exe> --sparse |
sparse/appxmanifest.xml + sparse/Assets/ |
| 2.生成并签名标识包 | winapp pack <appxmanifest.xml> --cert <pfx> |
<PackageName>.identity.msix |
| 3.将标识嵌入应用 | winapp embed-identity <exe> |
EXE 文件的 Fusion 清单中的 <msix> 元素 |
文档中的第 4–5 步(注册/注销该包)由 安装程序 负责——请参阅 安装程序集成。
何时使用稀疏封装
- 你已经有一个成熟的安装程序(Inno Setup、WiX、NSIS、MSI),并且不想切换到 MSIX 用于分发,但你需要身份标识受限的 Windows API。
- 你的应用必须安装到某个路径,或者采用 MSIX 不允许的布局方式。
- 需要最少的附加更改:保留现有安装流并添加一个
.msix注册步骤。
如果你是从头开始,并且可以采用 MSIX 分发,那么完整打包应用(winapp init + winapp pack <folder>)会更简单。
先决条件
- Windows 10 版本 2004(内部版本 19041)或更高版本。 稀疏包依赖
uap10:AllowExternalContent,后者要求版本为 19041 或更高。 -
winapp CLI - 通过 winget 安装(或更新(如果已安装):
winget install Microsoft.WinApp --source winget - 目标计算机上受信任的代码签名证书。 对于本地测试,请使用
winapp cert generate生成开发证书并信任该证书。 生产包必须使用主题与清单Publisher匹配的证书进行签名。
Walkthrough
以下示例假设在 ./bin/Release/net8.0-windows/MyApp.exe 处已有一个已构建的可执行文件。
步骤 1 — 创建稀疏标识清单
winapp init --exe ./bin/Release/net8.0-windows/MyApp.exe --sparse
这会从 exe(通过其文件版本信息)推断包名称、发布者、版本和说明,并提示你接受或替代它们。 添加 --use-defaults(或 --no-prompt)以跳过 CI 中的提示,以及使用 --name / --publisher 覆盖特定值:
winapp init --exe ./bin/Release/net8.0-windows/MyApp.exe --sparse --use-defaults `
--name "Contoso.MyApp" --publisher "CN=Contoso"
默认情况下,它会将以下内容写入当前目录中的专用 sparse/ 文件夹(可用 --output-dir 覆盖):
-
appxmanifest.xml— 一个稀疏清单,其中包含<uap10:AllowExternalContent>true</uap10:AllowExternalContent>(<Properties>下的一个元素)、ProcessorArchitecture="neutral"、一个win32App应用程序,以及填入Executable的 exe 名称。 -
Assets/— 占位符视觉资源(尽可能从 EXE 文件的图标中提取)。
为什么是一个
sparse/文件夹,而不是在 exe 旁边? 清单和Assets/是由winapp embed-identity和 使用的winapp pack——在运行时,没有任何内容会从 exe 旁边读取它们(运行时标识来自嵌入在 exe 中的<msix>元素,以及已注册包的外部位置;而且清单通过名称引用 exe,因此其位置独立于 exe 的所在位置)。 将它们放在一个专用且受源代码管理控制的文件夹中,可以避免把它们放到构建输出目录(如bin/)里,因为该目录会在清理/重新生成时被清除;同时也能确保该文件夹中没有二进制文件,从而让后续步骤保持干净。winapp pack并winapp embed-identity自动查找sparse/,因此你很少需要命名路径。
注意: 稀疏初始化流程会刻意 跳过所有 SDK/软件包安装——仅身份标识的软件包没有任何 SDK 依赖。
如果目标目录中已经存在一个 appxmanifest.xml,init 将停止运行,而不是覆盖它(以及它的 Assets/)。 使用 --force 重新运行以重新生成。
请确保生成的清单中的 Publisher 与您将用于签名的证书匹配。 根据需要进行编辑 appxmanifest.xml ,或在生成时传递 --publisher 。
步骤 2 - 生成和签名标识包
将 winapp pack 指向稀疏清单(文件,而不是文件夹):
winapp pack ./sparse/appxmanifest.xml --cert ./devcert.pfx
由于清单声明了 AllowExternalContent,winapp pack 会构建一个仅标识的 identity-only.msix,其中只包含清单——没有二进制文件,也没有资源文件。 输出默认位于当前目录中的 <PackageName>.identity.msix;使用 --output 可更改此位置。 签名仅在通过 --cert 时发生(或 --generate-cert)。
步骤 3 - 将标识嵌入应用中
嵌入元素<msix>,以便Windows将正在运行的 exe 连接到标识包:
# EXE mode — modify the built binary in place (uses mt.exe)
winapp embed-identity ./bin/Release/net8.0-windows/MyApp.exe
或者将并行清单作为签入文件维护并重新生成:
# XML mode — update an external SxS manifest, then rebuild your app
winapp embed-identity ./app.manifest
在 XML 模式下,元素 <msix> 插入到目标清单(或替换)中。 引用项目中的清单(对于.NET,设置<ApplicationManifest>app.manifest</ApplicationManifest>)并重新生成,以便元素嵌入到 exe 中。
这两种模式都从稀疏 appxmanifest.xml 中读取标识信息。 如果省略 --manifest,winapp 会先在目标旁边的 sparse/ 文件夹中查找(winapp init --exe --sparse 默认将其写入该文件夹),然后在当前目录中查找;如果仍未找到,则回退为在目标旁边和当前目录中查找;传递 --manifest 可将其指向其他位置。
注意: EXE 模式重写二进制
mt.exe文件,这会使任何现有的 Authenticode 签名失效。 在分发 exe 之前重新对 exe 签名(例如winapp sign ./MyApp.exe <cert.pfx>)。
步骤 4 - 注册(用于本地测试)
清单中的徽标是在运行时从外部位置解析的,而不是从仅含标识信息的 .msix 解析的。 步骤 1 将它们写在 ./sparse/Assets 下,因此请在注册之前先将它们复制到你的 exe 文件旁边(即外部位置)——否则 Windows 注册的将是一个缺少清单中引用的所有徽标的布局:
# Copy the generated assets into the external location (beside your exe)
Copy-Item ./sparse/Assets -Destination .\bin\Release\net8.0-windows\Assets -Recurse -Force
然后,针对该文件夹注册标识包( 外部位置):
Add-AppxPackage -Path .\MyApp.identity.msix `
-ExternalLocation (Resolve-Path .\bin\Release\net8.0-windows)
启动应用,并确认存在标识信息——例如,Windows.ApplicationModel.Package.Current.Id.FamilyName 应返回你的包系列名称,而不是引发异常。
要清理:
Remove-AppxPackage <full-package-name>
资产处理
稀疏 .msix 是 仅限标识。 清单中引用的视觉资源(Assets\StoreLogo.png、磁贴等)在运行时从 外部内容位置 解析,即从你的应用的安装目录解析,而不是从 .msix 内部解析。
这意味着你必须将 Assets/ 文件夹与应用程序一同部署(相对于外部位置,其布局应与清单预期的一致)。
步骤 2 直接打包清单文件(winapp pack ./sparse/appxmanifest.xml),仅根据该清单构建纯标识的 .msix——同级文件会被忽略,因此绝不会包含你的资源文件或二进制文件。 (如果你改为将winapp pack指向其清单声明了的AllowExternalContent,它会对其找到的任何资产或二进制文件发出警告,因为对于稀疏包,这些内容应位于外部位置,而不是在.msix内部。)
安装程序集成
注册和取消注册是安装程序的作业。 该模式在安装程序工具中是相同的:
-
安装: 将应用二进制文件、
Assets/文件夹和.msix安装目录复制到安装目录,然后运行Add-AppxPackage -Path "<install-dir>\MyApp.identity.msix" -ExternalLocation "<install-dir>"。 -
卸载: 在删除文件之前运行
Remove-AppxPackage <full-package-name>。
安全性: 安装目录在安装时解析,可以包含断出 PowerShell 字符串文本的字符(例如单引号)。 在将路径插值到
-Command字符串之前,请始终先对其进行转义或验证——下面的 WiX 和 NSIS 代码片段均假定安装路径是可信的,而 Inno Setup 示例则演示了如何进行安全转义。 最好通过-File将路径作为参数传递给脚本,而不是使用内联-Command插值。
Inno Setup
在 [Code] 函数中构建 PowerShell 参数,以便对运行时安装路径进行转义,使其适用于 PowerShell 中的单引号字符串字面量(包含 ' 的安装目录不得能够注入脚本):
[Files]
Source: "dist\*"; DestDir: "{app}"; Flags: recursesubdirs
Source: "MyApp.identity.msix"; DestDir: "{app}"
[Run]
Filename: "powershell.exe"; Parameters: "{code:RegisterParams}"; Flags: runhidden
[UninstallRun]
Filename: "powershell.exe"; \
Parameters: "-NoProfile -ExecutionPolicy Bypass -Command ""Get-AppxPackage -Name 'MyApp' | Remove-AppxPackage"""; \
Flags: runhidden
[Code]
function EscapePSLiteral(const Value: string): string;
var S: string;
begin
S := Value; StringChange(S, '''', ''''''); Result := S;
end;
function RegisterParams(Param: string): string;
var AppDir: string;
begin
AppDir := ExpandConstant('{app}');
{ -ErrorAction Stop + try/catch make a registration failure terminating, so powershell.exe
exits nonzero and the AfterInstall callback (see the full sample) can abort with rollback. }
Result := '-NoProfile -ExecutionPolicy Bypass -Command "try { Add-AppxPackage -Path ''' +
EscapePSLiteral(AppDir + '\MyApp.identity.msix') +
''' -ExternalLocation ''' + EscapePSLiteral(AppDir) + ''' -ErrorAction Stop } catch { Write-Error $_; exit 1 }"';
end;
下面的 WiX 和 NSIS 示例通过 -File 调用一个小型 register-sparse.ps1,以便将安装路径作为 参数 传递(PowerShell 会将其绑定为数据),而不是将其插入 -Command 字符串中。 这可以避免通过精心构造的安装目录进行脚本注入(例如,包含引号或 $(...) 的文件夹名称):
# register-sparse.ps1 — ship this alongside your installer
param(
[Parameter(Mandatory)] [string] $MsixPath,
[Parameter(Mandatory)] [string] $ExternalLocation,
[Parameter(Mandatory)] [string] $PackageName
)
$ErrorActionPreference = 'Stop'
try {
# Add-AppxPackage emits NON-terminating errors by default, so a failure would otherwise leave
# the process exit code at 0 and let the installer complete without identity. Try the add
# directly first: a fresh install or a version-bumped upgrade registers/updates in place
# without touching any existing registration. -ErrorAction Stop + the outer trap make a real
# failure terminating so the installer (WiX Return="check" / NSIS) sees it.
try {
Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
} catch {
# Only ONE failure is safe to resolve by unregister+retry: the exact same version is already
# registered (HRESULT 0x80073CFB, ERROR_PACKAGE_ALREADY_EXISTS — "already installed,
# reinstallation blocked"), which Add-AppxPackage rejects. Re-throw everything else
# (untrusted/corrupt .msix, unsupported OS, ...) so a bad new package can NEVER unregister a
# working prior registration and strip the installed app of the identity it already had.
if ($_.Exception.HResult -ne 0x80073CFB) { throw }
Get-AppxPackage -Name $PackageName | Remove-AppxPackage -ErrorAction SilentlyContinue
Add-AppxPackage -Path $MsixPath -ExternalLocation $ExternalLocation -ErrorAction Stop
}
} catch {
Write-Error $_
exit 1
}
WiX (v3)
按用户注册(Impersonate="yes"),因为 Add-AppxPackage 会为运行它的账户注册该包。 带有 LocalSystem 的延迟操作以 Impersonate="no" 运行,这样 不会向安装用户授予身份(并且通常会被拒绝)。 对于面向整台计算机的 MSI,请以模拟身份运行注册过程,以使其应用于调用该操作的用户。
延迟执行的自定义操作无法直接读取 INSTALLFOLDER(延迟操作在无法访问属性的上下文中运行),而且仅仅 声明 该操作并不会执行它。 因此,通过 CustomActionData 封送这些路径——这是一个即时 Type 51 操作,其 Property 名称等于延迟操作的 Id——并将 两者 都安排在 InstallFiles 之后:
<!-- Immediate: stash the command line (with the resolved paths) into the deferred action's
CustomActionData. Windows Installer copies the value of the property named the same as a
deferred action into that action's CustomActionData. -->
<CustomAction Id="SetRegisterSparseCmd" Property="RegisterSparse" Execute="immediate"
Value="powershell.exe -NoProfile -ExecutionPolicy Bypass -File "[INSTALLFOLDER]register-sparse.ps1" -MsixPath "[INSTALLFOLDER]MyApp.identity.msix" -ExternalLocation "[INSTALLFOLDER]" -PackageName "MyPackageIdentityName"" />
<!-- Deferred + impersonated: CAQuietExec reads its command line from CustomActionData when run
deferred, so it registers the package for the invoking user. Return="check" fails the
install if registration fails. -->
<CustomAction Id="RegisterSparse" BinaryKey="WixCA" DllEntry="CAQuietExec"
Execute="deferred" Impersonate="yes" Return="check" />
<InstallExecuteSequence>
<Custom Action="SetRegisterSparseCmd" After="InstallFiles">NOT Installed</Custom>
<Custom Action="RegisterSparse" After="SetRegisterSparseCmd">NOT Installed</Custom>
</InstallExecuteSequence>
CAQuietExec 包含在 WiX Util 扩展(WixUtilExtension)中;引用它,使 WixCA 二进制文件可用。
单个模拟执行的操作仅会为运行该安装程序的用户注册身份信息。 若要预配每台计算机安装的每个用户,请改为在首次启动时注册(每用户),或使用预配机制,例如
Add-AppxProvisionedPackage。
NSIS
Section
# Capture the PowerShell exit code and abort if registration failed. register-sparse.ps1 exits
# nonzero on failure (it sets $ErrorActionPreference='Stop' and traps), so without this check the
# installer would complete even though the app has no identity.
ExecWait 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\register-sparse.ps1" -MsixPath "$INSTDIR\MyApp.identity.msix" -ExternalLocation "$INSTDIR" -PackageName "MyPackageIdentityName"' $0
IntCmp $0 0 +2
Abort "Registering the sparse identity package failed (exit code $0). The app requires package identity."
SectionEnd
Troubleshooting
Package.Current 在运行时抛出 /“没有包标识”错误
- 标识包尚未注册,或者 exe 的 Fusion 清单中缺少
<msix>元素。 重新运行winapp embed-identity(如果使用 XML 模式,则重新构建),然后使用Add-AppxPackage -ExternalLocation重新注册。 - exe
<msix packageName>/ /publisherapplicationId中的属性必须与已注册的包的标识完全匹配。
资产/徽标未显示
- 确保将
Assets/文件夹部署到外部位置,并使其相对路径与清单预期的一致。 资产是从外部位置解析的,而不是.msix.
Add-AppxPackage因签名/信任错误而失败
-
.msix必须使用在该计算机上受信任且其主题与清单Publisher匹配的证书进行签名。 在本地测试时,请使用winapp cert generate生成并信任开发证书,并确保清单Publisher与该证书匹配。
MakeAppx:“RuntimeBehavior 值为‘win32App’的应用程序不得声明 EntryPoint”
- 稀疏
win32App应用程序不得声明EntryPoint。 由winapp init --sparse生成的清单已经是正确的;如果您手动编辑了该清单,请删除任何EntryPoint属性。
“输入是文件,但不是稀疏清单”
-
winapp pack <file>仅接受声明<uap10:AllowExternalContent>true</uap10:AllowExternalContent>的清单。 使用winapp init --exe <exe> --sparse生成一个,或传入一个输入文件夹来构建完整的 MSIX。