15分鐘學習CMake腳本(譯)
在上一篇博客中我們提到了每一個CMake項目都需要包含一個 CMakeLists.txt 腳本。這個腳本定義了目標文件(targets),也可以做很多其他的事情,例如:尋找第三方庫,生成C++頭文件等。 CMake腳本有很大的靈活性。
每當你集成一個外部庫,以及添加其他平臺的支持的大部分情況下,你需要編輯CMake腳本。我曾在並不動CMake腳本的情況下花了很長時間來編輯 CMakeLists.txt - 因為CMake腳本的文檔很分散。但最終我搞懂了CMake腳本,這篇博文的目的就是讓你盡快想我一樣了解CMake腳本。
這篇博文不會覆蓋所有的CMake命令(總共有數百個!),但這對CMake腳本的語法
Hello World
如果你創建一個包含以下內容的 hello.txt 文件:
message(“Hello World!”) # A message to print
你能夠在命令行中用 cmake -P hello.txt 來運行此文件。(-P 選項告訴cmake 運行指定腳本,但是不生成構建流水線(build pipeline))正如所期待的,運行此命令會輸出 “Hello World!”。
$ cmake -P hello.txt
Hello world!
所有的變量都是字符串
在CMake中,所有的變量都是字符串。你可以在字符串常量(string literal)中使用 ${} 代入變量, 這被稱作變量引用(variable reference)。修改 hello.txt 如下:
message("Hello ${NAME}!") # Substitute a variable into the message
現在,如果我們在 cmake 命令中使用 -D 選項定義 NAME, hello.txt將會使用此變量:
$ cmake -DNAME=Newman -P hello.txt
Hello Newman!
當一個變量未被定義時,默認值是一個空字符串:
$ cmake -P hello.txt
Hello !
我們還可以在腳本中使用 set 命令定義變量。set 的第一個參數時變量名,第二個參數時變量的值:
set(THING "funk") message("We want the ${THING}!")
只要字符串中沒有空格或變量引用,set 參數中的引號是可選的。例如,set("THING" funk) 和上面第一行的命令是等價的。對於大多數CMake命令(if 和 while 命令除外)而言,是否使用引號只是風格問題。當參數只是變量名時,我傾向於不使用引號。
用前綴(prefixes)模擬數據結構
CMake中沒有類(classes)的概念,但你可以通過定義一組有著同樣前綴(prefixes)的變量來模擬數據結構。使用時,你可以用嵌套的變量引用( ${} )來查找組內變量。例如,下述腳本將會輸出 “John Smith lives at 123 Fake St.”:
set(JOHN_NAME "John Smith")
set(JOHN_ADDRESS "123 Fake St")
set(PERSON "JOHN")
message("${${PERSON}_NAME} lives at ${${PERSON}_ADDRESS}.")
你甚至可以在set命令中使用變量引用。例如,如果 PERSON 變量的值仍然是 “JOHN”,下述代碼將會把變量 JOHN_NAME 的值置為 “John Goodman”:
set(${PERSON}_NAME "John Goodman")
每條聲明都是一條命令
在CMake中,每條聲明都是一條接收一列字符串參數並且無返回值的命令。參數間由空格隔開。正如我們已經見到的, set 命令在文件中(file scope)定義了一個變量。
另一個例子是CMake中執行算術運算的 math 命令。math命令的第一個參數必須是 EXPR , 第二個參數是被賦值變量的名字,第三個參數是被求值表達式,這三個參數均為字符串。註意以下第三行代碼,CMake在將參數傳入 math 命令前先把字符串 MY_SUM 的值替換掉。
math(EXPR MY_SUM "1 + 1") # Evaluate 1 + 1; store result in MY_SUM
message("The sum is ${MY_SUM}.")
math(EXPR DOUBLE_SUM "${MY_SUM} * 2") # Multiply by 2; store result in DOUBLE_SUM
message("Double that is ${DOUBLE_SUM}.")
幾乎所有你需要做的事情都有一個對應的CMake 命令(https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html)。 string 命令可以幫助你完成正則表達式替換等高級字符串操作。file 命令能讀寫文件或者操作文件系統路徑。
控制流命令
甚至控制流語句也是命令。if/endif 命令讓你根據條件執行被包圍的命令。控制流中的空格不影響功能,但通常還是會縮進條件命令間的命令以提高可讀性。如下的命令檢查 CMake 的內建變量 WIN 是否被賦值:
if(WIN32)
message("You‘re running CMake on Windows.")
endif()
CMake 還有 while/endwhile 命令來在條件為真時重復執行命令。如下循環將打印所有1000000以下的斐波那契數字:
set(A "1")
set(B "1")
while(A LESS "1000000")
message("${A}") # Print A
math(EXPR T "${A} + ${B}") # Add the numeric values of A and B; store result in T
set(A "${B}") # Assign the value of B to A
set(B "${T}") # Assign the value of T to B
endwhile()
CMake 中的 if 和 while 條件的用法和其他編程語言不同。如上所示,要執行數值比較,你必須指定 LESS 作為一個字符串參數。如下文檔(https://cmake.org/cmake/help/latest/command/if.html)詳細解釋了如何正確編寫條件語句。
if 和 while 命令有些特殊-如果指定的變量沒有引號包圍,if 和 while 將直接使用這些變量的值。在以上的代碼中,我利用了這一點。第三行寫作 while(A LESS “1000000”)而不是 while (${A} LESS "1000000"),雖然兩者是等價的。其他命令不會主動對變量求值。
列表(Lists)只是分號分隔的字符串
CMake 對沒有用引號的參數有特別的替換規則。如果整個參數是一個沒有引號的變量引用,而且變量的值中包含分號,那麽 CMake 將在分號處分隔該參數並作為多個參數傳入包含該變量的命令。例如,如下代碼將向 math 命令傳入三個參數:
set(ARGS "EXPR;T;1 + 1")
math(${ARGS}) # Equivalent to calling math(EXPR T "1 + 1")
另一方面,引號包圍的參數不會在被替換後被分號分隔。CMake 總是將引號字符串作為單獨的參數傳入,保持分號的完整性:
set(ARGS "EXPR;T;1 + 1")
message("${ARGS}") # Prints: EXPR;T;1 + 1
如果兩個以上的參數被傳入 set 命令,它們將會被分號連接起來,然後被賦值給指定變量。這實際上是用參數創建了一個列表(list):
set(MY_LIST These are separate arguments)
message("${MY_LIST}") # Prints: These;are;separate;arguments
你能通過 list 命令來操作列表:
set(MY_LIST These are separate arguments)
list(REMOVE_ITEM MY_LIST "separate") # Removes "separate" from the list
message("${MY_LIST}") # Prints: These;are;arguments
foreach/endforeach 命令接收多個參數並叠代除第一個參數以外的參數:
foreach(ARG These are separate arguments)
message("${ARG}") # Prints each word on a separate line
endforeach()
你可以通過傳入一個沒有引號的變量引用給 foreach 來進行叠代一個列表。就像其他命令一樣, CMake將以分號分隔該變量的值:
foreach(ARG ${MY_LIST}) # Splits the list; passes items as arguments
message("${ARG}") # Prints each item on a separate line
endforeach()
函數有作用域;宏沒有
在 CMake 中,你可以用 function/endfunction 命令來定義一個函數。以下代碼定義了一個將參數的數值翻倍並打印的函數 doubleIt:
function(doubleIt VALUE)
math(EXPR RESULT "${VALUE} * 2")
message("${RESULT}")
endfunction()
doubleIt("4") # Prints: 8
函數在自己的作用域中運行。函數中定義的局部變量不會汙染調用者的作用域。如果你想要返回值,你可以傳入你想要返回的變量,然後用特殊參數 PARENT_SCOPE 調用 set 命令:
function(doubleIt VARNAME VALUE)
math(EXPR RESULT "${VALUE} * 2")
set(${VARNAME} "${RESULT}" PARENT_SCOPE) # Set the named variable in caller‘s scope
endfunction()
doubleIt(RESULT "4") # Tell the function to set the variable named RESULT
message("${RESULT}") # Prints: 8
類似的,macro/endmacro 命令可定義一個宏。但是和函數不同,宏和它們的調用者在同一作用域工作。因此,宏中定義的所有變量在調用者的作用域中被set。我們可以用以下宏替代上述函數:
macro(doubleIt VARNAME VALUE)
math(EXPR ${VARNAME} "${VALUE} * 2") # Set the named variable in caller‘s scope
endmacro()
doubleIt(RESULT "4") # Tell the macro to set the variable named RESULT
message("${RESULT}") # Prints: 8
函數和宏都可以接受任意數量的參數。未命名參數通過一個特殊變量 ARGN 作為列表暴露給函數。以下函數將所有接收的參數翻倍,並將每個參數分行打印:
function(doubleEach)
foreach(ARG ${ARGN}) # Iterate over each argument
math(EXPR N "${ARG} * 2") # Double ARG‘s numeric value; store result in N
message("${N}") # Print N
endforeach()
endfunction()
doubleEach(5 6 7 8) # Prints 10, 12, 14, 16 on separate lines
包含其他腳本
CMake 變量都定義在文件域。include 命令在 調用文件的作用域 執行其他 CMake 腳本。這和 C/C++ 中的 #include 預處理命令很相似。include 命令通常用來調用其他腳本中定義一些常用的函數或者宏,此命令用 CMAKE_MODULE_PATH 變量中的值作為搜索路徑。
find_package 命令搜索形如 Find*.cmake 的腳本,並且在同一作用域中運行這些腳本。這些腳本通常用來幫助尋找外部庫。例如,如果在搜索路徑中存在一個叫做 FindSDL2.cmake 的文件, find_package(SDL2)等價於 include(FindSDL2.cmake)。(註意,此處只是 find_package 多種用法中的一種)
另一方面,add_subdirectory 命令會創建一個新的作用域,然後在新作用域中運行指定文件夾下的CMakeLists.txt 腳本。此命令通常用來添加另一個 CMake 子項目,例如一個庫或者是一個可執行文件,到本項目。如果不特別指定的話,子項目中定義的目標文件會被添加到構建流水線(build pipeline)中。子項目腳本中的變量不會汙染本項目的作用域-除非 set 命令中使用了 PARENT_SCOPE 選項。
例如,在Turf(https://github.com/preshing/turf)項目中,運行 CMake 時如下腳本會被用到:
讀取和設置屬性
CMake 使用 add_executable, add_library 或者 add_custom_target 命令來定義目標文件。目標文件被創建好之後,它們就有了屬性(properties),你可以用 get_property 和 set_property 命令來操作這些屬性。不像變量,目標文件在所有作用域中都是可見的,即使它們是在子項目中定義的。目標文件的所有屬性也都是字符串。
add_executable(MyApp "main.cpp") # Create a target named MyApp
# Get the target‘s SOURCES property and assign it to MYAPP_SOURCES
get_property(MYAPP_SOURCES TARGET MyApp PROPERTY SOURCES)
message("${MYAPP_SOURCES}") # Prints: main.cpp
常見的目標文件屬性還包括 LINK_LIBRARIES, INCLUDE_DIRECTORIES 和 COMPILE_DEFINITIONS。這些屬性會被 target_link_libraries ,target_include_directories 和 target_compile_definitions 命令間接修改。在腳本結束的時候,CMake會用這些目標文件屬性來生成構建流水線。
其他 CMake 實體(entities)也有自己的屬性。每個文件作用域都有一個目錄屬性(directory properties)的集合。還有一個所有腳本都可見的全局屬性(global properties)集合。每個 C/C++ 源文件也有一個源文件屬性(source file properties)集合。
原文鏈接:https://preshing.com/20170522/learn-cmakes-scripting-language-in-15-minutes/
15分鐘學習CMake腳本(譯)