CMake初步(1)


轉自:《你所不知的OSG》第一章:CMake初步(1)http://bbs.osgchina.org/forum.php?mod=viewthread&tid=1189&fromuid=3434

本章的主要目的並非介紹OpenSceneGraph的常用類和功能,而是介紹它的一個重要伙伴,抑或說,是一個被愈來愈多的開源軟件所青睞的強勁的輔助開發工具——CMake。

CMake的定義是:一個跨平台、開源、可擴展的軟件編譯生成系統,如果您熟悉與之同類的QMake(開源開發庫Qt的自動工程生成工具)或者Automake(Unix系統的常見工程生成工具)的話,想必更有助於對它的深入了解。用一句話來描述的話,CMake的工作就是:輔助生成不同平台上的Makefile腳本,從而建立整個軟件工程的編譯生成規則,以及它內部以及它與其它軟件工具之間的依賴關系。

不要認為Makefile只是Unix/Linux程序員的專屬用品;Windows的程序開發同樣離不開Makefile的概念。事實上,那些層出不窮的強大得令人眼花繚亂的IDE環境(譬如,家喻戶曉的Visual Studio)為開發者們提供了太多的便利條件,以至於大家都漸漸學得好吃懶做起來——按個按鈕,找個菜單項,或者極不情願地在命令窗口中輸入一串“start”字符……一切就可盡在掌握?抱歉,這世界上不會有那么多的便宜事。更多的時候,您不得不用着最簡陋的文本編輯器,一行一行敲擊,一行一行地耕耘。

以上就是我們即將面臨的學習內容——已經悠悠地打着哈欠了嗎?對於那些沒有Visual Studio,沒有窗口和進度條,甚至沒有鼠標可用的情景,一點都不願理會嗎?呃,那么敬請期待筆者的下一篇文字。(^_^)

不然的話,歡迎進入自由教程《你所不知的OSG》的第一章。工欲善其事,必先利其器。現在就來嘗試了解一下——也許你還有所不知的輔助編譯工具,CMake。

 

1.1 CMake概述

善用兵者,役不再籍,糧不三載。

CMake也許就可以喻為這樣一位英武的大將。雖然看起來那么麻煩,那么深不可測,但是真正摸透它的脾氣稟性之后,卻能夠得心應手,進而統領千軍萬馬,事半而功倍。

CMake的核心是腳本配置文件,也就是CMakeLists.txt這個簡單的腳本文件,瀏覽一下OpenSceneGraph的源代碼目錄就可以發現,從根目錄開始,幾乎每一個包含了子工程源代碼的目錄(src,examples,applications等)都會同時附帶一個CMakeLists.txt。在Windows下編譯OSG時,只要輕松地將根目錄的CMakeLists.txt拖動到CMake-GUI的窗口中,再進行相應的選項設置,就能夠生成OSG的VisualStudio工程文件(或者,以筆者的喜好,生成nmake可用的Makefile文件),進而編譯得到OSG動態鏈接庫。

Linux用戶所需的工作如出一轍,在根目錄下執行:
  1. # cmake . -DCMAKE_BUILD_TYPE=Release
復制代碼
然后直接調用生成的Makefile腳本:
  1. # make; make install
復制代碼
如此而已。

但是,試圖直接使用子目錄的CMakeLists.txt卻是無效的,原因很簡單,系統找不到許多在根目錄的CMakeLists文件中配置的參數和宏,因而會產生錯誤提示,無法繼續執行。
CMake可以針對不同的操作系統和IDE環境生成不同的腳本或工程文件,例如,VisualStudio解決方案,Mac OSX的XCode文件,Unix/Linux系統的Makefile文件等等。

說了這么多,不知您從中摘出了多少對自己有用的信息呢?也許您還在被那些惱人的問題折磨着吧——我怎么生成不了FreeType插件呢?OSG怎么找不到我的第三方庫呢?那么多的配置選項都是什么意思呢?本文無力解答這么多的問題,也許其中一些會在后繼的文字中得到解答,也許其中一些筆者和其他研究者們也未曾探究過,也許其中一些只有您自己鑽研得出答案……也許,也許您也曾一閃念想過,如果我的工程也使用CMake來配置,然后一個命令就整整齊齊地輸出鏈接庫和可執行文件來,讓其他撓着頭皮的同事們乍舌不已——那該有多么瀟灑呢?

那么,這才是本文的編寫目的所在:就是設法幫助您,初步初步學會使用CMake編寫自己的工程配置腳本,初步學會理解和閱讀他人的CMake配置腳本代碼,並因而能夠獨力閱讀和理解OpenSceneGraph那繁多的配置選項,並在不算浩瀚的CMake腳本代碼中(總比OSG的源代碼要簡單許多)找尋它們的芳蹤。
1.2 CMake腳本基本知識

不知讀者朋友們學習一門新的程序語言時,第一想要了解的是它的哪一方面內容?語法?關鍵字?應用范圍?函數接口?這些當然都很重要,不過本文卻要首先詳解CMake腳本語言的組織結構,這將有助於您對這個陌生工具的全面理解,並且在面對版本更替和新的功能實現時,不會一頭霧水,而是有的放矢,忙而不亂。

CMake包含了以下幾個基本概念:
  • CMakeLists.txt

之前已經簡單地介紹過了,這是CMake腳本代碼和配置參數的載體,源代碼目錄中沒有它的話,一個工程就不可能使用CMake的配置程序來完成自動Makefile腳本的生成工作。

  • 源碼樹和二進制樹

源代碼樹(Source Tree)和二進制樹(Binary Tree)的含義很好理解:前者表達了一個工程的所有頭文件(.h,.hxx,.hh,無擴展名,等等),源文件(.c,.cc,.cpp,.cxx,等等),CMake腳本(CMakeLists.txt),以及它們的目錄樹結構;后者則包括平台相關的解決方案或Makefile腳本,目標文件(.obj),編譯后的動態/靜態庫和可執行文件,以及其它編譯過程中生成的文件等。

CMake允許“源碼樹內生成”(in-source build)和“源碼樹外生成”(out-of-source build)這兩種工作方式。前者將會在源代碼的同一目錄下生成對應的Makefile腳本,目標文件以及結果;后者則是在不同的目錄下執行編譯生成的工作,源代碼樹則保持原樣,十分有利於代碼的版本更新,搜索管理,以及打包再發布。

對於Windows用戶,可以在CMake-GUI的“Where to build binaries”欄中輸入新的工作路徑以實現out-of-source的模式;Linux用戶則可以簡單地在外部目錄執行cmake指令,例如:
  1. # cmake /home/myproject –DCMAKE_BUILD_TYPE=Release
復制代碼
這里的/home/myproject即是工程的根目錄,其中必須包含有CMakeLists.txt文件。

對於發展和功能增補十分迅速的OSG而言,“源碼樹外生成”當然是首選的編譯方案。這樣的話,當我們不滿於動輒上G的臨時文件容量之時,只需要隨手刪除其所在目錄即可,不會影響到OSG源代碼分毫。

  • CMakeModules模塊

一個工程需要依賴於另一個工程的頭文件和庫文件(.lib),無論對商業還是開源軟件的開發流程來說,這都是不可或缺的一部分:GUI開發庫Qt的部分功能依賴於libJPEG、libPNG;商業GIS引擎Skyline依賴於GDAL;就連微軟的一些大型游戲都會依賴於開源工程OpenAL。

那么,如何告訴我們的工程,這些頭文件和LIB文件的位置呢?熟悉Visual Studio環境的朋友當然知道,在工程屬性的“C/C++”和“鏈接器”選項卡中,可以分別設置它們的路徑;而Linux編程時,則需要在腳本中手動添加-I以及-L、-l參數,保證#include宏不致無所適從,以及編譯器不會產生該死的LNK2001,LNK2019錯誤。

而對於使用CMake生成自動腳本的開發者來說,尋找頭文件和庫文件的工作,就交給CMakeModules中的各個模塊來完成。

在OSG的根目錄下有一個不太引人注意的子目錄文件夾,就是這個CMakeModules。其中的文件內容十分豐富,名稱則一目了然:FindFreeType.cmake,FindGDAL.cmake,諸如此類。倘若用戶機的設置得當,這些擴展名為.cmake的搜索腳本可以自動獲取依賴庫的路徑信息(但是並不會主動將它們追加到工程屬性中);不然的話,我們在配置OSG時屢屢需要設置的FREETYPE_INCLUDE_DIR,GDAL_LIBRARY等選項,也是出自這些腳本的手筆。

具體的搜索腳本閱讀和編寫方法稍后再說。感興趣的朋友不妨現在就用文本編輯工具打開一瞧,沒准它們也並不像您想象的那樣復雜也說不定。

  • CMake基本宏

細心的您想必已經發現了,前文我們在描述Linux下的CMake命令行時,有一個未作介紹的宏參數:-DCMAKE_BUILD_TYPE=Release。它可以被簡單地分割為三個部分,-D是命令前綴詞,CMAKE_BUILD_TYPE是宏命令的關鍵字,而Release則是對其賦值。這個內置宏標志的含義應當說不言而喻,它設置了工程即將采用的編譯類型,可使用的值通常包括Debug、Release、RelWithDebInfo和MinSizeRel四種。和我們在Visual Studio等工具中所作的設置相似,這將改變工程的調試等級和編譯生成的信息等諸多內容。

除了CMAKE_BUILD_TYPE之外,CMake中還包含一些基本的內置宏指令,典型的例如:

CMAKE_MODULE_PATH,設置搜索CMakeModules模塊(.cmake)的額外路徑。

CMAKE_INCLUDE_PATH,設置自動查找依賴工程頭文件的額外路徑,默認為腳本中指定的搜索路徑。

CMAKE_LIBRARY_PATH,設置自動查找依賴工程庫文件的額外路徑,默認為腳本中指定的搜索路徑。

CMAKE_INSTALL_PREFIX,設置安裝時的路徑。這是一個重要的配置參數,當鏈接庫和可執行文件的生成工作完畢時,往往需要將這些.lib,.dll,.exe和頭文件拷貝到一個獨立的文件夾下,以備調用和再次復制。在Visual Studio環境下我們通過INSTALL工程來完成這一安裝工作;而Unix/Linux下則是熟悉的make install。默認的安裝目錄為/usr/local/或者C:/Program Files/。

  • CMake緩存信息

當我們使用CMake生成了工程的解決方案或者Makefile腳本之后,再進入二進制樹目錄,也就是編譯過程文件存儲的位置(in-source模式為源代碼同一目錄,out-of-source模式為用戶自行指定的文件夾),可以看到新生成的CMakeCache.txt文件,即緩存信息文件。

CMakeCache.txt中存儲了所有自動搜索或者手動配置的路徑和腳本參數。當我們更新了工程的源代碼,並准備重新進行編譯時;使用這種緩存信息文件可以有效地加速CMake配置的過程,方法是直接將這個文件拖動到CMake-GUI窗口中,或者在命令行方式下執行:
  1. # cmake –C CMakeCache.txt
復制代碼
此時系統將自動讀入上一次配置的所有信息,使用CMake-GUI的用戶可以在對話框中再次進行參數的修改,並生成新的解決方案或者Makefile文件,以供下一步的工程編譯生成工作。
1.3 你好,世界

CMake是一種可擴展的腳本語言,要學會閱讀和編寫CMake腳本代碼,就必須要理解它的基本詞法和語法,以及理解它的接口擴充法則。而這一切的學習又都是建立在實踐的基礎上的,否則再好的教程課本和參考文檔都只是廢紙一張。

那么,是先羅列枯燥的關鍵字好呢?還是先打開任意的文本編輯器,小試牛刀一把好呢?筆者竊以為后者是通往CMake入門者殿堂的更快途徑。

那么就讓我們在不了解任何CMake腳本規范條目的前提下,開始CMake工程涉及的旅程吧。是不是有點瘋狂呢?沒關系,我們就用開發者最喜歡的“你好,世界”(Hello World)作為第一個實踐的項目。編碼過程中所涉及的各種詞法和語法規則,我們僅僅稍作解釋,只求給讀者留下CMake腳本開發的初步印象,以便在閱讀下一小節時能夠更加得心應手。

現在我們假設有這樣一個工程,它包括一個名為HelloLib的動態鏈接庫,以及一個依賴於這個庫的Test可執行程序。其文件夾結構為:



HelloLib子工程定義並實現了一個名為Hello的類,這個類只有一個公有方法sayHello(),作用是在屏幕上打印“Hello CMake”這一行簡單的字符。為此,這個目錄下將包含頭文件Hello以及源文件Hello.cpp,並期望能夠生成HelloLib.dll的動態鏈接庫,以便所有依賴於HelloLib的程序可以因此打印出友好的歡迎語句。(盡管這看起來很幼稚 ^_^)

Test子工程只有一個源文件test.cpp,它的工作僅僅是在main函數中調用sayHello()方法並即刻結束自己的使命。Test因而要依賴於HelloLib庫存在,並期望生成一個名為Test.exe的可執行文件,在控制台窗口中執行自己的簡單任務。

這三個文件的源代碼如下:
  1. /* Hello */
  2. #ifndef H_HELLOWORLD
  3. #define H_HELLOWORLD
  4. #if defined(_MSC_VER) || defined(__CYGWIN__) || defined(__MINGW32__)
  5. #    ifdef HELLOWORLD_LIBRARY
  6. #        define HELLOWORLD_EXPORT __declspec(dllexport)
  7. #    else
  8. #        define HELLOWORLD_EXPORT __declspec(dllimport)
  9. #    endif
  10. #else
  11. #    define HELLOWORLD_EXPORT
  12. #endif
  13. class HELLOWORLD_EXPORT Hello
  14. {
  15. public:
  16.     void sayHello();
  17. };
  18. #endif
復制代碼
  1. /* Hello.cpp */
  2. #include <iostream>
  3. #include "Hello"
  4. void Hello::sayHello()
  5. {
  6.     std::cout << "Hello CMake!" << std::endl;
  7. }
復制代碼
  1. /* test.cpp */
  2. #include <Hello>
  3. int main( int argc, char** argv )
  4. {
  5.     Hello obj;
  6.     obj.sayHello();
  7.     return 0;
  8. }
復制代碼
現在我們給每一個文件夾中都追加一個CMakeLists.txt文件(因此共有3個),從而形成完整的CMake自動編譯腳本樹。這其中,工程根目錄下的CMakeLists.txt決定了工程的基本屬性,並使用ADD_SUBDIRECTORY,我們暫且稱之為“命令”(Command),指向每一個工程子目錄;而子目錄下的CMakeLists.txt則決定了當前工程的屬性,依賴關系,源代碼文件以及編譯規則等等。

根目錄下的CMake腳本實現代碼如下:
  1. PROJECT( HelloWorld )
  2. CMAKE_MINIMUM_REQUIRED( VERSION 2.4.7 )
  3. ADD_SUBDIRECTORY( HelloLib )
  4. ADD_SUBDIRECTORY( Test )
復制代碼
不要急於看下面的內容,先看看這段CMake腳本代碼——也許您會發現它們其實很容易讀懂對不對?首先指定整個工程的名稱為HelloWorld,然后指定“所需的最低CMake版本”為2.4.7,並且添加兩個子目錄HelloLib和Test——這僅僅是我們根據英文關鍵字翻譯過來的內容,但也恰恰是這段腳本想要表達的意思。這里面的關鍵是ADD_SUBDIRECTORY命令,它指示系統到下一級子目錄去搜索CMakeLists.txt腳本的位置,並執行具體子工程的生成任務。

子工程HelloLib的任務是:生成動態鏈接庫HelloLib.dll(或者libHelloLib.so,Unix/Linux系統中),定義一個Hello類並指示動態鏈接庫將其輸出,以便依賴於HelloLib庫的程序可以聲明其實例並調用它的功能。

它的腳本實現代碼如下:
  1. ADD_DEFINITIONS( -DHELLOWORLD_LIBRARY )
  2. ADD_LIBRARY( HelloLib SHARED
  3.     Hello.cpp
  4. )
復制代碼
這里出現了另一個重要的命令:ADD_DEFINITIONS,用於定義程序中所需的預編譯宏,其固有參數格式通常是-D加上宏的名稱。例如此處定義了一個HELLOWORLD_LIBRARY宏,進而在程序中設置了Hello類的輸出方式。(Win32下往往使用dllexport來指定函數和類的輸出,其它系統則通常不必特殊指示)當然使用#define來進行定義也是相同的作用,但是能夠在編譯腳本中有選擇地完成此類工作的話,自然會給工程帶來更大的靈活性和平台兼容能力。

第二個重要的命令是ADD_LIBRARY,很明顯,它的任務是指示CMake系統添加一個新的鏈接庫(Library)子工程,其參數格式為:
  1. ( 庫名稱 庫類型 源文件 )
復制代碼
庫名稱為HelloLib,這也是最終生成的鏈接庫的名稱;類型為SHARED,即動態鏈接庫(.dll或.so),而STATIC自然就表示生成靜態鏈接庫了(.lib或.a);庫所需的源代碼文件只有一個,即Hello.cpp,其中所包含的頭文件Hello因為和.cpp文件處於同一目錄下,因此不必再特地指定。

不要在意這里ADD_LIBRARY書寫的縮進格式,甚至不必在意其大小寫,它只是一種排版和增加易讀性的方式而已,並無強制要求。

就這么簡單,一個新的鏈接庫工程就建立完成了。如果使用CMake自動生成Visual Studio解決方案的話,應該可以看到這個子工程的身影了。什么?您的做法是選擇Visual Studio菜單的“文件->新建->工程”,並且認為這樣更加簡單?目前也許如此,不過為何不晚些再下定論呢?

那么,按照我們之前的約定,現在要建立一個名為Test的可執行工程(.exe),並通過它來調用Hello::sayHello()方法,打印字符串“Hello CMake!”。

使用Visual Studio應該如何來操作呢?在同一個解決方案中再次選擇“文件->新建->工程”;選擇建立一個Win32控制台工程,將test.cpp拖動到源文件目錄;下一步,在“工程屬性->C/C++”中指定“附加頭文件目錄”為Hello所在的目錄;在“工程屬性->鏈接器”中指定“附加庫文件”為HelloLib.dll;然后指定HelloLib和Test兩者的依賴關系,編譯生成,一切完畢——嗯,有些麻煩對不對?也許此時您會稍作期盼,CMake的做法能否像剛剛建立鏈接庫那樣簡單呢?您說對了,就是那樣簡單:
  1. INCLUDE_DIRECTORIES( ${PROJECT_SOURCE_DIR}/HelloLib )
  2. ADD_EXECUTABLE( Test
  3.     test.cpp
  4. )
  5. TARGET_LINK_LIBRARIES( Test HelloLib )
復制代碼
這里涉及到一個“變量”的概念,即${PROJECT_SOURCE_DIR}。對於精通C/C++編程的您來說,這類情形其實再熟悉不過,例如:
  1. std::string str1 = “abc”;
  2. std::string str2 = str1 + “def”;
  3. std::string str3 = “str1 + def”;
復制代碼
您一定知道,這里str2的賦值為“abcdef”,而str3則是令人匪夷所思的“str1+def”。變量str1的職責和作用范圍也因此不言而喻。

CMake中的變量也是如此:我們可以使用SET命令來設置和傳遞各種用戶變量,也可以直接使用系統內置的全局變量,例如此處的PROJECT_SOURCE_DIR。注意所有對於變量值的使用都需要遵循“${……}”的格式,否則它會被識別為普通的字符串,就像str3中存儲的結果那樣;但是對於CMake而言,就算是被引號包含,也同樣可以使用“${……}”來指定使用變量中的內容,例如:
  1. SET( MY_SYSTEM “CMake building system” )
  2. MESSAGE( “${MY_SYSTEM} is good for developers.” )
復制代碼
將顯示“CMake building system is good for developers.”這一句完整的對白,而非毫無意義的MY_SYSTEM符號。當然,聰明的您也不需要筆者過多解釋此處MESSAGE命令的意思——它想必是用來顯示調試或警告信息的,通過控制台窗口或者對話框。

注意,CMake的變量是可以“自省”的。如果我們現在有一個名為MY_NAME的變量賦值為“WANG”,那么${MY_NAME}_LIB變量也就等同於WANG_LIB變量,在腳本中對於${${MY_NAME}_LIB}的使用也就等同於對${WANG_LIB}的使用。

還有一點需要注意,CMake中的變量類型只有•字符串•這一種!但是它卻可以被不同的腳本命令靈活地識別為字符串、布爾型、整型或浮點型,這樣也省卻了類型轉換的種種麻煩。

這樣繁瑣的講解是否有助於您的學習理解呢,抑或是啰嗦得使您覺得厭煩了?那么我們回到腳本代碼中來。明白了PROJECT_SOURCE_DIR的含義,您自然也能猜到這個變量中的內容是什么,也就是整個工程的根目錄了。它由系統自動檢測和賦值,不需要我們操心;而這個變量對於各個子工程之間的依賴關系設置,以及頭文件和源文件路徑設置顯然有着至關重要的作用!這一點不言自明。

在Test子工程的CMake腳本中,我們首先使用INCLUDE_DIRECTORIES命令設置附加頭文件目錄的位置;然后用ADD_EXECUTABLE指定要編譯的可執行程序名稱和相關源文件;最后使用TARGET_LINK_LIBRARIES設置Test所依賴的鏈接庫文件,即剛剛設置生成的HelloLib庫。

一切就是如此。那么,啟動您的CMake-GUI窗口或命令行,生成您鍾愛的工程或者Makefile,並且編譯運行。結果精致而簡單,過程也不復雜,收獲不多也不少——這樣的感覺,不是很愜意嗎?



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM