使用相依性指南

簡介

專案經常會相依於其他專案、資源和成品。CMake 提供了許多方法將這些東西整合到建置過程中。專案和使用者可以彈性地選擇最適合他們需求的方法。

將相依性導入建置的主要方法是 find_package() 命令和 FetchContent 模組。FindPkgConfig 模組也偶爾會被使用,儘管它缺乏其他兩者的整合性,並且在本指南中不再進一步討論。

相依性也可以透過自訂的 相依性提供者 來提供。這可能是第三方套件管理器,也可能是開發人員實作的自訂程式碼。相依性提供者與上述主要方法協同運作,以擴展其彈性。

使用預先建置的套件與 find_package()

專案所需的套件可能已經建置完成,並且可以在使用者系統上的某些位置取得。該套件可能也是由 CMake 建置的,或者它可能完全使用了不同的建置系統。它甚至可能只是一組不需要建置的檔案集合。CMake 提供了 find_package() 命令來應對這些情況。它會搜尋眾所周知的位置,以及專案或使用者提供的其他提示和路徑。它還支援套件組件和可選套件。提供的結果變數允許專案根據是否找到套件或特定組件來自訂其自身的行為。

在大多數情況下,專案通常應該使用 基本簽章。大多數時候,這將只涉及套件名稱、可能的版本約束,以及在相依性不是可選的情況下使用的 REQUIRED 關鍵字。也可以指定一組套件組件。

find_package() 基本簽章的範例
find_package(Catch2)
find_package(GTest REQUIRED)
find_package(Boost 1.79 COMPONENTS date_time)

find_package() 命令支援兩種主要的搜尋方法

設定模式

使用此方法,命令會尋找通常由套件本身提供的檔案。這是兩種方法中更可靠的方法,因為套件詳細資訊應始終與套件同步。

模組模式

並非所有套件都支援 CMake。許多套件沒有提供支援設定模式所需的檔案。對於這種情況,可以單獨提供 Find 模組檔案,可以由專案或 CMake 提供。Find 模組通常是一種啟發式實作,它知道套件通常提供什麼,以及如何將該套件呈現給專案。由於 Find 模組通常與套件分開發布,因此它們不如設定模式可靠。它們通常是獨立維護的,並且它們可能會遵循不同的發布時程,因此它們很容易過時。

根據使用的參數,find_package() 可能會使用上述兩種方法中的一種或兩種。透過將選項限制為僅基本簽章,設定模式和模組模式都可以用於滿足相依性。其他選項的存在可能會將呼叫限制為僅使用兩種方法中的一種,從而可能降低命令找到相依性的能力。有關此複雜主題的完整詳細資訊,請參閱 find_package() 文件。

對於這兩種搜尋方法,使用者也可以在 cmake(1) 命令列或在 ccmake(1)cmake-gui(1) UI 工具中設定快取變數,以影響和覆寫尋找套件的位置。有關如何設定快取變數的更多資訊,請參閱 使用者互動指南

設定檔套件

第三方提供可執行檔、函式庫、標頭和其他檔案以供 CMake 使用的首選方法是提供 設定檔。這些是隨套件一起發布的文字檔案,它們定義了 CMake 目標、變數、命令等等。設定檔是一個普通的 CMake 指令碼,由 find_package() 命令讀取。

設定檔通常可以在名稱符合 lib/cmake/<PackageName> 模式的目錄中找到,儘管它們也可能位於其他位置(請參閱 設定模式搜尋程序)。<PackageName> 通常是 find_package() 命令的第一個參數,它甚至可能是唯一的參數。也可以使用 NAMES 選項指定替代名稱

尋找套件時提供替代名稱
find_package(SomeThing
  NAMES
    SameThingOtherName   # Another name for the package
    SomeThing            # Also still look for its canonical name
)

設定檔的名稱必須是 <PackageName>Config.cmake<LowercasePackageName>-config.cmake (本指南的其餘部分使用前者,但兩者都支援)。此檔案是 CMake 套件的入口點。同一個目錄中可能還存在一個單獨的可選檔案,名為 <PackageName>ConfigVersion.cmake<LowercasePackageName>-config-version.cmake。CMake 使用此檔案來判斷套件版本是否滿足對 find_package() 的呼叫中包含的任何版本約束。即使存在 <PackageName>ConfigVersion.cmake 檔案,在呼叫 find_package() 時指定版本也是可選的。

如果找到 <PackageName>Config.cmake 檔案,並且滿足任何版本約束,則 find_package() 命令會認為已找到套件,並且假定整個套件按設計完成。

可能會有其他檔案提供 CMake 命令或 匯入目標 供您使用。CMake 不強制執行這些檔案的任何命名慣例。它們透過使用 CMake include() 命令與主要 <PackageName>Config.cmake 檔案相關聯。<PackageName>Config.cmake 檔案通常會為您包含這些檔案,因此除了呼叫 find_package() 之外,它們通常不需要任何額外步驟。

如果套件的位置在 CMake 已知的目錄 中,則 find_package() 呼叫應該會成功。CMake 已知的目錄是平台特定的。例如,在 Linux 上使用標準系統套件管理器安裝的套件將自動在 /usr 前綴中找到。在 Windows 上的 Program Files 中安裝的套件也將類似地自動找到。

如果套件位於 CMake 未知的位置,例如 /opt/mylib$HOME/dev/prefix,則在沒有幫助的情況下將不會自動找到套件。這是一種正常情況,CMake 提供了幾種方法供使用者指定在哪裡尋找這些函式庫。

CMAKE_PREFIX_PATH 變數可以 在調用 CMake 時設定。它被視為搜尋 設定檔 的基本路徑列表。安裝在 /opt/somepackage 中的套件通常會安裝設定檔,例如 /opt/somepackage/lib/cmake/somePackage/SomePackageConfig.cmake。在這種情況下,/opt/somepackage 應該新增到 CMAKE_PREFIX_PATH

環境變數 CMAKE_PREFIX_PATH 也可以使用要搜尋套件的前綴來填充。與 PATH 環境變數類似,這是一個列表,但它需要使用平台特定的環境變數列表項目分隔符號(Unix 上為 :,Windows 上為 ;)。

CMAKE_PREFIX_PATH 變數在需要指定多個前綴,或在同一個前綴下有多個套件可用時,提供了便利性。套件的路徑也可以透過設定與 <PackageName>_DIR 匹配的變數來指定,例如 SomePackage_DIR。請注意,這不是前綴,而應該是包含設定檔樣式套件檔案的目錄的完整路徑,例如上述範例中的 /opt/somepackage/lib/cmake/SomePackage。請參閱 find_package() 文件,以了解可能影響搜尋的其他 CMake 變數和環境變數。

尋找模組檔案

如果 FindSomePackage.cmake 檔案可用,則沒有提供設定檔的套件仍然可以使用 find_package() 命令找到。這些 Find 模組檔案與設定檔的不同之處在於

  1. Find 模組檔案不應由套件本身提供。

  2. Find<PackageName>.cmake 檔案的可用性並不表示套件或套件的任何特定部分可用。

  3. CMake 不會在 CMAKE_PREFIX_PATH 變數中指定的位置搜尋 Find<PackageName>.cmake 檔案。相反,CMake 會在 CMAKE_MODULE_PATH 變數給定的位置搜尋此類檔案。使用者在執行 CMake 時通常會設定 CMAKE_MODULE_PATH,並且 CMake 專案通常會附加到 CMAKE_MODULE_PATH 以允許使用本機 Find 模組檔案。

  4. CMake 為某些 第三方套件 提供了 Find<PackageName>.cmake 檔案。這些檔案是 CMake 的維護負擔,並且它們落後於與之關聯的套件的最新版本並不罕見。一般來說,不再向 CMake 新增 Find 模組。專案應鼓勵上游套件在可能的情況下提供設定檔。如果失敗,專案應為套件提供自己的 Find 模組。

有關如何編寫 Find 模組檔案的詳細討論,請參閱 Find 模組

匯入目標

設定檔和 Find 模組檔案都可以定義 匯入目標。這些目標通常具有 SomePrefix::ThingName 形式的名稱。如果這些目標可用,專案應優先使用它們,而不是可能也提供的任何 CMake 變數。此類目標通常帶有使用需求,並自動將標頭搜尋路徑、編譯器定義等應用於連結到它們的其他目標(例如,使用 target_link_libraries())。這比嘗試使用變數手動應用相同的東西更穩健且更方便。查看套件或 Find 模組的文件,以了解它定義了哪些匯入目標(如果有)。

匯入目標也應封裝任何特定於組態的路徑。這包括二進制檔案(函式庫、可執行檔)、編譯器標誌和任何其他與組態相關的量。與設定檔相比,Find 模組在提供這些詳細資訊方面可能不太可靠。

尋找第三方套件並使用其中一個函式庫的完整範例可能如下所示

cmake_minimum_required(VERSION 3.10)
project(MyExeProject VERSION 1.0.0)

# Make project-provided Find modules available
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

find_package(SomePackage REQUIRED)
add_executable(MyExe main.cpp)
target_link_libraries(MyExe PRIVATE SomePrefix::LibName)

請注意,上面對 find_package() 的呼叫可以透過設定檔或 Find 模組來解析。它僅使用 基本簽章 支援的基本參數。例如,${CMAKE_CURRENT_SOURCE_DIR}/cmake 目錄中的 FindSomePackage.cmake 檔案將允許 find_package() 命令使用模組模式成功執行。如果不存在此類模組檔案,則將搜尋系統以尋找設定檔。

使用 FetchContent 從原始碼下載和建置

相依性不一定必須預先建置才能與 CMake 一起使用。它們可以作為主要專案的一部分從原始碼建置。FetchContent 模組提供了下載內容(通常是原始碼,但可以是任何東西)並將其新增到主要專案的功能,如果相依性也使用 CMake。相依性的原始碼將與專案的其餘部分一起建置,就像原始碼是專案自身原始碼的一部分一樣。

一般模式是專案應首先宣告它要使用的所有相依性,然後請求使它們可用。以下範例示範了該原則(更多資訊請參閱 範例

include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        703bd9caab50b139428cea1aaff9974ebee5742e # release-1.10.0
)
FetchContent_Declare(
  Catch2
  GIT_REPOSITORY https://github.com/catchorg/Catch2.git
  GIT_TAG        605a34765aa5d5ecbf476b4598a862ada971b0cc # v3.0.1
)
FetchContent_MakeAvailable(googletest Catch2)

支援各種下載方法,包括從 URL 下載和解壓縮封存檔(支援一系列封存檔格式),以及許多儲存庫格式,包括 Git、Subversion 和 Mercurial。也可以使用自訂下載、更新和修補命令來支援任意使用案例。

當使用 FetchContent 將相依性新增到專案時,專案會連結到相依性的目標,就像專案中的任何其他目標一樣。如果相依性提供 SomePrefix::ThingName 形式的命名空間目標,則專案應連結到這些目標,而不是任何非命名空間目標。請參閱下一節,了解為何建議這樣做。

並非所有相依性都可以透過這種方式引入專案。某些相依性定義的目標名稱與專案或其他相依性的其他目標衝突。add_executable()add_library() 建立的具體可執行檔和函式庫目標是全域性的,因此每個目標在整個建置過程中都必須是唯一的。如果相依性會新增衝突的目標名稱,則無法使用此方法直接將其引入建置中。

FetchContentfind_package() 整合

在版本 3.24 中新增。

某些相依性支援透過 find_package()FetchContent 新增。此類相依性必須確保它們在已安裝和從原始碼建置的情況下都定義相同的命名空間目標。然後,使用專案會連結到這些命名空間目標,並且可以透明地處理這兩種情況,只要專案不使用兩種方法都未提供的任何其他東西即可。

專案可以使用 FIND_PACKAGE_ARGS 選項向 FetchContent_Declare() 指示它很樂意接受透過任一種方法提供的相依性。這允許 FetchContent_MakeAvailable() 嘗試首先透過呼叫 find_package() 來滿足相依性,如果有的話,則使用 FIND_PACKAGE_ARGS 關鍵字之後的參數。如果找不到相依性,則會改為如前所述從原始碼建置。

include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        703bd9caab50b139428cea1aaff9974ebee5742e # release-1.10.0
  FIND_PACKAGE_ARGS NAMES GTest
)
FetchContent_MakeAvailable(googletest)

add_executable(ThingUnitTest thing_ut.cpp)
target_link_libraries(ThingUnitTest GTest::gtest_main)

上面的範例首先呼叫 find_package(googletest NAMES GTest)。CMake 提供了 FindGTest 模組,因此如果它找到某處安裝的 GTest 套件,它將使其可用,並且相依性將不會從原始碼建置。如果找不到 GTest 套件,則從原始碼建置。在任何一種情況下,都希望定義 GTest::gtest_main 目標,因此我們將單元測試可執行檔連結到該目標。

也可以透過 FETCHCONTENT_TRY_FIND_PACKAGE_MODE 變數進行高階控制。可以將其設定為 NEVER 以停用所有重新導向到 find_package()。可以將其設定為 ALWAYS 以嘗試 find_package(),即使未指定 FIND_PACKAGE_ARGS 也是如此(應謹慎使用)。

專案也可能決定必須從原始碼建置特定的相依性。如果需要修補或未發布版本的相依性,或者為了滿足某些要求所有相依性都必須從原始碼建置的策略,則可能需要這樣做。專案可以透過將 OVERRIDE_FIND_PACKAGE 關鍵字新增到 FetchContent_Declare() 來強制執行此操作。然後,對該相依性的 find_package() 的呼叫將重新導向到 FetchContent_MakeAvailable()

include(FetchContent)
FetchContent_Declare(
  Catch2
  URL https://intranet.mycomp.com/vendored/Catch2_2.13.4_patched.tgz
  URL_HASH MD5=abc123...
  OVERRIDE_FIND_PACKAGE
)

# The following is automatically redirected to FetchContent_MakeAvailable(Catch2)
find_package(Catch2)

對於更進階的使用案例,請參閱 CMAKE_FIND_PACKAGE_REDIRECTS_DIR 變數。

相依性提供者

在版本 3.24 中新增。

前面的章節討論了專案可以用來指定其相依性的技術。理想情況下,專案不應該真正關心相依性來自何處,只要它提供專案期望的東西(通常只是一些匯入目標)。專案說明了它需要什麼,並且在沒有任何其他詳細資訊的情況下,也可能會指定從哪裡取得,以便仍然可以開箱即用。

另一方面,開發者可能更感興趣於控制相依性如何提供給專案。您可能想要使用您自己建置的特定套件版本。您可能想要使用第三方套件管理器。您可能想要將某些請求重新導向到您控制系統上的不同 URL,以基於安全性或效能考量。CMake 透過相依性提供者 (Dependency Providers) 支援這些情境。

可以設定相依性提供者來攔截 find_package()FetchContent_MakeAvailable() 呼叫。提供者有機會在回退到內建實作之前滿足這些請求,如果提供者沒有滿足請求的話。

只能設定一個相依性提供者,而且只能在 CMake 執行中非常早期的特定時間點設定。CMAKE_PROJECT_TOP_LEVEL_INCLUDES 變數列出了在處理第一個 project() 呼叫(且僅限於該呼叫)時將會讀取的 CMake 檔案。這是唯一可以設定相依性提供者的時機。在整個專案中,最多只預期使用一個提供者。

對於某些情境,使用者不需要知道相依性提供者是如何設定的。第三方可以提供一個可以添加到 CMAKE_PROJECT_TOP_LEVEL_INCLUDES 的檔案,這將代表使用者設定相依性提供者。這是套件管理器的建議方法。開發者可以像這樣使用這樣的文件

cmake -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=/path/to/package_manager/setup.cmake ...

有關如何實作您自己的自訂相依性提供者的詳細資訊,請參閱 cmake_language(SET_DEPENDENCY_PROVIDER) 命令。