iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Software Development

C++ 套件管理器求生指南 Conan vs Vcpkg系列 第 20 篇

D20:把自己的函式庫打包成 vcpkg port

  • 分享至 

  • xImage
  •  

連續講了 Conan 好幾天,今天我們要切換到 vcpkg 囉。

回到同一個假設情境,你為公司開發了一個內部小函式庫,取名叫做 Super Library,你覺得這東西真是太有用了,很多專案一定都會用到。

剛好公司內部用 vcpkg package manager,所以你想要把函式庫打包成 vcpkg 套件。

該怎麼做呢?

短答案:寫一個 port。

比較長的答案:vcpkg port 是一份指導 vcpkg 怎麼把函式庫包裝成套件的食譜(操作手冊)。

今天的目標,就是幫 super-lib 寫一份 vcpkg port,然後在另一個專案裡使用這個套件。

三個角色

今天的流程裡,我們會輪流扮演三個角色:

  1. 函式庫作者:寫程式碼給大家用的人。
  2. 打包者:告訴 vcpkg:原始碼在哪裡、怎麼建置、建好之後怎麼整理
  3. 消費端應用程式:對著 vcpkg 大喊 「我要安裝 super-lib!」

在公司裡面,這三個角色可能都是你自己。但分開來比較容易看懂每個步驟在做什麼。

以下就是我們今天的目錄結構,三個目錄對應三個角色

函式庫作者工作區:super-lib/
打包者工作區:my-ports/super-lib/
消費端應用程式:super-app/

workspace/
├── super-lib/             # 函式庫原始碼
│   ├── CMakeLists.txt
│   ├── LICENSE
│   ├── include/
│   └── src/
├── my-ports/              # vcpkg port 打包食譜
│   └── super-lib/
│       ├── vcpkg.json
│       └── portfile.cmake
└── super-app/             # 消費端應用程式

步驟一:超好用函式庫

super-lib 超好用函式庫非常好用,但是結構上來說,它就是一個普通到不行的 CMake Project。(你可以把它代入想像成任何一個你現在正在維護的函式庫專案。)

範例中用的原始碼我們沿用之前的 D15 範例。
D15:自己打包第一個 Conan 套件

super-lib/
├── CMakeLists.txt
├── LICENSE
├── include/
│   └── super_lib.h
└── src/
    └── super_lib.cpp

步驟二:產生函式庫的「使用說明書」super-lib-config.cmake

使用者裝好 super-lib 之後,要怎麼在自己的專案裡用它?關鍵就在一個 CMake config 檔案:super-lib-config.cmake。

你可以把它想成一份使用說明書。標頭檔在哪?.a/.lib 在哪?要不要連系統函式庫?這些都寫在裡面。使用者在 CMake 裡寫 find_package(super-lib CONFIG),CMake 就會去讀這份說明書。

我們現在就要想辦法來產生函式庫的使用說明書「super-lib-config.cmake」

咦,vcpkg 不會幫我們產生這份說明書嗎?

答案是「不會」。

vcpkg 整個生態系,倚靠的是 CMake 的現成機制。打包的時候,vcpkg 只是幫你呼叫 cmake --build 和 cmake --install,CMake 裝出什麼就是什麼。

所以這份說明書,函式庫得自己準備。好消息是你也不用手刻說明書,只要在 CMakeLists.txt 裡寫好 install(),CMake 就會幫我們產生。

下面是 super-lib 自己的 CMakeLists.txt 以及 install 的寫法:

cmake_minimum_required(VERSION 3.23)
project(super-lib VERSION 0.1.0 LANGUAGES CXX)

add_library(super-lib src/super_lib.cpp)
target_include_directories(super-lib
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include>
)

install(TARGETS super-lib
    EXPORT super-lib-targets
    ARCHIVE DESTINATION lib
    LIBRARY DESTINATION lib
    RUNTIME DESTINATION bin
)
install(FILES include/super_lib.h DESTINATION include)
install(EXPORT super-lib-targets
    FILE super-lib-config.cmake
    NAMESPACE super-lib::
    DESTINATION lib/cmake/super-lib
)

跟一般 CMake 專案比,多了三個東西,都是為了正確產生說明書:

  1. 兩個標頭檔位置。編譯套件時,標頭檔會放在原始碼目錄的 include/。安裝後,它會被複製到安裝目錄的 include/裡面。所以要寫兩個:$<BUILD_INTERFACE:...> 是編譯時的位置,$<INSTALL_INTERFACE:...> 是安裝後的位置,CMake 會自己挑一個。
  2. EXPORT super-lib-targets:跟 CMake 說「這個 target 要寫進說明書」。
  3. install(EXPORT ...):把說明書真的寫出來,也就是 super-lib-config.cmake。使用者呼叫 find_package(super-lib) 讀的就是它。

這樣函式庫本身就準備好了。

步驟三:建立 port 的 vcpkg.json

接下來我們轉換身分成打包者,請問到底要怎麼把 super-lib 打包成套件呢?

前面我們提過,打包套件需要撰寫一份食譜 (port),那麼該怎麼寫 vcpkg port 呢?

一份 port 通常包含兩個檔案:

  1. vcpkg.json 套件的主要描述檔
  2. portfile.cmake 取得來源、建置、安裝的細節

注意喔,這裡的 vcpkg.json 跟消費端使用同樣的檔名,但是兩者用途完全不同。這個是「生產端」的 port,說明套件自己的名字、版本號碼、建置時要用哪些工具等等。

這就是我們寫的第一份 port,檔案路徑在 my-ports/super-lib/vcpkg.json:

{
  "name": "super-lib",
  "version": "0.1.0",
  "dependencies": [
    { "name": "vcpkg-cmake", "host": true },
    { "name": "vcpkg-cmake-config", "host": true }
  ]
}

name 裡面寫 port 名稱,必須和資料夾名稱 my-ports/super-lib 完全一致,也寫了版本號碼。

dependencies 裡那兩個是 vcpkg 提供的 CMake 腳本,等等的 portfile.cmake 會需要。

最後標示成 host: true 表示這是建置用的工具依賴,不需要傳遞給下游消費端。比喻來說,這是做菜用的菜刀,最後不用端上桌給客人。

這樣 vcpkg.json 就寫好了。

步驟四:寫 portfile,告訴 vcpkg 怎麼建置

portfile.cmake 是 port 的建置腳本,告訴 vcpkg 去哪裡下載原始碼、怎麼編譯專案,以及要把什麼裝出來。

這就是我們的範例 my-ports/super-lib/portfile.cmake:

get_filename_component(SOURCE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../super-lib" ABSOLUTE)

vcpkg_check_linkage(ONLY_STATIC_LIBRARY)

vcpkg_cmake_configure(
    SOURCE_PATH "${SOURCE_PATH}"
)

vcpkg_cmake_install()

vcpkg_cmake_config_fixup(
    PACKAGE_NAME super-lib
    CONFIG_PATH lib/cmake/super-lib
)

file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include")
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE")

我們簡要的一行一行講解:

  • 設定 SOURCE_PATH:說函式庫原始碼在哪
    • 當前目錄往上兩層再進 super-lib/。(這是暫時寫法,通常正式寫法會用 Git URL)
  • 呼叫 vcpkg_check_linkage(ONLY_STATIC_LIBRARY):強制建立靜態庫
    • Windows Triplet 預設編譯成 DLL,但是一般函式庫沒特別改的話根本就不能當DLL用,所以強制建成靜態庫,省心省力
  • 呼叫 vcpkg_cmake_configure():跑 CMake configure,Debug & Release 各一次
  • 呼叫 vcpkg_cmake_install():跑 CMake build、install,Debug & Release 各一次
  • 呼叫 vcpkg_cmake_config_fixup:把兩套 Debug/Release 套件合併、修正路徑,搬到 vcpkg 規定的 share/super-lib/
    • CONFIG_PATH lib/cmake/super-lib 這行指出 config.cmake 的位置
  • file(REMOVE_RECURSE ...):清理 debug 多餘的標頭檔
  • 呼叫 vcpkg_install_copyright:規定每個套件都要附上授權,簡單放一個就好

通常正式上架的 port 會從 GitHub Repo 抓取原始碼,而非從本地編譯,還可以設定 tag 以及填寫 SHA512 驗證下載正確性。這個專案只是示範用途,所以寫死本機路徑。

到此為止,兩個用於打包套件的檔案,都準備好了。

步驟五:安裝並使用 super-lib

接下來我們切換角色,變成消費套件的應用程式。

請進入 super-app/ 應用程式專案,然後在 vcpkg.json 裡面宣告依賴:

{
  "dependencies": [
    "super-lib"
  ]
}

進入 CMakeLists.txt 裡加入 super-lib 相關的設定:

find_package(super-lib CONFIG REQUIRED)
target_link_libraries(super-app PRIVATE super-lib::super-lib)

然後我們就可以來編譯專案囉!

關鍵就是在 cmake 指令中把我們的 port 路徑傳進去:

Bash (macOS/Linux):

cmake -S . -B build \
  -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" \
  -DVCPKG_OVERLAY_PORTS="$PWD/../my-ports"

PowerShell (Windows):

cmake -S . -B build `
  -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" `
  -DVCPKG_OVERLAY_PORTS="$PWD/../my-ports"

我們傳入了兩個變數:

  • CMAKE_TOOLCHAIN_FILE:如之前所述,讓 CMake 透過 vcpkg 安裝並找到套件。
  • VCPKG_OVERLAY_PORTS:告訴 vcpkg「除了內建的套件清單,也看看這個資料夾,而且優先看這裡」。

接著應用程式應該就可以找到 super-lib 函式庫了。

用一般的 CMake 步驟去建置並編譯專案就行囉!

Bash:

cmake --build build

PowerShell:

cmake --build build --config Release

這個作法叫做 overlay port

像我們今天的這個作法叫做 overlay port。

overlay 就是一個資料夾,適合本機上測試與佈署自己的 port。團隊共用的話,就要用 registry 比較適合。

避免拿到過時套件

最後提醒一個坑:改了 super-lib 的原始碼之後重新編譯,vcpkg 可能會拿到舊版。

因為 port 沒變的話,vcpkg 安裝會直接從 binary cache 拿上次編好的結果。這時需要把快取資料夾刪掉再重跑。

Linux/macOS 的位置是 ~/.cache/vcpkg/archives,Windows 在 %LOCALAPPDATA%\vcpkg\archives


上一篇
D19:Conan 本機快取與 Remote:套件怎麼分享給同事?
下一篇
D21:vcpkg features 客製化套件
系列文
C++ 套件管理器求生指南 Conan vs Vcpkg 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言