【cmake系列使用教程】
這個系列的文章翻譯自官方cmake教程:cmake tutorial。
示例程式地址:github.com/rangaofei/t…
不會僅僅停留在官方教程。本人作為一個安卓開發者,實在是沒有linux c程式開發經驗,望大佬們海涵。教程是在macos下完成,大部分linux我也測試過,有特殊說明的我會標註出來。本教程基於cmake-3.10.2,同時認為你已經安裝好cmake。
“蛋疼的不止語法,還有文件”
cmake檔案格式
本節講的命令格式遵循如下語法:
格式 | 註釋 |
---|---|
<command> |
必須填寫的 |
[command] |
可寫也可不寫的 |
a\|b |
a或者b均可以 |
cmake能識別CMakeLists.txt和*.cmake格式的檔案。cmake能夠以三種方式 來組織檔案:
類別 | 檔案格式 |
---|---|
Dierctory (資料夾) |
CMakeLists.txt |
Script (指令碼) |
<script>.cmake |
Module (模組) |
<module>.cmake |
本系列主要以CMakeLists.txt的語法為主要講解內容,至於編寫這蛋疼的指令碼和模組假如我還寫的下去的話我就寫。
1. Directory
當CMake處理一個專案時,入口點是一個名為CMakeLists.txt
的原始檔,這個一定是根目錄下的CMakeLists.txt。這個檔案包含整個工程的構建規範,當我們有多個子資料夾需要編譯時,使用add_subdirectory(<dir_name>)
命令來為構建新增子目錄。新增的每個子目錄也必須包含一個CMakeLists.txt
檔案作為該子目錄的入口點。每個子目錄的CMakeLists.txt檔案被處理時,CMake在構建樹中生成相應的目錄作為預設的工作和輸出目錄。記住這一點非常關鍵,這樣我們就可以使用外部構建了,而不必每次都使用蛋疼的內部構建,然後刪除一堆檔案才能從新構建。
2. Script
一個單獨的<script>.cmake
原始檔可以使用cmake命令列工具cmake -P <script>.cmake
選項來執行指令碼。指令碼模式只是在給定的檔案中執行命令,並且不生成構建系統。它不允許CMake命令定義或執行構建目標。
3. Module
在Directory或Script中,CMake程式碼可以使用include()命令來載入.cmake。cmake內建了許多模組用來幫助我們構建工程,前邊文章中提到的CheckFunctionExists
。也可以提供自己的模組,並在CMAKE_MODULE_PATH
變數中指定它們的位置。
cmake基本編寫格式
先看一下定義的方式
名稱 | 表示式 | 我認為的 | 例子 |
---|---|---|---|
space | <match '[ \t]+'> |
任意個空格或者tab | a b |
newline | <match '\n'> |
換行符 | a\nb |
line_comment | '#' <any text not starting in a bracket_argument and not containing a newline> |
以'#"開頭,不在'[]'塊中,不包含換行符 | #bus |
separation | space\|newline |
空格或者換行 | a b=a\nb |
lineending | linecomment?newline |
從註釋開頭到換行符都不執行 | |
command_invocation | space* identifier space* '(' arguments ')' |
||
quoted_argument | "quoted_element* " |
用引號包裹的引數 | "a" |
文件看起來很蛋疼,我直接寫一個最簡單的
add_executable(hello world.c foo.c) #這是一個註釋
複製程式碼
也等於
add_executable(hello
world.c
foo.c)
#這是一個註釋
複製程式碼
就是這麼easy。注意引數這一塊,可以用引號包裹起來,這代表一個引數,假如一行不能寫完,則用\\
符號來表示連線成一行,也可以不用引號,但是假如引數帶有分隔符,則會被認為是多個引數。
定義變數
定義變數常用的函式是set(KEY VALUE)
,取消定義變數是unset(KEY)
。
它們的值始終是string型別的,有些命令可能將字串解釋為其他型別的值。變數名是區分大小寫的,可能包含任何文字,但是我們建議只使用字母數字字元加上_和-這樣的名稱。
變數的作用域和java基本一致,不多做講解。
變數引用的形式為${variable_name}
,並在引用的引數或未引用的引數中進行判斷。變數引用被變數的值替換,或者如果變數沒有設定,則由空字串替換。變數引用可以巢狀,並從內部進行替換,例如${outer_${inner_variable}veriable}
。
環境變數引用的形式為$ENV{VAR},並在相同的上下文中作為正常變數引用。
cmake構建系統
這算是進入正文了。扯淡的介紹就不多講了,直接乾貨。
可執行檔案和庫是使用add_executable()和add_library()命令定義的。生成的二進位制檔案有合適的字首、字尾和針對平臺的擴充套件。二進位制目標之間的依賴關係使用target_link_libraries()命令表示。
add_library(archive archive.cpp zip.cpp lzma.cpp)
add_executable(zipapp zipapp.cpp)
target_link_libraries(zipapp archive)
複製程式碼
在構建c程式的時候,因為要生成可執行檔案,add_executable
是必須的;構建安卓動態庫的時候,add_library
是必須的,因為jni需要呼叫動態庫。
外部構建
前邊提到過cmake的構建命令cmake .
,也就是在當前目錄構建工程,這樣會生成一系列的快取檔案在當前目錄,假如我們需要重新構建需要刪除這些檔案,其中CMakeCache.txt是必須刪除的,否則不會構建最新的程式。
看一下我們在工程根目錄下執行cmake
後是什麼情況:
~/Desktop/Tutorial/Step1/ tree -L 1
.
├── CMakeCache.txt
├── CMakeFiles
├── CMakeLists.txt
├── Makefile
├── Tutorial
├── TutorialConfig.h
├── TutorialConfig.h.in
├── cmake_install.cmake
└── tutorial.cxx
複製程式碼
生成的檔案與原始檔交叉在一起,相當混亂。我們可以採用外部構建來分隔原始檔與生成的檔案,當我們需要清空快取重新構建專案時,就可以刪除這個資料夾下的所有內容,重新執行構建命令,保持原始檔的整潔,從而更容易管理專案。
首先新建一個資料夾build
。這個資料夾就是我們用來存放生成的檔案的目錄,然後進入該目錄,執行構建命令。
mkdir build # 建立build目錄
cd build # 進入build目錄
cmake .. # 因為程式入口構建檔案在專案根目錄下,採用相對路徑上級目錄來使用根目錄下的構建檔案
複製程式碼
此時可以看到生成的檔案全部在build資料夾下了,構建完全沒問題。
~/Desktop/Tutorial/Step1/ tree -L 2
.
├── CMakeLists.txt
├── TutorialConfig.h.in
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ ├── Makefile
│ ├── TutorialConfig.h
│ └── cmake_install.cmake
└── tutorial.cxx
複製程式碼
以後的專案講解中將全部使用外部構建。
下面是我專門為講解一些基本指令編寫的程式碼
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
message(STATUS ${PROJECT_NAME})
message(STATUS ${PROJECT_SOURCE_DIR})
message(STATUS ${PROJECT_BINARY_DIR})
message(STATUS ${Tutorial_SOURCE_DIR})
message(STATUS ${Tutorial_BINARY_DIR})
# The version number.
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)
# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
"${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
"${PROJECT_BINARY_DIR}/TutorialConfig.h"
)
# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories("${PROJECT_BINARY_DIR}")
# add the executable
add_executable(Tutorial tutorial.cxx)
複製程式碼
- 首先看message用法 message類似於一個向控制檯輸出日誌的工具,但是功能又稍微強大一些,在一些模式下能夠終止程式構建。
message([<mode>] "message to display" ...)
複製程式碼
mode有以下幾個模式
模式 | 作用 |
---|---|
(none) | Important information |
STATUS | Incidental information |
WARNING | CMake Warning, continue processing |
AUTHOR_WARNING | CMake Warning (dev), continue processing |
SEND_ERROR | CMake Error, continue processing,but skip generation |
FATAL_ERROR | CMake Error, stop processing and generation |
DEPRECATION | CMake Deprecation Error or Warning if variable CMAKE_ERROR_DEPRECATED or CMAKE_WARN_DEPRECATED is enabled, respectively, else no message. |
STATUS我們經常用到。
- 來看第一行:
cmake_minimum_required (VERSION 2.6)
複製程式碼
為一個專案設定cmake的最低要求版本,並更新策略設定以匹配給定的版本(策略設定我永遠也不會講了)。無論是構建專案還是構建庫,都需要這個命令。 它的語法是這樣的
cmake_minimum_required(VERSION major.minor[.patch[.tweak]]
[FATAL_ERROR])
複製程式碼
版本號必須指定主次代號,後邊的可選,請忽略可選項[FATAL_ERROR]
,完全沒用。
假如你指定的版本號大於你安裝的cmake版本,將會停止構建並丟擲一個錯誤:
CMake Error at CMakeLists.txt:1 (cmake_minimum_required):
CMake 3.11 or higher is required. You are running version 3.10.2
-- Configuring incomplete, errors occurred!
複製程式碼
cmake_minimum_required
必須在專案根目錄下的最開始呼叫,也就是project()
之前。在function()中呼叫該指令也可以,作用域將侷限在函式內,但是必須以不影響全域性使用為前提
- 來看第二行
project (Tutorial)
複製程式碼
指定專案的名稱為Tutorial
,構建專案必須使用這個命令,構建庫可以不指定。文件如下:
project(<PROJECT-NAME> [LANGUAGES] [<language-name>...])
project(<PROJECT-NAME>
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[DESCRIPTION <project-description-string>]
[LANGUAGES <language-name>...])
複製程式碼
設定專案名稱並將該名稱儲存在PROJECT_NAME
變數中。同時也指定了四個變數:
PROJECT_SOURCE_DIR, <PROJECT-NAME>_SOURCE_DIR
PROJECT_BINARY_DIR, <PROJECT-NAME>_BINARY_DIR
複製程式碼
但是我們一般只使用前一個,這樣更容易更改。在上邊的程式碼中我們用message輸出了這些變數的資訊,執行構建命令後日志輸出:
-- Tutorial
-- /Users/saka/Desktop/Tutorial/Step1
-- /Users/saka/Desktop/Tutorial/Step1/build
-- /Users/saka/Desktop/Tutorial/Step1
-- /Users/saka/Desktop/Tutorial/Step1/build
複製程式碼
可以看到這幾個變數確實輸出了正確的值。
我們也可以在指定專案名稱時直接指定版本號,假如沒有指定,則版本號為空。 版本號儲存在下邊幾個變數中:
PROJECT_VERSION, <PROJECT-NAME>_VERSION
PROJECT_VERSION_MAJOR, <PROJECT-NAME>_VERSION_MAJOR
PROJECT_VERSION_MINOR, <PROJECT-NAME>_VERSION_MINOR
PROJECT_VERSION_PATCH, <PROJECT-NAME>_VERSION_PATCH
PROJECT_VERSION_TWEAK, <PROJECT-NAME>_VERSION_TWEAK
複製程式碼
通常我們推薦使用前一個。現在測試一下,在CMakeLists.txt檔案中修改程式碼:
project (Tutorial
VERSION 1.2.3
DESCRIPTION "this is description"
LANGUAGES CXX)
message(STATUS ${PROJECT_VERSION})
message(STATUS ${PROJECT_VERSION_MAJOR})
message(STATUS ${PROJECT_VERSION_MINOR})
message(STATUS ${PROJECT_VERSION_PATCH})
message(STATUS ${PROJECT_VERSION_TWEAK})
message(STATUS ${PROJECT_DESCRIPTION})
複製程式碼
輸出日誌如下:
-- 1.2.3
-- 1
-- 2
-- 3
--
-- this is description
複製程式碼
在這設定版本號和用set設定版本號效果一樣,取最後一次設定的值。由於我們沒有指定tweak版本,所以為空,同時看到description被儲存到PROJECT_DESCRIPTION
這個變數中了。
可以通過設定LANGUAGES
來指定程式語言是C、CXX(即c++)或者Fortran等,如果沒有設定此項,預設啟用C和CXX。設定為NONE,或者只寫LANGUAGES
關鍵字而不寫具體源語言,可以跳過啟用任何語言。一般都是用cmake來編譯c或者c++程式,所以用預設的就可以了。
- 來看重要的一行
configure_file
該命令的作用是複製檔案到另一個地方並修改檔案內容。語法如下:
configure_file(<input> <output>
[COPYONLY] [ESCAPE_QUOTES] [@ONLY]
[NEWLINE_STYLE [UNIX|DOS|WIN32|LF|CRLF] ])
複製程式碼
將檔案複製到
input檔案的定義形式為:
#cmakedefine VAR ..
複製程式碼
經過configure後生成的檔案內容被替換為:
#define VAR ... //替換成功
/* #undef VAR */ //未定義的變數
複製程式碼
生成的檔案將會保留在'#'與'cmakedefine'之間的空格和製表符。
此處有一點需說明,現在clion預設使用cmake來構建程式,但是在clion中不支援
cmakedefine
關鍵字,所以可以直接使在input檔案中填寫#define VAR ...
來編寫巨集定義,生成的結果與上邊完全一樣。clion有一個問題,就是直接用cmakedefine
定義巨集的時候假如#與cmakedefine之間有空格則不會替換cmakedefine
為define
,後邊的變數會替換,但是不能編譯成功,所以假如在clion中使用,要注意這幾點,直接使用#define
或者#cmakedefine
,儘量不要加空格。
介紹其中的選項:
input和output假如不指定絕對路徑,則會被預設設定為CMAKE_CURRENT_SOURCE_DIR
和CMAKE_CURRENT_BINARY_DIR
,也就是專案根目錄和構建的目錄;
COPYONLY
則只是複製檔案,不替換任何東西,不能和NEWLINE_STYLE <style>
一起使用。
ESCAPE_QUOTES
禁止為"
轉義。這個很蛋疼,不加這個命令的話假如變數中有a\"b
,則在生成的檔案中會直接使用轉義後的字元a"b
,加上指令後則按原來的文字顯示a\"b
;
@ONLY
只允許替換@VAR@
包裹的變數${VAR}
則不會被替換;
NEWLINE_STYLE <style>
設定換行符格式
現在舉個例子: foo.h.in檔案如下
#cmakedefine FOO_ENABLE
#cmakedefine FOO_STRING "@FOO_STRING@"
複製程式碼
CMakeLists.txt中新增程式碼來設定一個開關,下邊會執行if中的語句:
option(FOO_ENABLE "Enable Foo" ON)
if(FOO_ENABLE)
set(FOO_STRING "foo")
endif()
configure_file(foo.h.in foo.h @ONLY)
複製程式碼
生成的檔案foo.h
#define FOO_ENABLE
#define FOO_STRING "foo"
複製程式碼
假如設定為off,option(FOO_ENABLE "Enable Foo" ON)
,則不會執行if中的語句,生成的檔案如下:
/* #undef FOO_ENABLE */
/* #undef FOO_STRING */
複製程式碼
include_directories
這一行
這句話的意思將當前的二進位制目錄新增到編譯器搜尋include目錄中,這樣就可以直接使用上一步生成的標頭檔案了。
include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])
複製程式碼
將給定的目錄新增到編譯器用來搜尋包含檔案的目錄。相對路徑為相對於當前根目錄。
括號中的目錄被新增到當前CMakeLists檔案的INCLUDE_DIRECTORIES
目錄屬性中。它們也被新增到當前CMakeLists檔案中的每個目標的INCLUDE_DIRECTORIES目標屬性中。。
預設情況下,指定的目錄被追加到當前的include目錄列表中。通過將CMAKE_INCLUDE_DIRECTORIES_BEFORE
設定為ON,可以更改此預設行為。通過明確使用AFTER或BEFORE,您可以選擇新增和預先設定。
如果給出SYSTEM選項,那麼編譯器會被告知這些目錄在某些平臺上是指系統包含的目錄。
這翻譯的真是教我頭大。