> 原文鏈接:[http://www.aosabook.org/en/cmake.html](http://www.aosabook.org/en/cmake.html)
> 作者:Bill Hoffman, Kenneth Martin
1999年, 國家醫學圖書館(National Libray of Medicine)雇用了一個稱為Kitware的小公司,為支持復雜軟件的跨平臺配置,構建和發布來實現一個更好的解決方案。這個工作是ITK(一個醫學開源可視化軟件,Insight Segmentation and Registration Toolkit)項目的一部分。作為ITK工程的主導單位,Kitware負責開發一個供ITK項目研發人員使用的軟件構建系統,這個系統必須易于使用,并盡量不占用開發人員編程之外的時間。基于這個指導思想,CMake需要基于已有工具揚長避短,并能夠取代古老的autoconf/libtool方式。
經過多年的發展,CMake從最初的軟件構建系統演變成一個開發工具系列:CMake, CTest, CPack和CDash。CMake負責構建軟件。CTest是測試驅動工具,用于回歸測試(regression tests)。CPack是打包工具,將CMake構建的軟件發布成面向不同平臺的安裝軟件。CDash是一個Web應用程序,用于執行持續的集成測試并顯示測試結果。
## 5.1 CMake的歷史和需求
在開始開發CMake時,項目管理常見的做法是,在Unix平臺上使用configure腳本和Makefile文件,在Windows平臺上使用Visual Studio工程。這種構建系統的雙重性使得跨平臺開發變得十分枯燥,即使在工程中添加一個簡單的源碼文件都是非常痛苦的事情。開發者們希望能夠擁有一個統一的軟件構建系統,而CMake開發人員在這方面經驗豐富。歷史上,他們使用過兩種方法來解決這個問題:
一種方法是1999年開發的VTK構建系統。在這個系統中,Unix系統中使用configure腳本,而在Window系統中使用一個叫做`pcmaker`的可執行程序。`pcmaker`是一個C程序,它通過解析Unix Makefile文件來生成Windows下的NMake文件。?`pcmaker`的二進制可執行程序后來被簽入到了VTK 的CVS系統倉庫中。雖然從某種意義上講,這是一個統一的構建系統,但是其缺點是顯然的。 對于一些常見的情況,比如添加一個新的模塊,都需要修改`pcmaker`的源碼,然后再更新其系統倉庫中的可執行文件。
另外一種方式是為TargetJr開發的基于`gmake`的構建系統。TargetJr是一個C++編寫的計算機可視化環境,最初在Sun工作站上開發。一開始,TargetJr使用`imake`構建系統來創建Makefiles。但當有一天需要移植到Window時,就不得不開發出另外一個`gmake`構建系統。`gmake`構建系統同時支持Unix編譯器和Windows編譯器,但在使用前需要設置一些環境變量,否則,用戶特別是終端用戶容易產生一些難于調試的錯誤。
這兩種方法都有一個嚴重的不足: 它們要求Windows開發人員使用命令行。然而,熟練的Windows開發人員更傾向于使用集成開發環境(IDE),他們還是會選擇手動生成IDE文件然后添加到工程中去,使得構建系統又重新退化成了"雙系統"。除了缺乏IDE支持,上述兩種方法也使得合并第三方軟件的項目變得非常困難。比如,[VTK](http://www.aosabook.org/en/vtk.html)中罕有圖片加載模塊,主要是因為其構建系統難于利用libtiff和libjpeg等第三方庫。
因此,開發ITK和其它C++軟件都需要一個新的軟件構建系統。 這個新構建系統必須滿足一些限制條件:
* 對平臺的唯一的依賴: 操作系統中需要安裝C++編譯器
* 能夠生成Visual Studio IDE輸入文件
* 易于創建基本的構建系統的目標文件,包括靜態庫,動態庫,可執行文件,插件。
* 能夠運行構建時的代碼生成器
* 支持源碼樹和構建樹的分離
* 能夠執行系統"自省"(introspection),即能夠自動判斷目標系統能夠做什么,和不能夠做什么
* 能夠自動掃描C/C++頭文件的依賴關系
* 所有特性對所支持的平臺一視同仁
為了避免依賴于三方軟件庫和語法分析器,CMake在設計時只考慮了一個主要的依賴:C++編譯器(因為要構建的是C++代碼,所以我們可以放心地假設系統中已經安裝好C++編譯器)。當時,在許多流行的UNIX和Windows操作系統上構建和安裝Tcl之類的腳本語言是非常困難的。即便到如今,給超級計算機和沒聯網的安全計算機安裝軟件也非易事,所以編譯第三方軟件庫一直都是比較困難的。由于軟件構建系統是一個基本工具,因此其設計不應再引入其它的依賴關系。這確實限制了CMake提供自己的簡單的語言,導致至今都有人不太喜歡CMake。然而,如果CMake依賴于當時最流行的嵌入式Tcl語言,它大概不會達到今天這樣的流行程度。
生成IDE工程的能力是CMake的重要賣點,但這也限制了CMake不能提供本地IDE支持之外的特性。不過,支持本地IDE工程的重要性完全能夠彌補其不足。這個設計使得CMake的開發變得困難,卻令使用CMake的項目(如ITK)的開發更為容易,因為開發人員更喜歡使用自己熟悉的并且效率也更高的工具。允許開發人員選擇自己喜歡的工具, 項目就能充分利用最寶貴的人力資源。
所有的C/C++程序都至少包含以下的一個或多個基本的基本構建單元: 可執行文件,靜態鏈接庫,動態鏈接庫和插件。 CMake必須具備在所有支持的平臺上生成這些結果的能力。 雖然所有的平臺上都支持生成這些結果, 但不同的平臺和不同的編譯器會導致編譯器選項變化很大。 CMake將這些目標的構建過程抽象成一條條簡單的命令, 它們在實現上的復雜性和差異性則被隱藏了起來, 從而開發人員能夠同時在Windows, Unix和Mac上創建這些目標的本地版本。 這樣,開發人員得以專心于工程本身,而不是糾結于如何編譯一個動態鏈接庫這樣的細節上。
代碼生成器為構建系統增加了額外的復雜性。最開始,VTK提供了一個系統來解析C++頭文件,然后自動地將C++代碼封裝成Tcl,Python和Java代碼,并自動地生成一個封裝層。這要求構建系統先生成一個C/C++程序(封裝生成器),然后在編譯時運行此程序,以生成更多的C/C++源碼(特定模塊的封裝代碼)。隨后,生成的源碼將被編譯成可執行文件或動態鏈接庫。所有這些過程必須在IDE環境和生成的Makefile中實現。
當開發靈活的跨平臺C/C++軟件時,很重要的一點是面向功能編程, 而不是面向特定的平臺。autotool工具支持系統"自省"(introspection),即通過編譯少量的代碼來查詢平臺特征并存儲查詢結果。由于跨平臺的需要,CMake也必須采用類似的策略,使得開發人員只需要針對標準平臺編碼,而不需要考慮特定的平臺。由于編譯器和操作系統時時在變,這個策略對于代碼的可移植性非常重要。比如,下面的代碼:
~~~
#ifdef linux
// do some linux stuff
#endif
~~~
就顯得非常脆弱,不如寫成
~~~
#ifdef HAS_FEATURE
// do something with a feature
#endif
~~~
另外一個CMake早期的需求也來自于autotool: 在源碼樹外生成構建樹。這個特性使得從同一個源碼樹可得到多個不同的構建,使得不同構建之間的文件不會沖突,結合版本控制系統的時候顯得尤為有利。
構建系統一個最更要的功能是依賴關系的管理能力。 如果一個源碼文件發生變化,所有使用了這個文件的生成結果都必須重新構建。 對于C/C++代碼,被`.c`和`.cpp`文件包含的頭文件也需要檢查依賴關系。如果依賴關系理解錯誤,只有部分修改的代碼有可能導致全部重新編譯,從而浪費大量時間。
這個新的構建系統的所有的需求和功能都必須對所有支持的平臺一視同仁。CMake需要為開發者提供一個簡單的API,不需要關心平臺細節就可以創建復雜的軟件系統。事實上,使用CMake的軟件只不過是把構建復雜性轉移給了CMake開發人員。一旦這些基本的需求確定下來,就需要用敏捷的方式來實現CMake。ITK項目從第一天開始就需要這樣一個構建系統,但其第一個版本的CMake并沒有滿足所有的需求,但已足夠支持在Windows和Unix上構建軟件。
## 5.2 CMake是怎樣實現的
如前所述,CMake的開發語言是C和C++。為解釋其內部結構,本節將首先從用戶的角度介紹CMake的處理過程,然后再描述其結構。
**5.2.1 CMake處理過程**
CMake有兩個主要的階段。首先是"配置(configure)",在此階段CMake處理所有的輸入然后創建軟件構建過程的內部表達。第二個階段是"生成(generate)",負責創建出實際的構建文件。
**環境變量與緩存**
對1999年甚至是今天的許多構建系統來說,生成工程時都要用到底層(shell級別)的環境變量。典型的情況是,用PROJECT_ROOT環境變量來指向源碼樹的根目錄。環境變量還被用于指定可選軟件包和外部軟件包。但是使用環境變量的方法也有弊端,它需要每次構建時都重新設置環境變量。為解決這個問題,CMake使用緩存文件來存儲生成過程中用到的所有變量。這些變量不再是環境變量,而是CMake變量。CMake針對某個特定構建樹第一次運行時,會創建一個`CMakeCache.txt`文件,存儲當前構建過程中需要用到的CMake變量。這個緩存文件屬于構建樹的一部分,所以在之后的每次針對該構建樹的重新配置時, 這些變量都是可重用的。
**配置階段**
在配置階段,CMake首先嘗試讀取`CMakeCache.txt`文件,該文件在第一次運行時生成。然后,讀取源碼樹根目錄下的`CMakeLists.txt`文件,并使用CMake詞法分析器處理。`CMakeLists.txt`中的每條命令都由一個命令模式對象執行。通過`include`和`add_subdirectory`命令,更多的`CMakeLists.txt`得到執行。對于每條命令,CMake都有一個C++對象來處理,比如`add_library`,`if`,?`add_executable`,?`add_subdirectory`,`include`等。實際上,整個CMake語言就是以命令調用的方式實現的。詞法分析器只不過將輸入文件內容轉化為命令和命令參數而已。
配置階段主要是運行用戶定義的CMake代碼。等到執行完之后,以及所有緩存變量計算完成之后,CMake在內存中得到一個項目構建的內部表達。這個內存中的內部表達包括了所有的庫文件,可執行文件,定制的命令,以及生成指定generator(指特定的編譯環境)所需的其他必要信息。這時,`CMakeCache.txt`會被存儲到磁盤上,供以后重新運行CMake時使用。
項目在內存中的表達實際上是一些待生成的目標的集合,包括基本的庫文件和可執行文件。CMake還支持目標的定制,即用戶可以定義輸入和輸出,并提供定制的可在構建過程中運行的可執行文件或腳本。CMake將每個目標存儲在一個`cmTarget`對象中,然后多個`cmTarget`存儲在一個`cmMakefile`對象中,`cmMakefile`對象實際上用來存儲源碼樹中某個目錄中的所有目標。最后得到的結果是一棵`cmMakefile`對象的樹,樹結點中存儲`cmTarget`對象的映射。
**生成階段**
一旦配置(configure)階段完成,生成(generator)階段就可以開始了。生成階段將生成用戶指定類型(如Visual Studio或GNU/Linux GCC)的構建文件。這時,目標的內部表達(庫,可執行文件,定制目標)轉化為本地構建工具的輸入文件,如Visual Studio或Makefile文件。CMake由配置階段獲得的內部表達要盡可能地抽象和通用,這樣的數據結構才能被不同的本地構建工具所共享。
CMake處理過程簡圖如圖5.1所示。

圖5.1 CMake處理過程簡圖
**5.2.2 CMake的代碼**
**CMake中的對象**
CMake使用了繼承,設計模式和封裝等面向對象技術. 其主要的C++對象及相互關系如圖5.2所示:
圖5.2 CMake中的對象
每個`CMakeLists.txt`的解析結果都存儲在一個`cmMakefile`對象中。 除了存儲一個目錄的信息,`cmMakefile`對象還控制對?`CMakeLists.txt`的解析. CMake語言的解析函數使用了基于lex/yacc的分析器。 由于CMake語言的語法很少發生變化,而lex和yacc在本地系統上并不能保證已經安裝,因此lex和yacc的輸出文件被處理和保存到了`Source`目錄中,和其它手工編寫的文件一起加入到版本控制系統中。
CMake另一個重要的類是`cmCommand`。這是CMake語言中所有命令的實現類的基類。每個子類不僅提供命令的實現, 還包括其文檔。 比如, 下面`cmUnsetCommand`類的方法的作用是提供文檔:
~~~
virtual const char* GetTerseDocumentation()
{
return "Unset a variable, cache variable, or environment variable.";
}
/**
* More documentation
*/
virtual const char* GetFullDocumentation()
{
return
" unset(<variable> [CACHE])\n"
"Removes the specified variable causing it to become undefined."
"If CACHE is present then the variable is removed from the cache"
"instaead of the current scope. \n"
"<variable> can be an environment variable such as:\n"
" unset(ENV{LD_LIBRARY_PATH})\n"
"in which case the variable will be removed from the current "
"environment.";
}
~~~
**依賴分析**
CMake內置有強大的的依賴分析能力, 支持單個Fortran, C和C++的源碼文件。 因為集成開發環境(IDE)能夠支持和維護文件的依賴信息, 對于這類本地系統CMake將忽略依賴分析步驟, 只是創建一個本地IDE的輸入文件, 由IDE自行處理文件層次的依賴信息。而目標層次的依賴信息則轉換為IDE所支持的依賴信息格式.
對于基于Makefile的本地構建工具, 其`make`程序并不知道如何自動計算和更新依賴信息. 對于這樣的本地構建系統, CMake自動計算源碼(C,C++和Fortran)的依賴信息。 這些依賴關系的生成和維護都是由CMake完成的。 一旦一個項目由CMake首次配置完成, 用戶只需要運行`make`, 剩下的工作將由CMake完成.
雖然用戶不需要知道CMake是如何工作的, 但查看一個項目的依賴信息還是很有幫助的。 在CMake中,每個目標的依賴信息存儲在四個文件中:?`depend.make`,?`flags.make`,?`build.make`和`DependInfo.cmake`。?`depend.make`存儲指定目錄中所有對象(object)文件的依賴信息。`flags.make`包含了源碼文件的編譯選項,如果編譯選項發生變化,目標文件將被重新編譯。`DependInfo.cmake`用來維護和更新依賴關系, 它還存儲了工程中包含哪些文件和使用哪一種編碼語言等信息。?`build.make`則存儲創建依賴的規則。 如果一個目標的依賴關系過時了,其依賴信息將被重新計算,保持為最新狀態。 比如, 添加一個.h頭文件會導致增加一個新的依賴, 從而導致重新計算.
**CTest和CPack**
CMake由一個構建系統漸漸發展為集構建,測試和軟件打包為一體的工具家族。除了命令行工具`cmake`及CMake圖形界面(GUI)程序, CMake還包含測試工具CTest和打包工具CPack。 CTest和CPack共享CMake的底層代碼,但它們相對獨立并不依賴于基本的構建過程。
`ctest`可執行程序用于執行回歸測試。簡單地使用一個`add_test`命令,項目就可以使用CTest來創建測試。這些測試可使用CTest來運行,測試結果可以發送到CDash程序并顯示在網絡應用中。CTest和CDash結合起來就構成了類似于Hudson的測試工具。但兩者有很明顯的差別:CTest面向分布式測試環境, 客戶可以從版本控制系統中獲取代碼,運行測試,然后將測試結果發送到CDash。而Hudson,客戶機器必須給予Hudson足夠的ssh權限來訪問目標機器,測試才能進行。
`cpack`可執行程序用來生成項目的安裝程序。 CPack的執行和CMake的構建過程非常類似: 它也依賴于本地的工具. 比如, 在Windows上使用NSIS打包工具來生成項目安裝程序。 CPack執行項目的安裝規則生成一棵安裝樹, 然后使用本地的打包工具(如NSIS)來獲得安裝程序。 CPack還支持創建RPM軟件安裝包, Debian的`.deb`文件,?`.tar`文件,?`.tar.gz`文件, 以及自解壓的tar文件。
**5.2.3 圖形界面**
許多用戶對CMake的第一印象是CMake的用戶界面。 CMake有兩個主要的用戶界面:基于Qt的圖形界面程序,和基于命令行的圖形界面程序。這些GUI實際上是`CMakeCache.txt`的可視化編輯器。這些界面都非常簡單,只有兩個按鈕: 配置(configure)和生成(generate),對應于CMake的兩個主要的階段。命令行用戶界面用于Unix的TTY類型的終端和Cygwin, 而Qt圖形用戶界面則支持所有平臺。兩種GUI如圖5.3和圖5.4所示。

圖5.3 命令行用戶界面

圖5.4 圖形用戶界面
兩種GUI都在左邊顯示緩存變量的名稱,在右邊顯示變量的值,值可以由用戶修改。其中有兩種類型的變量,普通變量和高級變量。默認情況下只顯示有普通變量。在`CMakeLists.txt`中,項目可以指定哪些變量是高級變量。這個功能可以讓界面變得簡單,用戶配置時只需要考慮必要的選項。
由于緩存變量的值可能會隨著CMake命令的執行而變化,整個生成(generate)過程可能是遞歸的。比如,打開一個選項可能會引入更多的選項。由于這個原因,GUI在配置(configure)過程中是禁用生成(generate)按鈕的,只有當所有的選項都至少出現過一次時生成(generate)按鈕才可使用。每次按下配置(configure)按鈕,一些新出現的緩存變量將顯示為紅色。一旦不再有新的變量產生,生成(generate)按鈕就可以使用了。
**5.2.4 測試CMake**
任何一個新的CMake開發人員都會被首先介紹CMake開發中的測試過程,這個過程用到了多個CMake工具家族中的成員(CMake, CTest, CPack和CDash)。當CMake代碼經過開發并檢入到版本控制系統中后,運行持續集成測試的機器將使用CTest來自動構建和測試新的CMake代碼。其結果將發送到CDash服務器上,如果出現錯誤,警告或測試失敗的情況,則通過郵件來通知開發者。
這個處理過程是一個典型的持續集成測試。當新的代碼檢入到CMake代碼倉庫中時,在CMake支持的測試平臺上將自動實施測試過程。考慮到CMake需要支持大量的編譯器和平臺,這種測試系統對于開發一個穩定的系統是至關重要的。
比如, 如果一個新的開發者希望CMake能支持一個新的平臺, 他(她)首要要回答的問題是能否為CMake測試系統提供一個每晚dashboard的客戶端。 沒有經常性不斷的測試, 新系統就難以保證過一段時間后不會出問題。
## 5.3 經驗教訓
從構建ITK的第一天開始,CMake就一直在成功運行著,并成為了該項目的重要組成部分。如果重新來過,大概也不會有什么太大的不同。 但是,凡事有例外,總會有一些事情能夠做得更好。
**5.3.1 后向兼容**
維護后向兼容性對CMake團隊來說是很重要的。 CMake的主要目標是讓構建軟件更為簡單。 當一個工程或一個開發者選擇了CMake, 尊重其選擇并且不破壞其已有工作是非常重要的。 CMake 2.6實現了一個策略系統, 它會在用戶不遵守某個命令的當前行為時發出警告, 但仍會執行舊的行為。 每個`CMakeList.txt`都要求指定期望使用的CMake版本。如果當前運行的CMake版本比指定的版本更新,CMake會發出警告, 但仍然使用舊的版本的行為。
**5.3.2 語言,語言,語言**
CMake語言盡量設計得簡單, 然而, 讓一個新項目考慮使用CMake的主要障礙仍然是語言。 CMake固然發展得不錯, 但CMake語言中確實存在一些古怪的行為。 CMake語言的第一個語法分析器居然只是一個簡單的字符分析器, 而不是lex/yacc等高級工具。 如果有機會重新實現語言部分, 我們會花時間尋找一個漂亮的已有嵌入式語言。 Lua應該符合要求, 小且干凈。 即便不用Lua這樣的外部語言, 我也還是傾向于使用已有的語言。
**5.3.3 插件不能工作**
為了提供CMake語言的擴展能力,CMake有一個插件類,允許項目使用C語言創建一個新的CMake命令。當時,這聽起來是個不錯的主意。因為提供的是C語言接口,還可以支持多種編譯器。但是,隨著針對不同平臺(Windows和Linux,32位和64位)的API的出現,插件的兼容性變得難以維護。雖然只使用CMake語言顯得沒那么強大,但是至少不會令程序崩潰,項目也不會因為插件不能工作而無法繼續構建。
**5.3.4 減少外部接口**
在CMake的開發過程中得到的一個重要的教訓是, 你不需要維護用戶訪問不到的功能的后向兼容性。 有些時候, 用戶和客戶要求CMake封裝成一個軟件庫供其它語言來使用。但這樣做不僅會因為不同的CMake使用方式而分裂CMake用戶群,也會為CMake的開發帶來巨大的維護成本。
**腳注**
1. [http://www.itk.org/](http://www.itk.org/)
2. 本譯文由筆者先獨立翻譯,而后又參考他人的翻譯成果[http://www.sand-tower.net/archives/210](http://www.sand-tower.net/archives/210)。 主要是事先沒有上網調查,導致重復勞動。
- 前言(卷一)
- 卷1:第1章 Asterisk
- 卷1:第3章 The Bourne-Again Shell
- 卷1:第5章 CMake
- 卷1:第6章 Eclipse之一
- 卷1:第6章 Eclipse之二
- 卷1:第6章 Eclipse之三
- 卷1:第8章 HDFS——Hadoop分布式文件系統之一
- 卷1:第8章 HDFS——Hadoop分布式文件系統之二
- 卷1:第8章 HDFS——Hadoop分布式文件系統
- 卷1:第12章 Mercurial
- 卷1:第13章 NoSQL生態系統
- 卷1:第14章 Python打包工具
- 卷1:第15章 Riak與Erlang/OTP
- 卷1:第16章 Selenium WebDriver
- 卷1:第18章 SnowFlock
- 卷1:第22章 Violet
- 卷1:第24章 VTK
- 卷1:第25章 韋諾之戰
- 卷2:第1章 可擴展Web架構與分布式系統之一
- 卷2:第1章 可擴展Web架構與分布式系統之二
- 卷2:第2章 Firefox發布工程
- 卷2:第3章 FreeRTOS
- 卷2:第4章 GDB
- 卷2:第5章 Glasgow Haskell編譯器
- 卷2:第6章 Git
- 卷2:第7章 GPSD
- 卷2:第9章 ITK
- 卷2:第11章 matplotlib
- 卷2:第12章 MediaWiki之一
- 卷2:第12章 MediaWiki之二
- 卷2:第13章 Moodle
- 卷2:第14章 NginX
- 卷2:第15章 Open MPI
- 卷2:第18章 Puppet part 1
- 卷2:第18章 Puppet part 2
- 卷2:第19章 PyPy
- 卷2:第20章 SQLAlchemy
- 卷2:第21章 Twisted
- 卷2:第22章 Yesod
- 卷2:第24章 ZeroMQ