用 CMake 来构建 C/C++ 项目是业内的主流做法。最近,我们的项目代码做了一些拆分和合并:引入其他仓库代码,并且将公共部分拆分以供多个仓库同时使用。为此,就得修改项目中的 CMake 以满足需求。
在做这件事情时,过程是相当痛苦的,修改的难度超过了我的预期。这份痛苦的回忆,让我陷入了沉思:这 CMake 咋这么不好使,是我的使用姿势不对吗?CMake 的最佳实践是啥?
在经过一番搜索和学习,我开始了解 Modern CMake 的一些用法与理念,它主张放弃传统的基于变量的方法,而采用基于 target 的结构化模式,使其成为一个更可维护、更直观、更易集成、更具意义的方案。
在这里对自己的学习做一个总结,主要内容包括:
这部分内容是 An Introduction to Modern CMake 的总结,并不会讲的非常详细,希望通过几句话来高度总结各个用法,旨在了解 CMake 有哪能力,如果对某些部分感兴趣请大家自行查阅具体内容。
经典用法
~/package $ mkdir build
~/package $ cd build
~/package/build $ cmake ..
~/package/build $ make
新版本可以简单一点
~/package $ cmake -S . -B build
~/package $ cmake --build build
安装命令
#From the build directory (pick one)
~/package/build $ make install
~/package/build $ cmake --build . --target install
~/package/build $ cmake --install . # CMake 3.15+ only
#From the source directory (pick one)
~/package $ cmake --build build --target install
~/package $ cmake --install build # CMake 3.15+ only
推荐使用 --build
用法
-v
显示执行的构建命令: cmake --build build -v
--target
来选择目标:cmake --build --target install
设置 CC
和 CXX
环境变量来选择 C/C++ 编译器
~/package/build $ CC=clang CXX=clang++ cmake ..
选择不同的工具进行构建
cmake -G"My Tool"
设置构建器,例如 cmake -S . -B buildXcode -G"Xcode"
通过 -D
来设置选项,例如 cmake -S . -B build -DCMAKE_INSTALL_PREFIX=dist
--trace
打印 CMake configure 阶段的输出,例如 cmake -S . -B build --trace
不好的 CMake 的用法
link_directories
,include_libraries
,add_definitions
等,请你忘记它们PUBLIC
,除非有依赖传递,否则请你使用 PRIVATE
替换 PUBLIC
良好的 CMake 用法
最低版本
cmake_minimum_required(VERSION 3.1)
指定最低版本cmake_minimum_required(VERSION 3.7...3.18)
CMake 3.12+ 后,可以指定版本范围Project
VERSION
指定版本,并设置一系列变量,例如MyProject_VERSION
等DESCRIPTION
项目的描述LANGUAGES
支持 C/CXX/Fortran/ASM/CUDA(3.8+)/CSharp(3.8+)/SWIFT(3.15+)
, C/C++
为默认值project(MyProject VERSION 1.0
DESCRIPTION "Very nice project"
LANGUAGES CXX)
生成可执行文件
add_executable(one two.cpp three.h)
生成库
BUILD_SHARED_LIBS
的值将决定编译 STATIC 或者 SHAREDadd_library(one STATIC two.cpp three.h)
给 target 添加属性
target_link_libraries
进行传递# PUBLIC 表示外部也需要这个 include 目录
target_include_directories(one PUBLIC include)
add_library(another STATIC another.cpp another.h)
# 由于具有传递性,another 可以连接 one 的 include 目录
target_link_libraries(another PUBLIC one)
set(MY_VARIABLE "value")
设置局部变量,其作用域为当前文件夹,以及 add_subdirectory
进入的文件夹set(MY_VARIABLE "value" PARENT_SCOPE)
将作用域设置为父目录,通常用在子目录向父目录传递信息set(MY_LIST "one" "two")
会在变量值中间加入 “;”,等价于 set(MY_LIST "one;two")
set(MY_CACHE_VARIABLE "VALUE" CACHE STRING "Description")
设置缓存变量CMakeCache.txt
中cmake -DXXX
命令传递的参数为缓存变量关于局部变量与缓冲变量的示例,请参考 cmake-变量和全局变量缓存
set(ENV{variable_name} value)
设置环境变量$ENV{variable_name}
获取环境变量CMAKE_
开头的变量,例如设置 CMAKE_CXX_STANDARD
,将会设置 target 的 CXX_STANDARD
属性初始值。set_property
用于设置属性,get_property
用于获取属性。参看 cmake-properties 查阅有哪些属性控制流
if
语句来控制程序流if
语句中支持的关键字包括:
NOT
、‘TARGET’、EXISTS
、DEFINED
等STRQUAL
,AND
,OR
,MATCH
,VERSION_LESS
,VERSION_LESS_EQUAL
if(variable)
# If variable is `ON`, `YES`, `TRUE`, `Y`, or non zero number
else()
# If variable is `0`, `OFF`, `NO`, `FALSE`, `N`, `IGNORE`, `NOTFOUND`, `""`, or ends in `- NOTFOUND`
endif()
# If variable does not expand to one of the above, CMake will expand it then try again
if(NOT TARGET libA OR EXISTS "test.xml")
# If libA or test.xml exist
endif()
生成器表达式, generator-expressions
target_include_directories(MyTarget
PUBLIC
$
$
)
宏与函数
PARENT_SCOPE
来设置参数。可以通过 cmake_parse_arguments
来解析函数参数
configure_file
命令可以实现这个。version.h
中获取版本信息。file
命令可以实现这个。- project
- .gitignore
- README.md
- LICENCE.md
- CMakeLists.txt
- cmake
- FindSomeLib.cmake
- something_else.cmake
- include
- project
- lib.hpp
- src
- CMakeLists.txt
- lib.cpp
- apps
- CMakeLists.txt
- app.cpp
- tests
- CMakeLists.txt
- testlib.cpp
- docs
- CMakeLists.txt
- extern
- googletest
- scripts
- helper.py
execute_process
在 configure 阶段运行命令find_package(Git QUIET)
if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
RESULT_VARIABLE GIT_SUBMOD_RESULT)
if(NOT GIT_SUBMOD_RESULT EQUAL "0")
message(FATAL_ERROR "git submodule update --init failed with ${GIT_SUBMOD_RESULT}, please checkout submodules")
endif()
endif()
find_package(PythonInterp REQUIRED)
add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp"
COMMAND "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/GenerateHeader.py" --argument
DEPENDS some_target)
add_custom_target(generate_header ALL
DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp DESTINATION include)
cmake -E
运行内置的一些命令,例如解压、复制等,具体参看 Run a Command-Line Tooltarget_compile_features(myTarget PUBLIC cxx_std_11)
开启 c++11 特性,当然你也可以选择 cxx_std_14
和 cxx_std_17
set_target_properties(myTarget PROPERTIES CXX_EXTENSIONS OFF)
,关闭扩展特性set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set_target_properties(myTarget PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED YES
CXX_EXTENSIONS NO
)
地址无关代码(Position independent code; PIC),CMake 会自动将 -fPIC
添加到 SHARED 和 MODULE 库中,当然你也可以显示指定
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set_target_properties(lib1 PROPERTIES POSITION_INDEPENDENT_CODE ON)
小型库
dl
库,那么这么写最简单 target_link_libraries(libA PRIVATE ${CMAKE_DL_LIBS})
m
, pthread
等,但你可以这么写 find_library(MATH_LIBRARY m)
if(MATH_LIBRARY)
target_link_libraries(MyTarget PUBLIC ${MATH_LIBRARY})
endif()
Interprocedural optimization,运行时优化(link time optimization),即 -flto
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
set_target_properties(myTarget PROPERTIES
INTERPROCEDURAL_OPTIMIZATION ON)
check_ipo_supported
来检查当前版本是否支持 LTO include(CheckIPOSupported)
check_ipo_supported(RESULT result)
if(result)
set_target_properties(foo PROPERTIES
INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()
在 CMake 中可以配合其他工具
CMake Modules 非常有用,简单介绍一些常用的
include(CMakeDependentOption)
cmake_dependent_option(BUILD_TESTS "Build your tests" ON "VAL1;VAL2" OFF)
调试 CMake 代码
message(STATUS "MY_VARIABLE=${MY_VARIABLE}")
include(CMakePrintHelpers)
cmake_print_variables(MY_VARIABLE)
cmake_print_properties(
TARGETS my_target
PROPERTIES POSITION_INDEPENDENT_CODE)
--trace-source="filename"
让你观察 CMake 文件到底发生了什么,例如: cmake -S . -B build --trace-source=CMakeLists.txt
git submodule add ../../owner/repo.git extern/repo
将 extern 添加为子仓库find_package(Git QUIET)
if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
# Update submodules as needed
option(GIT_SUBMODULE "Check submodules during build" ON)
if(GIT_SUBMODULE)
message(STATUS "Submodule update")
execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
RESULT_VARIABLE GIT_SUBMOD_RESULT)
if(NOT GIT_SUBMOD_RESULT EQUAL "0")
message(FATAL_ERROR "git submodule update --init failed with ${GIT_SUBMOD_RESULT}, please checkout submodules")
endif()
endif()
endif()
if(NOT EXISTS "${PROJECT_SOURCE_DIR}/extern/repo/CMakeLists.txt")
message(FATAL_ERROR "The submodules were not downloaded! GIT_SUBMODULE was turned off or failed. Please update submodules and try again.")
endif()
ExternalProject_Add
)。这就导致你无法使用 add_subdirectory
。if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND BUILD_TESTING)
add_subdirectory(tests)
endif()
add_test(NAME TestName COMMAND TargetName)
注册测试AddGoogleTest
工具引入 GoogleTestFetchContent
引入 GoogleTest# Prepare "Catch" library for other executables
set(CATCH_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/extern/catch)
add_library(Catch2::Catch IMPORTED INTERFACE)
set_property(Catch2::Catch PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${CATCH_INCLUDE_DIR}")
ExternalProject
、FetchContent
或者 git submodule 引入 Catch,那么直接 add_subdirectory
即可介绍了引入 CUDA、OpenMP、Boost、MPI、ROOT、Minuit2 等库的标准姿势