連續講了 Conan 好幾天,今天我們要切換到 vcpkg 囉。
回到同一個假設情境,你為公司開發了一個內部小函式庫,取名叫做 Super Library,你覺得這東西真是太有用了,很多專案一定都會用到。
剛好公司內部用 vcpkg package manager,所以你想要把函式庫打包成 vcpkg 套件。
該怎麼做呢?
短答案:寫一個 port。
比較長的答案:vcpkg port 是一份指導 vcpkg 怎麼把函式庫包裝成套件的食譜(操作手冊)。
今天的目標,就是幫 super-lib 寫一份 vcpkg port,然後在另一個專案裡使用這個套件。
今天的流程裡,我們會輪流扮演三個角色:

在公司裡面,這三個角色可能都是你自己。但分開來比較容易看懂每個步驟在做什麼。
以下就是我們今天的目錄結構,三個目錄對應三個角色
函式庫作者工作區: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 之後,要怎麼在自己的專案裡用它?關鍵就在一個 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 專案比,多了三個東西,都是為了正確產生說明書:
include/。安裝後,它會被複製到安裝目錄的 include/裡面。所以要寫兩個:$<BUILD_INTERFACE:...> 是編譯時的位置,$<INSTALL_INTERFACE:...> 是安裝後的位置,CMake 會自己挑一個。EXPORT super-lib-targets:跟 CMake 說「這個 target 要寫進說明書」。install(EXPORT ...):把說明書真的寫出來,也就是 super-lib-config.cmake。使用者呼叫 find_package(super-lib) 讀的就是它。這樣函式庫本身就準備好了。
接下來我們轉換身分成打包者,請問到底要怎麼把 super-lib 打包成套件呢?
前面我們提過,打包套件需要撰寫一份食譜 (port),那麼該怎麼寫 vcpkg port 呢?
一份 port 通常包含兩個檔案:
vcpkg.json 套件的主要描述檔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.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):強制建立靜態庫
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-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。團隊共用的話,就要用 registry 比較適合。
最後提醒一個坑:改了 super-lib 的原始碼之後重新編譯,vcpkg 可能會拿到舊版。
因為 port 沒變的話,vcpkg 安裝會直接從 binary cache 拿上次編好的結果。這時需要把快取資料夾刪掉再重跑。
Linux/macOS 的位置是 ~/.cache/vcpkg/archives,Windows 在 %LOCALAPPDATA%\vcpkg\archives