跳到正文

Wiki

頂層CMake與公共編譯選項

約 9 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

本節講兩個檔案:

  1. 頂層 CMakeLists.txt
  2. cmake/ProjectOptions.cmake

這兩個檔案決定了整個工程的基本身份和公共編譯規則。

1.頂層 CMakeLists.txt

本模板的頂層檔案如下:

cmake_minimum_required(VERSION 3.25)

project(cmake_template VERSION 1.0.0 LANGUAGES C CXX)

include(GNUInstallDirs)

include("${CMAKE_CURRENT_LIST_DIR}/cmake/ProjectOptions.cmake")

add_subdirectory(src)

頂層 CMakeLists.txt 的原則是:只做總控,不堆具體業務和第三方依賴。

2.cmake_minimum_required

cmake_minimum_required(VERSION 3.25)

作用:

  1. 指定專案需要的最低 CMake 版本。
  2. 告訴 CMake 使用對應版本的策略行為。
  3. 太老的 CMake 會直接報錯,不會繼續配置。

可以填什麼:

cmake_minimum_required(VERSION 3.20)
cmake_minimum_required(VERSION 3.25)
cmake_minimum_required(VERSION 3.28)

選擇建議:

寫法 說明
3.10 太老,很多現代 CMake 寫法不方便
3.16 Ubuntu 20.04 常見版本,相容性好
3.22 Ubuntu 22.04 常見版本
3.25 本模板使用,和 preset version 6 搭配
3.28+ 適合較新的系統,但模板通用性下降

教學模板建議不要無限追新。只要滿足當前寫法即可。

3.project

project(cmake_template VERSION 1.0.0 LANGUAGES C CXX)

作用:

  1. 設定專案名。
  2. 設定專案版本。
  3. 宣告專案使用哪些語言。
  4. 初始化一批 CMake 變數。

3.1.專案名

project(cmake_template)

專案名會影響這些變數:

變數
PROJECT_NAME cmake_template
CMAKE_PROJECT_NAME 頂層專案名,通常也是 cmake_template

本模板在 src/CMakeLists.txt 中使用:

add_executable(${PROJECT_NAME}
  ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
)

所以最終執行檔名就是:

cmake_template

如果把專案名改成:

project(robot_app VERSION 1.0.0 LANGUAGES C CXX)

最終執行檔就會變成:

robot_app

3.2.VERSION

VERSION 1.0.0

專案版本號。

可以填什麼:

VERSION 0.1.0
VERSION 1.0.0
VERSION 2.3.4

CMake 會生成這些變數:

變數 示例值
PROJECT_VERSION 1.0.0
PROJECT_VERSION_MAJOR 1
PROJECT_VERSION_MINOR 0
PROJECT_VERSION_PATCH 0

以後如果做安裝包、匯出 CMake package、生成版本標頭檔案,這些變數會很有用。

3.3.LANGUAGES

LANGUAGES C CXX

宣告專案啟用哪些語言。

常見值:

說明
C C 語言
CXX C++
CUDA CUDA
ASM 彙編
Fortran Fortran

如果專案只有 C++,也可以寫:

project(cmake_template VERSION 1.0.0 LANGUAGES CXX)

本模板保留 C CXX,是為了允許 src/*.csrc/*.cpp 都能參與構建。

4.include(GNUInstallDirs)

include(GNUInstallDirs)

作用:載入 GNU 風格的安裝目錄變數。

常用變數:

變數 常見值 用途
CMAKE_INSTALL_BINDIR bin 執行檔
CMAKE_INSTALL_LIBDIR liblib64 動態庫、靜態庫
CMAKE_INSTALL_INCLUDEDIR include 標頭檔案
CMAKE_INSTALL_DATADIR share 資料檔案
CMAKE_INSTALL_DOCDIR share/doc/项目名 文件

為什麼不用手寫:

install(TARGETS app DESTINATION bin)
install(TARGETS lib DESTINATION lib)

因為不同 Linux 發行版可能使用 liblib64。用 GNUInstallDirs 後,CMake 會根據系統習慣設定。

模板中的使用示例:

install(TARGETS ${PROJECT_NAME}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

庫安裝:

install(TARGETS ${PREFIX}_src_lib
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

標頭檔案安裝:

install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/inc/"
  DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

5.include("${CMAKE_CURRENT_LIST_DIR}/cmake/ProjectOptions.cmake")

include("${CMAKE_CURRENT_LIST_DIR}/cmake/ProjectOptions.cmake")

作用:讀取另一個 CMake 檔案。

5.1.include(...)

include 會在當前作用域執行指定的 CMake 指令碼。

可以寫:

include(GNUInstallDirs)
include(cmake/ProjectOptions.cmake)
include("${CMAKE_CURRENT_LIST_DIR}/cmake/ProjectOptions.cmake")

區別:

寫法 說明
include(GNUInstallDirs) 載入 CMake 內建模組
include(cmake/ProjectOptions.cmake) 相對路徑寫法
include("${CMAKE_CURRENT_LIST_DIR}/cmake/ProjectOptions.cmake") 更穩,不受當前工作目錄影響

5.2.CMAKE_CURRENT_LIST_DIR

表示當前正在處理的 CMake 檔案所在目錄。

在頂層 CMakeLists.txt 中,它就是專案根目錄。

所以:

"${CMAKE_CURRENT_LIST_DIR}/cmake/ProjectOptions.cmake"

表示:

项目根目录/cmake/ProjectOptions.cmake

6.add_subdirectory(src)

add_subdirectory(src)

作用:進入 src 子目錄,繼續讀取 src/CMakeLists.txt

可以填什麼:

add_subdirectory(src)
add_subdirectory(test)
add_subdirectory(third_party/some_lib)

常見寫法:

add_subdirectory(<源码目录>)

也可以指定構建目錄:

add_subdirectory(src src_build)

但一般不需要。

頂層只寫:

add_subdirectory(src)

表示具體的執行檔、庫、依賴,都交給 src 下面的 CMake 檔案管理。

7.cmake/ProjectOptions.cmake

本模板內容如下:

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

add_library(project_options INTERFACE)
target_compile_features(project_options INTERFACE cxx_std_17)

add_library(project_warnings INTERFACE)
target_compile_options(project_warnings INTERFACE
  -Wall
  -Wextra
  -Wpedantic
)

這個檔案只放專案公共選項,不放 Eigen、OpenCV 等第三方依賴。

8.CMAKE_CXX_STANDARD

set(CMAKE_CXX_STANDARD 17)

設定預設 C++ 標準。

常見值:

標準
11 C++11
14 C++14
17 C++17
20 C++20
23 C++23

本模板使用 C++17:

set(CMAKE_CXX_STANDARD 17)

如果想用 C++20:

set(CMAKE_CXX_STANDARD 20)
target_compile_features(project_options INTERFACE cxx_std_20)

注意兩個地方最好一起改。

9.CMAKE_CXX_STANDARD_REQUIRED

set(CMAKE_CXX_STANDARD_REQUIRED ON)

作用:要求編譯器必須支援指定的 C++ 標準。

可以填什麼:

說明
ON 必須使用指定標準,不支援就報錯
OFF 不支援時可能退回較低標準

模板建議寫 ON。否則你以為在用 C++17,實際編譯器可能退回舊標準,錯誤會更隱蔽。

10.CMAKE_CXX_EXTENSIONS

set(CMAKE_CXX_EXTENSIONS OFF)

作用:控制是否使用編譯器擴充套件標準。

可以填什麼:

編譯選項傾向 說明
ON gnu++17 允許 GNU 擴充套件
OFF c++17 更標準、更可移植

Linux-only 專案也建議先寫 OFF,這樣程式碼更接近標準 C++。

11.C 標準相關選項

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)

含義和 C++ 對應選項類似。

常見 C 標準:

標準
99 C99
11 C11
17 C17
23 C23

如果專案沒有 .c 檔案,也可以只啟用 C++:

project(cmake_template VERSION 1.0.0 LANGUAGES CXX)

然後刪除 C 標準相關設定。

12.CMAKE_EXPORT_COMPILE_COMMANDS

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

作用:生成 compile_commands.json

可以填:

說明
ON 生成
OFF 不生成

這個選項在 CMakePresets.json 中也寫了:

"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"

兩個地方都寫不衝突。preset 的好處是 IDE 和命令列都能明確看到這個配置;ProjectOptions.cmake 的好處是即使不用 preset,也預設開啟。

13.add_library(project_options INTERFACE)

add_library(project_options INTERFACE)

建立一個 interface target。

INTERFACE target 沒有原始碼,不會生成 .a.so。它只用來攜帶編譯特性、巨集定義、include 路徑、連結依賴等"使用要求"。

add_library 常見型別:

型別 是否有原始碼 是否生成產物 用途
STATIC .a 靜態庫
SHARED .so 動態庫
MODULE 外掛式動態模組
OBJECT .o 集合 複用物件檔案
INTERFACE 傳遞配置

本模板用:

project_options

儲存 C++ 標準要求。

14.target_compile_features

target_compile_features(project_options INTERFACE cxx_std_17)

作用:宣告使用這個 target 的目標需要 C++17。

可以填什麼:

寫法 說明
cxx_std_11 至少 C++11
cxx_std_14 至少 C++14
cxx_std_17 至少 C++17
cxx_std_20 至少 C++20
cxx_std_23 至少 C++23

為什麼這裡又寫了一遍 C++17?

set(CMAKE_CXX_STANDARD 17)

是全域性預設值。

target_compile_features(project_options INTERFACE cxx_std_17)

是目標級要求。現代 CMake 更推薦用目標級要求,因為它能隨著 target 連結關係傳播。

15.add_library(project_warnings INTERFACE)

add_library(project_warnings INTERFACE)

建立一個專門儲存 warning 選項的 interface target。

這樣每個 target 可以選擇是否連結:

target_link_libraries(my_target
  PRIVATE
    project_warnings
)

16.target_compile_options

target_compile_options(project_warnings INTERFACE
  -Wall
  -Wextra
  -Wpedantic
)

作用:給目標新增編譯選項。

本模板選項:

選項 作用
-Wall 開啟一組常見警告
-Wextra 開啟更多警告
-Wpedantic 提醒不符合標準的寫法

常見可追加選項:

選項 說明
-Wconversion 型別轉換可能丟失資訊時警告
-Wshadow 變數名遮蔽時警告
-Werror 把 warning 當 error

不建議模板預設加 -Werror。因為不同編譯器、不同第三方標頭檔案可能產生不同 warning,初學階段容易被卡住。

17.PUBLICPRIVATEINTERFACE 的直覺

後面的 CMake 檔案會大量出現:

PUBLIC
PRIVATE
INTERFACE

它們控制"屬性是否傳遞給使用者"。

關鍵字 自己用 傳給別人
PRIVATE
PUBLIC
INTERFACE

例如:

target_link_libraries(lib1_src_lib
  PUBLIC
    project_options
  PRIVATE
    project_warnings
)

含義:

  1. lib1_src_lib 自己需要 project_options
  2. 連結 lib1_src_lib 的目標也會得到 project_options
  3. project_warnings 只用於編譯 lib1_src_lib 自己,不強迫下游也開啟相同 warning。