你拿到一個函式庫,第一個問題是什麼?
「要怎麼用啊?」
對吧?標頭檔在哪?要連結哪個 .a/.lib?要不要設定什麼環境變數?要不要連某個系統函式庫?
在套件管理器的生態裡,這些問題也同樣需要回答。這些資訊都要在依賴樹,裡忠實的往下傳達給消費端套件,套件才能被正確無痛的使用。
這些資訊都寫在哪裡?答案是寫在 Conan recipe 裡的 package_info() 方法。
以下就是今天的目標:
package_info() 在做什麼libs、system_libs、defines

測試環境:Debian 13、gcc 14.2、CMake 3.31、Conan 2.33.0。
D15 的 conanfile.py 的最後一個方法長這樣:
def package_info(self):
self.cpp_info.libs = ["super-lib"]
package_info() 肩負一個重要任務,就是擔任套件的說明書,告訴大家「這包函式庫要怎麼用」。
消費端的 CMakeDeps 會讀這份說明書,轉譯內容為對應的 CMake 設定。
例如這是 conan install 之後產生的 super-lib-release-x86_64-data.cmake 內容:
set(super-lib_INCLUDE_DIRS_RELEASE "${super-lib_PACKAGE_FOLDER_RELEASE}/include")
...
set(super-lib_LIB_DIRS_RELEASE "${super-lib_PACKAGE_FOLDER_RELEASE}/lib")
set(super-lib_BIN_DIRS_RELEASE )
...
set(super-lib_LIBS_RELEASE super-lib)
這些一個又一個的 CMake 變數,就是 Conan 轉譯 package_info() 的結果給消費端使用的。
以下就來說明書的幾個常用的欄位。
我們繼續延續 super-lib 超好用函式庫的情境,假設這個 super-lib 是靜態函式庫、編譯時需要使用系統上的 zlib,使用端需要定義 SUPER_LIB_STATIC,而且我們想沿用舊專案的 CMake 名稱:
以下一份標準的 package_info() 說明書寫法:
def package_info(self):
self.cpp_info.includedirs = ["include"]
self.cpp_info.libdirs = ["lib"]
self.cpp_info.bindirs = ["bin"]
self.cpp_info.libs = ["super-lib"]
self.cpp_info.system_libs = ["z"]
self.cpp_info.defines = ["SUPER_LIB_STATIC"]
self.cpp_info.set_property("cmake_file_name", "SuperLib")
self.cpp_info.set_property("cmake_target_name", "SuperLib::SuperLib")
self.runenv_info.define("SUPER_LIB_LOG_DIR", "/tmp/super-lib-logs")
怎麼這麼多欄位?別擔心,不是每個套件都需要照抄這份固定清單。實際要填哪些欄位,還是要看函式庫本身的需求。接下來我們一項一項拆開來看吧。
標頭檔、函式庫和執行檔分別放在哪個目錄?
self.cpp_info.includedirs = ["include"]
self.cpp_info.libdirs = ["lib"]
self.cpp_info.bindirs = ["bin"]
includedirs、libdirs、bindirs 這三個欄位可以指定目錄。
上面分別把這森個欄位指向 include、lib、bin 三個路徑。
Conan 預設就會使用 include、lib、bin,所以如果你就是採用這些慣例目錄結構,這三個欄位也可以省略不寫。
libs 是每個套件都必寫的欄位(幾乎啦)。
self.cpp_info.libs = ["super-lib"]
注意只寫名字就好,不用寫前綴和副檔名。Conan 會依平台自動轉換,比如 Linux 上會變成 libsuper-lib.a,Windows 上會變成 super-lib.lib。
假設 super-lib 需要用到系統上的 zlib,那該怎麼讓編譯器知道呢?
if self.settings.os == "Linux":
self.cpp_info.system_libs = ["z"]
CMakeLists.txt 也記得連結 z:
target_link_libraries(super-lib PRIVATE z)
如果明明使用了系統函式庫,但是又忘了寫,那編譯 test_package 的時候就會爆出錯誤。
Linux 上最常見的系統函式庫有 pthread、m、dl 等等。
macOS 上對應的是 frameworks,可以這樣寫:
if self.settings.os == "Macos":
self.cpp_info.frameworks = ["CoreFoundation"]
有些函式庫的標頭檔,要求使用者編譯時定義某個巨集。最常見的是 Windows 上的靜態庫,像 XXX_STATIC 這種,沒定義的話標頭檔會以為你用的是 DLL。
我們用一個簡單的例子:
self.cpp_info.defines = ["SUPER_LIB_STATIC"]
產生的 CMake Toolchain 設定檔裡就會多出一行:
set(super-lib_COMPILE_DEFINITIONS_RELEASE "SUPER_LIB_STATIC")
同理還有 cxxflags、cflags、sharedlinkflags、exelinkflags 等等可以塞額外的編譯和連結參數,不過用到的機會比較少。
如果你們公司的老舊專案已經用了不同的 CMake Target 名字。跟 Conan 預設產生小寫加連字號 super-lib::super-lib 不一樣怎麼辦呢,一定要讓舊專案配合改名字嗎?不用喔。
可以用 set_property() 來改名字:
self.cpp_info.set_property("cmake_file_name", "SuperLib")
self.cpp_info.set_property("cmake_target_name", "SuperLib::SuperLib")
cmake_file_name:find_package() 要找的名字cmake_target_name:target 的名字改完之後,記得把 test_package/CMakeLists.txt 裡面換成新名字:
find_package(SuperLib REQUIRED)
add_executable(example src/example.cpp)
target_link_libraries(example PRIVATE SuperLib::SuperLib)
之後產生的檔案也都會變成 SuperLibConfig.cmake、SuperLibTargets.cmake 了。公司舊專案就可以直接無痛使用了。
Conan 官方的 OpenSSL 套件包也是類似做法,把 target 改寫成符合外界大眾早已習慣使用多年的 OpenSSL::SSL 和 OpenSSL::Crypto。
除了編譯連結,有時候套件還要設定環境變數。
runenv_info 屬性是執行時用的,buildenv_info 屬性是編譯時用的。
self.runenv_info.define("SUPER_LIB_LOG_DIR", "/tmp/super-lib-logs")
使用端產生的 conanrunenv-release-x86_64.sh 裡就會出現:
export SUPER_LIB_LOG_DIR="/tmp/super-lib-logs"
最常見的用途是把套件的 bin目錄 加進 PATH,讓 Windows 執行時找得到 DLL。
如果一包套件裡面其實有好幾個函式庫,就像 OpenSSL 有 ssl 和 crypto,這時候可以寫 components 來表達這種結構。
最後我們用官方的 openssl conanfile.py作為範例結尾。
package_info() 方法節錄如下:
def package_info(self):
self.cpp_info.set_property("cmake_file_name", "OpenSSL")
self.cpp_info.components["ssl"].libs = ["ssl"]
self.cpp_info.components["crypto"].libs = ["crypto"]
self.cpp_info.components["ssl"].requires = ["crypto"]
if self.settings.os == "Windows":
self.cpp_info.components["crypto"].system_libs.extend(["crypt32", "ws2_32", "advapi32", "user32", "bcrypt"])
elif self.settings.os == "Linux":
self.cpp_info.components["crypto"].system_libs.extend(["dl", "rt"])
self.cpp_info.components["ssl"].system_libs.append("dl")
self.cpp_info.components["crypto"].set_property("cmake_target_name", "OpenSSL::Crypto")
self.cpp_info.components["ssl"].set_property("cmake_target_name", "OpenSSL::SSL")
今天講的東西,幾乎全部都用上了:
components["ssl"] 和 components["crypto"]
requires = ["crypto"] 設定模組間的相依關係。system_libs,Windows 和 Linux 要連的系統函式庫不一樣。set_property 重新命名 CMake Target: OpenSSL::SSL , OpenSSL::Crypto,符合外界流傳已久的習慣舊名。