CMake應用:CMakeLists.txt完全指南

CMake通過CMakeLists.txt配置項目的構建系統,配合使用cmake命令行工具生成構建系統並執行編譯、測試,相比於手動編寫構建系統(如Makefile)要高效許多。對於C/C++項目開發,非常值得學習掌握。

在前兩篇文章中已經介紹CMake的相關核心概念,使用的一般流程,以及CMake核心的語法和常用腳本命令:

  1. cmake應用:基礎篇
  2. cmake應用:核心語法篇

本文將會介紹如何書寫一個完備的CMakeLists.txt文件,滿足一般項目的基礎構建要求,CMake的語法將會更多介紹項目配置命令,主要有以下內容:

  1. 設置一些自定義編譯控制開關和自定義編譯變量控制編譯過程
  2. 根據不同編譯類型配置不同的編譯選項和鏈接選項
  3. 添加頭文件路徑、編譯宏等常規操作
  4. 編譯生成不同類型的目標文件,包括可執行文件、靜態鏈接庫和動態鏈接庫
  5. 安裝、打包和測試

本文較長,建議收藏、點贊(#^.^#),用到的時候可以隨時查閱。

文章目錄結構如下:

一 基礎配置

下面先介紹一些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來配置編譯類型,可設置爲:DebugReleaseRelWithDebInfoMinSizeRel等,比如:

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主要包括兩步:

  1. 編譯:確定編譯目標所需要的源文件
  2. 鏈接:確定鏈接的時候需要依賴的額外的庫

下面以開源項目(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

項目的構建任務爲:

  1. 將math目錄編譯成靜態庫,命名爲math
  2. 編譯main.c爲可執行文件demo,依賴math靜態庫
  3. 編譯test目錄下的測試程序,可以通過命令執行所有的測試
  4. 支持通過命令將編譯產物安裝及打包

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 安裝

對於安裝來說,其實就是要指定當前項目在執行安裝時,需要安裝什麼內容:

  1. 通過install命令來說明需要安裝的內容及目標路徑;
  2. 通過設置CMAKE_INSTALL_PREFIX變量說明安裝的路徑;
  3. 3.15往後的版本可以使用cmake --install --prefix <install-path>覆蓋指定安裝路徑。

比如,在示例項目中,把mathdemo兩個目標按文件類型安裝:

install(TARGETS math demo
        RUNTIME DESTINATION bin
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib)

這裏通過TARGETS參數指定需要安裝的目標列表;參數RUNTIME DESTINATIONLIBRARY DESTINATIONARCHIVE 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_NAMECPACK_PACKAGE_VERSIONCPACK_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的測試功能使用起來有幾個步驟:

  1. CMakeLists.txt中通過命令enable_testing()或者include(CTest)來啓用測試功能;
  2. 使用add_test命令添加測試樣例,指定測試的名稱和測試命令、參數;
  3. 構建編譯完成後使用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.cminus.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就開發完成了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章