CMake通過CMakeLists.txt配置項目的構建系統,配合使用cmake命令行工具生成構建系統並執行編譯、測試,相比於手動編寫構建系統(如Makefile)要高效許多。對於C/C++項目開發,非常值得學習掌握。
在前兩篇文章中已經介紹CMake的相關核心概念,使用的一般流程,以及CMake核心的語法和常用腳本命令:
本文將會介紹如何書寫一個完備的CMakeLists.txt
文件,滿足一般項目的基礎構建要求,CMake的語法將會更多介紹項目配置命令,主要有以下內容:
- 設置一些自定義編譯控制開關和自定義編譯變量控制編譯過程
- 根據不同編譯類型配置不同的編譯選項和鏈接選項
- 添加頭文件路徑、編譯宏等常規操作
- 編譯生成不同類型的目標文件,包括可執行文件、靜態鏈接庫和動態鏈接庫
- 安裝、打包和測試
本文較長,建議收藏、點贊(#^.^#),用到的時候可以隨時查閱。
文章目錄結構如下:
一 基礎配置
下面先介紹一些CMake項目通常都需要進行的配置。下面介紹的內容以make
作爲構建工具作爲示例。
下面的示例代碼可以在開源項目cmake-template中查看(當前commit id:c7c6b15
)。
把倉庫克隆下來結合源碼閱讀本文效果更佳,如果有幫助,請點下Star喲。
1 設置項目版本和生成version.h
一般來說,項目一般需要設置一個版本號,方便進行版本的發佈,也可以根據版本對問題或者特性進行追溯和記錄。
通過project命令配置項目信息,如下:
project(CMakeTemplate VERSION 1.0.0 LANGUAGES C CXX)
第一個字段是項目名稱;通過VERSION
指定版本號,格式爲main.minor.patch.tweak
,並且CMake會將對應的值分別賦值給以下變量(如果沒有設置,則爲空字符串):
PROJECT_VERSION, <PROJECT-NAME>_VERSION
PROJECT_VERSION_MAJOR, <PROJECT-NAME>_VERSION_MAJOR
PROJECT_VERSION_MINOR, <PROJECT-NAME>_VERSION_MINOR
PROJECT_VERSION_PATCH, <PROJECT-NAME>_VERSION_PATCH
PROJECT_VERSION_TWEAK, <PROJECT-NAME>_VERSION_TWEAK
因此,結合前一篇文章提到的configure_file
命令,可以配置自動生成版本頭文件,將頭文件版本號定義成對應的宏,或者定義成接口,方便在代碼運行的時候瞭解當前的版本號。
比如:
configure_file(src/c/cmake_template_version.h.in "${PROJECT_SOURCE_DIR}/src/c/cmake_template_version.h")
假如cmake_template_version.h.in
內容如下:
#define CMAKE_TEMPLATE_VERSION_MAJOR @CMakeTemplate_VERSION_MAJOR@
#define CMAKE_TEMPLATE_VERSION_MINOR @CMakeTemplate_VERSION_MINOR@
#define CMAKE_TEMPLATE_VERSION_PATCH @CMakeTemplate_VERSION_PATCH@
執行cmake配置構建系統後,將會自動生成文件:cmake_template_version.h
,其中@<var-name>@
將會被替換爲對應的值:
#define CMAKE_TEMPLATE_VERSION_MAJOR 1
#define CMAKE_TEMPLATE_VERSION_MINOR 0
#define CMAKE_TEMPLATE_VERSION_PATCH 0
2 指定編程語言版本
爲了在不同機器上編譯更加統一,最好指定語言的版本,比如聲明C使用c99
標準,C++使用c++11
標準:
set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 11)
這裏設置的變量都是CMAKE_
開頭(包括project
命令自動設置的變量),這類變量都是CMake的內置變量,正是通過修改這些變量的值來配置CMake構建的行爲。
CMAKE_
、_CMAKE
或者以下劃線開頭後面加上任意CMake命令的變量名都是CMake保留的。
3 配置編譯選項
通過命令add_compile_options
命令可以爲所有編譯器配置編譯選項(同時對多個編譯器生效);
通過設置變量CMAKE_C_FLAGS
可以配置c編譯器的編譯選項;
而設置變量CMAKE_CXX_FLAGS
可配置針對c++編譯器的編譯選項。
比如:
add_compile_options(-Wall -Wextra -pedantic -Werror)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -std=c99")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe -std=c++11")
4 配置編譯類型
通過設置變量CMAKE_BUILD_TYPE
來配置編譯類型,可設置爲:Debug
、Release
、RelWithDebInfo
、MinSizeRel
等,比如:
set(CMAKE_BUILD_TYPE Debug)
當然,更好的方式應該是在執行cmake
命令的時候通過參數-D
指定:
cmake -B build -DCMAKE_BUILD_TYPE=Debug
如果設置編譯類型爲Debug
,那麼對於c編譯器,CMake會檢查是否有針對此編譯類型的編譯選項CMAKE_C_FLAGS_DEBUG
,如果有,則將它的配置內容加到CMAKE_C_FLAGS
中。
可以針對不同的編譯類型設置不同的編譯選項,比如對於Debug
版本,開啓調試信息,不進行代碼優化:
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")
對於Release
版本,不包含調試信息,優化等級設置爲2:
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O2")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2")
5 添加全局宏定義
通過命令add_definitions
可以添加全局的宏定義,在源碼中就可以通過判斷不同的宏定義實現相應的代碼邏輯。用法如下:
add_definitions(-DDEBUG -DREAL_COOL_ENGINEER)
6 添加include目錄
通過命令include_directories
來設置頭文件的搜索目錄,比如:
include_directories(src/c)
二 編譯目標文件
一般來說,編譯目標(target)的類型一般有靜態庫、動態庫和可執行文件。
這時編寫CMakeLists.txt
主要包括兩步:
- 編譯:確定編譯目標所需要的源文件
- 鏈接:確定鏈接的時候需要依賴的額外的庫
下面以開源項目(cmake-template)來演示。項目的目錄結構如下:
./cmake-template
├── CMakeLists.txt
├── src
│ └── c
│ ├── cmake_template_version.h
│ ├── cmake_template_version.h.in
│ ├── main.c
│ └── math
│ ├── add.c
│ ├── add.h
│ ├── minus.c
│ └── minus.h
└── test
└── c
├── test_add.c
└── test_minus.c
項目的構建任務爲:
- 將math目錄編譯成靜態庫,命名爲math
- 編譯main.c爲可執行文件demo,依賴math靜態庫
- 編譯test目錄下的測試程序,可以通過命令執行所有的測試
- 支持通過命令將編譯產物安裝及打包
1 編譯靜態庫
這一步需要將項目目錄路徑src/c/math
下的源文件編譯爲靜態庫,那麼需要獲取編譯此靜態庫需要的文件列表,可以使用set
命令,或者file
命令來進行設置。比如:
file(GLOB_RECURSE MATH_LIB_SRC
src/c/math/*.c
)
add_library(math STATIC ${MATH_LIB_SRC})
使用file
命令獲取src/c/math
目錄下所有的*.c
文件,然後通過add_library
命令編譯名爲math
的靜態庫,庫的類型是第二個參數STATIC
指定的。
如果指定爲
SHARED
則編譯的就是動態鏈接庫。
2 編譯可執行文件
通過add_executable
命令來往構建系統中添加一個可執行構建目標,同樣需要指定編譯需要的源文件。但是對於可執行文件來說,有時候還會依賴其他的庫,則需要使用target_link_libraries
命令來聲明構建此可執行文件需要鏈接的庫。
在示例項目中,main.c
就使用了src/c/math
下實現的一些函數接口,所以依賴於前面構建的math
庫。所以在CMakeLists.txt
中添加以下內容:
add_executable(demo src/c/main.c)
target_link_libraries(demo math)
第一行說明編譯可執行文件demo
需要的源文件(可以指定多個源文件,此處只是以單個文件作爲示例);第二行表明對math
庫存在依賴。
此時可以在項目的根目錄下執行構建和編譯命令,並執行demo:
➜ # cmake -B cmake-build
➜ # cmake --build cmake-build
➜ # ./cmake-build/demo
Hello CMake!
10 + 24 = 34
40 - 96 = -56
三 安裝和打包
1 安裝
對於安裝來說,其實就是要指定當前項目在執行安裝時,需要安裝什麼內容:
- 通過
install
命令來說明需要安裝的內容及目標路徑; - 通過設置
CMAKE_INSTALL_PREFIX
變量說明安裝的路徑; -
3.15
往後的版本可以使用cmake --install --prefix <install-path>
覆蓋指定安裝路徑。
比如,在示例項目中,把math
和demo
兩個目標按文件類型安裝:
install(TARGETS math demo
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib)
這裏通過TARGETS
參數指定需要安裝的目標列表;參數RUNTIME DESTINATION
、LIBRARY DESTINATION
、ARCHIVE DESTINATION
分別指定可執行文件、庫文件、歸檔文件分別應該安裝到安裝目錄下個哪個子目錄。
如果指定CMAKE_INSTALL_PREFIX
爲/usr/local
,那麼math
庫將會被安裝到路徑/usr/local/lib/
目錄下;而demo
可執行文件則在/usr/local/bin
目錄下。
CMAKE_INSTALL_PREFIX
在不同的系統上有不同的默認值,使用的時候最好顯式指定路徑。
同時,還可以使用install
命令安裝頭文件:
file(GLOB_RECURSE MATH_LIB_HEADERS src/c/math/*.h)
install(FILES ${MATH_LIB_HEADERS} DESTINATION include/math)
假如將安裝到當前項目的output
文件夾下,可以執行:
➜ # cmake -B cmake-build -DCMAKE_INSTALL_PREFIX=./output
➜ # cmake --build cmake-build
➜ # cd cmake-build && make install && cd -
Install the project...
-- Install configuration: ""
-- Installing: .../cmake-template/output/lib/libmath.a
-- Installing: .../gitee/cmake-template/output/bin/demo
-- Installing: .../gitee/cmake-template/output/include/math/add.h
-- Installing: .../gitee/cmake-template/output/include/math/minus.h
可以看到安裝了前面install
命令指定要安裝的文件,並且不同類型的目標文件安裝到不同子目錄。
2 打包
要使用打包功能,需要執行include(CPack)
啓用相關的功能,在執行構建編譯之後使用cpack
命令行工具進行打包安裝;對於make工具,也可以使用命令make package
。
打包的內容就是install
命令安裝的內容,關鍵需要設置的變量有:
變量 | 含義 |
---|---|
CPACK_GENERATOR | 打包使用的壓縮工具,比如"ZIP" |
CPACK_OUTPUT_FILE_PREFIX | 打包安裝的路徑前綴 |
CPACK_INSTALL_PREFIX | 打包壓縮包的內部目錄前綴 |
CPACK_PACKAGE_FILE_NAME | 打包壓縮包的名稱,由CPACK_PACKAGE_NAME 、CPACK_PACKAGE_VERSION 、CPACK_SYSTEM_NAME 三部分構成 |
比如:
include(CPack)
set(CPACK_GENERATOR "ZIP")
set(CPACK_PACKAGE_NAME "CMakeTemplate")
set(CPACK_SET_DESTDIR ON)
set(CPACK_INSTALL_PREFIX "")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
假如:
CPACK_OUTPUT_FILE_PREFIX
設置爲/usr/local/package
;
CPACK_INSTALL_PREFIX
設置爲real/cool/engineer
;
CPACK_PACKAGE_FILE_NAME
設置爲CMakeTemplate-1.0.0
;
那麼執行打包文件的生成路徑爲:
/usr/local/package/CMakeTemplate-1.0.0.zip
解壓這個包得到的目標文件則會位於路徑下:
/usr/local/package/real/cool/engineer/
此時重新執行構建,使用cpack
命令執行打包:
➜ # cmake -B cmake-build -DCPACK_OUTPUT_FILE_PREFIX=`pwd`/output
➜ # cmake --build cmake-build
➜ # cd cmake-build && cpack && cd -
CPack: Create package using ZIP
CPack: Install projects
CPack: - Run preinstall target for: CMakeTemplate
CPack: - Install project: CMakeTemplate
CPack: Create package
CPack: - package: .../cmake-template/output/CMakeTemplate-1.0.0-Darwin.zip generated.
cpack
有一些參數是可以覆蓋CMakeLists.txt
設置的參數的,比如這裏的-G
參數就會覆蓋變量CPACK_GENERATOR
,具體細節可使用cpack --help
查看。
四 測試
CMake的測試功能使用起來有幾個步驟:
-
CMakeLists.txt
中通過命令enable_testing()
或者include(CTest)
來啓用測試功能; - 使用
add_test
命令添加測試樣例,指定測試的名稱和測試命令、參數; - 構建編譯完成後使用
ctest
命令行工具運行測試。
爲了控制是否開啓測試,可使用option
命令設置一個開關,在開關打開時才進行測試,比如:
option(CMAKE_TEMPLATE_ENABLE_TEST "Whether to enable unit tests" ON)
if (CMAKE_TEMPLATE_ENABLE_TEST)
message(STATUS "Unit tests enabled")
enable_testing()
endif()
這裏爲了方便後續演示,暫時是默認開啓的。
1 編寫測試程序
在此文的示例代碼中,針對add.c
和minus.c
實現了兩個測試程序,它們的功能是類似的,接受三個參數,用第一和第二個計算兩個參數的和或者差,判斷是否和第三個參數相等,如test_add.c
的代碼爲:
// @Author: Farmer Li, 公衆號: 很酷的程序員/RealCoolEngineer
// @Date: 2021-05-10
#include <stdio.h>
#include <stdlib.h>
#include "math/add.h"
int main(int argc, char* argv[]) {
if (argc != 4) {
printf("Usage: test_add v1 v2 expected\n");
return 1;
}
int x = atoi(argv[1]);
int y = atoi(argv[2]);
int expected = atoi(argv[3]);
int res = add_int(x, y);
if (res != expected) {
return 1;
} else {
return 0;
}
}
這裏需要注意的是,對於測試程序來說,如果返回值非零,則表示測試失敗。
2 添加測試
接下來先使用add_executable
命令生成測試程序,然後使用add_test
命令添加單元測試:
add_executable(test_add test/c/test_add.c)
add_executable(test_minus test/c/test_minus.c)
target_link_libraries(test_add math)
target_link_libraries(test_minus math)
add_test(NAME test_add COMMAND test_add 10 24 34)
add_test(NAME test_minus COMMAND test_minus 40 96 -56)
3 執行測試
現在重新執行cmake
命令更新構建系統,執行構建,再執行測試:
➜ # cmake -B cmake-build
➜ # cmake --build cmake-build
➜ # cd cmake-build && ctest && cd -
Test project /Users/Farmer/gitee/cmake-template/cmake-build
Start 1: test_add
1/2 Test #1: test_add ......................... Passed 0.00 sec
Start 2: test_minus
2/2 Test #2: test_minus ....................... Passed 0.01 sec
100% tests passed, 0 tests failed out of 2
使用ctest -VV
則可以看到更新詳細的測試流程和結果。
在CMake 3.20往後的版本中,
ctest
可以使用--test-dir
指定測試執行目錄。
至此,一個較爲完備的CMakeLists.txt
就開發完成了。