CMake 入門實戰
from:https://www.hahack.com/codes/cmake/
從例項入手,講解 CMake 的常見用法。
什麼是 CMake
All problems in computer science can be solved by another level of indirection.
David Wheeler
你或許聽過好幾種 Make 工具,例如GNU Make,QT 的qmake,微軟的MS nmake,BSD Make(pmake),Makepp,等等。這些 Make 工具遵循著不同的規範和標準,所執行的 Makefile 格式也千差萬別。這樣就帶來了一個嚴峻的問題:如果軟體想跨平臺,必須要保證能夠在不同平臺編譯。而如果使用上面的 Make 工具,就得為每一種標準寫一次 Makefile ,這將是一件讓人抓狂的工作。
CMake 就是針對上面問題所設計的工具:它首先允許開發者編寫一種平臺無關的 CMakeList.txt 檔案來定製整個編譯流程,然後再根據目標使用者的平臺進一步生成所需的本地化 Makefile 和工程檔案,如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程。從而做到“Write once, run everywhere”。顯然,CMake 是一個比上述幾種 make 更高階的編譯配置工具。一些使用 CMake 作為專案架構系統的知名開源專案有VTK、ITK、KDE、OpenCV、OSG等[1]。
在 linux 平臺下使用 CMake 生成 Makefile 並編譯的流程如下:
- 編寫 CMake 配置檔案 CMakeLists.txt 。
- 執行命令
cmake PATH
或者ccmake PATH
生成 Makefile(ccmake
和cmake
的區別在於前者提供了一個互動式的介面)。其中,PATH
是 CMakeLists.txt 所在的目錄。 - 使用
make
命令進行編譯。
本文將從例項入手,一步步講解 CMake 的常見用法,文中所有的例項程式碼可以在這裡找到。如果你讀完仍覺得意猶未盡,可以繼續學習我在文章末尾提供的其他資源。
入門案例:單個原始檔
本節對應的原始碼所在目錄:Demo1。
對於簡單的專案,只需要寫幾行程式碼就可以了。例如,假設現在我們的專案中只有一個原始檔
1
|
|
編寫 CMakeLists.txt
首先編寫 CMakeLists.txt 檔案,並儲存在與main.cc原始檔同個目錄下:
1
|
# CMake 最低版本號要求
|
CMakeLists.txt 的語法比較簡單,由命令、註釋和空格組成,其中命令是不區分大小寫的。符號#
後面的內容被認為是註釋。命令由命令名稱、小括號和引數組成,引數之間使用空格進行間隔。
對於上面的 CMakeLists.txt 檔案,依次出現了幾個命令:
cmake_minimum_required
:指定執行此配置檔案所需的 CMake 的最低版本;project
:引數值是Demo1
,該命令表示專案的名稱是Demo1
。add_executable
: 將名為main.cc的原始檔編譯成一個名稱為 Demo 的可執行檔案。
編譯專案
之後,在當前目錄執行cmake .
,得到 Makefile 後再使用make
命令編譯得到 Demo1 可執行檔案。
1
|
[ehome@xman Demo1]$ cmake .
|
多個原始檔
同一目錄,多個原始檔
本小節對應的原始碼所在目錄:Demo2。
上面的例子只有單個原始檔。現在假如把power
函式單獨寫進一個名為MathFunctions.c
的原始檔裡,使得這個工程變成如下的形式:
1
|
./Demo2
|
這個時候,CMakeLists.txt 可以改成如下的形式:
1
|
# CMake 最低版本號要求
|
唯一的改動只是在add_executable
命令中增加了一個MathFunctions.cc
原始檔。這樣寫當然沒什麼問題,但是如果原始檔很多,把所有原始檔的名字都加進去將是一件煩人的工作。更省事的方法是使用aux_source_directory
命令,該命令會查詢指定目錄下的所有原始檔,然後將結果存進指定變數名。其語法如下:
1
|
aux_source_directory(<dir> <variable>)
|
因此,可以修改 CMakeLists.txt 如下:
1
|
# CMake 最低版本號要求
|
這樣,CMake 會將當前目錄所有原始檔的檔名賦值給變數DIR_SRCS
,再指示變數DIR_SRCS
中的原始檔需要編譯成一個名稱為 Demo 的可執行檔案。
多個目錄,多個原始檔
本小節對應的原始碼所在目錄:Demo3。
現在進一步將 MathFunctions.h 和MathFunctions.cc檔案移動到 math 目錄下。
1
|
./Demo3
|
對於這種情況,需要分別在專案根目錄 Demo3 和 math 目錄裡各編寫一個 CMakeLists.txt 檔案。為了方便,我們可以先將 math 目錄裡的檔案編譯成靜態庫再由 main 函式呼叫。
根目錄中的 CMakeLists.txt :
1
|
# CMake 最低版本號要求
|
該檔案添加了下面的內容: 第3行,使用命令add_subdirectory
指明本專案包含一個子目錄 math,這樣 math 目錄下的 CMakeLists.txt 檔案和原始碼也會被處理 。第6行,使用命令target_link_libraries
指明可執行檔案 main 需要連線一個名為 MathFunctions 的連結庫 。
子目錄中的 CMakeLists.txt:
1
|
# 查詢當前目錄下的所有原始檔
|
在該檔案中使用命令add_library
將 src 目錄中的原始檔編譯為靜態連結庫。
自定義編譯選項
本節對應的原始碼所在目錄:Demo4。
CMake 允許為專案增加編譯選項,從而可以根據使用者的環境和需求選擇最合適的編譯方案。
例如,可以將 MathFunctions 庫設為一個可選的庫,如果該選項為ON
,就使用該庫定義的數學函式來進行運算。否則就呼叫標準庫中的數學函式庫。
修改 CMakeLists 檔案
我們要做的第一步是在頂層的 CMakeLists.txt 檔案中新增該選項:
1
|
# CMake 最低版本號要求
|
其中:
- 第7行的
configure_file
命令用於加入一個配置標頭檔案 config.h ,這個檔案由 CMake 從config.h.in生成,通過這樣的機制,將可以通過預定義一些引數和變數來控制程式碼的生成。 - 第13行的
option
命令添加了一個USE_MYMATH
選項,並且預設值為ON
。 - 第17行根據
USE_MYMATH
變數的值來決定是否使用我們自己編寫的 MathFunctions 庫。
修改main.cc檔案
之後修改main.cc檔案,讓其根據USE_MYMATH
的預定義值來決定是否呼叫標準庫還是 MathFunctions 庫:
1
|
#include <stdio.h>
|
編寫config.h.in檔案
上面的程式值得注意的是第2行,這裡引用了一個 config.h 檔案,這個檔案預定義了USE_MYMATH
的值。但我們並不直接編寫這個檔案,為了方便從 CMakeLists.txt 中匯入配置,我們編寫一個config.h.in檔案,內容如下:
1
|
#cmakedefine USE_MYMATH
|
這樣 CMake 會自動根據 CMakeLists 配置檔案中的設定自動生成 config.h 檔案。
編譯專案
現在編譯一下這個專案,為了便於互動式的選擇該變數的值,可以使用ccmake
命令(也可以使用cmake -i
命令,該命令會提供一個會話式的互動式配置介面):
CMake的互動式配置介面
從中可以找到剛剛定義的USE_MYMATH
選項,按鍵盤的方向鍵可以在不同的選項視窗間跳轉,按下enter
鍵可以修改該選項。修改完成後可以按下c
選項完成配置,之後再按g
鍵確認生成 Makefile 。ccmake 的其他操作可以參考視窗下方給出的指令提示。
我們可以試試分別將USE_MYMATH
設為ON
和OFF
得到的結果:
USE_MYMATH 為 ON
執行結果:
1
|
[ehome@xman Demo4]$ ./Demo
|
此時 config.h 的內容為:
1
|
#define USE_MYMATH
|
USE_MYMATH 為 OFF
執行結果:
1
|
[ehome@xman Demo4]$ ./Demo
|
此時 config.h 的內容為:
1
|
/* #undef USE_MYMATH */
|
安裝和測試
本節對應的原始碼所在目錄:Demo5。
CMake 也可以指定安裝規則,以及新增測試。這兩個功能分別可以通過在產生 Makefile 後使用make install
和make test
來執行。在以前的 GNU Makefile 裡,你可能需要為此編寫install
和test
兩個偽目標和相應的規則,但在 CMake 裡,這樣的工作同樣只需要簡單的呼叫幾條命令。
定製安裝規則
首先先在 math/CMakeLists.txt 檔案裡新增下面兩行:
1
|
# 指定 MathFunctions 庫的安裝路徑
|
指明 MathFunctions 庫的安裝路徑。之後同樣修改根目錄的 CMakeLists 檔案,在末尾新增下面幾行:
1
|
# 指定安裝路徑
|
通過上面的定製,生成的 Demo 檔案和 MathFunctions 函式庫 libMathFunctions.o 檔案將會被複制到/usr/local/bin
中,而 MathFunctions.h 和生成的 config.h 檔案則會被複制到/usr/local/include
中。我們可以驗證一下(順帶一提的是,這裡的/usr/local/
是預設安裝到的根目錄,可以通過修改CMAKE_INSTALL_PREFIX
變數的值來指定這些檔案應該拷貝到哪個根目錄):
1
|
[ehome@xman Demo5]$ sudo make install
|
為工程新增測試
新增測試同樣很簡單。CMake 提供了一個稱為 CTest 的測試工具。我們要做的只是在專案根目錄的 CMakeLists 檔案中呼叫一系列的add_test
命令。
1
|
# 啟用測試
|
上面的程式碼包含了四個測試。第一個測試test_run
用來測試程式是否成功執行並返回 0 值。剩下的三個測試分別用來測試 5 的 平方、10 的 5 次方、2 的 10 次方是否都能得到正確的結果。其中PASS_REGULAR_EXPRESSION
用來測試輸出是否包含後面跟著的字串。
讓我們看看測試的結果:
1
|
[ehome@xman Demo5]$ make test
|
如果要測試更多的輸入資料,像上面那樣一個個寫測試用例未免太繁瑣。這時可以通過編寫巨集來實現:
1
|
# 定義一個巨集,用來簡化測試工作
|
關於 CTest 的更詳細的用法可以通過man 1 ctest
參考 CTest 的文件。
支援 gdb
讓 CMake 支援 gdb 的設定也很容易,只需要指定Debug
模式下開啟-g
選項:
1
|
set(CMAKE_BUILD_TYPE "Debug")
|
之後可以直接對生成的程式使用 gdb 來除錯。
新增環境檢查
本節對應的原始碼所在目錄:Demo6。
有時候可能要對系統環境做點檢查,例如要使用一個平臺相關的特性的時候。在這個例子中,我們檢查系統是否自帶 pow 函式。如果帶有 pow 函式,就使用它;否則使用我們定義的 power 函式。
新增 CheckFunctionExists 巨集
首先在頂層 CMakeLists 檔案中新增 CheckFunctionExists.cmake 巨集,並呼叫check_function_exists
命令測試連結器是否能夠在連結階段找到pow
函式。
1
|
# 檢查系統是否支援 pow 函式
|
將上面這段程式碼放在configure_file
命令前。
預定義相關巨集變數
接下來修改config.h.in檔案,預定義相關的巨集變數。
1
|
// does the platform provide pow function?
|
在程式碼中使用巨集和函式
最後一步是修改main.cc,在程式碼中使用巨集和函式:
1
|
#ifdef HAVE_POW
|
新增版本號
本節對應的原始碼所在目錄:Demo7。
給專案新增和維護版本號是一個好習慣,這樣有利於使用者瞭解每個版本的維護情況,並及時瞭解當前所用的版本是否過時,或是否可能出現不相容的情況。
首先修改頂層 CMakeLists 檔案,在project
命令之後加入如下兩行:
1
|
set (Demo_VERSION_MAJOR 1)
|
分別指定當前的專案的主版本號和副版本號。
之後,為了在程式碼中獲取版本資訊,我們可以修改config.h.in檔案,新增兩個預定義變數:
1
|
// the configured options and settings for Tutorial
|
這樣就可以直接在程式碼中列印版本資訊了:
1
|
|
生成安裝包
本節對應的原始碼所在目錄:Demo8。
本節將學習如何配置生成各種平臺上的安裝包,包括二進位制安裝包和原始碼安裝包。為了完成這個任務,我們需要用到 CPack ,它同樣也是由 CMake 提供的一個工具,專門用於打包。
首先在頂層的 CMakeLists.txt 檔案尾部新增下面幾行:
1
|
# 構建一個 CPack 安裝包
|
上面的程式碼做了以下幾個工作:
- 匯入 InstallRequiredSystemLibraries 模組,以便之後匯入 CPack 模組;
- 設定一些 CPack 相關變數,包括版權資訊和版本資訊,其中版本資訊用了上一節定義的版本號;
- 匯入 CPack 模組。
接下來的工作是像往常一樣構建工程,並執行cpack
命令。
- 生成二進位制安裝包:
1
|
cpack -C CPackConfig.cmake
|
- 生成原始碼安裝包
1
|
cpack -C CPackSourceConfig.cmake
|
我們可以試一下。在生成專案後,執行cpack -C CPackConfig.cmake
命令:
1
|
[ehome@xman Demo8]$ cpack -C CPackSourceConfig.cmake
|
此時會在該目錄下建立 3 個不同格式的二進位制包檔案:
1
|
[ehome@xman Demo8]$ ls Demo8-*
|
這 3 個二進位制包檔案所包含的內容是完全相同的。我們可以執行其中一個。此時會出現一個由 CPack 自動生成的互動式安裝介面:
1
|
[ehome@xman Demo8]$ sh Demo8-1.0.1-Linux.sh
|
完成後提示安裝到了 Demo8-1.0.1-Linux 子目錄中,我們可以進去執行該程式:
1
|
[ehome@xman Demo8]$ ./Demo8-1.0.1-Linux/bin/Demo 5 2
|
關於 CPack 的更詳細的用法可以通過man 1 cpack
參考 CPack 的文件。
將其他平臺的專案遷移到 CMake
CMake 可以很輕鬆地構建出在適合各個平臺執行的工程環境。而如果當前的工程環境不是 CMake ,而是基於某個特定的平臺,是否可以遷移到 CMake 呢?答案是可能的。下面針對幾個常用的平臺,列出了它們對應的遷移方案。
autotools
- am2cmake可以將 autotools 系的專案轉換到 CMake,這個工具的一個成功案例是 KDE 。
- Alternative Automake2CMake可以轉換使用 automake 的 KDevelop 工程專案。
- Converting autoconf tests
qmake
- qmake converter可以轉換使用 QT 的 qmake 的工程。
Visual Studio
- vcproj2cmake.rb可以根據 Visual Studio 的工程檔案(字尾名是
.vcproj
或.vcxproj
)生成 CMakeLists.txt 檔案。 - vcproj2cmake.ps1vcproj2cmake 的 PowerShell 版本。
- folders4cmake根據 Visual Studio 專案檔案生成相應的 “source_group” 資訊,這些資訊可以很方便的在 CMake 指令碼中使用。支援 Visual Studio 9/10 工程檔案。
CMakeLists.txt 自動推導
- gencmake根據現有檔案推導 CMakeLists.txt 檔案。
- CMakeListGenerator應用一套檔案和目錄分析創建出完整的 CMakeLists.txt 檔案。僅支援 Win32 平臺。
相關連結
類似工具
- SCons:Eric S. Raymond、Timothee Besset、Zed A. Shaw 等大神力薦的專案架構工具。和 CMake 的最大區別是使用 Python 作為執行指令碼。