CMake 教學

簡介

CMake 教學提供逐步指南,涵蓋 CMake 協助解決的常見建置系統問題。在一個範例專案中了解各種主題如何協同運作會非常有幫助。教學文件和範例的原始碼可以在 CMake 原始碼樹的 Help/guide/tutorial 目錄中找到。每個步驟都有自己的子目錄,其中包含可用作起點的程式碼。教學範例是循序漸進的,因此每個步驟都為前一個步驟提供完整的解決方案。

基本的起點(步驟 1)

最基本的專案是從原始碼檔案建置的可執行檔。對於簡單的專案,只需要三行的 CMakeLists.txt 檔案。這將是我們教學的起點。在 Step1 目錄中建立一個 CMakeLists.txt 檔案,如下所示:

cmake_minimum_required(VERSION 3.10)

# set the project name
project(Tutorial)

# add the executable
add_executable(Tutorial tutorial.cxx)

請注意,此範例在 CMakeLists.txt 檔案中使用小寫指令。CMake 支援大寫、小寫和混合大小寫的指令。tutorial.cxx 的原始碼在 Step1 目錄中提供,可以用來計算數字的平方根。

加入版本號和設定檔頭檔

我們將加入的第一個功能是為我們的可執行檔和專案提供版本號。雖然我們可以完全在原始碼中執行此操作,但使用 CMakeLists.txt 可以提供更大的彈性。

首先,修改 CMakeLists.txt 檔案,使用 project 指令來設定專案名稱和版本號。

cmake_minimum_required(VERSION 3.10)

# set the project name and version
project(Tutorial VERSION 1.0)

然後,設定一個標頭檔,將版本號傳遞給原始碼

configure_file(TutorialConfig.h.in TutorialConfig.h)

由於設定的檔案將寫入二元樹,我們必須將該目錄新增到搜尋標頭檔的路徑清單中。將以下幾行新增到 CMakeLists.txt 檔案的末尾

target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           )

使用您最愛的編輯器,在原始碼目錄中建立一個名為 TutorialConfig.h.in 的檔案,內容如下

// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@

當 CMake 設定此標頭檔時,@Tutorial_VERSION_MAJOR@@Tutorial_VERSION_MINOR@ 的值將被取代。

接下來修改 tutorial.cxx 以包含設定的標頭檔 TutorialConfig.h

最後,讓我們透過更新 tutorial.cxx 如下來印出可執行檔名稱和版本號

  if (argc < 2) {
    // report version
    std::cout << argv[0] << " Version " << Tutorial_VERSION_MAJOR << "."
              << Tutorial_VERSION_MINOR << std::endl;
    std::cout << "Usage: " << argv[0] << " number" << std::endl;
    return 1;
  }

指定 C++ 標準

接下來,讓我們透過將 tutorial.cxx 中的 atof 取代為 std::stod,在專案中加入一些 C++11 功能。同時,移除 #include <cstdlib>

  const double inputValue = std::stod(argv[1]);

我們需要在 CMake 程式碼中明確聲明它應該使用正確的旗標。在 CMake 中啟用對特定 C++ 標準支援的最簡單方法是使用 CMAKE_CXX_STANDARD 變數。對於本教學,將 CMAKE_CXX_STANDARD 變數在 CMakeLists.txt 檔案中設定為 11,並將 CMAKE_CXX_STANDARD_REQUIRED 設定為 True。請確保在呼叫 add_executable 之前加入 CMAKE_CXX_STANDARD 宣告。

cmake_minimum_required(VERSION 3.10)

# set the project name and version
project(Tutorial VERSION 1.0)

# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

建置與測試

執行 cmake 可執行檔或 cmake-gui 來設定專案,然後使用您選擇的建置工具來建置它。

例如,從命令列我們可以導覽至 CMake 原始碼樹的 Help/guide/tutorial 目錄,並建立一個建置目錄

mkdir Step1_build

接下來,導覽至建置目錄並執行 CMake 以設定專案並產生原生建置系統

cd Step1_build
cmake ../Step1

然後呼叫該建置系統以實際編譯/連結專案

cmake --build .

最後,嘗試使用這些指令來使用新建立的 Tutorial

Tutorial 4294967296
Tutorial 10
Tutorial

加入函式庫(步驟 2)

現在,我們將為專案加入一個函式庫。這個函式庫將包含我們自己計算數字平方根的實作。然後,可執行檔可以使用此函式庫,而不是編譯器提供的標準平方根函式。

在本教學中,我們將把函式庫放在一個名為 MathFunctions 的子目錄中。此目錄已包含標頭檔 MathFunctions.h 和原始碼檔案 mysqrt.cxx。原始碼檔案有一個名為 mysqrt 的函式,其功能與編譯器的 sqrt 函式類似。

將以下一行的 CMakeLists.txt 檔案新增到 MathFunctions 目錄

add_library(MathFunctions mysqrt.cxx)

若要使用新的函式庫,我們將在頂層 CMakeLists.txt 檔案中新增一個 add_subdirectory 呼叫,以便建置該函式庫。我們將新的函式庫新增至可執行檔,並新增 MathFunctions 作為包含目錄,以便可以找到 mysqrt.h 標頭檔。頂層 CMakeLists.txt 檔案的最後幾行現在應該如下所示

# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(Tutorial tutorial.cxx)

target_link_libraries(Tutorial PUBLIC MathFunctions)

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC
                          "${PROJECT_BINARY_DIR}"
                          "${PROJECT_SOURCE_DIR}/MathFunctions"
                          )

現在,讓我們讓 MathFunctions 函式庫成為可選的。雖然對於本教學來說,實際上沒有任何這樣做的必要,但對於較大的專案來說,這很常見。第一步是在頂層 CMakeLists.txt 檔案中新增一個選項。

option(USE_MYMATH "Use tutorial provided math implementation" ON)

# configure a header file to pass some of the CMake settings
# to the source code
configure_file(TutorialConfig.h.in TutorialConfig.h)

此選項將顯示在 cmake-guiccmake 中,預設值為 ON,使用者可以變更。此設定將儲存在快取中,以便使用者每次在建置目錄上執行 CMake 時都不需要設定該值。

下一個變更是有條件地建置和連結 MathFunctions 函式庫。若要執行此操作,我們將頂層 CMakeLists.txt 檔案的結尾變更為如下所示

if(USE_MYMATH)
  add_subdirectory(MathFunctions)
  list(APPEND EXTRA_LIBS MathFunctions)
  list(APPEND EXTRA_INCLUDES "${PROJECT_SOURCE_DIR}/MathFunctions")
endif()

# add the executable
add_executable(Tutorial tutorial.cxx)

target_link_libraries(Tutorial PUBLIC ${EXTRA_LIBS})

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           ${EXTRA_INCLUDES}
                           )

請注意使用變數 EXTRA_LIBS 來收集稍後將連結到可執行檔的任何可選函式庫。變數 EXTRA_INCLUDES 也類似地用於可選的標頭檔。這是處理許多可選元件時的經典方法,我們將在下一步中介紹現代方法。

對應的原始碼變更非常簡單。首先,在 tutorial.cxx 中,如果我們需要,則包含 MathFunctions.h 標頭

#ifdef USE_MYMATH
#  include "MathFunctions.h"
#endif

然後,在同一個檔案中,讓 USE_MYMATH 控制要使用哪個平方根函式

#ifdef USE_MYMATH
  const double outputValue = mysqrt(inputValue);
#else
  const double outputValue = sqrt(inputValue);
#endif

由於原始碼現在需要 USE_MYMATH,我們可以透過以下程式碼行將其新增到 TutorialConfig.h.in

#cmakedefine USE_MYMATH

練習:為什麼在 USE_MYMATH 的選項之後設定 TutorialConfig.h.in 很重要?如果我們反轉這兩個會發生什麼事?

執行 cmake 可執行檔或 cmake-gui 來設定專案,然後使用您選擇的建置工具來建置它。接著執行建置好的 Tutorial 可執行檔。

現在讓我們更新 USE_MYMATH 的值。最簡單的方法是使用 cmake-gui 或如果您在終端機中使用 ccmake。或者,如果您想從命令列變更選項,請嘗試

cmake ../Step2 -DUSE_MYMATH=OFF

重新建置並再次執行教學程式。

哪個函數能提供較好的結果,sqrt 或 mysqrt?

為函式庫新增使用需求 (步驟 3)

使用需求允許更好地控制函式庫或可執行檔的連結和包含行,同時也更好地控制 CMake 內部目標的傳遞屬性。主要的使用需求命令為

讓我們重構來自新增函式庫 (步驟 2) 的程式碼,以使用現代 CMake 的使用需求方法。我們首先聲明任何連結到 MathFunctions 的都需要包含目前的原始碼目錄,而 MathFunctions 本身則不需要。因此這可以變成一個 INTERFACE 使用需求。

請記住 INTERFACE 表示使用者需要但產生者不需要的東西。將以下程式碼加入到 MathFunctions/CMakeLists.txt 的末尾

target_include_directories(MathFunctions
          INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
          )

現在我們已經為 MathFunctions 指定了使用需求,我們可以安全地從頂層 CMakeLists.txt 中移除我們對 EXTRA_INCLUDES 變數的使用,在此處

if(USE_MYMATH)
  add_subdirectory(MathFunctions)
  list(APPEND EXTRA_LIBS MathFunctions)
endif()

以及此處

target_include_directories(Tutorial PUBLIC
                           "${PROJECT_BINARY_DIR}"
                           )

完成後,執行 cmake 可執行檔或 cmake-gui 來設定專案,然後使用您選擇的建置工具或從建置目錄使用 cmake --build . 來建置它。

安裝和測試 (步驟 4)

現在我們可以開始為我們的專案新增安裝規則和測試支援。

安裝規則

安裝規則非常簡單:對於 MathFunctions,我們想要安裝函式庫和標頭檔,而對於應用程式,我們想要安裝可執行檔和設定好的標頭。

因此,我們將以下程式碼加入到 MathFunctions/CMakeLists.txt 的末尾

install(TARGETS MathFunctions DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)

並將以下程式碼加入到頂層 CMakeLists.txt 的末尾

install(TARGETS Tutorial DESTINATION bin)
install(FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"
  DESTINATION include
  )

這就是建立教學程式基本本地安裝所需的全部內容。

現在,執行 cmake 可執行檔或 cmake-gui 來設定專案,然後使用您選擇的建置工具來建置它。

接著從命令列使用 cmake 命令的 install 選項(在 3.15 中引入,較舊版本的 CMake 必須使用 make install)來執行安裝步驟。對於多組態工具,請不要忘記使用 --config 引數來指定組態。如果使用 IDE,只需建置 INSTALL 目標即可。此步驟將安裝適當的標頭檔、函式庫和可執行檔。例如

cmake --install .

CMake 變數 CMAKE_INSTALL_PREFIX 用於判斷檔案將安裝到的根目錄。如果使用 cmake --install 命令,可以使用 --prefix 引數覆寫安裝前置詞。例如

cmake --install . --prefix "/home/myuser/installdir"

導覽至安裝目錄並驗證安裝的 Tutorial 是否可以執行。

測試支援

接下來讓我們測試我們的應用程式。在頂層 CMakeLists.txt 檔案的末尾,我們可以啟用測試,然後新增一些基本測試,以驗證應用程式是否正常運作。

enable_testing()

# does the application run
add_test(NAME Runs COMMAND Tutorial 25)

# does the usage message work?
add_test(NAME Usage COMMAND Tutorial)
set_tests_properties(Usage
  PROPERTIES PASS_REGULAR_EXPRESSION "Usage:.*number"
  )

# define a function to simplify adding tests
function(do_test target arg result)
  add_test(NAME Comp${arg} COMMAND ${target} ${arg})
  set_tests_properties(Comp${arg}
    PROPERTIES PASS_REGULAR_EXPRESSION ${result}
    )
endfunction(do_test)

# do a bunch of result based tests
do_test(Tutorial 4 "4 is 2")
do_test(Tutorial 9 "9 is 3")
do_test(Tutorial 5 "5 is 2.236")
do_test(Tutorial 7 "7 is 2.645")
do_test(Tutorial 25 "25 is 5")
do_test(Tutorial -25 "-25 is [-nan|nan|0]")
do_test(Tutorial 0.0001 "0.0001 is 0.01")

第一個測試只是驗證應用程式是否可以執行,不會發生記憶體區段錯誤或其他崩潰,並且具有零傳回值。這是 CTest 測試的基本形式。

下一個測試使用 PASS_REGULAR_EXPRESSION 測試屬性,以驗證測試的輸出是否包含特定的字串。在此情況下,驗證在提供不正確的引數數量時是否印出使用訊息。

最後,我們有一個名為 do_test 的函數,它會執行應用程式並驗證計算出的平方根對於給定的輸入是否正確。對於每次呼叫 do_test,都會根據傳遞的引數,將另一個具有名稱、輸入和預期結果的測試新增至專案中。

重新建置應用程式,然後 cd 到二進位目錄並執行 ctest 可執行檔:ctest -Nctest -VV。對於多組態產生器(例如 Visual Studio),必須指定組態類型。例如,若要以偵錯模式執行測試,請從建置目錄(而不是偵錯子目錄!)使用 ctest -C Debug -VV。或者,從 IDE 建置 RUN_TESTS 目標。

新增系統內省 (步驟 5)

讓我們考慮在我們的專案中加入一些取決於目標平台可能不具有的功能的程式碼。在此範例中,我們將新增一些取決於目標平台是否具有 logexp 函數的程式碼。當然幾乎每個平台都有這些函數,但為了本教學課程,假設它們並不常見。

如果平台具有 logexp,那麼我們將使用它們來計算 mysqrt 函數中的平方根。我們首先使用 MathFunctions/CMakeLists.txt 中的 CheckSymbolExists 模組來測試這些函數的可用性。在某些平台上,我們需要連結到 m 函式庫。如果最初找不到 logexp,請要求 m 函式庫並再次嘗試。

include(CheckSymbolExists)
check_symbol_exists(log "math.h" HAVE_LOG)
check_symbol_exists(exp "math.h" HAVE_EXP)
if(NOT (HAVE_LOG AND HAVE_EXP))
  unset(HAVE_LOG CACHE)
  unset(HAVE_EXP CACHE)
  set(CMAKE_REQUIRED_LIBRARIES "m")
  check_symbol_exists(log "math.h" HAVE_LOG)
  check_symbol_exists(exp "math.h" HAVE_EXP)
  if(HAVE_LOG AND HAVE_EXP)
    target_link_libraries(MathFunctions PRIVATE m)
  endif()
endif()

如果可以的話,使用 target_compile_definitions 來將 HAVE_LOGHAVE_EXP 指定為 PRIVATE 編譯定義。

if(HAVE_LOG AND HAVE_EXP)
  target_compile_definitions(MathFunctions
                             PRIVATE "HAVE_LOG" "HAVE_EXP")
endif()

如果系統上有 logexp 可用,那麼我們將使用它們來計算 mysqrt 函數中的平方根。將以下程式碼新增到 MathFunctions/mysqrt.cxx 中的 mysqrt 函數中(別忘了在返回結果之前加上 #endif!)。

#if defined(HAVE_LOG) && defined(HAVE_EXP)
  double result = exp(log(x) * 0.5);
  std::cout << "Computing sqrt of " << x << " to be " << result
            << " using log and exp" << std::endl;
#else
  double result = x;

我們還需要修改 mysqrt.cxx 來包含 cmath

#include <cmath>

執行 cmake 可執行檔或 cmake-gui 來配置專案,然後使用您選擇的建置工具來建置它,並執行 Tutorial 可執行檔。

現在哪個函數的結果更好,sqrt 還是 mysqrt?

新增自訂命令和產生檔案(步驟 6)

假設,為了本教學的目的,我們決定永遠不使用平台 logexp 函數,而是想要產生一個預先計算好的數值表,以便在 mysqrt 函數中使用。在本節中,我們將在建置過程中建立表格,然後將該表格編譯到我們的應用程式中。

首先,讓我們移除 MathFunctions/CMakeLists.txt 中對 logexp 函數的檢查。然後從 mysqrt.cxx 中移除對 HAVE_LOGHAVE_EXP 的檢查。同時,我們可以移除 #include <cmath>

MathFunctions 子目錄中,已提供一個名為 MakeTable.cxx 的新原始碼檔案,用於產生表格。

檢閱檔案後,我們可以發現該表格是以有效的 C++ 程式碼形式產生,並且輸出檔案名稱作為引數傳入。

下一步是將適當的命令新增到 MathFunctions/CMakeLists.txt 檔案中,以建置 MakeTable 可執行檔,然後將其作為建置過程的一部分執行。需要幾個命令才能完成此操作。

首先,在 MathFunctions/CMakeLists.txt 的頂部,像新增任何其他可執行檔一樣新增 MakeTable 的可執行檔。

add_executable(MakeTable MakeTable.cxx)

然後,我們新增一個自訂命令,該命令指定如何透過執行 MakeTable 來產生 Table.h

add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  DEPENDS MakeTable
  )

接下來,我們必須讓 CMake 知道 mysqrt.cxx 依賴於產生的檔案 Table.h。這是透過將產生的 Table.h 新增到 MathFunctions 程式庫的來源清單中來完成的。

add_library(MathFunctions
            mysqrt.cxx
            ${CMAKE_CURRENT_BINARY_DIR}/Table.h
            )

我們還必須將目前的二進位目錄新增到包含目錄清單中,以便 mysqrt.cxx 可以找到並包含 Table.h

target_include_directories(MathFunctions
          INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
          PRIVATE ${CMAKE_CURRENT_BINARY_DIR}
          )

現在讓我們使用產生的表格。首先,修改 mysqrt.cxx 以包含 Table.h。接下來,我們可以重寫 mysqrt 函數以使用表格

double mysqrt(double x)
{
  if (x <= 0) {
    return 0;
  }

  // use the table to help find an initial value
  double result = x;
  if (x >= 1 && x < 10) {
    std::cout << "Use the table to help find an initial value " << std::endl;
    result = sqrtTable[static_cast<int>(x)];
  }

  // do ten iterations
  for (int i = 0; i < 10; ++i) {
    if (result <= 0) {
      result = 0.1;
    }
    double delta = x - (result * result);
    result = result + 0.5 * delta / result;
    std::cout << "Computing sqrt of " << x << " to be " << result << std::endl;
  }

  return result;
}

執行 cmake 可執行檔或 cmake-gui 來配置專案,然後使用您選擇的建置工具來建置它。

建置此專案時,它將首先建置 MakeTable 可執行檔。然後,它將執行 MakeTable 以產生 Table.h。最後,它將編譯包含 Table.hmysqrt.cxx,以產生 MathFunctions 程式庫。

執行 Tutorial 可執行檔並驗證它是否正在使用表格。

建置安裝程式(步驟 7)

接下來,假設我們想要將我們的專案發佈給其他人,以便他們可以使用它。我們想要在各種平台上提供二進位和原始碼發佈。這與我們先前在 安裝和測試(步驟 4) 中所做的安裝略有不同,我們在那裡安裝的是我們從原始碼建置的二進位檔。在此範例中,我們將建置支援二進位安裝和套件管理功能的安裝套件。為了實現這一點,我們將使用 CPack 來建立特定於平台的安裝程式。具體來說,我們需要在頂層 CMakeLists.txt 檔案的底部新增幾行。

include(InstallRequiredSystemLibraries)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set(CPACK_PACKAGE_VERSION_MAJOR "${Tutorial_VERSION_MAJOR}")
set(CPACK_PACKAGE_VERSION_MINOR "${Tutorial_VERSION_MINOR}")
include(CPack)

這就是全部的內容。我們先包含 InstallRequiredSystemLibraries。此模組將包含專案在目前平台上所需的任何執行時間程式庫。接下來,我們設定一些 CPack 變數,這些變數指示我們為此專案儲存授權和版本資訊的位置。版本資訊已在本教學稍早設定,而 license.txt 已包含在此步驟的頂層原始碼目錄中。

最後,我們包含 CPack 模組,該模組將使用這些變數和目前系統的其他一些屬性來設定安裝程式。

下一步是以通常的方式建置專案,然後執行 cpack 可執行檔。若要建置二進位發佈,請從二進位目錄執行

cpack

若要指定產生器,請使用 -G 選項。對於多配置建置,請使用 -C 來指定配置。例如

cpack -G ZIP -C Debug

若要建立原始碼發佈,您將輸入

cpack --config CPackSourceConfig.cmake

或者,從 IDE 執行 make package 或右鍵按一下 Package 目標,然後按一下 Build Project

執行在二進位目錄中找到的安裝程式。然後執行已安裝的可執行檔並驗證其是否正常運作。

新增儀表板的支援(步驟 8)

新增將我們的測試結果提交到儀表板的支援很簡單。我們已經在 測試支援 中為我們的專案定義了許多測試。現在我們只需要執行這些測試並將它們提交到儀表板。若要包含對儀表板的支援,我們需要在頂層 CMakeLists.txt 中包含 CTest 模組。

# enable testing
enable_testing()

替換為

# enable dashboard scripting
include(CTest)

CTest 模組將自動呼叫 enable_testing(),因此我們可以將其從我們的 CMake 檔案中移除。

我們還需要在頂層目錄中建立一個 CTestConfig.cmake 檔案,我們可以在其中指定專案的名稱和提交儀表板的位置。

set(CTEST_PROJECT_NAME "CMakeTutorial")
set(CTEST_NIGHTLY_START_TIME "00:00:00 EST")

set(CTEST_DROP_METHOD "http")
set(CTEST_DROP_SITE "my.cdash.org")
set(CTEST_DROP_LOCATION "/submit.php?project=CMakeTutorial")
set(CTEST_DROP_SITE_CDASH TRUE)

ctest 執行檔執行時,會讀取此檔案。要建立一個簡單的儀表板,您可以執行 cmake 執行檔或 cmake-gui 來設定專案,但先不要建置它。請改為將目錄變更至二元樹,然後執行

ctest [-VV] -D Experimental

請記住,對於多組態產生器(例如 Visual Studio),必須指定組態類型

ctest [-VV] -C Debug -D Experimental

或者,從 IDE 中,建置 Experimental 目標。

ctest 執行檔將會建置並測試專案,並將結果提交到 Kitware 的公開儀表板:https://my.cdash.org/index.php?project=CMakeTutorial

混合靜態和共享 (步驟 9)

在本節中,我們將展示如何使用 BUILD_SHARED_LIBS 變數來控制 add_library 的預設行為,並允許控制如何建置沒有明確類型(STATICSHAREDMODULEOBJECT)的程式庫。

為此,我們需要將 BUILD_SHARED_LIBS 加入到最上層的 CMakeLists.txt。我們使用 option 命令,因為它允許使用者選擇性地選擇值是否應為 ON 或 OFF。

接下來,我們將重構 MathFunctions,使其成為一個真正的程式庫,封裝使用 mysqrtsqrt 的邏輯,而不是要求呼叫程式碼執行此邏輯。這也表示 USE_MYMATH 不會控制建置 MathFunctions,而是控制此程式庫的行為。

第一步是更新最上層 CMakeLists.txt 的起始部分,使其看起來像這樣

cmake_minimum_required(VERSION 3.10)

# set the project name and version
project(Tutorial VERSION 1.0)

# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

# control where the static and shared libraries are built so that on windows
# we don't need to tinker with the path to run the executable
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}")

option(BUILD_SHARED_LIBS "Build using shared libraries" ON)

# configure a header file to pass the version number only
configure_file(TutorialConfig.h.in TutorialConfig.h)

# add the MathFunctions library
add_subdirectory(MathFunctions)

# add the executable
add_executable(Tutorial tutorial.cxx)
target_link_libraries(Tutorial PUBLIC MathFunctions)

現在我們已讓 MathFunctions 始終被使用,我們需要更新該程式庫的邏輯。因此,在 MathFunctions/CMakeLists.txt 中,我們需要建立一個 SqrtLibrary,當啟用 USE_MYMATH 時,它將會有條件地建置和安裝。現在,由於這是一個教學範例,我們將明確要求 SqrtLibrary 以靜態方式建置。

最終結果是 MathFunctions/CMakeLists.txt 應該看起來像這樣

# add the library that runs
add_library(MathFunctions MathFunctions.cxx)

# state that anybody linking to us needs to include the current source dir
# to find MathFunctions.h, while we don't.
target_include_directories(MathFunctions
                           INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
                           )

# should we use our own math functions
option(USE_MYMATH "Use tutorial provided math implementation" ON)
if(USE_MYMATH)

  target_compile_definitions(MathFunctions PRIVATE "USE_MYMATH")

  # first we add the executable that generates the table
  add_executable(MakeTable MakeTable.cxx)

  # add the command to generate the source code
  add_custom_command(
    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
    COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
    DEPENDS MakeTable
    )

  # library that just does sqrt
  add_library(SqrtLibrary STATIC
              mysqrt.cxx
              ${CMAKE_CURRENT_BINARY_DIR}/Table.h
              )

  # state that we depend on our binary dir to find Table.h
  target_include_directories(SqrtLibrary PRIVATE
                             ${CMAKE_CURRENT_BINARY_DIR}
                             )

  target_link_libraries(MathFunctions PRIVATE SqrtLibrary)
endif()

# define the symbol stating we are using the declspec(dllexport) when
# building on windows
target_compile_definitions(MathFunctions PRIVATE "EXPORTING_MYMATH")

# install rules
set(installable_libs MathFunctions)
if(TARGET SqrtLibrary)
  list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs} DESTINATION lib)
install(FILES MathFunctions.h DESTINATION include)

接下來,更新 MathFunctions/mysqrt.cxx 以使用 mathfunctionsdetail 命名空間

#include <iostream>

#include "MathFunctions.h"

// include the generated table
#include "Table.h"

namespace mathfunctions {
namespace detail {
// a hack square root calculation using simple operations
double mysqrt(double x)
{
  if (x <= 0) {
    return 0;
  }

  // use the table to help find an initial value
  double result = x;
  if (x >= 1 && x < 10) {
    std::cout << "Use the table to help find an initial value " << std::endl;
    result = sqrtTable[static_cast<int>(x)];
  }

  // do ten iterations
  for (int i = 0; i < 10; ++i) {
    if (result <= 0) {
      result = 0.1;
    }
    double delta = x - (result * result);
    result = result + 0.5 * delta / result;
    std::cout << "Computing sqrt of " << x << " to be " << result << std::endl;
  }

  return result;
}
}
}

我們還需要在 tutorial.cxx 中進行一些變更,使其不再使用 USE_MYMATH

  1. 始終包含 MathFunctions.h

  2. 始終使用 mathfunctions::sqrt

  3. 不要包含 cmath

最後,更新 MathFunctions/MathFunctions.h 以使用 dll 匯出定義

#if defined(_WIN32)
#  if defined(EXPORTING_MYMATH)
#    define DECLSPEC __declspec(dllexport)
#  else
#    define DECLSPEC __declspec(dllimport)
#  endif
#else // non windows
#  define DECLSPEC
#endif

namespace mathfunctions {
double DECLSPEC sqrt(double x);
}

此時,如果您建置所有內容,您可能會注意到連結失敗,因為我們將沒有位置獨立程式碼的靜態程式庫與具有位置獨立程式碼的程式庫結合在一起。解決方案是明確設定 SqrtLibrary 的 POSITION_INDEPENDENT_CODE 目標屬性,使其無論建置類型為何都為 True。

  # state that SqrtLibrary need PIC when the default is shared libraries
  set_target_properties(SqrtLibrary PROPERTIES
                        POSITION_INDEPENDENT_CODE ${BUILD_SHARED_LIBS}
                        )

  target_link_libraries(MathFunctions PRIVATE SqrtLibrary)

練習:我們修改了 MathFunctions.h 以使用 dll 匯出定義。使用 CMake 文件,您可以找到簡化此操作的輔助模組嗎?

新增產生器表達式 (步驟 10)

產生器 表達式在建置系統產生期間進行評估,以產生特定於每個建置組態的資訊。

產生器 表達式允許用於許多目標屬性的內容中,例如 LINK_LIBRARIESINCLUDE_DIRECTORIESCOMPILE_DEFINITIONS 和其他屬性。它們也可用於在使用命令來填入這些屬性時,例如 target_link_librariestarget_include_directoriestarget_compile_definitions 和其他命令。

產生器 表達式可用於啟用條件連結、編譯時使用的條件定義、條件包含目錄等。條件可能基於建置組態、目標屬性、平台資訊或任何其他可查詢的資訊。

有不同類型的 產生器 表達式,包括邏輯、資訊和輸出表達式。

邏輯表達式用於建立條件輸出。基本表達式是 0 和 1 表達式。$<0:...> 會產生空字串,而 <1:...> 會產生「…」的內容。它們也可以巢狀。

產生器 表達式 的常見用法是有條件地新增編譯器旗標,例如語言層級或警告的旗標。一種很好的模式是將此資訊與 INTERFACE 目標相關聯,允許此資訊傳播。讓我們從建構 INTERFACE 目標並指定所需的 C++ 標準層級 11 開始,而不是使用 CMAKE_CXX_STANDARD

因此,以下程式碼

# specify the C++ standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED True)

將被取代為

add_library(tutorial_compiler_flags INTERFACE)
target_compile_features(tutorial_compiler_flags INTERFACE cxx_std_11)

接下來,我們新增專案所需的編譯器警告旗標。由於警告旗標會因編譯器而異,因此我們使用 COMPILE_LANG_AND_ID 產生器表達式來控制要套用哪些旗標,給定語言和一組編譯器 ID,如下所示

set(gcc_like_cxx "$<COMPILE_LANG_AND_ID:CXX,ARMClang,AppleClang,Clang,GNU>")
set(msvc_cxx "$<COMPILE_LANG_AND_ID:CXX,MSVC>")
target_compile_options(tutorial_compiler_flags INTERFACE
  "$<${gcc_like_cxx}:$<BUILD_INTERFACE:-Wall;-Wextra;-Wshadow;-Wformat=2;-Wunused>>"
  "$<${msvc_cxx}:$<BUILD_INTERFACE:-W3>>"
)

查看此程式碼,我們看到警告旗標封裝在 BUILD_INTERFACE 條件內。這樣做是為了讓已安裝專案的使用者不會繼承我們的警告旗標。

練習:修改 MathFunctions/CMakeLists.txt,使所有目標都具有對 tutorial_compiler_flagstarget_link_libraries 呼叫。

新增匯出組態 (步驟 11)

在教學的安裝和測試 (步驟 4)中,我們新增了讓 CMake 安裝專案的程式庫和標頭的功能。在建置安裝程式 (步驟 7)中,我們新增了將此資訊封裝起來的功能,以便可以分發給其他人。

下一步是新增必要的資訊,以便其他 CMake 專案可以使用我們的專案,無論是從建置目錄、本機安裝還是封裝時。

第一步是更新我們的 install(TARGETS) 命令,不僅要指定 DESTINATION,還要指定 EXPORTEXPORT 關鍵字會產生並安裝一個 CMake 檔案,其中包含從安裝樹匯入 install 命令中列出的所有目標的程式碼。因此,讓我們繼續並明確地 EXPORT MathFunctions 函式庫,方法是更新 MathFunctions/CMakeLists.txt 中的 install 命令,使其看起來像這樣

set(installable_libs MathFunctions tutorial_compiler_flags)
if(TARGET SqrtLibrary)
  list(APPEND installable_libs SqrtLibrary)
endif()
install(TARGETS ${installable_libs}
        DESTINATION lib
        EXPORT MathFunctionsTargets)
install(FILES MathFunctions.h DESTINATION include)

現在我們已經匯出 MathFunctions,我們還需要明確安裝產生的 MathFunctionsTargets.cmake 檔案。這可以透過在頂層 CMakeLists.txt 的底部新增以下內容來完成

install(EXPORT MathFunctionsTargets
  FILE MathFunctionsTargets.cmake
  DESTINATION lib/cmake/MathFunctions
)

此時,您應該嘗試執行 CMake。如果一切設定正確,您會看到 CMake 會產生如下的錯誤

Target "MathFunctions" INTERFACE_INCLUDE_DIRECTORIES property contains
path:

  "/Users/robert/Documents/CMakeClass/Tutorial/Step11/MathFunctions"

which is prefixed in the source directory.

CMake 想要表達的是,在產生匯出資訊時,它會匯出一個本質上與目前機器相關的路徑,並且在其他機器上無效。解決方案是更新 MathFunctions target_include_directories,使其理解從建置目錄和從安裝/套件使用時,需要不同的 INTERFACE 位置。這意味著將 MathFunctions 的 target_include_directories 呼叫轉換成如下所示

target_include_directories(MathFunctions
                           INTERFACE
                            $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
                            $<INSTALL_INTERFACE:include>
                           )

更新完成後,我們可以重新執行 CMake 並驗證它不再發出警告。

此時,我們已讓 CMake 正確封裝所需的目標資訊,但我們仍然需要產生一個 MathFunctionsConfig.cmake,以便 CMake find_package 命令可以找到我們的專案。因此,讓我們繼續在專案的頂層新增一個名為 Config.cmake.in 的檔案,其內容如下


@PACKAGE_INIT@

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

然後,為了正確設定和安裝該檔案,請在頂層 CMakeLists.txt 的底部新增以下內容

install(EXPORT MathFunctionsTargets
  FILE MathFunctionsTargets.cmake
  DESTINATION lib/cmake/MathFunctions
)

include(CMakePackageConfigHelpers)
# generate the config file that is includes the exports
configure_package_config_file(${CMAKE_CURRENT_SOURCE_DIR}/Config.cmake.in
  "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake"
  INSTALL_DESTINATION "lib/cmake/example"
  NO_SET_AND_CHECK_MACRO
  NO_CHECK_REQUIRED_COMPONENTS_MACRO
  )
# generate the version file for the config file
write_basic_package_version_file(
  "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfigVersion.cmake"
  VERSION "${Tutorial_VERSION_MAJOR}.${Tutorial_VERSION_MINOR}"
  COMPATIBILITY AnyNewerVersion
)

# install the configuration file
install(FILES
  ${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsConfig.cmake
  DESTINATION lib/cmake/MathFunctions
  )

此時,我們已為我們的專案產生一個可重新定位的 CMake 設定,該設定可在專案安裝或封裝後使用。如果我們希望我們的專案也可以從建置目錄中使用,我們只需在頂層 CMakeLists.txt 的底部新增以下內容即可

export(EXPORT MathFunctionsTargets
  FILE "${CMAKE_CURRENT_BINARY_DIR}/MathFunctionsTargets.cmake"
)

透過此匯出呼叫,我們現在產生一個 Targets.cmake,允許其他專案使用建置目錄中設定的 MathFunctionsConfig.cmake,而無需安裝。

封裝偵錯和發行版本(步驟 12)

注意:此範例適用於單一設定產生器,不適用於多設定產生器(例如 Visual Studio)。

依預設,CMake 的模型是建置目錄只包含一個單一設定,無論是偵錯 (Debug)、發行 (Release)、最小大小發行 (MinSizeRel) 或帶偵錯資訊發行 (RelWithDebInfo)。但是,可以設定 CPack 來捆綁多個建置目錄,並建構一個包含同一專案多個設定的套件。

首先,我們要確保偵錯和發行版本針對將要安裝的可執行檔和函式庫使用不同的名稱。讓我們將 d 作為偵錯可執行檔和函式庫的後綴。

在頂層 CMakeLists.txt 檔案的開頭附近設定 CMAKE_DEBUG_POSTFIX

set(CMAKE_DEBUG_POSTFIX d)

add_library(tutorial_compiler_flags INTERFACE)

以及 tutorial 可執行檔上的 DEBUG_POSTFIX 屬性

add_executable(Tutorial tutorial.cxx)
set_target_properties(Tutorial PROPERTIES DEBUG_POSTFIX ${CMAKE_DEBUG_POSTFIX})

target_link_libraries(Tutorial PUBLIC MathFunctions)

讓我們也為 MathFunctions 函式庫新增版本編號。在 MathFunctions/CMakeLists.txt 中,設定 VERSIONSOVERSION 屬性

set_property(TARGET MathFunctions PROPERTY VERSION "1.0.0")
set_property(TARGET MathFunctions PROPERTY SOVERSION "1")

Step12 目錄中,建立 debugrelease 子目錄。佈局看起來會像這樣

- Step12
   - debug
   - release

現在我們需要設定偵錯和發行版本建置。我們可以使用 CMAKE_BUILD_TYPE 來設定組態類型

cd debug
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake --build .
cd ../release
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .

現在偵錯和發行版本建置都已完成,我們可以使用自訂設定檔將兩個版本建置封裝到單一發行版本中。在 Step12 目錄中,建立一個名為 MultiCPackConfig.cmake 的檔案。在此檔案中,首先包含由 cmake 可執行檔建立的預設設定檔。

接下來,使用 CPACK_INSTALL_CMAKE_PROJECTS 變數來指定要安裝的專案。在這種情況下,我們想要安裝偵錯和發行版本。

include("release/CPackConfig.cmake")

set(CPACK_INSTALL_CMAKE_PROJECTS
    "debug;Tutorial;ALL;/"
    "release;Tutorial;ALL;/"
    )

Step12 目錄中,執行 cpack,使用 config 選項指定我們的自訂設定檔

cpack --config MultiCPackConfig.cmake