使用相依性指南

簡介

專案經常會相依於其他專案、資產和成品。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。許多套件不提供支援組態模式所需的檔案。對於這種情況,可以單獨由專案或 CMake 提供尋找模組檔案。尋找模組通常是一種啟發式實作,它知道套件通常提供什麼以及如何將該套件呈現給專案。由於尋找模組通常與套件分開分發,因此它們不那麼可靠。它們通常是單獨維護的,並且很可能遵循不同的發佈時程,因此它們很容易過時。

根據所使用的參數,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() 呼叫中包含的任何版本約束。呼叫 find_package() 時,即使存在 <PackageName>ConfigVersion.cmake 檔案,也可以選擇不指定版本。

如果找到 <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 時設定 CMAKE_PREFIX_PATH 變數。它被視為搜尋 設定檔 的基礎路徑清單。安裝在 /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 的維護負擔,而且它們通常會落後於它們所關聯的套件的最新版本。一般來說,新的 Find 模組不再加入 CMake。專案應鼓勵上游套件在可能的情況下提供設定檔。如果失敗,專案應為套件提供自己的 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 透過 相依性提供者 支援這些情境。

可以將相依性提供者設定為攔截 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) 命令。