CMake 初探
目錄
概論
CMake是1999年推出的開源自由軟體計劃,目的是提供不同平臺之間共同的編譯環境。他的特點有:
- 支援不同平臺。
- 可以將Build和原本程式碼分開。不分開稱為in-place build,而分開的情況稱為out-place build。out-place build的附加功能就是同樣一包套件可以同時編譯成不同平臺的binary並且分別放在不同的目錄中。
- 支援cache加快編譯速度。
CMake的執行流程簡單來說是
- 開發者使用CMake語法寫編譯描述,存到CMakeLists.txt。
- 使用者執行cmake,cmake首先會根據開發描述的規格產生該平臺對應的編譯環境檔案如Makefile等。
- 使用者執行make或是平臺上的編譯方法產生最後的結果。
- 使用者執行cmake install安裝軟體。
另外一點值得注意的是cmake本身沒有提供uninstall功能。
CMakeLists.txt 語法簡介
CMake語法的格式為命令字串(參數)
,而相關規範可以分為「list和字串」、「變數」、「流程控制」、「Quotation」。分別討論如下:
list和字串
CMake的基本單位是字串,而多個字串可以透過空白或是;
組合成字串list。
有興趣的可以直接剪貼下面程式存成CMakeLists.txt後下cmake .看看結果。
```CMake CMakeLists.txt set(xxx a b c d) message(${xxx}) set(xxx e;f;g;h) message(${xxx})
---
<a name="cmx-syntax-var"></a>
### 變數
變數使用`set`命令設定,格式為`set(變數名稱 指定的值)`。使用`${變數名稱}`取值。
有興趣的可以直接剪貼下面程式存成CMakeLists.txt後下cmake .看看結果。
```CMake CMakeLists.txt
set(xxx a b c d)
message(${xxx})
set(xxx e;f;g;h)
message(${xxx})
另外這邊也列出了CMake內建好用的變數。
流程控制
流程控制又可以分成條件執行、迴圈、和巨集等情況討論:
條件執行
直接看範例,這個範例單純從command line吃變數值,做字串比對。
```CMake CMakeLists.txt if ( NOT DEFINED test) message("Use: cmake -Dtest:STRING=val to test") elseif(${test} STREQUAL yes) message("if: ${test}") elseif(${test} STREQUAL test1) message("else if: ${test}") else() message("else: ${test}") endif()
有幾點需要說明:
* `DEFINED`判斷是否該變數有被定義。
* command透過`-D變數名稱:變數型態=變數值`來設定CMakeList.txt內部的變數。
* 條件判斷方法順序可以參考[這邊](http://www.cmake.org/cmake/help/v2.8.7/cmake.html#command:if)。簡單翻譯一下:
* 先處理:EXISTS, COMMAND, DEFINED
* 再來是:EQUAL, LESS, GREATER, STRLESS, STRGREATER, STREQUAL, MATCHES
* 接下來是:NOT
* 最後才是:AND, OR
---
##### cache的補充
我們第一次 **不從command 帶參數** 執行結果:
$ cmake . -- The C compiler identification is GNU ... Use: cmake -Dtest:STRING=val to test
接下來我們從command **帶參數** 重新執行一次:
$ cmake . -Dtest:STRING=yes if: yes
然後 **不從command 帶參數** 再執行會發現test變數變成yes了
$ cmake . if: yes
看一下目前目錄會發現新的檔案`CMakeCache.txt`,找一下裡面的字串`test`會看到
$ grep test CMakeCache.txt ... test:STRING=yes ...
**結論就是CMake的確有cache,而且不小心cache會影響到執行的結果。**另外其實`set(..)`裡面也可以cache行為的相關參數,這邊就先跳過不談。
---
#### 迴圈
兩種為主,`foreach`和`while`,直接看範例。
* `foreach`
為何有`cmake_minimum_required(VERSION 2.8)`呢?因為不打執行`cmake .`會產生警告。有興趣的可以打`cmake --help-policy CMP0000`看說明。
```CMake CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
set(xxx e;f;g;h)
foreach(i ${xxx})
message(${i})
endforeach()
執行結果如下:
$ cmake .
e
f
g
h
- while 一個0~9的迴圈,需要透過math command做四則運算。
```CMake CMakeLists.txt cmake_minimum_required(VERSION 2.8) set(i 0) while(i LESS 10) message(${i}) math(EXPR i "${i} + 1") endwhile()
---
<a name="cmx-syntax-macro"></a>
#### 巨集和函數
這兩個差別是**在函數內產生的變數scope只存在函數內,而巨集是全域的。**直接看範例,範例中的巨集和函數都是在內部產生變數並且印出傳進來的參數。可以仔細看輸出結果的確函數內的變數呼叫完後就消失了。
```CMake CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
macro(mac_print msg)
set(mac "macro")
message("${mac}: ${msg}")
endmacro(mac_print)
function(func_print msg)
set (func "func")
message("${func}: ${msg}")
endfunction(func_print)
mac_print("test macro")
message("check var in macro: ${mac}")
func_print("test function")
message("check var in function: ${func}")
執行結果如下
$ cmake .
macro: test macro
check var in macro: macro
func: test function
check var in function:
Quotation
- 字串可以用成對的
"
表示 - 支援C語言型態控制字元如
\n``\t
- 可以使用跳脫字元顯示特殊意義符號如
\${var}
印出來就是${VAR}
專案產生檔案安裝
簡單的語法如下,詳細資料請參考這邊
- 執行檔安裝(一般安裝目錄路徑:bin)
install(TARGETS 執行檔名稱 DESTINATION 安裝目錄路徑)
- 函式庫安裝(一般安裝目錄路徑:lib)
install(TARGETS 函式庫名稱 LIBRARY DESTINATION 安裝目錄路徑)
- Header 檔安裝(一般安裝目錄路徑:include)
install(FILES Header檔名稱 DESTINATION 安裝目錄路徑)
這些安裝描述都是允許多個檔案。另外你可以在執行cmake帶-DCMAKE_INSTALL_PREFIX=安裝目錄
指定安裝的top目錄,或是make DESTDIR=安裝目錄
也有同樣效果。
範例: 產生執行檔和函式庫
前面有提到in-place和out-place的編譯方式。他們方式的差別是:
- in-place: 直接在CMakeLists.txt那層下
cmake . && make
- out-place: 直接在CMakeLists.txt那層下
mkdir build && cd build && cmake ../ && make
- build是慣用名稱,不需要強制使用。
範例程式
範例程式細節在這邊,檔案各別分配到src
, include
, libs
這三個目錄。不想看code只要知道每個檔案都有參考到某個自訂的header file就好了。
測試環境:Ubuntu 12.04
原始測試程式樹狀架構
├── include
│ ├── liba.h
│ └── libb.h
├── libs
│ ├── liba.c
│ └── libb.c
└── src
└── test.c
第一版:單一程式沒有函數庫
先暖身一下,只要在project最上層放一個CMakeLists.txt就好了。
這版本CMakeLists.txt不難理解,就做
- 填寫project資訊描述。
- 設定project相關header file路徑。
- 指定編譯要顯示細節,這是個人偏好習慣。
- 設定共用編譯參數,CMake可以更進一步地指定release mocde或debug mode的參數,以及指定套用這些參數檔案。
- 指定要編譯哪些檔案。
- 指定要編譯成執行檔
top 目錄的CMakeLists.txt如下:
CMake top 目錄的CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
# Project data
project(testcmake)
# Directories
set(SRC_DIR src)
set(LIB_DIR libs)
set(INC_DIR include)
# Release mode
set(CMAKE_BUILD_TYPE Debug)
# Compile flags
set(CMAKE_C_FLAGS "-Wall -Werror")
# I like verbose, must after project, do not know why
set(CMAKE_VERBOSE_MAKEFILE true)
# Where to include?
include_directories(${INC_DIR})
# Files to compile
set(test_SRCS ${SRC_DIR}/test.c ${LIB_DIR}/liba.c ${LIB_DIR}/libb.c)
add_executable(${PROJECT_NAME} ${test_SRCS})
這邊可以看到和原本的差別只有多了CMakeLists.txt檔而已。
├── CMakeLists.txt
├── include
│ ├── liba.h
│ └── libb.h
├── libs
│ ├── liba.c
│ └── libb.c
└── src
└── test.c
第二版:加入編譯函式庫
要編譯函式庫,要在top目錄下的CMakeLists.txt做以下的修改
add_library(檔案名稱 函式庫名稱)
告訴CMake要搬把哪些檔案編譯函式庫- 改成
add_library(檔案名稱 SHARED 函式庫名稱)
就變成shared library了。
- 改成
target_link_libraries(執行檔名稱 函式庫名稱)
- 告訴系統最後link要把函式庫一起link進來。
top 目錄的CMakeLists.txt如下:
CMake top 目錄的CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
# Project data
project(testcmake)
# Directories
set(SRC_DIR src)
set(LIB_DIR libs)
set(INC_DIR include)
# Release mode
set(CMAKE_BUILD_TYPE Debug)
# Compile flags
set(CMAKE_C_FLAGS "-Wall -Werror")
# I like verbose, must after project, do not know why
set(CMAKE_VERBOSE_MAKEFILE true)
# Where to include?
include_directories(${INC_DIR})
# Build libraries
set(liba_SRCS ${LIB_DIR}/liba.c)
set(libb_SRCS ${LIB_DIR}/libb.c)
add_library(a SHARED ${liba_SRCS})
add_library(b SHARED ${libb_SRCS})
# Build binary
set(test_SRCS ${SRC_DIR}/test.c)
add_executable(${PROJECT_NAME} ${test_SRCS})
target_link_libraries(${PROJECT_NAME} a b)
第三版:每個目錄單獨編譯
要做的事情很簡單,就是
- 在
src
和libs
下面加入CMakeLists.txt,描述編譯行為 - 把根目錄的對應編譯行為搬到子目錄的CMakeLists.txt
- 使用
add_subdirectory(子目錄名稱)
把要編譯的子目錄加進去
所以我們現在目錄樹狀結構會變成src和lib目錄都有CMakeLists.txt
├── CMakeLists.txt
├── include
│ ├── liba.h
│ └── libb.h
├── libs
│ ├── CMakeLists.txt
│ ├── liba.c
│ └── libb.c
├── readme.txt
└── src
├── CMakeLists.txt
└── test.c
每個目錄的CMakeLists.txt列出如下
- CMakeLists.txt
CMake CMakeLists.txt
cmake_minimum_required(VERSION 2.8)
# Project data
project(testcmake)
# Directories
set(SRC_DIR src)
set(LIB_DIR libs)
set(INC_DIR include)
# Release mode
set(CMAKE_BUILD_TYPE Debug)
# Compile flags
set(CMAKE_C_FLAGS "-Wall -Werror")
# I like verbose, must after project, do not know why
set(CMAKE_VERBOSE_MAKEFILE true)
# Where to include?
include_directories(${INC_DIR})
# Build library in libs directory or not?
# Dive into libs directory
add_subdirectory(${SRC_DIR})
add_subdirectory(${LIB_DIR})
- libs/CMakeLists.txt
CMake libs/CMakeLists.txt
# Build binary, inherit setting from parent
set(liba_SRCS liba.c)
set(libb_SRCS libb.c)
add_library(a ${liba_SRCS})
add_library(b ${libb_SRCS})
- src/CMakeLists.txt
CMake src/CMakeLists.txt
# Build binary
set(test_SRCS test.c)
add_executable(${PROJECT_NAME} ${test_SRCS})
target_link_libraries(${PROJECT_NAME} a b)
第四版:加入安裝產生的檔案描述
這邊就是單純把前面的install()命令套用到每一個目錄下的CMakeLists.txt。由於我們也要安裝header檔,所以在include目錄下面會新增CMakeLists.txt描述安裝header的細節。
所以我們現在目錄樹狀結構會變成每個目錄都有CMakeLists.txt
├── CMakeLists.txt
├── include
│ ├── CMakeLists.txt
│ ├── liba.h
│ └── libb.h
├── libs
│ ├── CMakeLists.txt
│ ├── liba.c
│ └── libb.c
├── readme.txt
└── src
├── CMakeLists.txt
└── test.c
而各CMakeLists.txt新增的描述為
- CMakeLists.txt
CMake CMakeLists.txt
add_subdirectory(${INC_DIR})
- libs/CMakeLists.txt
CMake libs/CMakeLists.txt
install(TARGETS a b LIBRARY DESTINATION lib)
- src/CMakeLists.txt
CMake src/CMakeLists.txt
install(TARGETS ${PROJECT_NAME} DESTINATION bin)
- include/CMakeLists.txt
CMake include/CMakeLists.txt
install(FILES liba.h libb.h DESTINATION include)
第四版執行結果
- 第四版執行結果
$ cmake ../ -DCMAKE_INSTALL_PREFIX=`pwd`/test && make && make install
...
$ tree test
test/
├── bin
│ └── testcmake
├── include
│ ├── liba.h
│ └── libb.h
└── lib
├── liba.so
└── libb.so
結論
本篇文章簡單介紹了CMake的語法,以及示範用CMake產生執行檔和函式庫。但是CMake還有太多東西值得去注意,例如把字串代換到程式碼,config.h的建立,搜尋depend 套件等。這部份以後有緣份會用到再跟各位分享。