CMake 入門¶
警告
免責聲明:本文件由 LLVM 專案貢獻者撰寫,並非任何隸屬於 CMake 專案的人士。本文件可能包含不準確的術語、措辭或技術細節。其提供是出於善意。
簡介¶
LLVM 專案和許多基於 LLVM 建構的核心專案都使用 CMake 建置。本文檔旨在為修改 LLVM 專案或在 LLVM 之上建構自己的專案的開發人員提供 CMake 的簡要概述。
官方 CMake 語言參考可在 cmake-language manpage 和cmake-language 線上文件中找到。
一萬英尺高度概觀¶
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})
在此範例中,extra_sources
變數僅在您以 Apple 平台為目標時定義。對於所有其他目標,在將參數提供給 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
區塊的 variable 參數部分可以包含解引用的列表、要迭代的值或兩者的混合
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 腳本檔案。它們可以包含在包含時執行的程式碼以及指令的定義。
在 CMake 中,巨集和函式通常被稱為指令,它們是定義可以多次呼叫的程式碼的主要方法。
在 LLVM 中,我們有幾個 CMake 模組作為我們發行版的一部分包含在內,供不從原始碼建置我們專案的開發人員使用。這些模組是使用 CMake 建置基於 LLVM 的專案所需的基本組件。我們也依賴模組作為組織建置系統功能的方式,以實現 LLVM 專案內的可維護性和重用。
參數處理¶
當定義 CMake 指令時,處理參數非常有用。本節中的範例都將使用 CMake function
區塊,但這一切也適用於 macro
區塊。
CMake 指令可以具有在每次呼叫站點都需要的具名參數。此外,所有指令都將隱式接受可變數量的額外參數(在 C 術語中,所有指令都是 varargs 函式)。當指令使用額外參數(超出具名參數)調用時,CMake 將把完整參數列表(具名和未具名)儲存在名為 ARGV
的列表中,並將未具名參數的子列表儲存在 ARGN
中。下面是為 CMake 的內建函式 add_dependencies
提供封裝函式的簡單範例。
function(add_deps target)
add_dependencies(${target} ${ARGN})
endfunction()
此範例定義了一個名為 add_deps
的新巨集,它接受一個必需的第一個參數,並且僅呼叫另一個函式,傳遞第一個參數和所有尾隨參數。
CMake 提供了一個模組 CMakeParseArguments
,它提供了進階參數解析的實作。我們在 LLVM 中廣泛使用它,並且建議用於任何具有基於複雜參數的行為或可選參數的函式。CMake 的官方模組文件在 cmake-modules
manpage 中,也可在 cmake-modules 線上文件中找到。
注意
從 CMake 3.5 開始,cmake_parse_arguments 指令已成為原生指令,CMakeParseArguments 模組為空,僅為相容性而保留。
函式 vs 巨集¶
函式和巨集在使用方式上看起來非常相似,但兩者之間存在一個根本的區別。函式有自己的作用域,而巨集沒有。這表示在巨集中設定的變數將洩漏到呼叫作用域中。這使得巨集僅適用於定義非常小的功能片段。
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
manpage 中,並可在 CMake 網站上找到