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 .看看結果。

set(xxx a b c d)
message(${xxx})
set(xxx e;f;g;h)
message(${xxx})

變數

變數使用set命令設定,格式為set(變數名稱 指定的值)。使用${變數名稱}取值。

有興趣的可以直接剪貼下面程式存成CMakeLists.txt後下cmake .看看結果。

set(xxx a b c d)
message(${xxx})
set(xxx e;f;g;h)
message(${xxx})

另外這邊也列出了CMake內建好用的變數。


流程控制

流程控制又可以分成條件執行、迴圈、和巨集等情況討論:


條件執行

直接看範例,這個範例單純從command line吃變數值,做字串比對。

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內部的變數。
  • 條件判斷方法順序可以參考這邊。簡單翻譯一下:
    • 先處理: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行為的相關參數,這邊就先跳過不談。


迴圈

兩種為主,foreachwhile,直接看範例。

  • foreach 為何有cmake_minimum_required(VERSION 2.8)呢?因為不打執行cmake .會產生警告。有興趣的可以打cmake --help-policy CMP0000看說明。
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_minimum_required(VERSION 2.8)
set(i 0)
while(i LESS 10)
    message(${i})
    math(EXPR i "${i} + 1")
endwhile()

巨集和函數

這兩個差別是**在函數內產生的變數scope只存在函數內,而巨集是全域的。**直接看範例,範例中的巨集和函數都是在內部產生變數並且印出傳進來的參數。可以仔細看輸出結果的確函數內的變數呼叫完後就消失了。

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-placeout-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)

第三版:每個目錄單獨編譯

要做的事情很簡單,就是

  • srclibs下面加入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 套件等。這部份以後有緣份會用到再跟各位分享。


參考資料


书籍推荐