撰寫 CMakeLists 檔案

本章將涵蓋為您的軟體撰寫有效 CMakeLists 檔案的基本知識。它將涵蓋您在大多數專案中需要處理的基本命令和問題。雖然 CMake 可以處理極其複雜的專案,但對於大多數專案,您會發現本章的內容會告訴您您需要知道的一切。CMake 由為軟體專案編寫的 CMakeLists.txt 檔案驅動。CMakeLists 檔案決定了一切,從向使用者呈現哪些選項到編譯哪些原始碼檔案。除了討論如何編寫 CMakeLists 檔案外,本章還將涵蓋如何使其具有穩健性和可維護性。

編輯 CMakeLists 檔案

CMakeLists 檔案幾乎可以在任何文字編輯器中編輯。某些編輯器,例如 Notepad++,內建了 CMake 語法高亮和縮排支援。對於 Emacs 或 Vim 等編輯器,CMake 包含縮排和語法高亮模式。這些可以在來源發佈的 Auxiliary 目錄中找到,或從 CMake 下載頁面下載。

在任何支援的產生器(Makefiles、Visual Studio 等)中,如果您編輯 CMakeLists 檔案並重新建置,則有一些規則會自動調用 CMake 來更新產生的檔案(例如 Makefiles 或專案檔案),視需要而定。這有助於確保您產生的檔案始終與您的 CMakeLists 檔案同步。

CMake 語言

CMake 語言由註解、命令和變數組成。

註解

註解以 # 開頭,並延伸到該行末尾。有關更多詳細資訊,請參閱cmake-language 手冊。

變數

CMakeLists 檔案使用變數的方式很像任何程式語言。CMake 變數名稱區分大小寫,且只能包含字母數字字元和底線。

CMake 會自動定義許多有用的變數,並在cmake-variables 手冊中討論。這些變數以 CMAKE_ 開頭。避免為您的專案特定變數使用此命名慣例(並且,理想情況下,建立您自己的命名慣例)。

所有 CMake 變數在內部都儲存為字串,儘管有時可能會被解釋為其他類型。

使用set 命令來設定變數值。以最簡單的形式,set 的第一個引數是變數的名稱,其餘引數是值。多個值引數會打包成以分號分隔的清單,並以字串形式儲存在變數中。例如

set(Foo "")      # 1 quoted arg -> value is ""
set(Foo a)       # 1 unquoted arg -> value is "a"
set(Foo "a b c") # 1 quoted arg -> value is "a b c"
set(Foo a b c)   # 3 unquoted args -> value is "a;b;c"

可以使用語法 ${VAR} 在命令引數中引用變數,其中 VAR 是變數名稱。如果未定義具名變數,則參考會被空字串取代;否則,它會被變數的值取代。取代會在展開未加引號的引數之前執行,因此包含分號的變數值會取代原始未加引號的引數,分割成零個或多個引數。例如

set(Foo a b c)    # 3 unquoted args -> value is "a;b;c"
command(${Foo})   # unquoted arg replaced by a;b;c
                  # and expands to three arguments
command("${Foo}") # quoted arg value is "a;b;c"
set(Foo "")       # 1 quoted arg -> value is empty string
command(${Foo})   # unquoted arg replaced by empty string
                  # and expands to zero arguments
command("${Foo}") # quoted arg value is empty string

系統環境變數和 Windows 登錄值可以直接在 CMake 中存取。若要存取系統環境變數,請使用語法 $ENV{VAR}。CMake 也可以使用 [HKEY_CURRENT_USER\\Software\\path1\\path2;key] 形式的語法在許多命令中參考登錄項目,其中路徑是由登錄樹和機碼建立的。

變數範圍

CMake 中的變數範圍與大多數語言略有不同。當您設定變數時,它對於目前的 CMakeLists 檔案或函數、任何子目錄的 CMakeLists 檔案、調用的任何函數或巨集,以及使用 include 命令包含的任何檔案都是可見的。當處理新的子目錄(或呼叫函數)時,會建立新的變數範圍,並使用呼叫範圍中所有變數的目前值進行初始化。在子範圍中建立的任何新變數,或對現有變數所做的變更,都不會影響父範圍。考慮以下範例

function(foo)
  message(${test}) # test is 1 here
  set(test 2)
  message(${test}) # test is 2 here, but only in this scope
endfunction()

set(test 1)
foo()
message(${test}) # test will still be 1 here

在某些情況下,您可能希望函數或子目錄在其父範圍中設定變數。CMake 有一種方法可以從函數傳回值,而且可以使用 set 命令的 PARENT_SCOPE 選項來完成。我們可以修改先前的範例,以便函數 foo 變更其父範圍中測試的值,如下所示

function(foo)
  message(${test}) # test is 1 here
  set(test 2 PARENT_SCOPE)
  message(${test}) # test still 1 in this scope
endfunction()

set(test 1)
foo()
message(${test}) # test will now be 2 here

CMake 中的變數會以執行 set 命令的順序來定義。

考慮以下範例

# FOO is undefined

set(FOO 1)
# FOO is now set to 1

set(FOO 0)
# FOO is now set to 0

若要了解變數的範圍,請考慮此範例

set(foo 1)

# process the dir1 subdirectory
add_subdirectory(dir1)

# include and process the commands in file1.cmake
include(file1.cmake)

set(bar 2)
# process the dir2 subdirectory
add_subdirectory(dir2)

# include and process the commands in file2.cmake
include(file2.cmake)

在此範例中,由於變數 foo 在開頭定義,因此在處理 dir1 和 dir2 時都會定義。相較之下,bar 只會在處理 dir2 時定義。同樣地,foo 在處理 file1.cmake 和 file2.cmake 時都會定義,而 bar 只會在處理 file2.cmake 時定義。

命令

命令包含命令名稱、左括號、以空格分隔的引數和右括號。每個命令都會按照其在 CMakeLists 檔案中出現的順序進行評估。請參閱cmake-commands 手冊,以取得 CMake 命令的完整清單。

CMake 不再區分命令名稱的大小寫,因此,當您看到 command 時,您可以使用 COMMANDCommand 來代替。最佳實務做法是使用小寫命令。所有空白(空格、換行、索引標籤)都會被忽略,除了分隔引數之外。因此,命令可以跨越多行,只要命令名稱和左括號在同一行即可。

CMake 命令引數以空格分隔且區分大小寫。命令引數可以加上引號或不加引號。加引號的引數以雙引號 (") 開始和結束,並且始終代表剛好一個引數。任何包含在值內的雙引號都必須使用反斜線逸出。請考慮針對需要逸出的引數使用括號引數,請參閱cmake-language 手冊。未加引號的引數以雙引號以外的任何字元開始(後面的雙引號是文字),並藉由在值內部分隔分號,自動展開為零個或多個引數。例如

command("")          # 1 quoted argument
command("a b c")     # 1 quoted argument
command("a;b;c")     # 1 quoted argument
command("a" "b" "c") # 3 quoted arguments
command(a b c)       # 3 unquoted arguments
command(a;b;c)       # 1 unquoted argument expands to 3

基本命令

如我們稍早所見,setunset 命令會明確地設定或取消設定變數。stringlistseparate_arguments 命令提供了字串和清單的基本操作。

add_executableadd_library 命令是用於定義要建置的可執行檔和程式庫,以及哪些原始碼檔案組成它們的主要命令。對於 Visual Studio 專案,原始碼檔案會像往常一樣顯示在 IDE 中,但專案使用的任何標頭檔案都不會顯示。若要讓標頭檔案顯示,只需將它們新增至可執行檔或程式庫的原始碼檔案清單;這可以針對所有產生器完成。任何不直接使用標頭檔案的產生器(例如基於 Makefile 的產生器)都會直接忽略它們。

流程控制

CMake 語言提供三種流程控制結構,以協助組織您的 CMakeLists 檔案並保持其可維護性。

條件語句

首先,我們將考慮 if 命令。在許多方面,CMake 中的 if 命令就像任何其他語言中的 if 命令一樣。它會評估其表達式,並使用它來執行其主體中的程式碼,或者選擇性地執行 else 子句中的程式碼。例如

if(FOO)
  # do something here
else()
  # do something else
endif()

CMake 也支援 elseif 以協助依序測試多個條件。例如

if(MSVC80)
  # do something here
elseif(MSVC90)
  # do something else
elseif(APPLE)
  # do something else
endif()

if 命令說明了它可以測試的許多條件。

迴圈結構

foreachwhile 命令允許您處理依序發生的重複性任務。break 命令會在 foreachwhile 迴圈正常結束之前跳出迴圈。

foreach 命令使您能夠針對清單中的成員重複執行一組 CMake 命令。考慮以下從 VTK 改編的範例

foreach(tfile
        TestAnisotropicDiffusion2D
        TestButterworthLowPass
        TestButterworthHighPass
        TestCityBlockDistance
        TestConvolve
        )
  add_test(${tfile}-image ${VTK_EXECUTABLE}
    ${VTK_SOURCE_DIR}/Tests/rtImageTest.tcl
    ${VTK_SOURCE_DIR}/Tests/${tfile}.tcl
    -D ${VTK_DATA_ROOT}
    -V Baseline/Imaging/${tfile}.png
    -A ${VTK_SOURCE_DIR}/Wrapping/Tcl
    )
endforeach()

foreach 命令的第一個參數是變數的名稱,該變數在迴圈的每次迭代中採用不同的值;其餘的參數是要迴圈的值的清單。在此範例中,foreach 迴圈的主體只有一個 CMake 命令,即 add_test。在 foreach 的主體中,每次引用迴圈變數(在此範例中為 tfile)時,都會以清單中的目前值取代。在第一次迭代中,${tfile} 的出現將會被 TestAnisotropicDiffusion2D 取代。在下一次迭代中,${tfile} 將會被 TestButterworthLowPass 取代。foreach 迴圈將會繼續迴圈,直到處理完所有參數為止。

值得一提的是,foreach 迴圈可以巢狀,並且迴圈變數會在任何其他變數擴展之前被取代。這表示在 foreach 迴圈的主體中,您可以使用迴圈變數來建構變數名稱。在下面的程式碼中,迴圈變數 tfile 會被擴展,然後與 _TEST_RESULT 連接。然後,新的變數名稱會被擴展並測試是否與 FAILED 相符。

if(${${tfile}_TEST_RESULT} MATCHES FAILED)
  message("Test ${tfile} failed.")
endif()

while 命令根據測試條件提供迴圈。while 命令中測試表達式的格式與前面所述的 if 命令相同。請考慮以下由 CTest 使用的範例。請注意,CTest 會在內部更新 CTEST_ELAPSED_TIME 的值。

#####################################################
# run paraview and ctest test dashboards for 6 hours
#
while(${CTEST_ELAPSED_TIME} LESS 36000)
  set(START_TIME ${CTEST_ELAPSED_TIME})
  ctest_run_script("dash1_ParaView_vs71continuous.cmake")
  ctest_run_script("dash1_cmake_vs71continuous.cmake")
endwhile()

程序定義

macrofunction 命令支援可能散佈在 CMakeLists 檔案中的重複性任務。一旦定義了巨集或函式,之後處理的任何 CMakeLists 檔案都可以使用它。

CMake 中的函式很像 C 或 C++ 中的函式。您可以將參數傳遞給它,它們會成為函式內的變數。同樣,會定義一些標準變數,例如 ARGCARGVARGNARGV0ARGV1 等。函式呼叫具有動態範圍。在函式中,您處於新的變數範圍中;這就像使用 add_subdirectory 命令進入子目錄並處於新的變數範圍中一樣。在呼叫函式時定義的所有變數仍然存在定義,但是對變數的任何變更或新的變數僅存在於函式內。當函式傳回時,這些變數將會消失。簡單來說:當您調用函式時,會推送一個新的變數範圍;當它傳回時,該變數範圍會彈出。

function 命令定義一個新的函式。第一個參數是要定義的函式名稱;所有其他參數都是函式的形式參數。

function(DetermineTime _time)
  # pass the result up to whatever invoked this
  set(${_time} "1:23:45" PARENT_SCOPE)
endfunction()

# now use the function we just defined
DetermineTime(current_time)

if(DEFINED current_time)
  message(STATUS "The time is now: ${current_time}")
endif()

請注意,在這個範例中,_time 用於傳遞返回變數的名稱。 set 命令會使用 _time 的值來調用,該值將會是 current_time。最後,set 命令使用 PARENT_SCOPE 選項,將變數設定在呼叫者的作用域中,而不是本機作用域中。

巨集(Macros)的定義和調用方式與函式相同。主要差異在於巨集不會推送和彈出新的變數作用域,並且巨集的參數不會被視為變數,而是在執行之前被替換為字串。這非常類似於 C 或 C++ 中巨集和函式之間的差異。第一個參數是要建立的巨集的名稱;所有其他參數都是巨集的形式參數。

# define a simple macro
macro(assert TEST COMMENT)
  if(NOT ${TEST})
    message("Assertion failed: ${COMMENT}")
  endif()
endmacro()

# use the macro
find_library(FOO_LIB foo /usr/local/lib)
assert(${FOO_LIB} "Unable to find library foo")

上面的簡單範例建立了一個名為 assert 的巨集。這個巨集被定義為接受兩個參數;第一個是要測試的值,第二個是測試失敗時要印出的註解。巨集的主體是一個簡單的 if 命令,其中包含一個 message 命令。當找到 endmacro 命令時,巨集主體結束。可以簡單地像使用命令一樣使用巨集的名稱來調用巨集。在上面的範例中,如果找不到 FOO_LIB,則會顯示一則訊息,指出錯誤狀況。

macro 命令也支援定義接受可變參數列表的巨集。如果您想定義一個具有可選參數或多個簽名的巨集,這會很有用。可變參數可以使用 ARGCARGV0ARGV1 等來引用,而不是形式參數。ARGV0 代表巨集的第一个参数;ARGV1 代表下一个参数,依此类推。您也可以混合使用形式參數和可變參數,如下例所示。

# define a macro that takes at least two arguments
# (the formal arguments) plus an optional third argument
macro(assert TEST COMMENT)
  if(NOT ${TEST})
    message("Assertion failed: ${COMMENT}")

    # if called with three arguments then also write the
    # message to a file specified as the third argument
    if(${ARGC} MATCHES 3)
      file(APPEND ${ARGV2} "Assertion failed: ${COMMENT}")
    endif()

  endif()
endmacro()

# use the macro
find_library(FOO_LIB foo /usr/local/lib)
assert(${FOO_LIB} "Unable to find library foo")

在此範例中,兩個必要的參數是 TESTCOMMENT。這些必要的參數可以透過名稱來引用,如本範例所示,或透過引用 ARGV0ARGV1 來引用。如果您想將參數作為列表來處理,請使用 ARGVARGN 變數。ARGV(與 ARGV0ARGV1 等相反)是巨集的所有參數的列表,而 ARGN 是形式參數之後的所有參數的列表。在您的巨集中,您可以使用 foreach 命令來迭代 ARGVARGN,視需要而定。

return 命令會從函式、目錄或檔案返回。請注意,巨集與函式不同,它是原地展開的,因此無法處理 return

正規表示式

一些 CMake 命令,例如 ifstring,會使用正規表示式或可以將正規表示式作為參數。以其最簡單的形式來說,正規表示式是用於搜尋精確字元比對的字元序列。然而,很多時候要尋找的確切序列是未知的,或者只需要在字串的開頭或結尾進行比對。由於指定正規表示式有幾種不同的慣例,因此 CMake 的標準在 string 命令文件中進行說明。該描述是基於德州儀器公司的開源正規表示式類別,CMake 使用該類別來剖析正規表示式。

進階命令

有一些命令可能非常有用,但通常不會在編寫 CMakeLists 檔案時使用。本節將討論其中一些命令以及它們何時有用。

首先,請考慮 add_dependencies 命令,它會在兩個目標之間建立相依性。當 CMake 可以判斷時,它會自動在目標之間建立相依性。例如,CMake 將會自動為相依於程式庫目標的可執行目標建立相依性。add_dependencies 命令通常用於指定目標之間的跨目標相依性,其中至少有一個目標是自訂目標(請參閱新增自訂命令章節)。

include_regular_expression 命令也與相依性有關。此命令控制用於追蹤原始碼相依性的正規表示式。預設情況下,CMake 將會追蹤原始檔的所有相依性,包括系統檔案(例如 stdio.h)。如果您使用 include_regular_expression 命令指定正規表示式,則將會使用該正規表示式來限制處理哪些 include 檔案。例如,如果您的軟體專案的 include 檔案都以 foo 字首開頭(例如 fooMain.c fooStruct.h 等),您可以指定正規表示式 ^foo.*$,以將相依性檢查限制為僅限於您專案的檔案。