平時開發中我們已經習慣了讓IDE爲我們做好一切,大部分情況下基本上不需要手動去編寫項目的make文件,但是在規模較大的項目中,make其實非常重要,甚至可以說會不會make決定了你是否真的瞭解項目的整體架構並駕馭它。因爲自己在Android開發中發現項目中的NDK部分已經拋棄了傳統的Android.mk,與時俱進用上了CMake,因此打算靜下心來好好學習學習,本文開始對學習CMake的過程做個筆記,以加深印象。參考書籍是《mastering cmake》,這應該算得上是關於cmake的一部經典之作,感興趣的讀者可以下載電子書或者購買一本學習。
1 爲什麼要用CMake
大家可能發現現在CMake用的越來越多,就以Android爲例,以前NDK開發時都是用的安卓特有的Android.mk,但是現在基本上都用CMake了,雖然Android.mk依然支持,但是筆者所參與的項目中,它已經被無情的拋棄了。爲什麼用CMake?簡單一句話概括就是:
跨平臺:一份make可以支持多個平臺。
想象一下牛逼的你寫了一個牛逼的庫,然後你想讓多個平臺的開發者都能享用你的庫。假如寫這個庫的時候還沒有CMake這個東西,那麼你要怎麼辦呢?你必須寫一份unix系統上的make file以便這個庫能夠在類unix系統上構建;然後你得搞份Android.mk以便它同樣能在Android上構建;你還得考慮Windows上用visual studio的開發者......,如此衆多的平臺,想想都頭疼。
CMake就是幫我們解決這個問題的,它讓make的編寫對特定的平臺透明:開發者只需要按照CMake的語法寫make,不需要考慮具體平臺,最終由CMake爲我們生成原生的構建工具(比如Windows上的visual studio,Mac上的XCode,unix/linux的make)所需要的構建文件。使用CMake可以讓我們享受到很多的福利,我們列舉出一些來感受下:
(1) 自動搜索你的軟件所依賴的庫、頭文件,CMake在搜索的時候會將環境變量和註冊表(Windows平臺)也包含在內;
(2) 項目的構建目錄和源碼目錄分離:也就是說可以在項目源碼目錄之外單獨建立一個構建目錄,用於存放構建過程中生成的文件。比如下面這樣的目錄結構:
+ src //項目的源碼
|
-------- main.cpp
|
+ bin //項目的構建目錄
|
-------- test.so
構建目錄和源碼目錄分離,使得我們可以隨時刪除構建文件而不用擔心會誤刪掉項目的源碼文件。
(3) 在配置階段選擇可選的組件:例如你的項目需要用到某個功能,有兩個庫都可以完成此功能,其中一個庫體積大但是效率很高,另一個庫體積小但是效率較低。通過CMake你可以方便的根據你項目的情況決定選擇哪個庫鏈接,例如Window平臺,你可以選擇體積大,效率高的庫,而Android平臺考慮到移動終端容量限制,你可以選擇使用體積小,效率低一點的庫。
(4) 很容易在共享庫和靜態庫的構建上進行切換:你可以方便的指定是要構建共享庫還是靜態庫,CMake在背後幫我們處理了構建共享庫所需要的平臺相關的鏈接器選項。
(5) CMake能自動生成項目文件的依賴關係,同時絕大多數平臺上支持並行構建。
如果你在開發一個跨平臺項目,比如說一個跨平臺的視頻播放器,CMake還會帶來一些額外福利:
(1) CMake可以幫我們檢測機器的字節序以及一些特定於硬件的信息,這一點很重要,在跨平臺項目中,忘記字節序是一些很難追查的bug的根源之一;
(2) 如前文所說:一份cmake構建腳本在多個平臺上使用。
2 CMake的歷史
CMake如此牛逼,它是怎麼來滴呢?CMake最初是ITK項目的一部分,ITK項目始於1999年,由美國麥迪遜國家實驗室贊助。ITK項目規模比較龐大,並且需要在多個平臺上運行,同時還依賴於其他一些軟件庫。爲了滿足軟件的構建需求,需要一個功能足夠強大同時又簡單易用的構建工具,於是ITK項目的開發者設計出了CMake來滿足需求。當CMake誕生以後,因爲它的靈活易用和強大的功能,越來越多的項目中都用上了它,這其中最著名的案例就是KDE,一個龐大的開源項目,KDE採用了cmake來進行構建,這也證明了cmake確實是一個能夠解決大型項目構建問題的解決方案。
CMake最近一次的更新中,加入了CTest和CPack,CMake能夠支持軟件測試,而CPack則支持跨平臺的軟件發佈,CPack利用已有的比較受大衆青睞的工具包比如RPM,Cygwin,PackageMaker等,創建各種原生系統上的軟件安裝包。
當然CMake還加入了其他一些有用特性,例如支持XCdoe和Visual Studio 10。CMake現在還支持嵌入式設備和其他操作系統的交叉編譯。總之CMake作爲一個開源項目,自身在不斷的進化,功能也會越來越強大。
3 初識CMake
本節我們通過一個沒有什麼實際意義的示例項目,來演示一下CMake的用法,讓大家對CMake建立一個宏觀印象,一些細節可以不用太關注,後續文章還會有更詳細的說明。
3.1 示例項目
示例項目是這樣的:
(1) 我們自己寫了一個數學函數庫mymath,提供一些數學運算。
(2) 項目最終生成一個可執行文件Test,它連接我們的mymath數學庫完成相關運算。
(3) 我們的項目需要使用boost庫的相關特性,因此可執行文件Test還需要鏈接boost庫。
(4) 需要使用C++11提供的一些特性;
(5) 在MAC系統上,我們使用clang++來編譯,在linux系統上我們使用g++編譯;
再次說明不要關注這個示例的代碼,我們只是用它來演示CMake的相關特性。示例項目的目錄結構如下:
從上面的目錄結構中注意這樣一個事實:根目錄Test和數學庫目錄math下各有一個CMakeLists.txt,這是因爲math目錄下將編譯出一個靜態庫目標文件:my_math靜態庫,而利用根目錄下的文件將編出可執行目標:Test。
我們將構建目錄和源碼目錄分離,Test目錄爲項目的源代碼目錄,bin目錄爲構建目錄,存放構建過程中生成的文件。
3.2 數學庫
我們的數學庫非常簡單,提供一個計算任意數平方的接口和一個統計數組中小於某個值的數字的個數的接口,頭文件如下:
//計算任意數的平方
template <typename T>
auto square(const T& t) -> decltype(t * t) {
return t * t;
}
//統計數組中小於指定數的個數
extern int count(int *arr, int len, int val);
注意到計算任意數平方的函數,我們使用了C++11的特性。
math.cpp的實現非常簡單,這裏就不在貼出來了。
然後我們需要在math子目錄下編譯出一個靜態庫目標,在math子目錄下添加一個名爲CMakeFileLists.txt的文件,cmake將解析這個文件,執行其中的命令。對於示例程序而言很簡單,只需要告訴cmake根據指定的c++源文件生成一個靜態庫而已,因此math/CMakeFileLists.txt就一句話:
#生成一個靜態庫目標
add_library (${MATH_LIB} STATIC math.cpp)
CMake的命令具有如下這樣的格式:
command ( arg1 arg2 .... )
其中command爲cmake支持的命令,括號中是提供給命令的參數,參數之間以空格分開。
上面這條add_library命令,告訴cmake生成一個庫目標,第一個參數表示生成的庫目標的名字,這裏取上層目錄傳入的變量MATH_LIB的值作爲目標名,第二個參數STATIC表示生成靜態庫,隨後的參數math.cpp告訴cmake編譯靜態庫需要的源代碼文件。
關於目標(target)的概念,我們放在下一篇文章在解釋,現在只需要知道target通常是一個可執行文件或者是庫文件就可以了。
好了,到目前爲止,我們已經知道了cmake中的一些概念:命令(command),目標(target)和變量,我們還看到了CMAKE中取變量值的方法:${VAR},而且我們也知道了用add_library命令給項目生成一個庫目標(靜態/動態)的方法。
3.3 可執行文件
我們已經寫好了數學庫的CMakeFileLists.txt,現在我們需要寫一個程序來引用數學庫了,直接在main.cpp中做就好了:
#include <iostream>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <thread>
#include <functional>
#include <math2.h>
using namespace std;
static void square_int(int i) {
cout << "square(" << i << ")=" << square(i) << endl;
}
static double square_double(double d) {
cout << "square(" << d << ")=" << square(d) << endl;
}
int main(int argc, char **argv) {
int array[10] = {5, 20, 30, 6, 0, 40, 3, 100, 9, 88};
cout << "number less than 10:" << count(array, 10, 10) << endl;
boost::thread_group c;
c.create_thread(boost::bind(&square_int, 4));
c.create_thread(boost::bind(&square_double, 5.6));
c.create_thread(boost::bind(&square_double, 10004.135));
c.join_all();
return 0;
}
再次申明示例代碼只是演示cmake用,不要在意它的實現方式。在main中,我們使用了boost庫的thread_group,向線程組中添加了3個線程,線程引用了數學庫的square方法來計算平方,另外還調用數學庫的count方法統計了一個數組中小於10的數字的個數。
OK,目前爲止這個示例程序已經用到了boost庫和c++11的特性,同時還用到了我們自己的數學庫的功能,我們看看如何編寫CMakeFileLists.txt,讓cmake將這一切整合起來,爲我們生成可執行文件,我們來看看最終的CMakeFileLists.txt:
#一般都以這一行開始
cmake_minimum_required (VERSION 2.6)
#項目名
project (TEST)
#選擇編譯器,LINUX上選擇g++,MAC OS上選擇clang++
if (APPLE)
set (CMAKE_CXX_COMPILER clang++)
elseif (UNIX)
set (CMAKE_CXX_COMPILER g++)
endif()
#設置編譯器選項支持c++11
set (CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -std=c++11)
#查找boost庫
find_library (BOOST_SYSTEM
NAMES boost_system
PATHS /usr/lib /usr/local/lib
)
find_library (BOOST_THREAD
NAMES boost_thread
PATHS /usr/lib /usr/local/lib
)
#設置數學庫的目標名,該變量在子目錄也是可見的
set (MATH_LIB "my_math")
#添加頭文件搜索路徑
include_directories (./math)
#添加子目錄,這樣math目錄纔會被編譯
add_subdirectory (math)
#添加可執行目標文件Test
add_executable (Test main.cpp)
#LIBS變量存儲所有需要鏈接的庫
set (LIBS ${MATH_LIB})
if (BOOST_SYSTEM)
set (LIBS ${LIBS} ${BOOST_SYSTEM})
endif()
if (BOOST_THREAD)
set (LIBS ${LIBS} ${BOOST_THREAD})
endif()
#爲可執行文件鏈接數學庫
target_link_libraries (Test "${LIBS}")
首先以cmake_minimum_required命令告訴cmake編譯這個項目需要的最小的cmake版本,一般都以它爲開始。
然後命令project (TEST)告訴cmake工程的名字。
接下來我們根據當前所在的系統來指定編譯器,在蘋果上我們用clang++來編譯項目,在UNIX系統上用g++來編譯,這可以通過下面的代碼實現:
if (APPLE)
set (CMAKE_CXX_COMPILER clang++)
elseif (UNIX)
set (CMAKE_CXX_COMPILER g++)
endif()
APPLE和UNIX是CMake內置的值,可以直接用來判斷用戶當前的系統。CMAKE_CXX_COMPILER是CMake內置的變量,用來告訴cmake編譯器的位置,通過set命令可以設定變量的值。
除了上面這種通過修改CMAKE_CXX_COMPILER變量的方式指定編譯器的方法外,還有兩種方法也可以達成這個目的:一種是在系統環境變量中增加名爲CC或者CXX環境變量(分別對應C和C++編譯器),另外一種方法是在命令行執行cmake命令式,通過-D選項來指定:cmake -DCMAKE_CXX_COMPILER=clang++。推薦的方式是在環境變量中指定編譯器。
繼續,因爲我們的示例項目中用到了c++11的特性,因此我們還要指定編譯器選項以開啓c++11,與設定編譯器一樣,可以通過set命令設定CMake的內置變量CMAKE_CXX_FLAGS來做到:
set ( CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -std=c++11 )
set命令可以給變量設置一組值,例如set (Foo a b c),則變量Foo可以理解爲是一個字符串數組:Foo = {a, b, c}。假設cmake執行了下面兩條指令:
set (Foo a b c)
set (Foo ${Foo} d)
則最終變量Foo = {a, b, c, d}
同樣的,除了設置CMake內置變量的方式外,編譯器選項也可以在環境變量中設置:CFLAGS和CXXFLAGS分別對應C和C++編譯器的編譯選項。
如果在環境變量中設置編譯器和編譯器選項,上面的這一部分代碼就可以從CMakeFileLists.txt中幹掉了。
現在,我們已經開啓了C++11的特性,接下來,我們要準備將我們自己的數學庫加入進來:
set (MATH_LIB "my_math")
include_directories (./math)
add_subdirectory (math)
還記得之前math目錄的CMakeFileLists.txt嗎?在那裏我們用add_library生成一個名字由變量MATH_LIB的值指定的靜態庫目標,那裏用到的變量MATH_LIB就是在這裏傳入的。由此我們也知道了CMake中外層創建的變量,會自動傳遞給子目錄。我們用set命令指定數學庫目標的名字爲my_math。
然後我們用include_directories命令來指定頭文件的搜索路徑,這相當於編譯器的-I選項,告訴編譯器去哪裏找頭文件。這樣我們在代碼中可以直接通過#include <math2.h>的方式引入數學庫頭文件。
最後用add_subdirectory命令將數學庫的子目錄math加入進來,這一步是必須的,只有這樣,math子目錄下的CMakeFileLists纔會被cmake解析並構建出指定的target。
現在,math數學庫也已經準備好了,接下來就需要加入boost庫的支持了,我們直接用CMake的find_library來查找需要的庫,在示例程序中,需要用到兩個boost庫:boost_system和boost_thread:
find_library (BOOST_SYSTEM
NAMES boost_system
PATHS /usr/lib /usr/local/lib
)
find_library (BOOST_THREAD
NAMES boost_thread
PATHS /usr/lib /usr/local/lib
)
find_library命令用來查找庫,第一個參數用來存放查找的結果,第二個參數NAMES指定要查找的庫的名,可以有多個,例如boost的thread就有boost_thread和boost_thread_mt兩個版本,PATHS用來指定在哪裏搜尋指定的庫。現在我們用一個變量LIBS把程序需要鏈接的庫都保存起來:
set (LIBS ${MATH_LIB})
if ( BOOST_SYSTEM)
set (LIBS ${LIBS} ${BOOST_SYSTEM})
endif()
if (BOOST_THREAD)
set (LIBS ${LIBS} ${BOOST_THREAD})
endif()
萬事俱備,只欠東風,最後我們就要讓cmake爲我們生成可執行文件了:
add_executable (Test main.cpp)
add_executable命令生成一個可執行的目標,第一個參數爲可執行目標的名稱,隨後的參數是構建該目標需要的源文件。
光這樣還不行,我們還得把數學庫和需要的boost庫鏈接到可執行文件上,否則就會出現undefined symbol這樣的錯誤。通過target_link_libraries命令,我們將需要的庫鏈接進來,示例程序需要的庫已經保存在了LIBS變量中:
target_link_libraries (Test "${LIBS}")
target_link_libraries命令用來鏈接指定的庫到目標,第一個參數是目標名,隨後是目標需要鏈接的庫所在路徑。
3.4 運行CMake
我們的CMakeFileLists.txt構建腳本已經編寫完成了,接下來就要運行cmake,CMake有多種運行方式,可以通過cmake gui,通過圖形界面的形式,也可以在命令行直接運行,這裏只說明在命令行運行的方式。
因爲示例項目採用的是構建目錄和源代碼目錄分離的方式,對於這種結構,可以按如下步驟運行cmake:
首先cd到構建目錄bin:
cd bin
然後cmake,指定源代碼目錄:
cmake ../Test
如果你的CMakeFileLists腳本寫的沒有錯誤,一切正常,那麼這條命令執行完以後cmake已經爲我們生成了特定平臺上的構建文件(例如UNIX系統上,當然就是makefile咯)以及數學庫math,查看此時的bin目錄,會發現生成了下面這些東西:
現在cmake已經爲我們生成了原生構建系統需要的項目構建文件,接下來直接make就可以了:
make
如果你的代碼中沒有任何語法錯誤,那麼恭喜你,系統已經爲你生成了最終的可執行文件:
Test就是我們最終的可執行文件了。
4 小結
在本文介紹了CMake的歷史以及爲什麼要用CMake,它能爲我們帶來什麼好處。通過一個示例項目的構建展示了CMake腳本的語法和構成,讓讀者對CMake有一個宏觀印象,最後在總結一下:
(1) CMake是由構成項目的所有目錄下的CMakeFileLists.txt構建腳本控制構建過程的;
(2) CMake最大的好處是跨平臺,一份cmake構建腳可以運用在多個平臺上;
(3) CMakeFileLists.txt主要是由命令command構成,command具有如下格式:
command (arg1 arg2 ...)
(4) CMake可以通過set 命令給變量設置一個或者一組值,用${VAR}的方式可以取變量的值;
(5) 通過3種方式可以爲CMake指定編譯器:設定環境變量CC或CXX、cmake命令行中使用-DCMKAE_CXX_COMPILER=以及在CMakeFileLists.txt構建腳本中設定內置變量CMAKE_CXX_COMPILER的值;
(6) 可以通過cmake gui或者直接在命令行啓動cmake。