CMake 全稱“cross platform make”,是開源、跨平臺的自動化構建系統。CMake 由 Kitware 開發與維護,來自使用者的貢獻使得 CMake 快速成長。
CMake 并不直接建構出最終的軟件,而是依照平臺、編譯器產生標準的建構檔(如 Unix Makefile 或 Visual Studio 的 projects/workspaces),然后再依一般的生成方式使用。和標準的 GNU 開發工具相比,CMake 的角色比 Make 更高階,比較接近 Autotools,而且支持多種不同的平臺與編譯器。
雖然跨平臺是 CMake 的重要特色,但由于 CMake 的簡單與彈性,在單一平臺上使用也很便利。
前言
- 每一個需要進行 CMake 操作的目錄下面,都必須存在文件 CMakeLists.txt 。
- CMake 命令不區分大小寫。習慣上,CMake 命令全小寫,預定義變量全大寫。
- 變量使用
${}
方式取值,但是在if
控制語句中是直接使用變量名。 -
command(parameter1 parameter2 …)
,參數使用括號括起,參數之間使用空格或分號分開。
常用預定義變量
CMake 的預定義變量
-
PROJECT_SOURCE_DIR
:工程根目錄; -
PROJECT_BINARY_DIR
:運行 CMake 命令的目錄。建議定義為${PROJECT_SOURCE_DIR}/build
下; -
CMAKE_INCLUDE_PATH
:環境變量,非 CMake 變量; -
CMAKE_LIBRARY_PATH
:環境變量; -
CMAKE_CURRENT_SOURCE_DIR
:當前處理的 CMakeLists.txt 文件所在路徑; -
CMAKE_CURRENT_BINARY_DIR
:target 編譯目錄;- 使用
add_subdirectory
命令可以更改該變量的值; -
set(EXECUTABLE_OUTPUT_PATH <dir>)
命令不會對該變量有影響,但改變了最終目標文件的存儲路徑。
- 使用
-
CMAKE_CURRENT_LIST_FILE
:輸出調用該變量的 CMakeLists.txt 的完整路徑; -
CMAKE_CURRENT_LIST_LINE
:輸出該變量所在的行; -
CMAKE_MODULE_PATH
:定義自己的 CMake 模塊所在路徑; -
EXECUTABLE_OUTPUT_PATH
:重新定義目標二進制可執行文件的存放位置; -
LIBRARY_OUTPUT_PATH
:重新定義目標鏈接庫文件的存放位置; -
PROJECT_NAME
:返回由project
命令定義的項目名稱; -
CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS
:用來控制if…else…
語句的書寫方式。
系統信息預定義變量
-
CMAKE_MAJOR_VERSION
:CMake 主版本號,如 3.12.0 中的 3; -
CMAKE_MINOR_VERSION
:CMake 次版本號,如 3.12.0 中的 12; -
CMAKE_PATCH_VERSION
:CMake 補丁等級,如 3.12.0 中的 0; -
CMAKE_SYSTEM
:系統名稱,例如 Windows-10.0.17134; -
CMAKE_SYSTEM_NAME
:不包含版本號的系統名,如 Windows; -
CMAKE_SYSTEM_VERSION
:系統版本號,如 10.0.17134; -
CMAKE_SYSTEM_PROCESSOR
:處理器架構,如 AMD64; -
UNIX
:在所有的類 UNIX 平臺為 true,包括 macOS 和 Cygwin; -
WIN32
:在所有的 Win32 平臺為 true,包括 Cygwin。
開關選項
-
BUILD_SHARED_LIBS
:控制默認的庫編譯方式;- 如果未進行設置,使用 add_library 時又沒有指定庫類型,默認編譯生成的庫都是靜態庫。
-
CMAKE_C_FLAGS
:設置 C 編譯選項; -
CMAKE_CXX_FLAGS
:設置 C++ 編譯選項。
常用命令
cmake_minimum_required
該語句一般放置在 CMakeLists.txt 的開頭,用于說明 CMake 最低版本要求。
cmake_minimum_required(VERSION 3.5)
上述示例指 CMake 的版本號最低為 3.5。
project
project(<PROJECT-NAME>)
-
<PROJECT-NAME>
指工程名稱。
該命令一般緊跟 cmake_minimum_required
命令之后,定義了工程的名稱。但項目最終編譯生成的可執行文件并不一定是這個項目名稱,而是由另一條命令確定的,后面會介紹。
執行了該命令之后,將會自動創建兩個變量:
-
<PROJECT-NAME>_BINARY_DIR
:二進制文件保存路徑; -
<PROJECT-NAME>_SOURCE_DIR
:源代碼路徑。
project(Lab)
執行了上一條指令,即定義了一個項目名稱 Lab
,相應的會生成兩個變量:Lab_BINARY_DIR
、Lab_SOURCE_DIR
。
CMake 中預定義了兩個變量:PROJECT_BINARY_DIR
和 PROJECT_SOURCE_DIR
。
在這個例子中:
-
PROJECT_BINARY_DIR
等價于Lab_BINARY_DIR
; -
PROJECT_SOURCE_DIR
等價于Lab_SOURCE_DIR
。
建議直接使用 PROJECT_BINARY_DIR
與 PROJECT_SOURCE_DIR
,這樣即使項目名稱發生了變化,也不會影響 CMakeLists.txt 文件。
關于 PROJECT_BINARY_DIR
與 PROJECT_SOURCE_DIR
這兩個變量是否相同的問題,涉及到編譯方法是內部編譯還是外部編譯。如果是內部編譯,則這兩個變量相同;如果是外部編譯,則兩個變量不同。此處對內部編譯與外部編譯做出介紹:
外部構建與內部構建
假設此時已經完成了 CMakeLists.txt 的編寫,在 CMakeLists.txt 所在目錄下,有兩種執行 CMake 的方法:
cmake .\\
make
以及
mkdir build
cd .\\build
cmake ..\\
make
第一種方法是內部構建,第二種方法是外部構建。上述兩種方法中,最大不同在于 CMake 與 Make 的工作路徑不同。
內部構建方法中,CMake 生成的中間文件和可執行文件都會存放在項目目錄中;外部構建方法中,中間文件與可執行文件都存放在 build 目錄中。
建議使用外部構建方法。優點顯而易見:最大限度地保持了代碼目錄的整潔,生成、編譯與安裝是不同于項目目錄的其他目錄中,在外部構建方法下,PROJECT_SOURCE_DIR
指向目錄與內部構建相同,為 CMakeLists.txt 所在根目錄;而 PROJECT_BINARY_DIR
不同,它指向 CMakeLists.txt 所在根目錄下的 build 目錄。
set
set(<variable> <value>… CACHE <type> <docstring> [FORCE])
示例:
set(CMAKE_INSTALL_PREFIX C:\\Program Files\\${PROJECT_NAME})
該示例顯式地將 CMAKE_INSTALL_PREFIX
的值定義為 C:\\Program Files\\${PROJECT_NAME}
。如此,在外部構建情況下執行 make install
命令時,Make 會將生成的可執行文件拷貝到 C:\\Program Files\\${PROJECT_NAME}\\bin
目錄下。
當然,可執行文件的安裝路徑 CMAKE_INSTALL_PREFIX
也可以在執行 cmake
命令的時候指定,cmake
參數如下:
cmake -D CMAKE_INSTALL_PREFIX="C:\\Program Files\\…"
如果 cmake
參數和 CMakeLists.txt 文件中都不指定該值的話,則該值為默認值(Windows 下為 C:\\Program Files\\${PROJECT_NAME}
,UNIX 下為 /usr/local
)。
add_subdirectory
add_subdirectory(source_dir [binary_dir]
[EXCLUDE_FROM_ALL])
-
source_dir
:源文件路徑; -
[binary_dir]
:中間二進制與目標二進制文件存放路徑; -
[EXECLUDE_FROM_ALL]
:將這個目錄從編譯過程中排除。
這個命令用于向當前工程添加存放源文件的子目錄。
include_directories
include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 …])
-
[AFTER|BEFORE]
:追加標志,指定控制追加或預先添加; -
[SYSTEM]
:指定該目錄為系統包含目錄; -
dir1, …, dir n
:添加的一系列頭文件搜索路徑。
向工程添加多個特定的頭文件搜索路徑,路徑之間用空格分隔。類似于 GCC 中的編譯參數 -l
,即指定編譯過程中編譯器搜索頭文件的路徑。當項目需要的頭文件不在系統默認的搜索路徑時,則指定該路徑。
add_executable
add_executable(<name> [WIN32] [MACOSX_BUNDLE]
[EXCLUDE_FROM_ALL]
[source1] [source2 …])
-
name
:可執行文件名; -
source1, …, source n
:生成該可執行文件的源文件。
該命令給出源文件,并指出需要編譯出的可執行文件名。
示例 1:
add_executable(hello main.cpp)
該示例中,利用源文件 main.cpp
,編譯出名為 hello
的可執行文件。
示例 2:
set(SRC_LIST main.cc
rpc/CRNode.cpp
rpc/Schd_types.cpp
task/TaskExecutor.cpp
task/TaskMoniter.cpp
util/Const.cpp
util/Globals.cc
)
add_executable(CRNode ${SRC_LIST})
該示例中,定義了該工程會生成一個名為 CRNode
的可執行文件,所依賴的源文件是變量 SRC_LIST
定義的源文件列表。
如果前面 project()
命令中定義的項目名稱也是 CRNode
,沒有什么問題,兩者之間沒有任何關系。
add_library
add_library(<name> [STATIC|SHARED|MODULE]
[EXCLUDE_FROM_ALL]
[source1] [source2 …])
-
name
:庫文件名稱; -
[STATIC|SHARED|MODULE]
:生成的庫的文件類型(靜態庫/共享庫); -
[EXCLUDE_FROM_ALL]
:表示該庫不會被默認構建; -
source1, …, sourceN
:生成庫所依賴的源文件。
示例:
add_library(hello SHARED ${LIB_hello_SRC})
link_directories
link_directories(directory1 directory2 …)
該命令用于添加外部庫的搜索路徑。
target_link_libraries
target_link_libraries(<target> … <item>…)
-
target
:目標文件; -
item
:鏈接外部庫文件。
指定鏈接目標文件時需要鏈接的外部庫,效果類似于 GCC 編譯參數 -L
,解決外部庫依賴的問題。
message
message([<mode>] "message to display" …)
-
mode
:確定消息的類型:模式 描述 (none)
重要信息 STATUS
附帶信息 WARNING
CMake 警告,繼續處理 AUTHOR_WARNING
CMake 警告(dev),繼續處理 SEND_ERROR
CMake 錯誤,繼續處理,但跳過生成過程 FATAL_ERROR
CMake 錯誤,停止處理和生成過程 DEPRECATION
如果分別啟用 CMAKE_ERROR_DEPRECATED
或CMAKE_WARN_DEPRECATED
,則棄用 CMake 錯誤或警告,否則無消息 "message to display"
:需要顯示的文字消息。
set_target_properties
set_target_properties(target1 target2 …
PROPERTIES prop1 value1
prop2 value2 …)
設置目標的某些屬性,改變它們構建的方式。
該指令為一個目標設置屬性,語法是列出所有用戶想要變更的文件,然后提供想要設置的值。用戶可以使用任何想用的屬性與對應的值,并在隨后的代碼中調用 GET_TARGET_PROPERTY
命令取出屬性的值。
影響目標輸出文件的屬性PROPERTIES
詳述如下:
PREFIX、SUFFIX
-
PREFIX
覆蓋了默認的目標名前綴(如 lib); -
SUFFIX
覆蓋了默認的目標名后綴(如 .so)。
IMPORT_PREFIX、IMPORT_SUFFIX
與 PREFIX
、SUFFIX
是等價的屬性,但針對的是 DLL 導入庫(即共享庫目標)。
OUTPUT_NAME
構建目標時,OUTPUT_NAME
用來設置目標的真實名稱。
LINK_FLAGS
為一個目標的鏈接階段添加額外標志。
LINK_FLAGS_<CONFIG>
將為配置 <CONFIG>
添加鏈接標志,如 Debug
、Release
、RelWithDebInfo
、MinSizeRel
。
COMPILE_FLAGS
設置附加的編譯器標志,在構建目標內的源文件時用到。
LINKER_LANGUAGE
改變鏈接可執行文件或共享庫的工具。默認值是設置與庫中文件相匹配的語言。
CXX 與 C 是該屬性的公共值。
VERSION、SOVERSION
VERSION
指定構建的版本號,SOVERSION
指定構建的 API 版本號。
構建或安裝時,如果平臺支持符號鏈接,且鏈接器支持 so 名稱,那么將會創建恰當的符號鏈接。
如果只指定兩者中的一個,缺失的另一個假定為具有相同版本號。
示例 1:
set_target_properties(hello_static PROPERTIES OUTPUT_NAME "hello")
示例 2:
set_target_properties(hello PROPERTEIES VERSION 1.2 SOVERSION 1)
該命令用于控制版本,VERSION
指代動態庫版本,SOVERSION
指代 API 版本。
aux_source_directory
查找某個路徑下的所有源文件,并將源文件列表存儲到一個變量中。
aux_source_directory(<dir> <variable>)
示例:
aux_source_directory(. SRC_LIST)
該指令將當前目錄下的文件列表全部存入變量 SRC_LIST
中。
install
install
命令可以按照對象的不同分為多種類型:目標文件、非目標文件、目錄。
目標文件
install(TARGETS targets… [EXPORT <export-name>]
[[ARCHIVE|LIBRARY|RUNTIME|OBJECTS|FRAMEWORK|BUNDLE|
PRIVATE_HEADER|PUBLIC_HEADER|RESOURCE]
[DESTINATION <dir>]
[PERMISSIONS permissions…]
[CONFIGURATIONS [Debug|Release|…]]
[COMPONENT <component>]
[NAMELINK_COMPONENT <component>]
[OPTIONAL] [EXCLUDE_FROM_ALL]
[NAMELINK_ONLY|NAMELINK_SKIP]
] […]
[INCLUDES DESTINATION [<dir> …]]
)
-
TARGETS targets
:targets
即通過add_executable
或add_library
定義的目標文件,可能是可執行二進制文件、動態庫、靜態庫; -
DESTINATION <dir>
:dir
即定義的安裝路徑。安裝路徑可以是絕對/相對路徑。在絕對路徑的情況下,CMAKE_INSTALL_PREFIX
就無效了。- 如果希望使用
CMAKE_INSTALL_PREFIX
定義安裝路徑,就需要使用相對路徑,這時候安裝后的路徑就是${CMAKE_INSTALL_PREFIX}\\<dir>
。
- 如果希望使用
非目標文件
.sh 腳本文件,即為典型的非目標文件的可執行程序。
install(<FILES|PROGRAMS> files… DESTINATION <dir>
[PERMISSIONS permissions…]
[CONFIGURATIONS [Debug|Release|…]]
[COMPONENT <component>]
[RENAME <name>] [OPTIONAL] [EXCLUDE_FROM_ALL])
使用方法和上述目標文件指令的 install
基本相同。唯一的區別是,安裝非目標文件之后的權限還包括 OWNER_EXECUTE
、GOUP_EXECUTE
、WORLD_EXECUTE
,即 755 權限目錄的安裝。
目錄
install(DIRECTORY dirs… DESTINATION <dir>
[FILE_PERMISSIONS permissions…]
[DIRECTORY_PERMISSIONS permissions…]
[USE_SOURCE_PERMISSIONS] [OPTIONAL] [MESSAGE_NEVER]
[CONFIGURATIONS [Debug|Release|…]]
[COMPONENT <component>] [EXCLUDE_FROM_ALL]
[FILES_MATCHING]
[[PATTERN <pattern> | REGEX <regex>]
[EXCLUDE] [PERMISSIONS permissions…]] […])
-
DIRECTORY dirs
:dirs
是所在源文件目錄的相對路徑。但必須注意,abc
與abc/
有很大區別:-
abc
:該目錄將被安裝為目標路徑的abc
; -
abc/
:將該目錄內容安裝到目標路徑,但不包括該目錄本身。
-
示例:
install(DIRECTORY icons scripts/ DESTINATION share/myproj
PATTERN "CVS" EXCLUDE
PATTERN "scripts/*" PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ)
該指令的執行結果是:
- 將 icons 目錄安裝到
<prefix>/share/myproj
; - 將 scripts/ 中的內容安裝到
<prefix>/share/myproj
; - 不包含目錄名為 CVS 的目錄;
- 對于 scripts/* 文件指定權限為
OWNER_EXECUTE
、OWNER_WRITE
、OWNER_READ
、GROUP_EXECUT
、GROUP_READ
。
基本控制語法
if
if…else… 語法格式有些類似于 Visual Basic .NET:
if(<expression>)
# then section.
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
#…
elseif(<expression2>)
# elseif section.
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
#…
else(<expression>)
# else section.
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
#…
endif(<expression>)
其中,一定要有 endif()
與 if()
對應。
if 基本用法
if(<expression>)
:expression
不為0
、OFF
、NO
、FALSE
、N
、IGNORE
、NOTFOUND
、空字符串,或者不含后綴-NOTFOUND
時,為真;if(NOT <expression>
:與上一條相反;if(<expr1> AND <expr2>)
if(<expr1> OR <expr2>
if(COMMAND command-name)
:如果command-name
是命令、宏或函數并可調用,為真;if(EXISTS path-to-file-or-directory)
:如果給定路徑的文件或目錄存在,為真;if(file1 IS_NEWER_THAN file2)
:當 file1 比 file2 新,或 file1/file2 中有一個不存在時為真,文件名需使用全路徑;if(IS_DIRECTORY path-to-directory)
:當給定路徑是目錄時,為真。注意使用全路徑;if(DEFINED <variable>)
:如果變量已被定義,為真;-
if(<variable|string> MATCHES regex)
:當給定變量或字符串能匹配正則表達式 regex 時,為真。此處的variable
直接使用變量名,而非${variable}
。-
示例:
if("hello" MATCHES "ell") message("true") endif("hello" MATCHES "ell")
-
數字比較表達式
if(<var> LESS <number>)
if(<var> GREATER <number>)
if(<var> EQUAL <number>)
字母表順序比較
if(<var1> STRLESS <var2>)
if(<var1> STRGREATER <var2>)
if(<var1> STREQUAL <var2>)
示例 1:
判斷平臺差異。
if(WIN32)
message(STATUS "This is windows.")
else(WIN32)
message(STATUS "This is not windows.")
endif(WIN32)
上述代碼可以控制不同平臺進行不同控制。
也許 else(WIN32)
之類的語句閱讀起來很不舒服,這時候可以加上語句:
set(CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS ON)
這時候上述結構就可以寫成:
if(WIN32)
message(STATUS "This is windows.")
else()
message(STATUS "This is not windows.")
endif()
示例 2:
if(WIN32)
#do something related to WIN32
elseif(UNIX)
#do something related to UNIX
elseif(APPLE)
#do something related to APPLE
endif(WIN32)
while
while(<condition>)
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
…
endwhile(<condition>)
類似于 endif
,while()
也需要 endwhile()
匹配。
真假判斷條件可以參考 if
指令。
foreach
foreach
有多種使用形式的語法,且每個 foreach()
都需要一個 endforeach()
與之匹配。
列表語法
foreach(<loop_var> <arg1> <arg2> …)
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
…
endforeach(<loop_var>)
示例:
aux_source_directory(. SRC_LIST)
foreach(F ${SRC_LIST})
message(${F})
endforeach(F)
該示例中,先將當前路徑下的所有源文件列表賦值給變量 SRC_LIST
,然后遍歷 SRC_LIST
中的文件,并持續輸出信息,信息內容是當前路徑下所有源文件的名稱。
范圍語法
foreach(<loop_var> RANGE <total>)
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
…
endforeach(<loop_var>)
示例:
foreach(v RANGE 10)
message(${v})
endforeach(v)
該示例從 0 到 total
(此處為 10),以 1 為步進。此處輸出為:012345678910
。
范圍步進語法
foreach(<loop_var> RANGE <start> <stop> [<step>])
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
…
endforeach(<loop_var>)
從 start
開始,到 stop
結束,以 step
為步進。
示例:
foreach(a RANGE 5 15 3)
message(${a})
endforeach(a)
此處輸出為 581114
。
迭代語法
foreach
還可以循環訪問生成的數字范圍。這種迭代有三種類型:
- 指定單個數字時,范圍將包含元素
[0, …, total]
(包括total
); - 指定兩個數字時,范圍將包含從第一個數字到第二個數字(包括)的元素;
- 第三個可選數字是用于從第一個數字迭代到第二個數字(包括)的增量。
foreach(<loop_var> IN [LISTS [<list1> …]]
[ITEMS [<item1> …]])
COMMAND1(<ARGS> …)
COMMAND2(<ARGS> …)
…
endforeach(<loop_var>)
foreach
循環訪問一個精確的項列表:
-
LISTS
:給要遍歷的列表值變量命名,包括空元素(空字符串是零長度列表)。注意宏參數不是變量。 -
ITEMS
:結束參數解析并包含迭代中它后面的所有參數。