cmake-packages(7)

簡介

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

使用套件

CMake 直接支援兩種形式的套件:設定檔套件Find-module 套件。透過 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-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_<PackageName> 變數設定為 TRUE,將不會搜尋 <PackageName> 套件,且永遠為 NOTFOUND。同樣地,將 CMAKE_REQUIRE_FIND_PACKAGE_<PackageName> 設定為 TRUE 將使套件成為 REQUIRED。

設定檔套件

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

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

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

Find-module 套件

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

與上游提供的套件設定檔的情況不同,沒有單一參考點可以識別套件是否已找到,因此 <PackageName>_FOUND 變數不會由 find_package() 指令自動設定。但是,仍然可以期望按照慣例設定它,並且應該由 Find-module 的作者設定。同樣地,沒有 <PackageName>_DIR 變數,但是每個產出物(例如程式庫位置和標頭檔位置)都提供單獨的快取變數。

請參閱 cmake-developer(7) 文件說明,以取得有關建立 Find-module 檔案的更多資訊。

套件佈局

設定檔套件包含一個 套件設定檔,以及可選的 套件版本檔,它們與專案發行版一起提供。

套件設定檔

考慮一個專案 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 表達式將匹配 <prefix>/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) 指令用於匯出 ClimbingStatsTargets 匯出集中 (export-set) 的目標,該匯出集先前由 install(TARGETS) 指令定義。此指令產生 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 提示,當下游使用 target_link_libraries() 指令時,該名稱是 IMPORTED 目標。這樣,如果尚未找到提供它的套件,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 已適當地填入。然後,這些匯入的目標可以與 ClimbingStatstarget_link_libraries() 指令一起使用

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

透過這種方法,套件僅透過 IMPORTED 目標 的名稱參考其外部相依性。當使用者使用已安裝的套件時,使用者將執行適當的 find_package() 指令(透過上面描述的 find_dependency 巨集)來尋找相依性,並使用其自身機器上的適當路徑填入匯入的目標。

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

  • 在建置套件時,將每個 Foo_LIBRARY 快取條目指定為僅程式庫名稱,例如 -DFoo_LIBRARY=foo。這會告訴對應的 find module 使用 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 不提供介面來移除引用現有建置樹的條目。但是,如果專案從建置樹中移除其套件組態檔,則引用該位置的條目將被視為過時。