使用 CMake 進行跨平台軟體開發
原文:Cross-Platform Software Development Using CMake http://www.linuxjournal.com/article/6700
作者:Andrej Cedilnik 翻譯:宇舟
在每個系統構建你的工程,而無須關心建立可執行檔和動態庫的具體方法。
當觀察一大堆工程,會發現一件事:對構建過程的描述總是儲存在一組檔案中。這些檔案可能是簡單的shell指令碼、Makefiles、Jam檔案、基於複雜的指令碼的工程像Autoconf和Automake。
最近,一個新的玩家 CMake 加入了軟體構建遊戲。CMake 使用原生構建工具,像 Make 甚至是微軟的 Visual Studio,而不直接是一個的構建程式。支援多個平台,in-source 和 out-source 構建,跨庫依賴檢測,並行構建,可配置的標頭檔。極大的降低了跨平台軟體開發和維護過程的複雜性。
構建系統
觀察下大部分軟體開發工程,無疑你會面對一個共同的問題。你有一大堆源檔案,一些檔案依賴於其他的,你想產生最終的二進位檔案。有時候你想做更複雜的事,但是在大多數情況下就這樣。
你有個小的工程想在你linux上構建。你坐下很快的寫出下面的 Makefile:
main.o: main.c main.h
cc -c main.c
MyProgram: main.o
cc -o MyProgram main.o -lm -lz
當檔案寫好後,你要做的只是輸入 make 命令,然後工程被構建。如果任何檔案被改變,所有必要的檔案會被重新構建。很好,現在你可以祝賀自己並喝一杯。
除非,你的老闆過來說:“我們剛得到一個新的XYZ型電腦,你要在上面構建這個軟體。”所以,你把檔案複製過去,輸入 make 命令,並得到下面的錯誤資訊:
cc: Command not found
你知道那個XYZ型電腦上有個編譯器叫做 cc-XYZ ,所以修改 Makefile 後重試。但是發現系統沒有zlib。所以你去掉 -lz 參數,直接包進zlib的代碼,搞定。
就像你看到的,這個問題是當使用 Makefile 時,只要檔案移到新的使用不同編譯器名字或參數的平台,make失效了。
看個這個問題的更詳細的例子。讓我們看下我們最愛的壓縮庫 zlib 。 zlib 是個相當簡單的庫,由17個C源檔案和11個標頭檔組成。編譯zlib是簡單的。所有要做的是編譯每個C檔案然後把他們連結到一起。你可以寫個 Makefile 解決他,但是在每個單獨平台下,你必須去修改 Makefile 以便可用。
像 Autoconf 和 Automake 之類的工具在UNIX和UNIX類平台下很好的解決了這個問題。但是他們通常太複雜。使事情變的更糟糕了,在大多數工程中,開發人員最終要在 Autoconf的輸入檔案中寫shell指令碼。結果很快變成依賴於開發人員的假設。因為,Autoconf的結果依賴於shell,這些設定檔在沒有 Bourne Shell或者類似標準/bin/sh的平台上是無效的。Autoconf 和 Automake 也依賴於幾個系統上安裝的工具。
CMake 是這些問題的一個解決方案。相對於其他類似工具,CMake 對底層系統做更少的假設。CMake使用標準C++實現,所以他可以在大多數現代作業系統上運行。它不使用除了系統的本地構建工具外的其他的工具。
Installing CMake
在一些平台下,像 Debian GNU/Linux ,CMake 是標準包。對於大多數其他平台,包括UNIX、Mac OS X、Microsof Windows,CMake 二進位包可以直接從 CMake Web site 下載。你可以嘗試執行 cmake --help 以檢測CMake是否被安裝。這個命令會顯示CMake版本和使用資訊。
Simple CMake
現在CMake安裝好了,我們可以在我們的工程中使用他了。我們要先準備CMake的輸入檔案叫做 CMakeLists.txt 。例如,下面是個簡單的 CMakeLists.txt:
PROJECT(MyProject C)
ADD_LIBRARY(MyLibrary STATIC libSource.c)
ADD_EXECUTABLE(MyProgram main.c)
TARGET_LINK_LIBRARIES(MyProgram MyLibrary z m)
使用 CMake 構建工程是極簡單的。在包含CMakeLists.txt 的目錄中,輸入下面的2個命令,path是到源碼的路徑:
cmake path
make
CMake 讀取 CMakeLists.txt 檔案從來源目錄,在目前的目錄下為系統產生適當的Makefiles。CMake 維護依賴的標頭檔的列表,所以依賴檢測是被確保的。如果你要添加更多的源檔案,只要簡單的添加到列表中。當 Makefiles 產生後,你不必再執行 CMake ,因為對 CMakeLists.txt 的依賴檢測已經添加到產生的 Makefils 中。如果你想確保依賴重新產生,你可以執行 make depend 。
CMake Commands
CMake 本質上是一個簡單的解譯器。CMake 輸入檔案有一個極度簡單但是強大的文法。它由命令、原始流量控制構造、宏和變數組成。所有的命令有完全一樣的文法:
COMMAND_NAME(ARGUMENT1 ARGUMENT2 ...)
例如,命令 ADD_LIBRARY 指定一個庫應該被建立。第一個參數是庫的名字,第二個選擇性參數是用來指定這個庫是靜態還是動態,其他的參數是源檔案的列表。你想要動態庫?簡單的用 SHARED 替換 STATIC。所有的命令的列表請看 CMake 的文檔。
還有一些流量控制構造,例如 IF 和 FOREACH 。IF 中的運算式不能包含其他命令,可以使用NOT、AND、OR。下面是一個通常使用 IF 語句的例子:
IF(UNIX)
IF(APPLE)
SET(GUI "Cocoa")
ELSE(APPLE)
SET(GUI "X11")
ENDIF(APPLE)
ELSE(UNIX)
IF(WIN32)
SET(GUI "Win32")
ELSE(WIN32)
SET(GUI "Unknown")
ENDIF(WIN32)
ENDIF(UNIX)
MESSAGE("GUI system is ${GUI}")
這個例子展現了對 IF 語句和變數的使用。
FOREACH 命令的參數,第一個是儲存迭代內容的變數,後面的是被迭代的列表。例如,如果有一列可執行檔需要建立,每個可執行檔是被從同名源檔案建立,可以像下面這樣使用FOREACH:
SET(SOURCES source1 source2 source3)
FOREACH(source ${SOURCES})
ADD_EXECUTABLE(${source} ${source}.c)
ENDFOREACH(source)
使用宏構造可以定義一個宏。當我們要經常建立一些可執行檔並且要連結一些庫。下面的宏可以是我們的生活更簡單點。在這個例子 中,CREATE_EXECUTABLE 是宏的名字,其他的是參數。在宏內部,所有的參數被看作變數。宏一被建立,即可被當作常規命令使用。CREATE_EXECUTABLE的定義和使用象這 樣:
MACRO(CREATE_EXECUTABLE NAME
SOURCES LIBRARIES)
ADD_EXECUTABLE(${NAME} ${SOURCES})
TARGET_LINK_LIBRARIES(${NAME}
${LIBRARIES})
ENDMACRO(CREATE_EXECUTABLE)
ADD_LIBRARY(MyLibrary libSource.c)
CREATE_EXECUTABLE(MyProgram main.c MyLibrary)
宏不等價於一般程式語言中的函數或過程,宏不能被遞迴調用。
條件編譯
好的構建程式的一個重要特性是能將構建的一部分開啟或關閉。構建程式也應該能找到和設定好你的工程需要的系統資源的位置。所有這些功能在 CMake 中使用條件編譯實現。讓我示範一個例子。讓我們假設你的工程有2個模式,常規模式和偵錯模式。偵錯模式添加一些調試代碼到常規代碼中。因此,你的代碼中充 滿這樣的程式碼片段:
#ifdef DEBUG
fprintf(stderr,
"The value of i is: %dn", i);
#endif /* DEBUG */
為了告訴 CMake 添加一個 -DDEBUG 到編譯命令中,你可以對屬性COMPILE_FLAGS使用SET_SOURCE_FILES_PROPERTIES命令。但是可能你不想每次都通過修改 CMakeLists.txt 檔案以在偵錯模式和常規模式中切換。OPTION命令可以用來建立一個布爾型變數可以用於被在構建工程前設定。前一個例子可以被改進為:
OPTION(MYPROJECT_DEBUG
"Build the project using debugging code"
ON)
IF(MYPROJECT_DEBUG)
SET_SOURCE_FILE_PROPERTIES(
libSource.c main.c
COMPILE_FLAGS -DDEBUG)
ENDIF(MYPROJECT_DEBUG)
現在,你問:“我該怎樣設定這個變數?” CMake 帶了3個GUI工具。在UNIX類系統中,有一個終端GUI工具叫做ccmake,這是一個基於文本的,可以在一個遠程終端串連上使用的。CMake也有微軟Windows和Mac OS X版本的GUI工具。
當你在使用CMake產生的Makefiles,如果你已經執行過CMake,現在只要輸入命令 make edit_cache 。這個命令會運行一個合適的GUI工具。在所有的GUI工具中,你有一些用於設定變數的選項。就像你看到的,CMake有幾個預設選項,例如 EXECUTABLE_OUTPUT_PATH 、LIBRARY_OUTPUT_PATH(可執行文結合庫檔案被輸出到的路徑),在我們前面的例子中還有MYPROJECT_DEBUG。當改變一些變 量的值後,你按下配置按鈕或c鍵(在ccmake中)。
在GUI工具中,你將設定幾種不同類型的條目。MYPROJECT_DEBUG是布爾型的,另一種常見的變數類型是路徑。假設我們的程式依賴於Pyhon.h檔案的位置。我們在CMakeLists.txt檔案中加入下面的用於嘗試尋找一個檔案的命令:
FIND_PATH(PYTHON_INCLUDE_PATH Python.h
/usr/include
/usr/local/include)
因為在每個單獨的工程中都去重複的指定所有的位置是浪費的。你可以包含(include)其他叫做模組(modules)的CMakes檔案。 CMake內建一些有用模組,從用於搜尋不同軟體包的到幹一些實際的事或定義一些宏的模組。全部的模組列表請看CMake的模組子目錄。例如,有個模組叫 做FindPythonLibs.cmake,可以在大多數系統的用於尋找Python庫檔案和標頭檔路徑。然而如果CMake不能找到你要的,你總是可 以在GUI工具中指定。你也可以通過命令列訪問CMake變數。下面的一行命令設定MYPROJECT_DEBUG為OFF:
cmake -DMYPROJECT_DEBUG:BOOL=OFF
What about Subdirectories? 子目錄?
作為一個軟體開發人員,你可能要把原始碼組織到子目錄中。不同的子目錄可以代表不同的庫、可執行檔、測試、文檔。現在我們可以啟用或禁用子目錄以便 構建工程的一部分跳過另一部分。使用SUBDIRS 命令告訴 CMake 處理一個子目錄。這個命令使CMake到指定的子目錄下去找 CMakeLists.txt 檔案。使用這個命令可以使我們的工程更有組織。我們移動所有的庫檔案到庫子目錄中,頂級 CMakeLists.txt 現在看起來像這樣:
PROJECT(MyProject C)
SUBDIRS(SomeLibrary)
INCLUDE_DIRECTORIES(SomeLibrary)
ADD_EXECUTABLE(MyProgram main.c)
TARGET_LINK_LIBRARIES(MyProgram MyLibrary)
INCLUDE_DIRECTORIES 命令告訴編譯器到哪裡去找 main.c 用到的標頭檔。因此,即使你的工程有500個子目錄,你把你的所有源檔案都放到子目錄中,也不會有依賴問題。CMake幫你都幹了。
回到Zlib
現在,我們要個“cmake化”的zlib。從一個簡單的 CMakeLists.txt 檔案開始:
PROJECT(ZLIB)
# source files for zlib
SET(ZLIB_SRCS
adler32.c gzio.c
inftrees.c uncompr.c
compress.c infblock.c
infutil.c zutil.c
crc32.c infcodes.c
deflate.c inffast.c
inflate.c trees.c
)
ADD_LIBRARY(zlib ${ZLIB_SRCS})
ADD_EXECUTABLE(example example.c)
TARGET_LINK_LIBRARIES(example zlib)
現在你可以構建它了。然而,有些小事要記住。首先,zlib在有些平台需要unistd.h檔案。因此,我們加上下面的測試代碼:
INCLUDE (
${CMAKE_ROOT}/Modules/CheckIncludeFile.cmake)
CHECK_INCLUDE_FILE(
"unistd.h" HAVE_UNISTD_H)
IF(HAVE_UNISTD_H)
ADD_DEFINITION(-DHAVE_UNISTD_H)
ENDIF(HAVE_UNISTD_H)
在Windows下,我們也必須為共用庫做一些而外的事。zlib需要加上-DZLIB_DLL參數編譯,以使匯出宏正確。因此,我們加上下面的選項:
OPTION(ZLIB_BUILD_SHARED
"Build ZLIB shared" ON)
IF(WIN32)
IF(ZLIB_BUILD_SHARED)
SET(ZLIB_DLL 1)
ENDIF(ZLIB_BUILD_SHARED)
ENDIF(WIN32)
IF(ZLIB_DLL)
ADD_DEFINITION(-DZLIB_DLL)
ENDIF(ZLIB_DLL)
雖然這樣也能工作,但是有一個更好的方法。我們可以配置一個標頭檔以代替傳入ZLIB_DLL和HAVE_UNISTD_H。我們要準備一個使用cmake標記的輸入檔案。下面是zlibConfig.h,一個zlib包含檔案的例子:
#ifndef _zlibConfig_h
#define _zlibConfig_h
#cmakedefine ZLIB_DLL
#cmakedefine HAVE_UNISTD_H
#endif
這裡,#cmakedefine VAR 被替換為 #define VAR或者#undef VAR,依賴於cmake中VAR是否被定義。我們使用下面的CMake命令讓CMake建立檔案 zlibConfig.h
CONFIGURE_FILE(
${ZLIB_SOURCE_DIR}/zlibDllConfig.h.in
${ZLIB_BINARY_DIR}/zlibDllConfig.h)
To Infinity and Further
通過這篇文章,你可以開始在你的日常工作中使用CMake。如果你的代碼足夠可以移植,現在你可以串連到你的朋友的AIX系統上構建你的工程。CMake檔案也比Makefiles檔案容易理解,因此你的朋友可以檢查你的遺漏。
然而,這個例子涉及的比較淺,CMake有能力幹許多其他的工作。在1.6版中,你可以做平台獨立的TRY_RUN和TRY_COMPILE構建, 以方便的測試系統的能力。CMake原生支援C和C++,但是有限支援構建java檔案。通過一些努力,你可以構建任何東西從Python、Emacs腳 本到LaTeX文檔。使用CMake作為測試架構驅動,你可以實現平台獨立的迴歸測試。如果你想走的更遠,你可以使用CMake的C API為CMake寫一個外掛程式,以添加你自己的命令。
CMake is being actively used in several projects such as VTK and ITK. Its benefits are enormous in traditional software development, however they become even more apparent, when portability is necessary. By using CMake for software development, your code will be significantly more "open", because it will build on a variety of platforms.
譯者註:
CMake 已經發展到版本2.6了,這篇文章原文是2003寫的,當時才1.6。現在已經有了很大改進了。KDE4整個工程檔案就是用CMake的。