开发工具使用之 Cmake

Example

常用的编译脚本和 CmakeLists.txt

build.sh

#!bin/sh

#    Create makefile
cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=./build/release \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON

# Compile
cmake --build build --parallel 96

# Install
cmake --build build --target install

# Compile and install
cmake --build build \
--parallel 96 \
--target install


CMakeLists.txt

cmake_minimum_required(VERSION 3.16)

project(learning_cmake LANGUAGES C CXX)

if (PROJECT_BINARY_DIR STREQUAL PROJECT_SOURCE_DIR)
    message(WARNING "The binary directory of CMake cannot be the same as source directory!")
endif()

add_executable(main)

aux_source_directory(. SOURCES)
aux_source_directory(src SOURCES)

target_sources(main PUBLIC ${SOURCES})

####################### Begin build dog shared library #######################

add_library(dog SHARED dog/src/dog.cc)

target_include_directories(dog
    PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/dog/include
)

set_target_properties(dog
    PROPERTIES
        CXX_STANDARD 17
        CXX_STANDARD_REQUIRED ON
        CXX_EXTENSIONS OFF
)

####################### End build dog shared library #######################

set_target_properties(main
    PROPERTIES
        CXX_STANDARD 17
        CXX_STANDARD_REQUIRED ON
        CXX_EXTENSIONS OFF
)

target_link_libraries(main PUBLIC dog)

target_include_directories(main
  PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}/include
)



编译的两阶段

第一阶段是 cmake -B build,称为配置阶段(configure),这时只检测环境并生成构建规则,会在 build 目录下生成本地构建系统能识别的项目文件(Makefile 或是 .sln)。

  • 在配置阶段,可以利用 -D 指定配置变量(即一些缓存变量),这些变量会被在本地缓存,即使后续只是 cmake -B build ,前面的缓存也会被使用到
  • 注意:在一些场景下,可能存在修改了CMakeLists 文件,但是无论如何编译,修改的变量都没有生效,这个时候需要删除 CMakeCache.txt 文件

第二阶段是 cmake --build build,称为构建阶段(build),这时才实际调用编译器来编译代码。




常见的编译参数

  • -DCMAKE_EXPORT_COMPILE_COMMANDS 用于生成 json 数据库文件,以便第三方插件进行代码跳转
  • -DCMAKE_INSTALL_PREFIX 指定安装路径,同时也用于寻找依赖的第三方库
  • -DCMAKE_BUILD_TYPE 指定编译的版本,可选值有 release,debug 和 profile
  • -DCMAKE_TOOLCHAINS_FILE 指定交叉编译工具链的 .cmake 文件



生成可执行文件

实际上有非常多的方法,将 .cc/.cpp 文件编译成可执行,这里只推荐两种

# 第一种
# 先设置最终得到的可执行文件,再用一个变量存储源文件,
# 最后使用 target_sources 将源文件添加到可执行中
add_executable(main)
set(sources main.cc other.cc)
target_sources(main PUBLIC ${sources})

# 第二种
# 使用 aux_source_directory 自动搜集需要文件的后缀名
add_executable(main)
aux_source_directory(. sources)
aux_source_directory(src sources)
target_sources(main PUBLIC ${sources})



生成动态/静态/对象库

其中对象库类似于静态库,但不生成 .a 文件,只由 CMake 记住该库生成了哪些对象文件

对象库不生成 .a 文件,只由 CMake 记住该库生成了哪些对象文件,因此就有人推荐用对象库代替静态库,避免跨平台的麻烦

# 第一个是库的名字
# 第二个表示要将当前库编译成静态、动态还是对象库
# 最后是要编译的文件
add_library(mylib STATIC mylib.cc)
add_library(mylib SHARED mylib.cc)
add_library(mylib OBJECT mylib.cc)

add_executable(main main.cc)

target_link_libraries(main PUBLIC mylib)

一个比较坑的问题,动态库无法连接静态库,解决办法就是设置 set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# 设置全体库为 PIC
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

add_library(otherlib STATIC otherlib.cc)

add_library(mylib SHARED mylib.cc)
target_link_libraries(mylib PUBLIC otherlib)

add_executable(main main.cc)
target_link_libraries(main PUBLIC mylib)



设置头文件的搜索路径

个人非常建议以 target 的形式,管理头文件。

只有在非常非常少见的场景或是需求,才需要用 include_directories 来指定目录。

一般有两种方式来设置头文件的检索路径:

  • 第一种是使用 include_directories 指定目录

include_directories用于将给定的目录添加到编译器,便于搜索包含文件的目录

# 将当前目录下的 include 目录添加到后续 .h 文件的搜索路径中
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)

也就是说,后续在 .cc 文件中填写的如果是相对路径,那么就会被解释为在当前这个源目录下的路径

比如说我们想要引入 include 文件夹下的 test.h 文件,文件真实路径为 include/test.h

那么在主目录中的 main.cc 就只需要 #include "test.h"即可引入



target_link_directories则更加细粒度的规定了头文件的引用范围,作用和include_directories相同


PRIVATE

被设定为 PRIVATE 的目录,下属的文件不能对项目外暴露,也不对项目内设为 PUBLIC 的文件暴露

举例: include 下有两个目录:private 和 public,二者都存放 .h 文件,src 目录存放 .cc 文件

其中目录 private 被设为 PRIVATE,目录 public 被设为 PUBLIC

则,src 下的 .cc 文件可以使用 private 的文件,include/public 下的头文件和项目外的文件不可以使用 private 的文件


从两种目录结构的角度进行理解:

角度一:项目目录的结构是,将 .h 文件统一放到 include 中,再在 include 中拆分出 private 和 public

  • 则 PRIVATE 表示该目录下的头文件,只能给本目录下的 .cc 文件使用,不对外提供接口(例如一些辅助性的类)

角度二:项目目录的结构是,将 .h 文件和 .cc 文件都放在一起,按照库或模块对文件进行分类

  • 设库 B 为该项目对外提供的库,库 B 依赖于库 A,库 A 的代码在库 B 的代码结构中,而将库 A 设为 PRIVATE 后,则表示库 A 只会提供给库 B 使用,而不会传导给库 B 的使用者,即没有传递性
target_include_directories(dog PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/private)

INTERFACE

被设定为 INTERFACE 的目录,下属的文件对项目外的文件以及项目内被设为 PUBLIC 的文件暴露,不对项目内的可执行文件暴露

举例: include 下有两个目录:interface 和 public,二者都存放 .h 文件,src 目录存放 .cc 文件

其中目录 interface 被设为 INTERFACE,目录 public 被设为 PUBLIC

则,src 下的 .cc 文件不可以使用 interface 的文件,include/public 下的头文件和项目外的文件可以使用 interface 的文件


从两种目录结构的角度进行理解:

角度一:项目目录的结构是,将 .h 文件统一放到 include 中,再在 include 中拆分出 interface 和 public

  • 则 INTERFACE 表示该目录下的头文件,是对外提供接口的(因此可以被设为 PUBLIC 的头文件使用),但是不能对当前目录下 .cc 文件暴露

角度二:项目目录的结构是,将 .h 文件和 .cc 文件都放在一起,按照库或模块对文件进行分类

  • 设库 B 为该项目对外提供的库,库 B 不依赖于库 A,库 A 的代码在库 B 的代码结构中,而将库 A 设为 INTERFACE 后,则表示库 A 不会提供给库 B 使用,而是会直接给使用者用户,也不算有传递性
target_include_directories(dog INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include/public)

PUBLIC

被设定为 PUBLIC 的目录,谁都可以使用


从两种目录结构的角度进行理解:

角度一:项目目录的结构是,将 .h 文件统一放到 include 中,再在 include 中拆分出 interface 和 public

  • 则 PUBLIC 表示该目录下的头文件,谁都可以使用

角度二:项目目录的结构是,将 .h 文件和 .cc 文件都放在一起,按照库或模块对文件进行分类

  • 设库 B 为该项目对外提供的库,库 A 的代码在库 B 的代码结构中,无论库 B 是否依赖于库 A,只要将库 A 设为 PUBLIC 后,则库 B 和用户使用者都可以使用,即有传递性
target_include_directories(dog PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include/public)



添加构建子模块

add_subdirectory(dog)

具体格式:add_subdirectory (source_dir [binary_dir] [EXCLUDE_FROM_ALL])

必选参数:source_dir 指定一个子目录,子目录下应该包含 CMakeLists.txt 文件和代码文件(可以是相对路径或绝对路径)

可选参数:binary_dir 指定一个子目录,用于存放子目录的二进制文件(如果不指明的话,那么生成的二进制文件和 makefile 就会和主目录是在同一个路径下),也可以写相对路径(比如是在主目录下的 cmakelists.txt 中写的,那么相对路径就可以写为 ../demo-apps/build)




Install

DIRECTORY,例如 include(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include ESTINATION${CMAKE_INSTALL_INCLUDEIR})

就是将指定目录下的文件(include 下的文件),全部移动到指定文件目录下

FILE,用法同上,就是将指定文件,移动到指定目录下

TARGET,安装指定的目标文件

# 将二进制可执行文件 myrun 安装到目录 ${PROJECT_SOURCE_DIR}/build/bin
install(TARGETS myrun
    RUNTIME DESTINATION ${PROJECT_SOURCE_DIR}/build/bin # 二进制可执行文件
    LIBRARY DESTINATION ${PROJECT_SOURCE_DIR}/build/lib # 动态库
    ARCHIVE DESTINATION ${PROJECT_SOURCE_DIR}/build/lib # 静态库
)



常用语法

list

向指定的 list 进行操作

list(APPEND  [...])

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake")

向 list 中添加数据 element

PS:这里给 .cmake 模块指定了文件夹以后,后续的子模块可以直接 include 对应的 .cmake 文件并使用,而不需要重新指定路径



option

option(ARA_CXX_STANDARD_EXTENSIONS "Description" OFF)

一般用于控制编译流程,可以理解为 c 语言里面的宏,即条件编译

option(<variable> "<help_text>" [value])

  • variable:定义选项名称
  • help_text:说明选项的含义
  • value:定义选项默认状态,一般是 OFF 或者 ON,除去 ON 之外,其他所有值都为认为是 OFF
  • PS:如果主目录的 cmakelists 和子目录的 cmakelists 中定义的变量值不同,比如主目录中定义为 ON,子目录中定义为 OFF,那么遵循主目录中的内容

这里看到主目录定义了 ARA_CXX_STANDARD_EXTENSIONS ,后面的子目录就会用这个宏,来判断是否要加上 c++ 的编译拓展



if

if (CMAKE_BINARY_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
    #    如果 CMAKE_BINARY_DIR 等于 CMAKE_CURRENT_SOURCE_DIR,则__
endif()


message

message 一般用于输出日志,可以分为不同的等级

# 输出字符串内容
message("hello world!")

# STATUS 输出状态信息,会带有 -- 前缀
message(STATUS "hello world!")

# FATAL_ERROR 输出错误信息,终止程序执行
message(FATAL_ERROR "Test1")

# WARNING 输出警告信息,继续执行
message(WARNING "test in warning")






cmake 的基本配置

# 指定最低所需的 cmake 版本,当用户使用的 cmake 小于这个版本的时候,就会报错
cmake_minimum_required(VERSION 3.16 FATAL_ERROR) # 在2.6以后的版本会被接受但是忽略
cmake_minimum_required(VERSION 3.15...3.20) # 指定使用 cmake 的版本范围

# project 初始化项目信息,并把当前 cmakelists.txt 所在位置作为根目录
# 第一个 slot (NAME)用于指定项目的名称
# 第二个 slot (VERSION)用于指定项目的版本号(可以把当前项目的版本号设定为 x.y.z;再通过一些宏可以获得具体的版本号)
# 第三个 slot (DESCRIPTION)用于补充项目的具体信息
# 第四个 slot (LANGUAGES)用于指定项目使用的语言(可以同时写多种语言)
project(LearningCpp VERSION 1.0.0 LANGUAGES CXX)
project(LearningC
    VERSION 1.0.0
    DESCRIPTION "a free open-source"
    HOMEPAGE_URL http://www.baidu.com
    LANGUAGES CXX C
    )

# 一些常用的宏
# CMAKE_CURRENT_SOURCE_DIR 表示当前源码目录的位置,例如 ~/hellocmake
# CMAKE_CURRENT_BINARY_DIR 表示当前输出目录的位置,例如 ~/hellocmake/build
# PROJECT_SOURCE_DIR 表示最近一次调用 project 的 CMakeLists.txt 所在的源码目录(我理解为就是根目录的路径)
# CMAKE_CURRENT_SOURCE_DIR 表示当前 CMakeLists.txt 所在的源码目录
# CMAKE_SOURCE_DIR 表示最为外层 CMakeLists.txt 的源码根目录
# CMAKE_BINARY_DIR 指的是存放二进制文件的文件夹

# 设置 cpp 的编译选项
set(CMAKE_CXX_STANDARD 17) # 选择 cpp 的编译版本;因此不要用 -std=17 指定版本,因为这样跨平台就会出问题;不过这里配置的一般是当前主项目的编译版本,其中子项目,例如编译成的相关静态库则需要用 set_target_properties 来表示
set(CMAKE_CXX_STANDARD_REQUIRED ON) # bool 类型,表示是否一定要支持上述指定的 cpp 标准,off 表示 CMake 检测到编译器不支持 C++17 时不报错,而是默默调低到 C++14 给你用;on 则发现不支持报错,更安全
set(CMAKE_CXX_EXTENSIONS ON) # 设为 ON 表示启用 GCC 特有的一些扩展功能;OFF 则关闭 GCC 的扩展功能,只使用标准的 C++;要兼容其他编译器(如 MSVC)的项目,都会设为 OFF 防止不小心用了 GCC 才有的特性;此外,最好是在 project 指令前设置 CMAKE_CXX_STANDARD 这一系列变量,这样 CMake 可以在 project 函数里对编译器进行一些检测,看看他能不能支持 C++17 的特性



TODO

记录一个之前遇到的小bug:

set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 这句话不起作用,感觉是版本的原因

需要修改为set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL ""),这样才能生成 json 文件,以便 clang 实现代码跳转

2025.03,16 怀疑是 CMakeCache 文件没有删除

cmake中的 public,interface 和 private 大体思路上都是一样的,表示传递的依赖性:

public表示自己可以使用指定的头文件或是链接的库

interface表示自己不可以使用指定的头文件或是链接的库,但是当别的编译单元 target 指定当前单元时,可以使用 interface 标记的头文件和链接的库

private表示自己可以使用指定的头文件或是链接的库,但是别的编译单元使用当前单元时,就无法使用指定的头文件和链接的库

https://blog.csdn.net/sinat_37231928/article/details/121684722

https://zhuanlan.zhihu.com/p/82244559

需要学习如何使用 ninja 进行编译,cmake -g 指定编译工具?

关于 .cmake 文件

背景:为了防止 cmakelists.txt 文件过长,因此将其分为几个模块,同时也方便平台的组件共用该 cmake 文件,cmake 模块文件一般是后缀名为 .cmake

CMAKE_CURRENT_LIST_DIR 是用分号分隔的目录列表(是一个 list ,也就是说可以有多个路径),cmake 使用该路径检查附带的 .cmake 文件模块;默认为空

如何在 windows 下使用 cmake

先去官网上下载 msi 安装包,然后安装

接着在 vscode 中安装插件 cmake 和 cmake tools

接着,cmake 有 makefile 和 ninja,所以需要把他们的可执行程序 exe 放到 cmake/bin 的目录下(当然,这个目录是被加入到系统变量中的)

  • ninja 可以直接在官网的 github 上下载二进制程序
  • cmake 则需要先下载 mingw32 (我这里是和 llvm 一起下载了),然后把其中 llvm/bin 的可执行程序 mingw32-make 放到 cmake/bin 的目录下,再改名为 make

最后是配置插件,cmake generator 将其配置为 Unix makefiles

注意:这个插件每次一打开目录,就会将 cmakelists 中的内容编译一遍,而上面则是用于修改 cmake 的默认生成构造器


记录一次灵异事件:在 windows 上构建 cmake 项目,始终出现找不到编译器的情况

-- Building for: NMake Makefiles
-- The C compiler identification is unknown
-- The CXX compiler identification is unknown
CMake Error at CMakeLists.txt:11 (project):
  The CMAKE_C_COMPILER:

    cl

  is not a full path and was not found in the PATH.

后续使用了指令 cmake -G”Unix Makefiles” -B build ,才恢复正常

而且,必须要最开始执行这条指令,指定了 makefile 作为构造器,才能够正常运行


 上一篇
一些 cpp 的编码规范 一些 cpp 的编码规范
什么时候使用前置声明为了防止循环引用的情况发生 避免头文件的使用者引入过多的无用的头文件(希望屏蔽底层的实现细节) 是否需要有 assertassert 只在 debug 的模式下才会生效,而 debug 模式并不会是生产模式下
2024-03-17
下一篇 
CMU 15-445 08-Index Concurrency CMU 15-445 08-Index Concurrency
Observation前面的操作中,都假设只有一个线程去操作数据结构,但实际操作中是有多个线程同时操作数据库的 因此需要研究多线程如何安全的执行查询 并且在实现线程安全的同时,也要注意如何优化磁盘IO 存在一些只支持单线程的数据库(比如
2023-02-12
  目录