系統檢查

本章將說明如何使用 CMake 來檢查軟體建置所在系統的環境。這是建立跨平台應用程式或程式庫的關鍵因素。它涵蓋如何尋找和使用系統和使用者安裝的標頭檔和程式庫。它還涵蓋了 CMake 的一些更進階的功能,包括 try_compiletry_run 命令。這些命令是非常強大的工具,可用於確定託管您軟體的系統和編譯器的功能。

使用標頭檔和程式庫

許多 C 和 C++ 程式都依賴外部程式庫;然而,當涉及到編譯和連結專案的實際層面時,對於開發人員和使用者來說,利用現有的程式庫可能會很困難。問題通常在軟體建置於開發所在的系統以外的系統上時就會出現。當程式庫和標頭檔沒有安裝在新的電腦上的相同位置,且建置系統無法找到它們時,關於程式庫和標頭檔位置的假設就會顯現出來。CMake 有許多功能可以幫助開發人員將外部軟體程式庫整合到專案中。

與此類整合最相關的 CMake 命令是 find_filefind_libraryfind_pathfind_programfind_package 命令。對於大多數 C 和 C++ 程式庫,find_libraryfind_path 的組合就足以與已安裝的程式庫進行編譯和連結。命令 find_library 可用於定位程式庫或允許使用者定位程式庫,而 find_path 可用於尋找專案中代表性的包含檔案的路徑。例如,如果您想連結到 tiff 程式庫,您可以在 CMakeLists.txt 檔案中使用以下命令

# find libtiff, looking in some standard places
find_library(TIFF_LIBRARY
             NAMES tiff tiff2
             PATHS /usr/local/lib /usr/lib
            )

# find tiff.h looking in some standard places
find_path(TIFF_INCLUDES tiff.h
           /usr/local/include
           /usr/include
          )

add_executable(mytiff mytiff.c )

target_link_libraries(mytiff ${TIFF_LIBRARY})

target_include_directories(mytiff ${TIFF_INCLUDES})

第一個使用的命令是 find_library,在這種情況下,它將尋找名稱為 tiff 或 tiff2 的程式庫。find_library 命令只需要程式庫的基本名稱,而不需要任何平台特定的前綴或後綴,例如 .lib 和 .dll。當 CMake 嘗試尋找程式庫時,會自動將適用於執行 CMake 系統的前綴和後綴新增到程式庫名稱。所有的 FIND_* 命令都會在 PATH 環境變數中尋找。此外,這些命令允許將其他搜尋路徑指定為 PATHS 標記引數之後列出的引數。除了支援標準路徑之外,Windows 登錄檔項目和環境變數還可用於建構搜尋路徑。登錄檔項目的語法如下

[HKEY_CURRENT_USER\\Software\\Kitware\\Path;Build1]

由於軟體可以安裝在許多不同的位置,因此 CMake 無法每次都找到程式庫,但應該涵蓋大多數標準安裝。find_* 命令會自動建立一個快取變數,以便使用者可以從 CMake GUI 覆寫或指定位置。這樣,如果 CMake 無法找到它正在尋找的檔案,使用者仍然有機會指定它們。如果 CMake 找不到檔案,則值會設定為 VAR-NOTFOUND;此值會告知 CMake 每次執行 CMake 的配置步驟時都應該繼續尋找。請注意,在 if 陳述式中,VAR-NOTFOUND 的值會評估為 false。

下一個使用的命令是 find_path,這是一個通用命令,在此範例中,它用於定位程式庫中的標頭檔。標頭檔和程式庫通常安裝在不同的位置,而且編譯和連結使用它們的程式都需要這兩個位置。find_path 的使用方式與 find_library 類似,儘管它只支援一個名稱和搜尋路徑清單。

CMakeLists 檔案的其餘部分可以使用 find_* 命令建立的變數。可以使用這些變數而無需檢查有效值,因為如果沒有設定任何必要的變數,CMake 會列印錯誤訊息通知使用者。然後,使用者可以設定快取值並重新配置,直到訊息消失。或者,CMakeLists 檔案可以使用 if 命令來使用替代的程式庫或選項來建置專案,如果找不到程式庫。

從上面的範例中,您可以看到使用 find_* 命令如何幫助您的軟體在各種系統上編譯。值得注意的是,find_* 命令會從第一個引數和第一個路徑開始搜尋相符的項目,因此在列出路徑和程式庫名稱時,請先列出您偏好的路徑和名稱。如果有多個版本的程式庫,而且您偏好 tiff 而不是 tiff2,請確保它們依該順序列出。

系統屬性

雖然在 C 和 C++ 程式碼中,將平台特定的程式碼新增到前處理器 ifdef 指令中是一種常見的做法,但為了獲得最大的可攜性,應該避免這樣做。軟體不應該使用 ifdefs 來調整到特定平台,而是應該調整到由一組功能組成的規範系統。針對特定系統進行編碼會降低軟體的移植性,因為系統及其支援的功能會隨著時間的推移而變化,甚至會因系統而異。過去可能無法在平台上運作的功能,在未來可能成為該平台的必要功能。下列程式碼片段說明了針對規範系統和特定系統進行編碼之間的差異

// coding to a feature
#ifdef HAS_FOOBAR_CALL
  foobar();
#else
  myfoobar();
#endif

// coding to specific platforms
#if defined(SUN) && defined(HPUX) && !defined(GNUC)
  foobar();
#else
  myfoobar();
#endif

第二種方法的問題在於,程式碼必須針對編譯軟體的每個新平台進行修改。例如,未來版本的 SUN 可能不再有 foobar 呼叫。使用 HAS_FOOBAR_CALL 方法,只要正確定義 HAS_FOOBAR_CALL,軟體就能正常運作,而這正是 CMake 可以發揮作用的地方。CMake 可以透過使用 try_compiletry_run 命令,正確且自動地定義 HAS_FOOBAR_CALL。這些命令可用於在 CMake 設定步驟期間編譯和執行小型測試程式。測試程式將被傳送到將用於建置專案的編譯器,如果發生錯誤,則可以停用該功能。這些命令要求您編寫一個小型 C 或 C++ 程式來測試該功能。例如,要測試系統是否提供 foobar 呼叫,請嘗試編譯一個使用 foobar 的簡單程式。首先,編寫簡單的測試程式(在此範例中為 testNeedFoobar.c),然後將 CMake 呼叫新增至 CMakeLists 檔案中,以嘗試編譯該程式碼。如果編譯成功,則 HAS_FOOBAR_CALL 將設定為 true。

// --- testNeedFoobar.c -----

#include <foobar.h>
main()
{
  foobar();
}
# --- testNeedFoobar.cmake ---

try_compile (HAS_FOOBAR_CALL
  ${CMAKE_BINARY_DIR}
  ${PROJECT_SOURCE_DIR}/testNeedFoobar.c
  )

現在 HAS_FOOBAR_CALL 已在 CMake 中正確設定,您可以使用 target_compile_definitions 命令,在您的原始碼中使用它。或者,也可以設定標頭檔。這將在名為「如何設定標頭檔」的章節中進一步討論。

有時,編譯測試程式還不夠。在某些情況下,您可能實際上想要編譯並執行程式以取得其輸出。這方面的一個很好的例子是測試機器的位元組順序。以下範例顯示如何編寫一個 CMake 將編譯和執行的小程式,以判斷機器的位元組順序。

// ---- TestByteOrder.c ------

int main () {
  /* Are we most significant byte first or last */
  union
  {
    long l;
    char c[sizeof (long)];
  } u;
  u.l = 1;
  exit (u.c[sizeof (long) - 1] == 1);
}
# ----- TestByteOrder.cmake-----

try_run(RUN_RESULT_VAR
  COMPILE_RESULT_VAR
  ${CMAKE_BINARY_DIR}
  ${PROJECT_SOURCE_DIR}/Modules/TestByteOrder.c
  OUTPUT_VARIABLE OUTPUT
  )

執行的回傳結果將進入 RUN_RESULT_VAR,編譯的結果將進入 COMPILE_RESULT_VAR,而執行的任何輸出將進入 OUTPUT。您可以使用這些變數向專案的使用者報告偵錯資訊。

對於小型測試程式,可以使用具有 WRITE 選項的 file 命令,從 CMakeLists 檔案建立原始碼檔案。以下範例測試 C 編譯器,以驗證其是否可以執行。

file(WRITE
  ${CMAKE_BINARY_DIR}/CMakeTmp/testCCompiler.c
  "int main(){return 0;}"
  )

try_compile(CMAKE_C_COMPILER_WORKS
  ${CMAKE_BINARY_DIR}
  ${CMAKE_BINARY_DIR}/CMakeTmp/testCCompiler.c
  OUTPUT_VARIABLE OUTPUT
  )

對於更進階的 try_compiletry_run 操作,可能需要將旗標傳遞給編譯器或 CMake。這兩個命令都支援可選的引數 CMAKE_FLAGSCOMPILE_DEFINITIONSCMAKE_FLAGS 可用於將 -DVAR:TYPE=VALUE 旗標傳遞給 CMake。COMPILE_DEFINITIONS 的值會直接傳遞到編譯器命令列。

CMake 中提供了一些預先定義的 try-run 和 try-compile 模組 cmake-modules(7),其中一些列在下方。這些模組允許執行一些常見的檢查,而無需為每個測試建立原始碼檔案。許多這些模組會查看 CMAKE_REQUIRED_FLAGSCMAKE_REQUIRED_LIBRARIES 變數的目前值,以將額外的編譯旗標或連結程式庫新增至測試中。

CheckIncludeFile

提供一個巨集,可透過採用兩個引數來檢查系統上的包含檔案,第一個引數是要尋找的包含檔案,第二個引數是儲存結果的變數。額外的 CFlags 可以作為第三個引數傳遞,或透過設定 CMAKE_REQUIRED_FLAGS 來傳遞。

CheckIncludeFileCXX

提供一個巨集,可透過採用兩個引數來檢查 C++ 程式中的包含檔案,第一個引數是要尋找的包含檔案,第二個引數是儲存結果的變數。額外的 CFlags 可以作為第三個引數傳遞。

CheckIncludeFiles

提供一個巨集,可檢查是否可以一起包含指定的標頭檔。如果設定了 CMAKE_REQUIRED_FLAGS,則此巨集會使用它,並且當您想要檢查的標頭檔依賴於首先包含另一個標頭檔時,此巨集非常有用。

CheckLibraryExists

提供一個巨集,可透過採用四個引數來檢查程式庫是否存在,第一個引數是要檢查的程式庫名稱;第二個引數是應該位於該程式庫中的函數名稱;第三個引數是要尋找程式庫的位置;第四個引數是儲存結果的變數。如果設定了 CMAKE_REQUIRED_FLAGSCMAKE_REQUIRED_LIBRARIES,則此巨集會使用它們。

CheckSymbolExists

提供一個巨集,可透過採用三個引數來檢查是否在標頭檔中定義了符號,第一個引數是要尋找的符號;第二個引數是要嘗試包含的標頭檔清單;第三個引數是儲存結果的位置。如果設定了 CMAKE_REQUIRED_FLAGSCMAKE_REQUIRED_LIBRARIES,則此巨集會使用它們。

CheckTypeSize

提供一個巨集,可透過採用兩個引數來判斷變數類型的大小 (以位元組為單位),第一個引數是要評估的類型,第二個引數是儲存結果的位置。如果設定了 CMAKE_REQUIRED_FLAGSCMAKE_REQUIRED_LIBRARIES,則會使用它們。

CheckVariableExists

提供一個巨集,可透過採用兩個引數來檢查全域變數是否存在,第一個引數是要尋找的變數,第二個引數是儲存結果的變數。此巨集會建立指定變數的原型,然後嘗試使用它。如果測試程式編譯成功,則表示變數存在。這僅適用於 C 變數。如果設定了 CMAKE_REQUIRED_FLAGSCMAKE_REQUIRED_LIBRARIES,則此巨集會使用它們。

請考慮以下範例,其中顯示了如何使用這些模組的各種方式來計算平台的屬性。在本範例的開頭,會從 CMake 載入四個模組。本範例的其餘部分使用這些模組中定義的巨集,來測試標頭檔、程式庫、符號和類型大小。

# Include all the necessary files for macros
include(CheckIncludeFiles)
include(CheckLibraryExists)
include(CheckSymbolExists)
include(CheckTypeSize)

# Check for header files
set(INCLUDES "")
check_include_files("${INCLUDES};winsock.h" HAVE_WINSOCK_H)

if(HAVE_WINSOCK_H)
  set(INCLUDES ${INCLUDES} winsock.h)
endif()

check_include_files("${INCLUDES};io.h" HAVE_IO_H)
if (HAVE_IO_H)
  set(INCLUDES ${INCLUDES} io.h)
endif()

# Check for all needed libraries
set(LIBS "")
check_library_exists("dl;${LIBS}" dlopen "" HAVE_LIBDL)
if(HAVE_LIBDL)
  set(LIBS ${LIBS} dl)
endif()

check_library_exists("ucb;${LIBS}" gethostname "" HAVE_LIBUCB)
if(HAVE_LIBUCB)
  set(LIBS ${LIBS} ucb)
endif()

# Add the libraries we found to the libraries to use when
# looking for symbols with the check_symbol_exists macro
set(CMAKE_REQUIRED_LIBRARIES ${LIBS})

# Check for some functions that are used
check_symbol_exists(socket "${INCLUDES}" HAVE_SOCKET)
check_symbol_exists(poll "${INCLUDES}" HAVE_POLL)

# Various type sizes
check_type_size(int SIZEOF_INT)
check_type_size(size_t SIZEOF_SIZE_T)

如何將參數傳遞給編譯

一旦您判斷了您感興趣的系統功能,就該根據已發現的功能來設定軟體了。有兩種常見的方式可以將此資訊傳遞給編譯器:在編譯行上,或使用預先設定的標頭。第一種方式是在編譯行上傳遞定義。可以使用 target_compile_definitions 命令,從 CMakeLists 檔案將前置處理器定義傳遞給編譯器。例如,C 程式碼中的常見做法是能夠選擇性地編譯/取消偵錯陳述式。

#ifdef DEBUG_BUILD
  printf("the value of v is %d", v);
#endif

可以使用 option 命令,使用 CMake 變數來開啟或關閉偵錯建置

option(DEBUG_BUILD
      "Build with extra debug print messages.")

if(DEBUG_BUILD)
  target_compile_definitions(mytarget PUBLIC DEBUG_BUILD)
endif()

另一個範例是告訴編譯器本章先前討論的 HAS_FOOBAR_CALL 測試的結果。您可以使用以下方式執行此操作

if (HAS_FOOBAR_CALL)
  target_compile_definitions(mytarget PUBLIC HAS_FOOBAR_CALL)
endif()

如何設定標頭檔

將定義傳遞至原始碼的第二種方法是設定標頭檔。標頭檔將包含建置專案所需的所有 #define 巨集。若要使用 CMake 設定檔案,則會使用 configure_file 命令。此命令需要一個由 CMake 剖析的輸入檔案,以產生一個展開或替換所有變數的輸出檔案。在 configure_file 的輸入檔案中,有三種方式可以指定變數。

#cmakedefine VARIABLE

如果 VARIABLE 為 true,則結果將會是

#define VARIABLE

如果 VARIABLE 為 false,則結果將會是

/* #undef VARIABLE */

在撰寫要設定的檔案時,請考慮使用 @VARIABLE@ 而非 ${VARIABLE} 作為預期會由 CMake 展開的變數。由於 ${} 語法通常被其他語言使用,因此使用者可以透過傳遞 @ONLY 選項給 configure_file 命令,來指示命令僅展開使用 @var@ 語法的變數;如果您正在設定可能包含您想要保留的 ${var} 字串的指令碼,這會很有用。這一點很重要,因為如果 var 未在 CMake 中定義,CMake 會將所有出現的 ${var} 替換為空字串。

以下範例為包含前置處理器變數的專案設定 .h 檔案。第一個定義指出程式庫中是否存在 FOOBAR 呼叫,而下一個定義則包含建置樹的路徑。

# ---- CMakeLists.txt file-----

# Configure a file from the source tree
# called projectConfigure.h.in and put
# the resulting configured file in the build
# tree and call it projectConfigure.h
configure_file(
   ${PROJECT_SOURCE_DIR}/projectConfigure.h.in
   ${PROJECT_BINARY_DIR}/projectConfigure.h
   @ONLY
   )
// -----projectConfigure.h.in file------
/* define a variable to tell the code if the */
/* foobar call is available on this system */
#cmakedefine HAS_FOOBAR_CALL

/* define a variable with the path to the */
/* build directory  */
#define PROJECT_BINARY_DIR "@PROJECT_BINARY_DIR@"

務必將檔案設定到二進位樹,而不是原始樹。單一原始樹可能會被多個建置樹或平台共用。透過將檔案設定到二進位樹,建置或平台之間的差異將會被隔離在建置樹中,並且不會損壞其他建置。這表示您需要使用 target_include_directories 命令,將您設定標頭檔的建置樹目錄包含到專案的包含目錄清單中。