cmake-packages(7)

簡介

套件為 CMake 建置系統提供相依性資訊。套件使用 find_package() 命令來尋找。使用 find_package() 的結果,會是 一組 IMPORTED 目標,或一組對應於建置相關資訊的變數。

使用套件

CMake 直接支援兩種形式的套件,設定檔套件尋找模組套件。也透過 FindPkgConfig 模組間接支援 pkg-config 套件。在所有情況下,find_package() 呼叫的基本形式都相同。

find_package(Qt4 4.7.0 REQUIRED) # CMake provides a Qt4 find-module
find_package(Qt5Core 5.1.0 REQUIRED) # Qt provides a Qt5 package config file.
find_package(LibXml2 REQUIRED) # Use pkg-config via the LibXml2 find-module

在已知上游提供套件設定檔,且僅應使用該設定檔的情況下,可以將 CONFIG 關鍵字傳遞給 find_package()

find_package(Qt5Core 5.1.0 CONFIG REQUIRED)
find_package(Qt5Gui 5.1.0 CONFIG)

同樣地,MODULE 關鍵字表示僅使用尋找模組。

find_package(Qt4 4.7.0 MODULE REQUIRED)

明確指定套件類型,可以改善在找不到時向使用者顯示的錯誤訊息。

兩種套件類型也都支援指定套件的元件,可以放在 REQUIRED 關鍵字之後。

find_package(Qt5 5.1.0 CONFIG REQUIRED Widgets Xml Sql)

或作為單獨的 COMPONENTS 清單。

find_package(Qt5 5.1.0 COMPONENTS Widgets Xml Sql)

或作為單獨的 OPTIONAL_COMPONENTS 清單。

find_package(Qt5 5.1.0 COMPONENTS Widgets
                       OPTIONAL_COMPONENTS Xml Sql
)

COMPONENTSOPTIONAL_COMPONENTS 的處理方式由套件定義。

CMAKE_DISABLE_FIND_PACKAGE_<套件名稱> 變數設定為 TRUE,則不會搜尋 <套件名稱> 套件,並且始終為 NOTFOUND。同樣地,將 CMAKE_REQUIRE_FIND_PACKAGE_<套件名稱> 設定為 TRUE 會將套件設為 REQUIRED。

設定檔套件

設定檔套件是上游提供給下游使用的一組檔案。CMake 會在許多位置搜尋套件設定檔,如 find_package() 文件中所述。CMake 使用者告訴 cmake(1) 在非標準前綴中搜尋套件的最簡單方法是設定 CMAKE_PREFIX_PATH 快取變數。

設定檔套件由上游供應商作為開發套件的一部分提供,也就是說,它們屬於標頭檔和提供給下游以協助使用套件的任何其他檔案。

使用設定檔套件時,也會自動設定一組提供套件狀態資訊的變數。根據是否找到套件,<套件名稱>_FOUND 變數會設定為 true 或 false。<套件名稱>_DIR 快取變數會設定為套件設定檔的位置。

尋找模組套件

尋找模組是一個檔案,其中包含一組用於尋找相依性的必要部分的規則,主要是標頭檔和程式庫。通常,當上游不是使用 CMake 建置,或不夠瞭解 CMake 以其他方式提供套件設定檔時,就需要尋找模組。與套件設定檔不同,它不是隨上游一起提供的,而是由下游使用,透過猜測檔案位置和平台特定的提示來尋找檔案。

與上游提供的套件設定檔的情況不同,沒有單一的參考點可以識別已找到套件,因此 find_package() 命令不會自動設定 <套件名稱>_FOUND 變數。不過,按照慣例,仍然可以預期會設定此變數,並且應由尋找模組的作者設定。同樣地,沒有 <套件名稱>_DIR 變數,但每個成品(例如程式庫位置和標頭檔位置)都會提供單獨的快取變數。

如需更多關於建立尋找模組檔案的資訊,請參閱 cmake-developer(7) 文件。

套件佈局

設定檔套件由 套件設定檔 和專案發行版本中選擇性提供的 套件版本檔 組成。

套件設定檔

考慮一個專案 Foo,它會安裝下列檔案

<prefix>/include/foo-1.2/foo.h
<prefix>/lib/foo-1.2/libfoo.a

它也可以提供 CMake 套件設定檔

<prefix>/lib/cmake/foo-1.2/FooConfig.cmake

其內容定義 IMPORTED 目標,或定義變數,例如

# ...
# (compute PREFIX relative to file location)
# ...
set(Foo_INCLUDE_DIRS ${PREFIX}/include/foo-1.2)
set(Foo_LIBRARIES ${PREFIX}/lib/foo-1.2/libfoo.a)

如果另一個專案想要使用 Foo,它只需要找到 FooConfig.cmake 檔案並載入它,即可取得所有關於套件內容位置的資訊。由於套件設定檔是由套件安裝所提供,因此它已經知道所有檔案位置。

可以使用 find_package() 命令來搜尋套件設定檔。此命令會建構一組安裝前綴,並在每個前綴下的數個位置中搜尋。給定名稱 Foo,它會尋找名為 FooConfig.cmakefoo-config.cmake 的檔案。完整的位置集合在 find_package() 命令文件中指定。它尋找的位置之一是

<prefix>/lib/cmake/Foo*/

其中 Foo* 是不區分大小寫的 globbing 表達式。在我們的範例中,globbing 表達式將會比對 <前綴>/lib/cmake/foo-1.2,並且將會找到套件設定檔。

一旦找到套件的組態檔,它會立即被載入。它與套件版本檔案一起,包含了專案使用該套件所需的所有資訊。

套件版本檔案

find_package() 命令找到一個候選的套件組態檔時,它會接著在旁邊尋找一個版本檔案。載入版本檔案是為了測試套件版本是否與請求的版本可接受地匹配。如果版本檔案聲稱相容,則接受組態檔。否則,將忽略它。

套件版本檔案的名稱必須與套件組態檔的名稱相符,但在 .cmake 副檔名之前附加了 -versionVersion。例如,檔案

<prefix>/lib/cmake/foo-1.3/foo-config.cmake
<prefix>/lib/cmake/foo-1.3/foo-config-version.cmake

<prefix>/lib/cmake/bar-4.2/BarConfig.cmake
<prefix>/lib/cmake/bar-4.2/BarConfigVersion.cmake

分別是套件組態檔和對應的套件版本檔案的配對。

find_package() 命令載入版本檔案時,它首先設定以下變數

PACKAGE_FIND_NAME

<PackageName>

PACKAGE_FIND_VERSION

完整的請求版本字串

PACKAGE_FIND_VERSION_MAJOR

如果已請求,則為主要版本,否則為 0

PACKAGE_FIND_VERSION_MINOR

如果已請求,則為次要版本,否則為 0

PACKAGE_FIND_VERSION_PATCH

如果已請求,則為修補程式版本,否則為 0

PACKAGE_FIND_VERSION_TWEAK

如果已請求,則為調整版本,否則為 0

PACKAGE_FIND_VERSION_COUNT

版本組件的數量,0 到 4

版本檔案必須使用這些變數來檢查它是否與請求的版本相容或完全匹配,並使用結果設定以下變數

PACKAGE_VERSION

完整提供的版本字串

PACKAGE_VERSION_EXACT

如果版本完全匹配,則為 True

PACKAGE_VERSION_COMPATIBLE

如果版本相容,則為 True

PACKAGE_VERSION_UNSUITABLE

如果作為任何版本都不適合,則為 True

版本檔案在巢狀範圍內載入,因此它們可以自由地設定它們希望作為計算一部分的任何變數。當版本檔案完成且它已檢查輸出變數時,find_package 命令會清除範圍。當版本檔案聲稱與請求的版本可接受地匹配時,find_package 命令會設定以下變數供專案使用

<PackageName>_VERSION

完整提供的版本字串

<PackageName>_VERSION_MAJOR

如果已提供,則為主要版本,否則為 0

<PackageName>_VERSION_MINOR

如果已提供,則為次要版本,否則為 0

<PackageName>_VERSION_PATCH

如果已提供,則為修補程式版本,否則為 0

<PackageName>_VERSION_TWEAK

如果已提供,則為調整版本,否則為 0

<PackageName>_VERSION_COUNT

版本組件的數量,0 到 4

這些變數報告實際找到的套件版本。它們名稱的 <PackageName> 部分與給予 find_package() 命令的引數匹配。

建立套件

通常,上游依賴於 CMake 本身,並且可以使用一些 CMake 工具來建立套件檔案。考慮一個提供單個共享程式庫的上游

project(UpstreamLib)

set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE ON)

set(Upstream_VERSION 3.4.1)

include(GenerateExportHeader)

add_library(ClimbingStats SHARED climbingstats.cpp)
generate_export_header(ClimbingStats)
set_property(TARGET ClimbingStats PROPERTY VERSION ${Upstream_VERSION})
set_property(TARGET ClimbingStats PROPERTY SOVERSION 3)
set_property(TARGET ClimbingStats PROPERTY
  INTERFACE_ClimbingStats_MAJOR_VERSION 3)
set_property(TARGET ClimbingStats APPEND PROPERTY
  COMPATIBLE_INTERFACE_STRING ClimbingStats_MAJOR_VERSION
)

install(TARGETS ClimbingStats EXPORT ClimbingStatsTargets
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  RUNTIME DESTINATION bin
  INCLUDES DESTINATION include
)
install(
  FILES
    climbingstats.h
    "${CMAKE_CURRENT_BINARY_DIR}/climbingstats_export.h"
  DESTINATION
    include
  COMPONENT
    Devel
)

include(CMakePackageConfigHelpers)
write_basic_package_version_file(
  "${CMAKE_CURRENT_BINARY_DIR}/ClimbingStats/ClimbingStatsConfigVersion.cmake"
  VERSION ${Upstream_VERSION}
  COMPATIBILITY AnyNewerVersion
)

export(EXPORT ClimbingStatsTargets
  FILE "${CMAKE_CURRENT_BINARY_DIR}/ClimbingStats/ClimbingStatsTargets.cmake"
  NAMESPACE Upstream::
)
configure_file(cmake/ClimbingStatsConfig.cmake
  "${CMAKE_CURRENT_BINARY_DIR}/ClimbingStats/ClimbingStatsConfig.cmake"
  COPYONLY
)

set(ConfigPackageLocation lib/cmake/ClimbingStats)
install(EXPORT ClimbingStatsTargets
  FILE
    ClimbingStatsTargets.cmake
  NAMESPACE
    Upstream::
  DESTINATION
    ${ConfigPackageLocation}
)
install(
  FILES
    cmake/ClimbingStatsConfig.cmake
    "${CMAKE_CURRENT_BINARY_DIR}/ClimbingStats/ClimbingStatsConfigVersion.cmake"
  DESTINATION
    ${ConfigPackageLocation}
  COMPONENT
    Devel
)

CMakePackageConfigHelpers 模組提供了一個用於建立簡單 ConfigVersion.cmake 檔案的巨集。此檔案設定套件的版本。當呼叫 find_package() 時,CMake 會讀取此檔案,以確定與請求版本的相容性,並設定一些特定於版本的變數 <PackageName>_VERSION<PackageName>_VERSION_MAJOR<PackageName>_VERSION_MINOR 等。 install(EXPORT) 命令用於匯出先前由 install(TARGETS) 命令定義的 ClimbingStatsTargets 匯出集中的目標。此命令生成 ClimbingStatsTargets.cmake 檔案以包含 IMPORTED 目標,適合下游使用,並安排將其安裝到 lib/cmake/ClimbingStats。生成的 ClimbingStatsConfigVersion.cmakecmake/ClimbingStatsConfig.cmake 安裝到相同的位置,完成套件。

生成的 IMPORTED 目標具有設定適當屬性,以定義其使用需求,例如 INTERFACE_INCLUDE_DIRECTORIESINTERFACE_COMPILE_DEFINITIONS 和其他相關的內建 INTERFACE_ 屬性。在 COMPATIBLE_INTERFACE_STRING 和其他相容介面屬性中列出的使用者定義屬性的 INTERFACE 變體也會傳播到生成的 IMPORTED 目標。在上述情況中,ClimbingStats_MAJOR_VERSION 定義為一個字串,在任何依賴項的相依性之間必須相容。通過在此版本和 ClimbingStats 的下一個版本中設定此自訂使用者屬性,如果嘗試將版本 3 與版本 4 一起使用,cmake(1) 將發出診斷訊息。如果套件的不同主要版本設計為不相容,則套件可以選擇採用這種模式。

當匯出目標以進行安裝時,會指定帶有雙冒號的 NAMESPACE。雙冒號的約定會提示 CMake,該名稱是一個 IMPORTED 目標,當下游使用 target_link_libraries() 命令時。這樣,如果尚未找到提供它的套件,CMake 可以發出診斷訊息。

在這種情況下,當使用 install(TARGETS) 時,指定了 INCLUDES DESTINATION。這會導致 IMPORTED 目標的 INTERFACE_INCLUDE_DIRECTORIES 會填入 CMAKE_INSTALL_PREFIX 中的 include 目錄。當下游使用 IMPORTED 目標時,它會自動取用該屬性中的條目。

建立套件組態檔

在這個情況下,ClimbingStatsConfig.cmake 檔案可以簡單到如下所示

include("${CMAKE_CURRENT_LIST_DIR}/ClimbingStatsTargets.cmake")

這樣可以讓下游使用 IMPORTED 目標。如果 ClimbingStats 套件應該提供任何巨集,它們應該放在一個獨立的檔案中,該檔案安裝到與 ClimbingStatsConfig.cmake 檔案相同的位置,並從那裡包含進來。

這也可以擴展到涵蓋依賴關係

# ...
add_library(ClimbingStats SHARED climbingstats.cpp)
generate_export_header(ClimbingStats)

find_package(Stats 2.6.4 REQUIRED)
target_link_libraries(ClimbingStats PUBLIC Stats::Types)

由於 Stats::Types 目標是 ClimbingStatsPUBLIC 依賴項,下游也必須找到 Stats 套件並連結到 Stats::Types 函式庫。為了確保這一點,應在 ClimbingStatsConfig.cmake 檔案中找到 Stats 套件。CMakeFindDependencyMacro 中的 find_dependency 巨集透過傳播套件是否為 REQUIREDQUIET 等來幫助實現這一點。套件的所有 REQUIRED 依賴項都應在 Config.cmake 檔案中找到。

include(CMakeFindDependencyMacro)
find_dependency(Stats 2.6.4)

include("${CMAKE_CURRENT_LIST_DIR}/ClimbingStatsTargets.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/ClimbingStatsMacros.cmake")

如果找不到依賴項,find_dependency 巨集也會將 ClimbingStats_FOUND 設定為 False,並提供診斷資訊,指出沒有 Stats 套件就無法使用 ClimbingStats 套件。

如果下游使用 find_package() 時指定了 COMPONENTS,它們會列在 <PackageName>_FIND_COMPONENTS 變數中。如果特定元件是非選用的,則 <PackageName>_FIND_REQUIRED_<comp> 將為 true。這可以使用套件組態檔中的邏輯來測試

include(CMakeFindDependencyMacro)
find_dependency(Stats 2.6.4)

include("${CMAKE_CURRENT_LIST_DIR}/ClimbingStatsTargets.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/ClimbingStatsMacros.cmake")

set(_ClimbingStats_supported_components Plot Table)

foreach(_comp ${ClimbingStats_FIND_COMPONENTS})
  if (NOT ";${_ClimbingStats_supported_components};" MATCHES ";${_comp};")
    set(ClimbingStats_FOUND False)
    set(ClimbingStats_NOT_FOUND_MESSAGE "Unsupported component: ${_comp}")
  endif()
  include("${CMAKE_CURRENT_LIST_DIR}/ClimbingStats${_comp}Targets.cmake")
endforeach()

在這裡,ClimbingStats_NOT_FOUND_MESSAGE 設定為指出找不到套件的原因是指定了無效元件的診斷訊息。對於將 _FOUND 變數設定為 False 的任何情況,都可以設定此訊息變數,並將會向使用者顯示。

為建置樹建立套件組態檔

export(EXPORT) 命令會建立一個特定於建置樹,且不可重定位的 IMPORTED 目標定義檔。這可以類似地與合適的套件組態檔和套件版本檔案一起使用,以定義用於建置樹的套件,該套件可以在不安裝的情況下使用。建置樹的使用者可以簡單地確保 CMAKE_PREFIX_PATH 包含建置目錄,或將快取中的 ClimbingStats_DIR 設定為 <build_dir>/ClimbingStats

建立可重定位的套件

可重定位的套件不得參照套件建置所在電腦上,在可能安裝套件的電腦上不存在的檔案絕對路徑。

install(EXPORT) 建立的套件設計為可重定位的,使用相對於套件本身位置的路徑。在定義 EXPORT 目標的介面時,請記住,包含目錄應指定為相對於 CMAKE_INSTALL_PREFIX 的相對路徑。

target_include_directories(tgt INTERFACE
  # Wrong, not relocatable:
  $<INSTALL_INTERFACE:${CMAKE_INSTALL_PREFIX}/include/TgtName>
)

target_include_directories(tgt INTERFACE
  # Ok, relocatable:
  $<INSTALL_INTERFACE:include/TgtName>
)

$<INSTALL_PREFIX> 產生器 表達式 可以用作安裝首碼的預留位置,而不會導致產生不可重定位的套件。如果使用複雜的產生器表達式,這是必要的

target_include_directories(tgt INTERFACE
  # Ok, relocatable:
  $<INSTALL_INTERFACE:$<$<CONFIG:Debug>:$<INSTALL_PREFIX>/include/TgtName>>
)

這也適用於參照外部依賴項的路徑。不建議填入任何可能包含路徑的屬性,例如 INTERFACE_INCLUDE_DIRECTORIESINTERFACE_LINK_LIBRARIES,並使用與依賴項相關的路徑。例如,此程式碼可能不適用於可重定位的套件

target_link_libraries(ClimbingStats INTERFACE
  ${Foo_LIBRARIES} ${Bar_LIBRARIES}
  )
target_include_directories(ClimbingStats INTERFACE
  "$<INSTALL_INTERFACE:${Foo_INCLUDE_DIRS};${Bar_INCLUDE_DIRS}>"
  )

參照的變數可能包含函式庫和包含目錄的絕對路徑,**如同在建立套件的電腦上找到的一樣**。這會建立一個具有硬式編碼依賴項路徑,且不適合重定位的套件。

理想情況下,應透過它們自己的 IMPORTED 目標 使用此類依賴項,這些目標有自己的 IMPORTED_LOCATION 和使用需求屬性,例如適當填入的 INTERFACE_INCLUDE_DIRECTORIES。然後,這些匯入的目標可以使用 target_link_libraries() 命令用於 ClimbingStats

target_link_libraries(ClimbingStats INTERFACE Foo::Foo Bar::Bar)

透過這種方法,套件僅透過 IMPORTED 目標 的名稱來參照其外部依賴項。當使用者使用已安裝的套件時,使用者將執行適當的 find_package() 命令(透過上述的 find_dependency 巨集)來尋找依賴項,並使用其自身電腦上的適當路徑來填入匯入的目標。

不幸的是,CMake 隨附的許多 模組 尚未提供 IMPORTED 目標,因為它們的開發早於此方法。這可能會隨著時間推移而逐步改進。使用此類模組建立可重定位套件的解決方法包括

  • 在建置套件時,將每個 Foo_LIBRARY 快取項目指定為僅函式庫名稱,例如 -DFoo_LIBRARY=foo。這會告知對應的尋找模組只用 foo 來填入 Foo_LIBRARIES,要求連結器搜尋函式庫,而不是硬式編碼路徑。

  • 或者,在安裝套件內容之後,但在建立用於重新散發的套件安裝二進位檔案之前,手動將絕對路徑替換為預留位置,以便在安裝套件時由安裝工具替換。

套件登錄檔

CMake 提供了兩個中心位置來登錄已在系統上的任何位置建置或安裝的套件

登錄檔對於協助專案在非標準安裝位置或直接在它們自己的建置樹中尋找套件特別有用。專案可以填入使用者或系統登錄檔(使用其自己的方式,請參閱下文)來參照其位置。在任一情況下,套件都應在已登錄的位置儲存一個套件組態檔 (<PackageName>Config.cmake) 和一個可選的套件版本檔案 (<PackageName>ConfigVersion.cmake)。

find_package() 命令會在其文件中指定的兩個搜尋步驟中搜尋兩個套件登錄檔。如果它具有足夠的權限,它也會移除參照不存在或不包含相符套件組態檔的目錄的過時套件登錄檔項目。

使用者套件登錄檔

使用者套件註冊表儲存在每個使用者的位置。export(PACKAGE) 命令可以用於在使用者套件註冊表中註冊專案的建置樹。CMake 目前沒有提供介面來將安裝樹新增至使用者套件註冊表。安裝程式必須手動學習註冊它們的套件(如果需要的話)。

在 Windows 上,使用者套件註冊表儲存在 Windows 登錄檔中,位於 HKEY_CURRENT_USER 底下的金鑰中。

在登錄檔金鑰下可能出現 <PackageName>

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

作為一個 REG_SZ 值,具有任意名稱,指定包含套件組態檔的目錄。

在 UNIX 平台上,使用者套件註冊表儲存在使用者家目錄下的 ~/.cmake/packages 中。在該目錄下可能出現 <PackageName>

~/.cmake/packages/<PackageName>

作為一個檔案,具有任意名稱,其內容指定包含套件組態檔的目錄。

系統套件註冊表

系統套件註冊表儲存在系統範圍的位置。CMake 目前沒有提供介面來新增至系統套件註冊表。安裝程式必須手動學習註冊它們的套件(如果需要的話)。

在 Windows 上,系統套件註冊表儲存在 Windows 登錄檔中,位於 HKEY_LOCAL_MACHINE 底下的金鑰中。在登錄檔金鑰下可能出現 <PackageName>

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

作為一個 REG_SZ 值,具有任意名稱,指定包含套件組態檔的目錄。

在非 Windows 平台上沒有系統套件註冊表。

停用套件註冊表

在某些情況下,使用套件註冊表可能不盡理想。CMake 允許使用以下變數來停用它們。

套件註冊表示例

命名套件註冊表條目的一個簡單約定是使用內容雜湊。它們是確定性的,並且不太可能發生衝突(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

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

那麼 CMakeLists.txt 程式碼

find_package(MyPackage)

將會在註冊的位置搜尋套件組態檔(MyPackageConfig.cmake)。單一套件的套件註冊表條目之間的搜尋順序是未指定的,並且條目名稱(在此範例中為雜湊)沒有任何意義。註冊的位置可以包含套件版本檔案(MyPackageConfigVersion.cmake)來告知 find_package() 某個特定位置是否適合所要求的版本。

套件註冊表所有權

套件註冊表條目由它們所引用的專案安裝個別擁有。套件安裝程式負責新增它自己的條目,而對應的解除安裝程式則負責移除它。

export(PACKAGE) 命令會以專案建置樹的位置填入使用者套件註冊表。建置樹往往會被開發人員刪除,並且沒有可以觸發移除其條目的「解除安裝」事件。為了保持註冊表的乾淨,find_package() 命令如果具有足夠的權限,會自動移除它遇到的過時條目。一旦呼叫 export(PACKAGE),CMake 不提供任何介面來移除引用現有建置樹的條目。但是,如果專案從建置樹中移除其套件組態檔,則會認為引用該位置的條目已過時。