關於端對端的有效範例(WPF 應用程式 + Inno 設定安裝程式),請參考 sparse-app 範例。
標準桌面執行檔——使用 dotnet build、MSBuild、CMake 或其他工具鏈所建構——沒有套件身份。 沒有身份,它無法使用許多現代 Windows API(吐司通知、背景任務、分享目標、啟動任務、應用程式資料 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> |
<msix> EXE 融合清單中的元素 |
文件中的第 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
這會從執行檔(透過檔案版本資訊)推斷出套件名稱、發佈者、版本和描述,並提示你接受或覆寫它們。 加入 --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的執行檔名稱。 -
Assets/— 佔位視覺資產(盡可能從執行檔圖示擷取)。
為什麼
sparse/是資料夾而不是 exe 檔旁邊? manifest 和Assets/是 建置時輸入,由winapp pack和winapp embed-identity使用——執行階段不會有任何東西從 exe 旁邊讀取它們(執行階段身分來自嵌入於 exe 中的<msix>元素,以及已註冊套件的外部位置;而 manifest 是以 名稱 參照 exe,因此其位置不受 exe 所在位置影響)。 將它們寫入專用、納入原始碼版本控制的資料夾,可避免把它們放進建置輸出目錄(例如bin/);這類目錄在執行 clean/rebuild 時會被清除,也能讓該資料夾不含二進位檔,使後續步驟維持乾淨。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 會建構一個僅含識別資訊.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 的 set<ApplicationManifest>app.manifest</ApplicationManifest>),然後重建,讓元素嵌入 exe 裡。
兩種模式皆從稀疏 appxmanifest.xml中讀取恆等。 當你省略 --manifest 時,winapp 會先在目標旁的 winapp init --exe --sparse 資料夾中尋找(sparse/ 預設會將它寫入該處),接著在目前目錄中尋找;若仍找不到,則回頭改在目標旁與目前目錄中尋找;可傳遞 --manifest 以指定其他位置。
註: EXE 模式會將二進位檔重寫為
mt.exe,這會使任何現有的 Authenticode 簽名失效。 在發佈執行檔前,請重新簽署(例如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會為執行它的帳戶註冊該套件。 延遲Impersonate="no"LocalSystem動作為 ,該動作不會賦予安裝使用者身份(且通常被拒絕)。 對於每台機器的 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的佈建機制。
國家情報局
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 在執行階段拋出 /「沒有套件識別」
- 身份套件沒有被註冊,或者執行檔的 Fusion 清單缺少這個
<msix>元素。 重新執行winapp embed-identity(若使用 XML 模式則重新建置),然後向Add-AppxPackage -ExternalLocation重新註冊。 - exe 中的 必須
<msix packageName>/publisher/applicationId與註冊套件的身份完全吻合。
資產/標誌不出現
- 確保將
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>的 manifest 。 使用winapp init --exe <exe> --sparse產生一個,或傳入 資料夾 作為輸入來建置完整的 MSIX。