使用 Winapp CLI 搭配 C++ 和 CMake

本指南示範如何在 C++ 應用程式中使用 winapp CLI 進行除錯,並以套件身份碼子將應用程式打包為 MSIX。

套件身份是 Windows app 模型中的核心概念。 它讓你的應用程式能存取特定的 Windows API(例如通知、安全、AI API 等)、乾淨安裝/卸載體驗等等。

標準執行檔(例如用 cmake --build建立的)沒有套件身份。 本指南說明如何新增它用於除錯,然後再打包以供發佈。

先決條件

  1. 建置工具:使用 CMake 支援的編譯器工具鏈。 這個例子使用 Visual Studio。 你可以安裝社群版(或如果已經安裝,則更新):

    winget install --id Microsoft.VisualStudio.Community --source winget --override "--add Microsoft.VisualStudio.Workload.NativeDesktop --includeRecommended --passive --wait"
    

    安裝後重新啟動。

  2. CMake:安裝 CMake(若已安裝則更新):

    winget install Kitware.CMake --source winget
    
  3. Winapp CLI:透過 Winget 安裝 winapp CLI(若已安裝則更新):

    winget install Microsoft.winappcli --source winget
    

1. 建立一個新的 C++ 應用程式

先從建立一個簡單的 C++ 應用程式開始。 為你的 project 建立一個新目錄:

mkdir cpp-app
cd cpp-app

建立一個包含基本「Hello, world!」程式的 main.cpp 檔案:

#include <iostream>

int main() {
    std::cout << "Hello, world!" << std::endl;
    return 0;
}

建立 CMakeLists.txt 一個檔案來設定建置:

cmake_minimum_required(VERSION 3.20)
project(cpp-app)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(cpp-app main.cpp)

建立並執行它,確保一切正常運作:

cmake -B build
cmake --build build --config Debug
.\build\Debug\cpp-app.exe

輸出應該是「哈囉,世界!」

2. 更新代碼以驗證身份

我們會更新應用程式,以檢查它是否運行於具有套件識別的狀態。 這將幫助我們在後續步驟中驗證身份是否正確運作。 我們會使用 Windows 執行階段 C++ API 來存取套件 API。

首先,在你的 CMakeLists.txt 末尾加上以下一行,以連結到 Windows 應用程式 模型庫:

# Link Windows Runtime libraries
target_link_libraries(cpp-app PRIVATE WindowsApp.lib OneCoreUap.lib)

接著,將整個 main.cpp 內容替換成以下程式碼。 此程式碼嘗試使用 Windows 執行階段 API 取得目前的套件識別碼。 若成功,則會列印封包族名;否則,會顯示「未包裝」。

#include <iostream>
#include <windows.h>
#include <appmodel.h>

int main() {
    UINT32 length = 0;
    LONG result = GetCurrentPackageFamilyName(&length, nullptr);
    
    if (result == ERROR_INSUFFICIENT_BUFFER) {
        // We have a package identity
        std::wstring familyName;
        familyName.resize(length);
        
        result = GetCurrentPackageFamilyName(&length, familyName.data());
        
        if (result == ERROR_SUCCESS) {
            std::wcout << L"Package Family Name: " << familyName.c_str() << std::endl;
        } else {
            std::wcout << L"Error retrieving Package Family Name" << std::endl;
        }
    } else {
        // No package identity
        std::cout << "Not packaged" << std::endl;
    }

    return 0;
}

3. 無身份運行

現在,重新建置並照常執行應用程式:

cmake --build build --config Debug
.\build\Debug\cpp-app.exe

你應該會看到輸出「未打包」。 這確認了標準執行檔在沒有任何套件身份的情況下執行。

4. 使用 Winapp CLI 初始化 Project

winapp init 指令一次設定你需要的所有東西:應用程式清單、資產,以及可選的 Windows 應用程式 SDK C++ 開發標頭。

執行以下指令並依照提示操作:

winapp init .

出現提示時:

  • 套件名稱:按 Enter 接受預設(cpp-app)
  • Publisher name:按 Enter 接受預設或輸入你的名字
  • 版本:按下 "Enter" 以接受 1.0.0.0
  • 入口點:按 Enter 接受預設(cpp-app.exe)
  • Setup SDKs:選擇「穩定 SDK」下載 Windows 應用程式 SDK 並產生 C++ 標頭

這個命令將會執行以下作業:

  • Create Package.appxmanifest — 定義你應用程式身份的清單
  • 建立 Assets 資料夾 — MSIX 包裝與商店提交所需的圖示
  • 建立一個.winapp資料夾,裡面有Windows 應用程式 SDK標頭和函式庫
  • 建立一個 winapp.yaml 設定檔來鎖定 SDK 版本

你可以開啟 Package.appxmanifest 來進一步自訂屬性,例如顯示名稱、發佈者和功能。

新增執行別名(用於控制台應用程式)

執行別名讓使用者能在任何終端機(例如 cpp-app)以名稱執行你的應用程式。 它在開發期間也會啟用 winapp run --with-alias,使主控台輸出保留在目前的終端機而非開啟新視窗。

你可以自動新增一個:

winapp manifest add-alias

或者手動:打開Package.appxmanifest,如果缺少命名空間,則在<Package>標籤中加入uap5命名空間,然後在<Applications><Application><Extensions>...中加入擴充功能。

<Package
  ...
  xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
+ xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
  IgnorableNamespaces="uap uap2 uap3 rescap desktop desktop6 uap10">

  ...
  <Applications>
    <Application ...>
      ...
+     <Extensions>
+       <uap5:Extension Category="windows.appExecutionAlias">
+         <uap5:AppExecutionAlias>
+           <uap5:ExecutionAlias Alias="cpp-app.exe" />
+         </uap5:AppExecutionAlias>
+       </uap5:Extension>
+     </Extensions>
    </Application>
  </Applications>
</Package>

5. 利用身份驗證除錯

若要測試需要身份識別的功能(例如通知),但不完全打包應用程式,你可以使用 winapp run。 這會註冊一個鬆散的版面套件(就像真正的 MSIX 安裝一樣),然後一步啟動應用程式。 除錯不需要憑證或簽署。

  1. 建立執行檔

    cmake --build build --config Debug
    
  2. 以身份行事

    winapp run .\build\Debug --with-alias
    

這個 --with-alias 旗標會透過執行別名啟動應用程式,因此主控台輸出會留在目前的終端機。 這需要我們在步驟 4 中加入的uap5:ExecutionAlias

小提示

winapp run 同時也會在你的系統上註冊包裹。 這也是為什麼當你在第 8 步嘗試安裝時,MSIX 可能會顯示為「已安裝」。 完成後,請使用 winapp unregister 清理開發套件。

你現在應該會看到類似的輸出:

Package Family Name: cpp-app_12345abcde

這能確認你的應用程式是以有效的套件身份執行!

替代方案:Sparse 封裝識別

如果您特別需要稀疏套件的特性(保持不複製檔案的原貌),您可以改用 create-debug-identity

winapp create-debug-identity .\build\Debug\cpp-app.exe
.\build\Debug\cpp-app.exe

小提示

關於進階除錯工作流程(附加除錯器、IDE 設定、啟動除錯),請參閱 除錯指南

6. 使用 Windows 應用程式 SDK(可選)

如果你選擇在 winapp init 時設定 SDK,現在你就能在 .winapp/include 資料夾裡存取Windows 應用程式 SDK標頭。 這能讓你使用現代 Windows API,如通知、視窗功能、裝置內建 AI 等功能。 如果您只需要發佈時的套件識別,可跳至步驟 7。

我們再舉一個簡單的範例,列印 Windows 應用程式 執行時版本。

更新 CMakeLists.txt

在你的 CMakeLists.txt 末尾加上以下一行,包含 Windows 應用程式 SDK 標頭:

# Add Windows App SDK include directory
target_include_directories(cpp-app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/.winapp/include)

更新main.cpp

main.cpp 的整個內容替換為使用 Windows 應用程式 Runtime API:

#include <iostream>
#include <windows.h>
#include <appmodel.h>
#include <winrt/Microsoft.Windows.ApplicationModel.WindowsAppRuntime.h>

int main() {
    // Initialize WinRT
    winrt::init_apartment();
    
    UINT32 length = 0;
    LONG result = GetCurrentPackageFamilyName(&length, nullptr);
    
    if (result == ERROR_INSUFFICIENT_BUFFER) {
        // We have a package identity
        std::wstring familyName;
        familyName.resize(length);
        
        result = GetCurrentPackageFamilyName(&length, familyName.data());
        
        if (result == ERROR_SUCCESS) {
            std::wcout << L"Package Family Name: " << familyName.c_str() << std::endl;
            
            // Get Windows App Runtime version using the API
            auto runtimeVersion = winrt::Microsoft::Windows::ApplicationModel::WindowsAppRuntime::RuntimeInfo::AsString();
            std::wcout << L"Windows App Runtime Version: " << runtimeVersion.c_str() << std::endl;
        } else {
            std::wcout << L"Error retrieving Package Family Name" << std::endl;
        }
    } else {
        std::cout << "Not packaged" << std::endl;
    }
    
    return 0;
}

建置並執行

用 Windows 應用程式 SDK 標頭重建應用程式:

cmake --build build --config Debug
winapp run .\build\Debug --with-alias

你現在應該會看到輸出如下:

Package Family Name: cpp-app_12345abcde
Windows App Runtime Version: 1.8-stable (1.8.0)

.winapp/include 目錄包含所有必要的 Windows 應用程式 SDK 標頭,包括:

  • winrt/ - 用於存取Windows 執行階段 API 的 WinRT C++ 投影標頭
  • Microsoft.UI.*.h - 用於現代 UI 元件的 WinUI 3 標頭
  • MddBootstrap.h - Windows 應用程式 SDK 引導初始化
  • WindowsAppSDK-VersionInfo.h - 版本資訊
  • 以及更多 Windows 應用程式 SDK 元件

想了解更進階的Windows 應用程式 SDK使用,請參考 Windows 應用程式 SDK 文件

7. 必要時還原標頭

.winapp 資料夾會由winapp init 自動加入.gitignore ,因此不會被納入版本控制。 當其他人複製你的專案時,他們需要先還原這些檔案才能建構。

手動設定

複製庫後執行以下兩個指令:

# Restore Windows App SDK headers
winapp restore

# Generate development certificate (optional - only if planning to package the app and sideload)
winapp cert generate --if-exists skip

然後,你可以正常地使用cmake -B buildcmake --build build --config Debug來建立並執行。

使用 CMake 的自動設定

或者,你也可以透過在CMakeLists.txt中加入設定邏輯來自動化此過程。 以下是完整的 CMakeLists.txt,提供自動化、正確連結和最少使用的 C++20 標準:

cmake_minimum_required(VERSION 3.20)
project(cpp-app)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Download winapp CLI if not available in PATH
find_program(WINAPP_CLI winapp)
if(NOT WINAPP_CLI)
    set(WINAPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.winapp-tools")
    set(WINAPP_CLI "${WINAPP_DIR}/winapp.exe")
    
    if(NOT EXISTS "${WINAPP_CLI}")
        message(STATUS "Downloading winapp CLI...")
        
        # Determine architecture
        if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64")
            set(WINAPP_ARCH "arm64")
        else()
            set(WINAPP_ARCH "x64")
        endif()
        
        # Download and extract
        set(WINAPP_ZIP "${CMAKE_CURRENT_BINARY_DIR}/winappcli.zip")
        file(DOWNLOAD 
            "https://github.com/microsoft/WinAppCli/releases/latest/download/winappcli-${WINAPP_ARCH}.zip"
            "${WINAPP_ZIP}"
            SHOW_PROGRESS
        )
        
        file(ARCHIVE_EXTRACT INPUT "${WINAPP_ZIP}" DESTINATION "${WINAPP_DIR}")
        file(REMOVE "${WINAPP_ZIP}")
        message(STATUS "winapp CLI downloaded to ${WINAPP_DIR}")
    endif()
endif()

# Automatically restore Windows App SDK headers and generate certificate if needed
# This runs once during CMake configuration, not on every build
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.winapp/include")
    message(STATUS "Restoring Windows App SDK headers...")
    execute_process(
        COMMAND "${WINAPP_CLI}" restore
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        RESULT_VARIABLE RESTORE_RESULT
    )
    if(NOT RESTORE_RESULT EQUAL 0)
        message(WARNING "Failed to restore Windows App SDK. Run 'winapp restore' manually.")
    endif()
endif()

if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/devcert.pfx")
    message(STATUS "Generating development certificate...")
    execute_process(
        COMMAND "${WINAPP_CLI}" cert generate --if-exists skip
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        RESULT_VARIABLE CERT_RESULT
    )
    if(NOT CERT_RESULT EQUAL 0)
        message(WARNING "Failed to generate certificate. Run 'winapp cert generate' manually.")
    endif()
endif()

add_executable(cpp-app main.cpp)

# Link Windows Runtime libraries
target_link_libraries(cpp-app PRIVATE WindowsApp.lib OneCoreUap.lib)

# Add Windows App SDK include directory
target_include_directories(cpp-app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/.winapp/include)

用這個配置:

  • 當有人複製倉庫並執行 cmake -B build時,如果在 PATH 中未找到 winapp,則會自動下載 winapp。
  • Windows 應用程式 SDK 標頭和憑證會自動還原
  • 這些指令在設定時只會執行一次(不是每次建置都執行),因為會檢查檔案是否已經存在
  • 如果指令失敗,CMake 會顯示警告並指示手動執行
  • 下載的應用程式將儲存在 .winapp-tools/(如有需要,可以將這個加入 .gitignore

8. MSIX 封裝

當你準備好發佈應用程式時,可以用相同的清單打包成 MSIX。 MSIX 提供乾淨安裝/卸載、自動更新及可信賴的安裝體驗。

準備套件目錄

首先,將應用程式建置在發佈模式以達到最佳效能:

cmake --build build --config Release

接著,建立一個只包含分發所需檔案的目錄,並複製你的發佈執行檔:

mkdir dist
copy .\build\Release\cpp-app.exe .\dist\

產生開發證書

MSIX 套件必須簽署。 本地測試時,產生自簽開發憑證:

winapp cert generate --if-exists skip

小提示

證書的發行者必須與您 Publisher 中的 Package.appxmanifest 相符。 cert generate指令會自動從你的清單讀取這些資訊。

簽名與包裝

現在你可以打包並簽名:

# package and sign the app with the generated certificate
winapp pack .\dist --cert .\devcert.pfx 

小提示

pack 指令會自動從你目前的目錄中使用 Package.appxmanifest,並在打包前將其複製到目標資料夾。 產生 .msix 的檔案會放在目前的目錄中。

安裝憑證

在安裝 MSIX 套件之前,你需要先信任你機器上的開發憑證。 以管理員身份執行這個指令(你只需要在每個憑證上執行一次):

winapp cert install .\devcert.pfx

安裝並執行

小提示

如果你在步驟5使用 winapp run 過,包裹可能已經在你的系統上註冊了。 使用 winapp unregister 先移除開發註冊,然後再安裝正式發行套件。

這個 winapp pack 指令會在你的專案根目錄中產生 MSIX 檔案。 請雙擊產生 .msix 的檔案,或使用 PowerShell 安裝套件:

Add-AppxPackage .\cpp-app_1.0.0.0_x64.msix

小提示

MSIX 檔名包含版本與架構(例如 cpp-app_1.0.0.0_arm64.msix)。 請檢查你的目錄,確認確切的檔名。

現在你可以在終端機的任何地方輸入以下關鍵字來執行應用程式:

cpp-app

你應該會看到「Package Family Name」的輸出,確認它已安裝並以身份執行。

小提示

如果你需要重新打包應用程式(例如程式碼變更後),在再次執行winapp pack前,先在你的Package.appxmanifest中增加Version。 Windows 需要更高的版本號才能更新已安裝的套件。

Tips

  1. 一旦準備好分發,你可以用憑證授權中心的程式碼簽署憑證來簽署 MSIX,讓使用者不必安裝自簽憑證。
  2. Azure 信任簽署 服務是安全管理憑證並將簽約整合進 CI/CD 管線的絕佳方式。
  3. Microsoft Store 會幫你簽署 MSIX,提交前不需要簽名。
  4. 你可能需要建立多個 MSIX 套件,分別針對你支援的架構(x64、Arm64)。 請用適當的產生器與架構旗標配置 CMake。

後續步驟