先修:Makefile

好用的cmake学习资料:

现代cmake

重要概念:

  1. Project:
    project()命令用于定义一个项目的名称和版本,它设定了项目的上下文,并允许CMake进行版本检查等。项目是所有构建目标(如库、程序等)的容器。

  2. Target:
    target代表了构建过程中的一个输出实体,如可执行文件、库文件、模块等。libraryprogramtarget的两种常见类型。一个项目可以包含多个目标。add_executable(), add_library(), add_custom_target()等命令可添加目标。

  3. Program (可执行文件):
    程序通常是指通过add_executable()命令创建的可执行文件目标。它是由源代码文件编译并链接而成的,可以直接在操作系统上运行。

  4. Library (库):
    库是通过add_library()命令创建的,它可以是静态库(.a, .lib)或动态库(.so, .dll)。库包含可以被其他程序或库使用的代码和数据。

  5. Source (源代码):
    源代码是指用编程语言编写的文本文件,这些文件包含程序或库的指令。在CMake中,源代码文件通常被指定为add_executable()add_library()等命令的参数。CMake负责编译这些源代码文件来生成目标(如程序或库)。

  6. Package:
    它指的是一个可安装和可重用的软件组件,该组件可能包含库、可执行文件、头文件、配置文件等。CMake通过install()命令支持将构建的目标(如库和程序)以及其他文件安装到指定的目录结构中,从而创建可分发的软件包。

CMake和vcpkg的安装与使用

一些反复出现的命令:

1
2
3
4
5
6
7
8
9
10
include()                       #  直接插入另一个文件内容

find_package() # 查找并加载外部项目(如库、框架等)的配置文件
find_library() # 查找并设置指定名称的库文件的路径

add_library() # 用于添加一个库目标到项目中

target_sources() # 向目标(如库或可执行文件)添加源文件
target_link_libraries() # 用于指定一个目标(如可执行文件或库)应该链接哪些库
target_include_directories() # 用于向目标添加包含目录

根目录中的CMakeLists.txt文件

  • cmake_minimum_required(VERSION x.y.z):指定CMake的最低版本要求。
  • project(MyProject VERSION x.y.z):定义项目的名称和版本。
  • find_package():查找并加载外部项目(如依赖库)的配置文件。
  • add_subdirectory():添加子目录,CMake将递归地处理这些子目录中的CMakeLists.txt文件。
  • (可选)set()option():设置全局变量或选项。

子目录(如src)中的CMakeLists.txt文件

子目录中的CMakeLists.txt文件通常负责定义该子目录下的目标(如源文件、库、可执行文件)以及它们之间的依赖关系。

  • add_executable()add_library():定义可执行文件或库目标,并列出该目标的源文件。
  • target_sources():向已定义的目标添加额外的源文件。
  • target_include_directories():为目标指定包含目录,以便编译器能够找到这些目录下的头文件。
  • target_compile_definitions():为目标指定编译定义。
  • target_compile_options():为目标指定编译选项。
  • target_link_libraries():指定目标应该链接哪些库。
  • (可选)include():包含其他CMake文件,以重用配置或函数。

示例

**项目根目录的CMakeLists.txt**:

1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.10)
project(MyLargeProject VERSION 1.0)

# 查找并加载依赖库
find_package(Boost REQUIRED COMPONENTS filesystem)

# 添加子目录
add_subdirectory(src)

**src目录下的CMakeLists.txt**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 定义库
add_library(MyLib STATIC
MyLib.cpp
MyLibHelper.cpp
)

# 为库指定包含目录
target_include_directories(MyLib PRIVATE include)

# 定义可执行文件
add_executable(MyApp
main.cpp
)

# 链接库到可执行文件
target_link_libraries(MyApp PRIVATE MyLib)

# 如果MyLib还依赖于Boost
target_link_libraries(MyLib PRIVATE Boost::filesystem)

在这个例子中,根目录的CMakeLists.txt负责设置CMake版本、项目名称和版本,查找依赖库,并添加src子目录。而src目录下的CMakeLists.txt则定义了库MyLib和可执行文件MyApp,并指定了它们之间的依赖关系以及包含目录。

命令行调用

1
2
3
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel 4
cmake --build build --target install

命令行技巧

-D 选项:指定配置变量(又称缓存变量)

CMake 项目的构建分为两步:

  • cmake -B build 配置阶段(configure),检测环境并生成构建规则,在 build 目录下生成本地构建系统能识别的项目文件(Makefile 或 .sln)
  • cmake --build build,构建阶段(build),调用编译器来编译代码

在配置阶段可以通过 -D 设置缓存变量。
第二次配置时,之前的 -D 添加仍然会被保留。

  • cmake -B build -DCMAKE_INSTALL_PREFIX=/opt/openvdb-8.0
    设置安装路径为 /opt/openvdb-8.0(会安装到 /opt/openvdb-8.0/lib/libopenvdb.so)
  • cmake -B build -DCMAKE_BUILD_TYPE=Release
    设置构建模式为发布模式(开启全部优化)
  • cmake -B build 第二次配置时没有 -D 参数,但是之前的 -D 设置的变量都会被保留
    (此时缓存里仍有你之前定义的 CMAKE_BUILD_TYPE 和 CMAKE_INSTALL_PREFIX)

-G 选项:指定要用的生成器

  • Linux 系统上的 CMake 默认用是 Unix Makefiles 生成器;Windows 系统默认是 Visual Studio 2019 生成器;MacOS 系统默认是 Xcode 生成器。
  • 可以用 -G 参数改用别的生成器,例如 cmake -GNinja 会生成 Ninja 这个构建系统的构建规则。Ninja 是一个高性能,跨平台的构建系统,Linux、Windows、MacOS 上都可以用。
  • 而 Ninja 则是专为性能优化的构建系统,和 CMake 结合是行业标准。
  • 性能上:Ninja > Makefile > MSBuild

添加源文件

当源码在同一目录下的多文件中:

使用 GLOB 自动查找当前目录下指定扩展名的文件,实现批量添加源文件
启用 CONFIGURE_DEPENDS 选项,当添加新文件时,自动更新变量

1
2
3
add_executable(main)
file(GLOB sources CONFIGURE_DEPENDS *.cpp *.h)
target_sources(main PUBLIC ${sources})

当源码在子文件中:

1
2
3
4
add_executable(main)
aux_source_directory(. sources) # aux_source_directory 自动搜集需要的文件后缀名
aux_source_directory(mylib sources)
target_sources(main PUBLIC ${sources})
1
2
3
add_executable(main)
file(GLOB_RECURSE sources CONFIGURE_DEPENDS *.cpp *.h) # GLOB_RECURSE 自动包含所有子文件夹下的文件
target_sources(main PUBLIC ${sources})

GLOB_RECURSE 的问题:会把 build 目录里生成的临时 .cpp 文件也加进来

解决方案:把源码统一放到 src 目录下

项目配置变量

CMAKE_BUILD_TYPE 是 CMake 中一个特殊的变量,用于控制构建类型,他的值可以是:

  • Debug 调试模式,完全不优化,生成调试信息,方便调试程序
  • Release 发布模式,优化程度最高,性能最佳,但是编译比 Debug 慢
  • MinSizeRel 最小体积发布,生成的文件比 Release 更小,不完全优化,减少二进制体积
  • RelWithDebInfo 带调试信息发布,生成的文件比 Release 更大,因为带有调试的符号信息
  • 默认情况下 CMAKE_BUILD_TYPE 为空字符串,这时相当于 Debug。

标准模板:

1
2
3
if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()

链接库文件

main.cpp 调用 mylib.cpp 里的 say_hello 函数

mylib 作为一个静态库

1
2
3
add_library(mylib STATIC mylib.cpp)
add_executable(main main.cpp)
target_libraries(main PUBLIC mylib)

mylib 作为一个动态库

1
2
3
add_library(mylib SHARED mylib.cpp)
add_executable(main main.cpp)
target_libraries(main PUBLIC mylib)

mylib 作为一个对象库

1
2
3
add_library(mylib OBJECT mylib.cpp)
add_executable(main main.cpp)
target_libraries(main PUBLIC mylib)
  • 对象库类似于静态库,但不生成 .a 文件,只由 CMake 记住该库生成了哪些对象文件
  • 对象库是 CMake 自创的,绕开了编译器和操作系统的各种繁琐规则,保证了跨平台统一性。
  • 在自己的项目中,推荐全部用对象库(OBJECT)替代静态库(STATIC)避免跨平台的麻烦。
  • 对象库仅仅作为组织代码的方式,而实际生成的可执行文件只有一个,减轻了部署的困难。
  • 对象库可以绕开编译器的不统一:保证不会自动剔除没引用到的对象文件
  • 虽然动态库也可以避免剔除没引用的对象文件,但引入了运行时链接的麻烦

设置对象属性的三种方式

set_property设置单个属性

1
2
3
4
5
6
7
8
add_executable(main main.cpp)  
set_property(TARGET main PROPERTY CXX_STANDARD 17) # 采用C++17标准进行编译(默认11)
set_property(TARGET main PROPERTY CXX_STANDARD_REQUIRED ON) # 如果编译器不支持C++17,则直接报错(默认OFF)
set_property(TARGET main PROPERTY WIN32_EXECUTABLE ON) # 在Windows系统中,运行时不启动控制台窗口,只有GUI界面(默认OFF)
set_property(TARGET main PROPERTY LINK_WHAT_YOU_USE ON) # 告诉编译器不要自动剔除没有引用符号的链接库(默认OFF)
set_property(TARGET main PROPERTY LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) # 设置动态链接库的输出路径(默认${CMAKE_BINARY_DIR})
set_property(TARGET main PROPERTY ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib) # 设置静态链接库的输出路径(默认${CMAKE_BINARY_DIR})
set_property(TARGET main PROPERTY RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin) # 设置可执行文件的输出路径(默认${CMAKE_BINARY_DIR})

set_target_properties 设置多个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# CMakeLists.txt  

# 添加可执行文件目标,名为main,依赖于main.cpp文件
add_executable(main main.cpp)

# 设置main目标的属性
set_target_properties(main PROPERTIES
CXX_STANDARD 17 # 采用C++17标准进行编译(默认11)
CXX_STANDARD_REQUIRED ON # 如果编译器不支持C++17,则直接报错(默认OFF)
WIN32_EXECUTABLE ON # 在Windows系统中,运行时不启动控制台窗口,只有GUI界面(默认OFF)
LINK_WHAT_YOU_USE ON # 告诉编译器不要自动剔除没有引用符号的链接库(默认OFF)
)

# 设置动态链接库的输出路径
set(LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib)

# 设置静态链接库的输出路径
set(ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib)

# 设置可执行文件的输出路径
set(RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/bin)

set 通过全局变量,让之后创建的所有对象都享有同样的属性

要注意此时 set(CMAKE_xxx) 必须在 add_executable 之前才有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cmake_minimum_required(VERSION 3.x) # 注意:这里假设需要CMake的某个3.x版本,但具体版本号需要您根据需求填写  

# 设置C++标准为17
set(CMAKE_CXX_STANDARD 17)
# 如果编译器不支持C++17,则直接报错
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 在Windows系统中,运行时不启动控制台窗口,只有GUI界面
set(CMAKE_WIN32_EXECUTABLE ON)

# 告诉编译器不要自动剔除没有引用符号的链接库
set(CMAKE_LINK_WHAT_YOU_USE ON)

# 设置动态链接库的输出路径
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/lib")
# 注意:原文本中的Set(CMAKE_LIBRARY_OUTPUT_DIRECTORY${CMAKE_SOURCE_DIR}/lib)缺少了空格和引号,这里已修正

# 设置静态链接库的输出路径(这里似乎有一个拼写错误,应该是Lib而不是lib,但通常我们会保持一致)
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/lib")
# 注意:原文本中的Set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/Lib)缺少了空格和可能的拼写错误,这里已修正为常见的lib

# 设置可执行文件的输出路径
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/bin")

# 添加可执行文件目标
add_executable(main main.cpp)

Windows 链接 dll 找不到

  • 这是因为你的 dll 和 exe 不在同一目录。Windows 比较蠢,他只会找当前 exe 所在目录,然后查找 PATH,找不到就报错。而你的 dll 在其他目录,因此 Windows 会找不到 dll。
  • 解决1:把 dll 所在位置加到你的 PATH 环境变量里去,一劳永逸。
  • 解决2:把这个 dll,以及这个 dll 所依赖的其他 dll,全部拷贝到和 exe 文件同一目录下。

解决1:设置 mylib 对象的 xx_OUTPUT_DIRECTORY 系列属性

1
2
3
4
5
6
7
8
9
10
add_library(mylib SHARED mylib.cpp mylib.h)  
set_property(TARGET mylib PROPERTY RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY RUNTIME_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY ARCHIVE_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY LIBRARY_OUTPUT_DIRECTORY_DEBUG ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY RUNTIME_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR})
set_property(TARGET mylib PROPERTY LIBRARY_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR})

链接第三方库

find_package

find_package(TBB REQUIRED)find_package(TBB CONFIG REQUIRED) 区别

  • find_package(TBB REQUIRED) 会查找 /usr/lib/cmake/TBB/TBBConfig.cmake 这个配置文件,并根据里面的配置信息创建 TBB::tbb 这个伪对象(他实际指向真正的 tbb 库文件路径 /usr/lib/libtbb.so),之后通过 target_link_libraries 链接 TBB::tbb 就可以正常工作了。
  • 其实更好的是通过 find_package(TBB CONFIG REQUIRED),添加了一个 CONFIG 选项。这样他会优先查找 TBBConfig.cmake(系统自带的)而不是 FindTBB.cmake(项目作者常把他塞在 cmake/ 目录里并添加到 CMAKE_MODULE_PATH)。这样能保证寻找包的这个 .cmake 脚本是和系统自带的 tbb 版本是适配的,而不是项目作者当年下载的那个版本的 .cmake 脚本。
  • 当然,如果你坚持要用 find_package(TBB REQUIRED) 也是可以的。
  • 没有 CONFIG 选项:先找 FindTBB.cmake,再找 TBBConfig.cmake,找不到则报错
  • 有 CONFIG 选项:只会找 TBBConfig.cmake,找不到则报错
    此外,一些老年项目(例如 OpenVDB)只提供 Find 而没有 Config 文件,这时候就必须用 find_package(OpenVDB REQUIRED) 而不能带 CONFIG 选项。

组件

find_package 生成的伪对象(imported target)都按照“包名::组件名”的格式命名。
你可以在 find_package 中通过 COMPONENTS 选项,后面跟随一个列表表示需要用的组件。

示例:

1
2
find_package(Qt5 COMPONENTS Widgets Gui REQUIRED)
target_link_libraries(main PUBLIC Qt5::Widgets Qt5::Gui)

输出与变量

message(STATUS “…”) 表示信息类型是状态信息,有 – 前缀
message(WARNING “…”) 表示是警告信息
message(AUTHOR_WARNING “…”) 表示是仅仅给项目作者看的警告信息
AUTHOR_WARNING 的不同之处:可以通过 -Wno-dev 关闭
message(FATAL_ERROR “…”) 表示是错误信息,会终止 CMake 的运行
message(SEND_ERROR “…”) 表示是错误信息,但之后的语句仍继续执行

变量与缓存

清除缓存,其实只需删除 build/CMakeCache.txt 就可以了
设置缓存变量
语法是:set(变量名 “变量值” CACHE 变量类型 “注释”)
更新缓存变量的正确方法,是通过命令行参数:cmake -B build -Dmyvar=world
缓存变量更新:删 build
set 可以在后面加一个 FORCE 选项,表示不论缓存是否存在,都强制更新缓存。
不过这样会导致没办法用 -Dmyvar=othervalue 来更新缓存变量。