1. 程式人生 > >15分鐘學習CMake腳本(譯)

15分鐘學習CMake腳本(譯)

targe result 變量名 問題 項目 right tab 長時間 接受

在上一篇博客中我們提到了每一個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腳本(譯)