撰寫 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
時,您可以使用 COMMAND
或 Command
來代替。最佳實務做法是使用小寫命令。所有空白(空格、換行、索引標籤)都會被忽略,除了分隔引數之外。因此,命令可以跨越多行,只要命令名稱和左括號在同一行即可。
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
基本命令¶
如我們稍早所見,set
和 unset
命令會明確地設定或取消設定變數。string
、list
和 separate_arguments
命令提供了字串和清單的基本操作。
add_executable
和 add_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
命令說明了它可以測試的許多條件。
迴圈結構¶
foreach
和 while
命令允許您處理依序發生的重複性任務。break
命令會在 foreach
或 while
迴圈正常結束之前跳出迴圈。
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()
程序定義¶
macro
和 function
命令支援可能散佈在 CMakeLists 檔案中的重複性任務。一旦定義了巨集或函式,之後處理的任何 CMakeLists 檔案都可以使用它。
CMake 中的函式很像 C 或 C++ 中的函式。您可以將參數傳遞給它,它們會成為函式內的變數。同樣,會定義一些標準變數,例如 ARGC
、ARGV
、ARGN
和 ARGV0
、ARGV1
等。函式呼叫具有動態範圍。在函式中,您處於新的變數範圍中;這就像使用 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
命令也支援定義接受可變參數列表的巨集。如果您想定義一個具有可選參數或多個簽名的巨集,這會很有用。可變參數可以使用 ARGC
和 ARGV0
、ARGV1
等來引用,而不是形式參數。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")
在此範例中,兩個必要的參數是 TEST
和 COMMENT
。這些必要的參數可以透過名稱來引用,如本範例所示,或透過引用 ARGV0
和 ARGV1
來引用。如果您想將參數作為列表來處理,請使用 ARGV
和 ARGN
變數。ARGV
(與 ARGV0
、ARGV1
等相反)是巨集的所有參數的列表,而 ARGN
是形式參數之後的所有參數的列表。在您的巨集中,您可以使用 foreach
命令來迭代 ARGV
或 ARGN
,視需要而定。
正規表示式¶
一些 CMake 命令,例如 if
和 string
,會使用正規表示式或可以將正規表示式作為參數。以其最簡單的形式來說,正規表示式是用於搜尋精確字元比對的字元序列。然而,很多時候要尋找的確切序列是未知的,或者只需要在字串的開頭或結尾進行比對。由於指定正規表示式有幾種不同的慣例,因此 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.*$
,以將相依性檢查限制為僅限於您專案的檔案。