稀疏封包:賦予未封裝應用程式身份

關於端對端的有效範例(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>()會比較簡單。

先決條件

  1. Windows 10,版本 2004(版本 19041)或更新版本。 稀疏套件依賴 uap10:AllowExternalContent,其需要 19041 以上版本。
  2. WinApp CLI — 透過 Winget 安裝(若已安裝則更新):
    winget install Microsoft.WinApp --source winget
    
  3. 目標機器上受信任的程式碼簽署憑證。 若要在本機進行測試,請使用 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 packwinapp 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

因為資訊清單宣告了 AllowExternalContentwinapp 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;

如需完整且可運作的,請參考 setup.iss 範例。

以下的 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 &quot;[INSTALLFOLDER]register-sparse.ps1&quot; -MsixPath &quot;[INSTALLFOLDER]MyApp.identity.msix&quot; -ExternalLocation &quot;[INSTALLFOLDER]&quot; -PackageName &quot;MyPackageIdentityName&quot;" />

<!-- 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。

另請參閱