logo头像
Snippet 博客主题

CMake进阶

CMake是什么

CMake是一种跨平台的自动化构建工具,可以生成跨平台的构建文件。比如Makefile、Visual Studio、XCode解决方案。使用CMake可以使得编译和构建过程更加简单。
CMake使用CMakeLists.txt文件描述项目的构建过程,通过一系列的命令来描述如何编译、链接和打包源代码。

CMake的优点

  • 跨平台支持:CMake可以在windows、linux、macos等操作系统使用,并且可以支持多种编译器和构建工具。
  • 灵活的构建过程:CMake的构建过程非常灵活,可以自定义编译选项、链接库等。
  • 可拓展性:CMake可以使用自定义的模块拓展功能,可以方便的添加新的构建规则和命令。
  • 高效的构建:支持多线程构建,可以加快构建速度。

语法

设置变量

可以使用set命令设置变量参数,变量可以包含字符串和列表,命令是不区分大小写的

1
2
3
4
# 设置变量project为test
set(project "test")
# 设置projects位列表
set(projects test1 test2)

引用变量

使用${}的方式引用定义的变量内容

1
2
# 下面语句输出test
message(STATUS ${project})

条件语句

cmake中的条件语句可以使用if、elseif、endif

1
2
3
4
5
6
7
8
9
10
# 下面代码根据CMAKE_SYSTEM_NAME判断不同的平台环境
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
# do something for Windows
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
# do something for macOS
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
# do something for Linux
else()
message(FATAL_ERROR "Unsupported system: ${CMAKE_SYSTEM_NAME}")
endif()

循环语句

cmake中循环语句包含while,foreach/endforeach

函数和宏定义

cmake中使用function来定义函数,使用macro来定义宏。
函数和宏都可以用来封装一些操作或者功能,并且在需要的时候调用他们。存在以下区别:

  • 参数传递方式不同:函数的参数是以变量的形式传递的,而宏的参数则是以文本替换的方式传递的。
  • 变量作用域不同:函数中定义的变量只能在函数内部使用,而宏中定义的变量则可以在宏的调用位置和宏内部使用。
  • 函数返回值类型只能为字符串:在CMake中,函数的返回值只能是字符串类型,而宏没有返回值。
  • 函数可以使用return命令返回值:函数中可以使用return命令来返回一个字符串值,而宏没有此功能。
  • 宏可以在其调用位置使用命令:宏可以在其调用位置执行一些命令,而函数则不能。
1
2
3
4
5
6
7
8
function(func)

endfunction()


macro(func2)

endmacro()

注释

在cmake中行注释以 # 字符开始,直到行结束 。
块注释以 #[[ 开始 , 以 #]] 结束。

常用内置变量

CMAKE_SOURCE_DIR

项目源码根目录的绝对路径。

CMAKE_BINARY_DIR

CMake生成的Makefile和可执行文件等二进制文件所在目录的绝对路径。

CMAKE_CURRENT_SOURCE_DIR

当前处理的CMakeLists.txt所在的目录的绝对路径。

CMAKE_CURRENT_BINARY_DIR

当前处理的CMakeLists.txt生成的目标文件所在的目录的绝对路径。

CMAKE_INSTALL_PREFIX

安装目录的根目录。

CMAKE_CXX_COMPILER

C++编译器的完整路径。

CMAKE_C_COMPILER

C编译器的完整路径。

CMAKE_BUILD_TYPE

构建类型,通常为Debug或Release。

CMAKE_LIBRARY_OUTPUT_DIRECTORY

生成的动态库库文件输出目录。

CMAKE_RUNTIME_OUTPUT_DIRECTORY

生成的可执行文件输出目录。

CMAKE_ARCHIVE_OUTPUT_DIRECTORY

生成的静态链接库文件的输出目录

CMAKE_MODULE_PATH

指定一组目录列表,用于cmake在构建过程中去查找模块或者脚本,这些模块或者脚本可以提供一些常用的功能,用于拓展cmake的功能。

常用命令

add_executable

add_executable用来生成一个可执行文件的目标

1
add_executable(target_name source_file1 [source_file2 ...])

其中,target_name是要生成的可执行文件的名称,source_file1、source_file2等是用于生成该可执行文件的源文件。
例如,如果我们有两个源文件main.cpp和helper.cpp,并且希望生成一个名为my_program的可执行文件,则可以在CMakeLists.txt文件中使用如下命令:

1
add_executable(my_program main.cpp helper.cpp)

add_library

add_library命令用来创建一个链接库目标。语法格式如下:

1
add_library(target_name [STATIC | SHARED | MODULE] source1 [source2 ...])

target_name为要创建的目标的名称。STATIC表示生成静态链接库,SHARED参数表示生成动态链接库,MODULE表示动态加载模块

在C++开发中,我们一般需要告诉编辑器头文件和库文件的搜索路径(1. C/C++->附加包含目录 2. 链接器->常规->附加库目录和输入->附加依赖项)。对应cmake中就是下面这两个命令:[target_]include_directories,[target_]link_directories,注意带target_前缀的添加搜索路径只是针对当前目标库添加,而不带的是全局添加,后面所有库都会附加。

include_directories

include_directories用于向编译器添加一个或多个头文件搜索路径。该命令将指定的路径添加到编译器的搜索路径中,以便编译器可以找到指定路径中的头文件。命令语法如下:

1
include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])

link_directories用于向链接器添加一个或多个库文件搜索路径。该命令将指定的路径添加到链接器的搜索路径中,以便链接器可以找到指定路径中的库文件

add_definitions

add_definitions用于向编译器添加定义,它可以将一个或者多个定义添加到所有源文件的编译器命令中,影响编译器的行为。

1
add_definitions(-DFOO -DBAR ...)

需要注意的是上面三个命令会影响所有的构建项目,包括子项目。如果想要只影响指定的项目,使用target前缀相关的命令。

target_include_directories

target_include_directories用于为一个目标添加头文件的搜索路径,可以添加一个或者多个目录,从而使编译器能找到头文件。

1
2
3
target_include_directories(<target> [SYSTEM] [AFTER|BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

target_link_libraries用于为一个目标文件添加链接库。它可以向一个目标文件的链接器命令添加一个或者多个链接库。从而使编译器能够正确的链接到指定库。语法如下:

1
2
3
4
5
6
7
target_link_libraries(<target>
<PRIVATE|PUBLIC|INTERFACE> item1 item2 ...
[LINK_PRIVATE|LINK_PUBLIC|LINK_INTERFACE]
[LINK_OPTIONS|INTERFACE_LINK_OPTIONS options...]
[LINK_LIBRARIES|INTERFACE_LINK_LIBRARIES item1 item2 ...]
)

target_compile_definitions

target_compile_definitions用于为一个指定目标文件添加编译器定义。

1
2
3
4
5
6
target_compile_definitions(<target>
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])

target_compile_definitions(foo PUBLIC -DWIN32)
target_compile_definitions(foo PUBLIC -DFOO=1)

以上三个target相关的命令都包含了PRIVATE、PUBLIC、INTERFACE三个不同的访问控制级别。相关含义如下:

  • PRIVATE 表示为当前target设置的头文件路径、链接库和定义只对当前target可见(不具有传染性)。
  • PUBLIC 表示为当前target设置的相关属性可以被其他引用了该target的其他目标文件可见(具有传染性)。

include

用于在CMAKE_MODULE_PATH中加载指定的脚本或者模块文件,语法如下

1
include(filename)

cmake文件引入成功之后就可以使用文件中定义的函数、变量、宏等

find_package

用于在系统中查找已安装的库,并将其导入到CMake项目中。它通常用于在项目中引入第三方库。
find_package的语法如下:

1
find_package(<package_name> [version] [EXACT] [QUIET] [MODULE] [REQUIRED] [COMPONENTS   ...])

其中,是要查找的库的名称,version指定了所需的版本,EXACT表示查找确切版本,QUIET表示不向用户输出查找过程,MODULE表示只查找CMake模块,REQUIRED表示库是必需的,如果找不到则会出现错误。find_package还可以指定库的组件,通过COMPONENTS选项指定。比如Qt库中就包含组件Core、Gui等。可以按照项目实际需求指定库的组件。
当find_package找到库时,会在CMAKE变量中设置一些有用的变量。比如

  • _FOUND:表示库是否已经找到。
  • _INCLUDE_DIRS:表示库头文件所在目录的列表。
  • _LIBRARIES:表示包含的链接库列表
    1
    2
    3
    4
    5
    6
    7
    find_package(Boost 1.72.0 REQUIRED COMPONENTS filesystem system)

    if(Boost_FOUND)
    include_directories(${Boost_INCLUDE_DIRS})
    add_executable(myapp main.cpp)
    target_link_libraries(myapp ${Boost_LIBRARIES})
    endif()
    find_package有两种搜索模式:
  • 模块模式(Module mode)
    在该模式下,cmake会搜索一个名为Find.cmake的文件。搜索路径的顺序依次是CMAKE_MODULE_PATH指定的目录、cmake安装目录中查找。如果找到了相应的文件,那么就会读取并处理该文件。
  • 配置模式(Config mode)
    该模式下,CMake会搜索-config.cmake文件或Config.cmake文件,我很少用。

帮助文档与示例

上面只是介绍了一些cmake常用、基本的操作。还有很多特性、命令我们都可以通过cmd下cmake --help去查询了解。

最后是一个正式项目的示例:这是一个windows平台的QT项目,通过vscode使用cmake + ninja + msvc构建、编译。

注:ninja与msvc(window平台编译器)关系类似make与gcc,前者是项目构建工具,后者是源码编译器。可以简单这么理解这个流程,cmake通过解析CMakeLists.txt构建工程,而解析操作则是由ninja完成的,最终编译代码则是用msvc完成。

ninja是谷歌的一个高效的构建工具,一般比vsstudio的快不少。

vscode环境搭建步骤:

  1. 安装QT4.8.7、vs2010、cmake(3.24.1)、vscode的cmake插件。

  2. 配置示例工程根目录的ninja到环境变量。

  3. Ctrl+Shift+p依次执行命令:

    a. CMake:Configure,只需要执行一次,第一次会提示选择编译器(VisualStudio.10.0-x86)。

    b. Cmake:Build,如果修改了代码重新Build不生效,可以先CMake:Clean,再CMake:Build。

    c. 编译完就可以F5调试运行了。

示例下载