尋找套件

許多軟體專案提供工具和函式庫,作為其他專案和應用程式的建構基礎。依賴外部套件的 CMake 專案使用 find_package 命令來定位其相依性。一個典型的調用形式如下:

find_package(<Package> [version])

其中 <Package> 是要尋找的套件名稱,而 [version] 是一個可選的版本請求(格式為 major[.minor.[patch]])。該命令對套件的概念與 CPack 的概念不同,CPack 是用於建立原始碼和二進位發行版和安裝程式。

該命令以兩種模式運作:Module 模式和 Config 模式。在 Module 模式下,該命令搜尋一個 find 模組:一個名為 Find<Package>.cmake 的檔案。它首先在 CMAKE_MODULE_PATH 中查找,然後在 CMake 安裝目錄中查找。如果找到 find 模組,則會載入該模組以搜尋套件的各個元件。Find 模組包含套件特定的函式庫和其他預期找到的檔案資訊,並且在內部使用諸如 find_library 之類的命令來定位它們。CMake 為許多常見套件提供 find 模組;請參閱 cmake-modules(7) 手冊。

find_package 的 Config 模式透過與要尋找的套件合作提供了一個強大的替代方案。在未能找到 find 模組或在呼叫者明確請求時,它會進入此模式。在 Config 模式下,該命令會搜尋 套件 組態 檔案:一個名為 <Package>Config.cmake<package>-config.cmake 的檔案,該檔案由要尋找的套件提供。給定套件的名稱,find_package 命令知道如何在安裝前綴的深處搜尋如下位置:

<prefix>/lib/<package>/<package>-config.cmake

(請參閱 find_package 命令的文件以取得完整的位置清單)。CMake 會建立一個名為 <Package>_DIR 的快取項目,以儲存找到的位置或允許使用者設定該位置。由於套件組態檔案隨其套件的安裝一起提供,因此它確切知道在哪裡找到安裝所提供的所有內容。一旦 find_package 命令找到該檔案,它會提供套件元件的位置,而無需進行額外的搜尋。

[version] 選項要求 find_package 定位特定版本的套件。在 Module 模式下,該命令會將請求傳遞給 find 模組。在 Config 模式下,該命令會在每個候選套件組態檔案旁邊尋找一個 套件 版本 檔案:一個名為 <Package>ConfigVersion.cmake<package>-config-<version>.cmake 的檔案。載入版本檔案以測試套件版本是否與請求的版本可接受匹配(請參閱 find_package 的文件以取得版本檔案 API 規格)。如果版本檔案聲稱相容,則會接受組態檔案,否則會忽略。這種方法允許每個專案定義自己的版本相容性規則。

內建 Find 模組

CMake 有許多預定義的模組,可以在 CMake 的 Modules 子目錄中找到。這些模組可以找到許多常見的軟體套件。有關詳細清單,請參閱 cmake-modules(7) 手冊。

每個 Find<XX>.cmake 模組都定義了一組變數,這些變數將允許專案在使用該軟體套件後使用它。這些變數都以找到的軟體名稱 開頭。使用 CMake,我們嘗試建立這些變數的命名慣例,但您應該閱讀模組頂部的註解以獲得更明確的答案。以下變數在需要時按慣例使用

<XX>_INCLUDE_DIRS

在哪裡可以找到套件的標頭檔,通常是 <XX>.h 等。

<XX>_LIBRARIES

要連結以使用 <XX> 的函式庫。其中包括完整路徑。

<XX>_DEFINITIONS

在編譯使用 <XX> 的程式碼時要使用的預處理器定義。

<XX>_EXECUTABLE

在哪裡可以找到作為套件一部分的 <XX> 工具。

<XX>_<YY>_EXECUTABLE

在哪裡可以找到 <XX> 附帶的 <YY> 工具。

<XX>_ROOT_DIR

在哪裡可以找到 <XX> 安裝的基本目錄。這對於想要參考相對於通用基礎(或根)目錄的許多檔案的大型套件非常有用。

<XX>_VERSION_<YY>

如果為 true,則已找到套件的版本 <YY>。find 模組的作者應確保其中最多只有一個為 true。例如 TCL_VERSION_84。

<XX>_<YY>_FOUND

如果為 false,則表示 <XX> 套件的可選 <YY> 部分不可用。

<XX>_FOUND

如果我們尚未找到或不想使用 <XX>,則設定為 false 或未定義。

並非所有變數都存在於每個 FindXX.cmake 檔案 中。但是,在大多數情況下都應該存在 <XX>_FOUND。如果 <XX> 是函式庫,則也應定義 <XX>_LIBRARIES,並且通常應定義 <XX>_INCLUDE_DIR

可以使用 include 命令或 find_package 命令將模組包含在專案中。

find_package(OpenGL)

等效於

include(${CMAKE_ROOT}/Modules/FindOpenGL.cmake)

include(FindOpenGL)

如果專案轉換為 CMake 作為其建置系統,如果套件提供 <XX>Config.cmake 檔案,則 find_package 仍然可以工作。如何在稍後在本章中介紹如何建立 CMake 套件。

建立 CMake 套件組態檔案

專案必須提供套件組態檔案,以便外部應用程式可以找到它們。考慮一個簡單的專案「Gromit」,它提供一個可執行檔來產生原始碼和一個生成的程式碼必須連結到的函式庫。CMakeLists.txt 檔案可能以以下內容開頭:

cmake_minimum_required(VERSION 3.20)
project(Gromit C)
set(version 1.0)

# Create library and executable.
add_library(gromit STATIC gromit.c gromit.h)
add_executable(gromit-gen gromit-gen.c)

為了安裝 Gromit 並匯出其目標以供外部專案使用,請新增以下程式碼:

# Install and export the targets.
install(FILES gromit.h DESTINATION include/gromit-${version})
install(TARGETS gromit gromit-gen
        DESTINATION lib/gromit-${version}
        EXPORT gromit-targets)
install(EXPORT gromit-targets
        DESTINATION lib/gromit-${version})

最後,Gromit 必須在其安裝樹中提供套件組態檔案,以便外部專案可以使用 find_package 定位它

# Create and install package configuration and version files.
configure_file(
   ${Gromit_SOURCE_DIR}/pkg/gromit-config.cmake.in
   ${Gromit_BINARY_DIR}/pkg/gromit-config.cmake @ONLY)

configure_file(
   ${Gromit_SOURCE_DIR}/gromit-config-version.cmake.in
   ${Gromit_BINARY_DIR}/gromit-config-version.cmake @ONLY)

install(FILES ${Gromit_BINARY_DIR}/pkg/gromit-config.cmake
         ${Gromit_BINARY_DIR}/gromit-config-version.cmake
         DESTINATION lib/gromit-${version})

此程式碼會組態和安裝套件組態檔案和對應的套件版本檔案。套件組態輸入檔案 gromit-config.cmake.in 具有以下程式碼:

# Compute installation prefix relative to this file.
get_filename_component(_dir "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(_prefix "${_dir}/../.." ABSOLUTE)

# Import the targets.
include("${_prefix}/lib/gromit-@version@/gromit-targets.cmake")

# Report other information.
set(gromit_INCLUDE_DIRS "${_prefix}/include/gromit-@version@")

安裝後,組態的套件組態檔案 gromit-config.cmake 會知道相對於自身的其他已安裝檔案的位置。對應的套件版本檔案是從其輸入檔案 gromit-config-version.cmake.in 組態的,該檔案包含如下程式碼:

set(PACKAGE_VERSION "@version@")
if(NOT "${PACKAGE_FIND_VERSION}" VERSION_GREATER "@version@")
  set(PACKAGE_VERSION_COMPATIBLE 1) # compatible with older
  if("${PACKAGE_FIND_VERSION}" VERSION_EQUAL "@version@")
    set(PACKAGE_VERSION_EXACT 1) # exact match for this version
  endif()
endif()

使用 Gromit 套件的應用程式可能會建立如下所示的 CMake 檔案:

cmake_minimum_required(VERSION 3.20)
project(MyProject C)

find_package(gromit 1.0 REQUIRED)
include_directories(${gromit_INCLUDE_DIRS})
# run imported executable
add_custom_command(OUTPUT generated.c
                   COMMAND gromit-gen generated.c)
add_executable(myexe generated.c)
target_link_libraries(myexe gromit) # link to imported library

find_package 的呼叫會定位 Gromit 的安裝,如果找不到任何安裝,則會終止並顯示錯誤訊息(由於 REQUIRED)。在命令成功後,已載入 Gromit 套件組態檔案 gromit-config.cmake,因此已匯入 Gromit 目標,並且已定義諸如 gromit_INCLUDE_DIRS 之類的變數。

上述範例會建立一個套件組態檔,並將其放置於 install 目錄樹中。也可以在 build 目錄樹中建立套件組態檔,以允許應用程式在不安裝的情況下使用專案。為了實現這一點,可以使用以下程式碼擴展 Gromit 的 CMake 檔案

# Make project usable from build tree.
export(TARGETS gromit gromit-gen FILE gromit-targets.cmake)
configure_file(${Gromit_SOURCE_DIR}/gromit-config.cmake.in
               ${Gromit_BINARY_DIR}/gromit-config.cmake @ONLY)

configure_file 呼叫使用不同的輸入檔案 gromit-config.cmake.in,其中包含

# Import the targets.
include("@Gromit_BINARY_DIR@/gromit-targets.cmake")

# Report other information.
set(gromit_INCLUDE_DIRS "@Gromit_SOURCE_DIR@")

放置在 build 目錄樹中的套件組態檔 gromit-config.cmake 提供與 install 目錄樹中相同的資訊給外部專案,但它會參考原始碼和 build 目錄樹中的檔案。它與放置在 install 目錄樹中的套件版本檔 gromit-config-version.cmake 共用。

CMake 套件註冊表

CMake 提供兩個中央位置來註冊在系統上任何地方建立或安裝的套件:一個*使用者套件註冊表*和一個*系統套件註冊表*。find_package 命令會在其文件中指定的搜尋步驟中搜尋這兩個套件註冊表。這些註冊表對於協助專案在非標準的安裝位置或直接在套件建置樹中尋找套件特別有用。專案可以(使用自己的方法)填入使用者或系統註冊表以參考其位置。在任一情況下,套件都應將套件組態檔儲存在已註冊的位置,並選擇性地儲存本章前面所述的套件版本檔。

「使用者套件註冊表」儲存在每個使用者的特定於平台的儲存位置。在 Windows 上,它儲存在 Windows 登錄檔的 HKEY_CURRENT_USER 金鑰下。一個 <package> 可能會出現在登錄檔金鑰下

HKEY_CURRENT_USER\Software\Kitware\CMake\Packages\<package>

作為一個 REG_SZ 值,其名稱可任意指定,該值會指定包含套件組態檔的目錄。在 UNIX 平台上,使用者套件註冊表儲存在使用者主目錄下的 ~/.cmake/packages 中。一個 <package> 可能會出現在目錄下

~/.cmake/packages/<package>

作為一個名稱任意的檔案,其內容會指定包含套件組態檔的目錄。export(PACKAGE) 命令可用於在使用者套件註冊表中註冊專案的建置樹。CMake 目前未提供將安裝樹新增至使用者套件註冊表的介面;如果需要,必須手動教導安裝程式來註冊其套件。

「系統套件註冊表」儲存在特定於平台的系統範圍位置。在 Windows 上,它儲存在 Windows 登錄檔的 HKEY_LOCAL_MACHINE 金鑰下。一個 <package> 可能會出現在登錄檔金鑰下

HKEY_LOCAL_MACHINE\Software\Kitware\CMake\Packages\<package>

作為一個 REG_SZ 值,其名稱可任意指定,該值會指定包含套件組態檔的目錄。在非 Windows 平台上,沒有系統套件註冊表。CMake 未提供新增至系統套件註冊表的介面;如果需要,必須手動教導安裝程式來註冊其套件。

套件註冊表條目由其引用的專案安裝單獨擁有。套件安裝程式負責新增其自己的條目,而對應的解除安裝程式負責移除它。但是,為了保持註冊表乾淨,如果 find_package 命令遇到過時的套件註冊表條目,且它具有足夠的權限,則會自動移除這些條目。如果條目指的是不存在或不包含相符套件組態檔的目錄,則該條目會被視為過時。這對於由 export(PACKAGE) 命令為沒有解除安裝事件且開發人員直接刪除的建置樹所建立的使用者套件註冊表條目特別有用。

套件註冊表條目可以使用任意名稱。一種簡單的命名慣例是使用內容雜湊值,因為它們是確定性的並且不太可能發生衝突。export(PACKAGE) 命令使用此方法。引用特定目錄的條目名稱只是目錄路徑本身的內容雜湊值。例如,專案可能會建立如下的套件註冊表條目

> reg query HKCU\Software\Kitware\CMake\Packages\MyPackage
HKEY_CURRENT_USER\Software\Kitware\CMake\Packages\MyPackage
 45e7d55f13b87179bb12f907c8de6fc4
                          REG_SZ    c:/Users/Me/Work/lib/cmake/MyPackage
 7b4a9844f681c80ce93190d4e3185db9
                          REG_SZ    c:/Users/Me/Work/MyPackage-build

在 Windows 上,或

$ cat ~/.cmake/packages/MyPackage/7d1fb77e07ce59a81bed093bbee945bd
/home/me/work/lib/cmake/MyPackage
$ cat ~/.cmake/packages/MyPackage/f92c1db873a1937f3100706657c63e07
/home/me/work/MyPackage-build

在 UNIX 上。命令 find_package(MyPackage) 將搜尋已註冊位置的套件組態檔。單一套件的套件註冊表條目之間的搜尋順序未指定。已註冊的位置可能包含套件版本檔,以告知 find_package 特定位置是否適合所要求的版本。