cmake使用教程(六)-蛋疼的語法

saka發表於2018-02-02

【cmake系列使用教程】

cmake使用教程(一)-起步

cmake使用教程(二)-新增庫

cmake使用教程(三)-安裝、測試、系統自檢

cmake使用教程(四)-檔案生成器

cmake使用教程(五)-cpack生成安裝包

cmake使用教程(六)-蛋疼的語法

cmake使用教程(七)-流程和迴圈

cmake使用教程(八)-macro和function

這個系列的文章翻譯自官方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] ])
複製程式碼

檔案複製到檔案中,並在輸入檔案內容中替換@VAR@${VAR}的變數值。每個變數引用將被替換為變數的當前值,如果變數的值未被定義,則為空字串。(VAR必須與cmakelist.txt中的變數保持一直,否則會生成註釋)

input檔案的定義形式為:

#cmakedefine VAR ..
複製程式碼

經過configure後生成的檔案內容被替換為:

#define VAR ...  //替換成功
/* #undef VAR */ //未定義的變數
複製程式碼

生成的檔案將會保留在'#'與'cmakedefine'之間的空格和製表符。

此處有一點需說明,現在clion預設使用cmake來構建程式,但是在clion中不支援cmakedefine關鍵字,所以可以直接使在input檔案中填寫#define VAR ...來編寫巨集定義,生成的結果與上邊完全一樣。clion有一個問題,就是直接用cmakedefine定義巨集的時候假如#與cmakedefine之間有空格則不會替換cmakedefinedefine,後邊的變數會替換,但是不能編譯成功,所以假如在clion中使用,要注意這幾點,直接使用#define或者#cmakedefine,儘量不要加空格。

介紹其中的選項: input和output假如不指定絕對路徑,則會被預設設定為CMAKE_CURRENT_SOURCE_DIRCMAKE_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選項,那麼編譯器會被告知這些目錄在某些平臺上是指系統包含的目錄。

這翻譯的真是教我頭大。

相關文章