CMake 入門¶
警告
免責聲明:本文件由 LLVM 專案貢獻者撰寫,並非 與 CMake 專案相關的人員。本文件可能包含不正確的術語、措辭或技術細節。它是本著最佳的意圖提供的。
簡介¶
LLVM 專案和許多建構於 LLVM 之上的核心專案都使用 CMake 進行建構。本文件旨在為修改 LLVM 專案或在 LLVM 之上建構自己專案的開發人員提供 CMake 的簡要概述。
官方 CMake 語言參考位於 cmake-language 線上手冊頁和 cmake-language 線上文件。
10,000 英尺視圖¶
CMake 是一個工具,它會讀取以其自身語言編寫的腳本檔案,這些檔案描述了軟體專案的建構方式。當 CMake 評估腳本時,它會建構軟體專案的內部表示。一旦腳本已完全處理完畢,如果沒有錯誤,CMake 將會產生建構檔案來實際建構專案。CMake 支援為各種命令列建構工具以及熱門的 IDE 產生建構檔案。
當使用者執行 CMake 時,它會執行各種檢查,類似於 autoconf 過去的工作方式。在檢查和評估建構描述腳本期間,CMake 會將值快取到 CMakeCache 中。這非常有用,因為它允許建構系統在漸進式開發期間跳過長時間執行的檢查。CMake 快取也有一些缺點,但稍後會討論。
腳本概覽¶
CMake 的腳本語言具有非常簡單的語法。每個語言結構都是一個與模式 _name_(_args_) 相符的命令。命令主要有三種類型:語言定義的(在 CMake 中以 C++ 實作的命令)、已定義的函式和已定義的巨集。CMake 發行版還包含一組 CMake 模組,其中包含有用功能的定義。
以下範例是用於建構 C++「Hello World」程式的完整 CMake 建構。該範例僅使用 CMake 語言定義的函式。
cmake_minimum_required(VERSION 3.20.0)
project(HelloWorld)
add_executable(HelloWorld HelloWorld.cpp)
CMake 語言以 foreach 迴圈和 if 區塊的形式提供流程控制結構。為了使上面的範例更複雜,您可以新增一個 if 區塊來在針對 Apple 平台時定義「APPLE」
cmake_minimum_required(VERSION 3.20.0)
project(HelloWorld)
add_executable(HelloWorld HelloWorld.cpp)
if(APPLE)
target_compile_definitions(HelloWorld PUBLIC APPLE)
endif()
變數、類型和作用域¶
解除參考¶
在 CMake 中,變數是「字串化」類型的。所有變數在評估過程中都表示為字串。將變數括在 ${}
中會對其進行解引用,並將名稱替換為值。CMake 在其文件中將此稱為「變數評估」。解引用在呼叫的指令接收參數「之前」執行。這表示對清單進行解引用會導致將多個獨立的參數傳遞給指令。
變數解引用可以巢狀,並且可以用來建模複雜的資料。例如
set(var_name var1)
set(${var_name} foo) # same as "set(var1 foo)"
set(${${var_name}}_var bar) # same as "set(foo_var bar)"
對未設定的變數進行解引用會導致空展開。在 CMake 中,常見的模式是在知道變數將在未設定變數的程式碼路徑中使用的情況下,有條件地設定變數。在整個 LLVM CMake 建置系統中都有這樣的範例。
變數空展開的範例是
if(APPLE)
set(extra_sources Apple.cpp)
endif()
add_executable(HelloWorld HelloWorld.cpp ${extra_sources})
在此範例中,只有在您以 Apple 平台為目標時,才會定義 extra_sources
變數。對於所有其他目標,在 add_executable 取得其參數之前,extra_sources
將被評估為空。
清單¶
在 CMake 中,清單是以分號分隔的字串,強烈建議您避免在清單中使用分號;這會導致問題。以下是一些定義清單的範例
# Creates a list with members a, b, c, and d
set(my_list a b c d)
set(my_list "a;b;c;d")
# Creates a string "a b c d"
set(my_string "a b c d")
清單的清單¶
CMake 中比較複雜的模式之一是清單的清單。因為清單不能包含帶有分號的元素來構造清單的清單,所以您需要建立一個變數名稱的清單,這些變數名稱指向其他清單。例如
set(list_of_lists a b c)
set(a 1 2 3)
set(b 4 5 6)
set(c 7 8 9)
使用此佈局,您可以迭代清單的清單,並使用以下程式碼列印每個值
foreach(list_name IN LISTS list_of_lists)
foreach(value IN LISTS ${list_name})
message(${value})
endforeach()
endforeach()
您會注意到內部 foreach 迴圈的清單被解引用兩次。這是因為第一次解引用會將 list_name
轉換為子清單的名稱(在範例中為 a、b 或 c),然後第二次解引用是為了取得清單的值。
此模式在整個 CMake 中都有使用,最常見的範例是編譯器旗標選項,CMake 使用以下變數展開來引用這些選項:CMAKE_${LANGUAGE}_FLAGS 和 CMAKE_${LANGUAGE}_FLAGS_${CMAKE_BUILD_TYPE}。
其他類型¶
已快取或在指令列上指定的變數可以具有與其關聯的類型。變數的類型由 CMake 的 UI 工具用於顯示正確的輸入欄位。變數的類型通常不會影響評估,但是 CMake 對某些變數(例如 PATH)有特殊的處理方式。您可以在 CMake 的 set 文件 中閱讀有關特殊處理的更多資訊。
範圍¶
CMake 本質上具有基於目錄的範圍。在 CMakeLists 檔案中設定變數將會為該檔案和所有子目錄設定變數。在包含於 CMakeLists 檔案中的 CMake 模組中設定的變數將在其包含的範圍和所有子目錄中設定。
當在子目錄中再次設定已設定的變數時,它會覆寫該範圍和任何更深層子目錄中的值。
CMake set 指令提供了兩個與範圍相關的選項。PARENT_SCOPE 在父範圍(而不是當前範圍)中設定變數。CACHE 選項在 CMakeCache 中設定變數,這會導致在所有範圍中設定該變數。除非指定了 FORCE 選項,否則 CACHE 選項不會設定 CACHE 中已存在的變數。
除了基於目錄的作用域之外,CMake 函數也有自己的作用域。這意味著在函數內設置的變數不會影響到父作用域。宏則不然,因此 LLVM 在合理的情況下偏好函數而不是宏。
備註
與基於 C 語言的語言不同,CMake 的循環和控制流程區塊沒有自己的作用域。
控制流程¶
CMake 具備您在任何腳本語言中期望的基本控制流程結構,但有一些特殊之處,因為與 CMake 中的所有內容一樣,控制流程結構也是命令。
If、ElseIf、Else¶
備註
如需 CMake if 命令的完整文件,請參閱此處。該資源更加完整。
一般來說,CMake if 區塊的運作方式與您預期的一樣
if(<condition>)
message("do stuff")
elseif(<condition>)
message("do other stuff")
else()
message("do other other stuff")
endif()
對於具有 C 語言背景的人來說,關於 CMake if 區塊最重要的一點是,它們沒有自己的作用域。在條件區塊內設置的變數在 endif()
之後仍然存在。
迴圈¶
CMake foreach
區塊最常見的形式是
foreach(var ...)
message("do stuff")
endforeach()
foreach
區塊的變數參數部分可以包含解引用列表、要迭代的值,或兩者的混合
foreach(var foo bar baz)
message(${var})
endforeach()
# prints:
# foo
# bar
# baz
set(my_list 1 2 3)
foreach(var ${my_list})
message(${var})
endforeach()
# prints:
# 1
# 2
# 3
foreach(var ${my_list} out_of_bounds)
message(${var})
endforeach()
# prints:
# 1
# 2
# 3
# out_of_bounds
還有一個更新穎的 CMake foreach 語法。以下程式碼等效於上面的程式碼
foreach(var IN ITEMS foo bar baz)
message(${var})
endforeach()
# prints:
# foo
# bar
# baz
set(my_list 1 2 3)
foreach(var IN LISTS my_list)
message(${var})
endforeach()
# prints:
# 1
# 2
# 3
foreach(var IN LISTS my_list ITEMS out_of_bounds)
message(${var})
endforeach()
# prints:
# 1
# 2
# 3
# out_of_bounds
與條件語句類似,這些語句通常會按照您預期的方式運行,並且它們沒有自己的作用域。
CMake 也支援 while
迴圈,儘管它們在 LLVM 中並不常用。
模組、函數和巨集¶
模組¶
模組是 CMake 實現程式碼重複使用的工具。CMake 模組只是 CMake 腳本檔案。它們可以包含在 include 時執行的程式碼,以及命令的定義。
在 CMake 中,巨集和函數統稱為命令,它們是定義可多次呼叫的程式碼的主要方法。
在 LLVM 中,我們有幾個 CMake 模組作為我們發佈的一部分包含在內,供未從原始碼建置專案的開發人員使用。這些模組是用 CMake 建置基於 LLVM 的專案所需的基本組成部分。我們還依靠模組作為一種組織建置系統功能的方式,以便在 LLVM 專案中維護和重複使用。
參數處理¶
在定義 CMake 命令時,處理參數非常有用。本節中的範例都將使用 CMake function
區塊,但這一切都適用於 macro
區塊。
CMake 命令可以具有在每個呼叫站點都需要的具名引數。此外,所有命令都將隱式接受可變數量的額外引數(用 C 語言來說,所有命令都是可變引數函數)。當使用額外引數(超出具名引數)呼叫命令時,CMake 會將完整的引數列表(包括具名和未具名)儲存在名為 ARGV
的列表中,並將未具名引數的子列表儲存在 ARGN
中。以下是一個為 CMake 的內建函數 add_dependencies
提供包裝函數的簡單範例。
function(add_deps target)
add_dependencies(${target} ${ARGN})
endfunction()
這個範例定義了一個名為 add_deps
的新巨集,它接受一個必要的初始引數,並只呼叫另一個函數,傳遞初始引數和所有尾隨引數。
CMake 提供了一個模組 CMakeParseArguments
,它提供進階引數解析的實作。我們在整個 LLVM 中都使用這個模組,建議任何具有複雜的基於引數的行為或可選引數的函數都使用它。CMake 的官方文件在 cmake-modules
線上手冊頁中,也可以在 cmake-modules 線上文件 中找到。
備註
從 CMake 3.5 開始,cmake_parse_arguments 命令已成為原生命令,而 CMakeParseArguments 模組是空的,僅為了相容性而保留。
函數與巨集¶
函數和巨集在使用方式上看起來非常相似,但兩者之間有一個根本區別。函數有自己的作用域,而巨集沒有。這意味著在巨集中設定的變數會滲透到呼叫作用域中。這使得巨集只適用於定義非常小的功能。
CMake 函數和巨集的另一個區別是引數的傳遞方式。傳遞給巨集的引數不會被設定為變數,而是在執行巨集之前,會解析巨集中對參數的解引用。如果使用未引用的變數,這可能會導致一些意外的行為。例如
macro(print_list my_list)
foreach(var IN LISTS my_list)
message("${var}")
endforeach()
endmacro()
set(my_list a b c d)
set(my_list_of_numbers 1 2 3 4)
print_list(my_list_of_numbers)
# prints:
# a
# b
# c
# d
一般來說,這個問題並不常見,因為它需要使用在父作用域中名稱重疊的未解引用變數,但重要的是要意識到這一點,因為它可能會導致難以察覺的錯誤。
LLVM 專案包裝函數¶
LLVM 專案提供了許多圍繞關鍵 CMake 內建命令的包裝函數。我們使用這些包裝函數來在 LLVM 元件之間提供一致的行為,並減少程式碼重複。
我們通常(但不總是)遵循以下慣例:以 llvm_
開頭的命令僅用作其他命令的建構區塊。旨在直接使用的包裝函數通常以命令名稱中間的專案命名(例如,add_llvm_executable
是 add_executable
的包裝函數)。LLVM add_*
包裝函數都在 AddLLVM.cmake
中定義,該檔案作為 LLVM 發行版的一部分安裝。任何需要 LLVM 的 LLVM 子專案都可以包含和使用它。
備註
並非所有 LLVM 專案在所有用例中都需要 LLVM。例如,compiler-rt 可以在沒有 LLVM 的情況下建構,並且 compiler-rt sanitizer 庫與 GCC 一起使用。
實用的內建指令¶
CMake 擁有多個實用的內建指令。本文不會深入探討它們,因為 CMake 專案本身已經提供了完善的說明文件。以下列出了一些實用的函式:
CMake 指令的完整說明文件可在 cmake-commands
線上手冊頁面中找到,並可在 CMake 網站 上取得