背景
CMake是一个跨平台的构建系统,它能自动生成各种平台和编译器的构建文件,这对于C++开发人员来说是必须掌握使用的工具。CMake的特点包括:
-
跨平台构建:CMake支持多种操作系统,包括Windows、Linux、macOS等。学会使用CMake可以让你轻松地为不同平台生成构建文件,提高项目的可移植性。
-
编译器和构建工具的独立性:CMake可以生成各种编译器和构建工具的项目文件,例如Visual Studio、Xcode、Makefile等。这意味着你的项目可以在多种开发环境中使用,而无需为每个环境编写特定的构建脚本。
-
简化构建过程:CMake可以帮助你管理复杂项目的构建过程。它可以自动检测依赖关系、生成目标文件、编译静态库和动态库等。通过CMake配置文件(CMakeLists.txt),你可以灵活地控制整个构建过程,减轻手动管理的负担。
-
便于协作开发:使用CMake使得项目的构建方式更加标准化,有利于多人协作开发。团队成员无需花费大量时间配置开发环境,只需遵循CMakeLists.txt的规则,即可轻松地构建和运行项目。
-
开源生态系统:CMake是一个开源项目,有庞大的社区支持。这意味着你可以找到大量的教程、示例项目和技巧来学习和解决问题。此外,许多开源库已经采用CMake作为构建系统,因此学习CMake可以让你更方便地集成和使用这些库。
学习CMake将使你能够更高效地管理C++项目的构建过程,提高项目的可移植性和协作效率。
关键知识
首先,我们需要弄清楚两个两个概念,以免在描述中发生混淆:
-
在命令行中输入的 CMake 命令通常称为CMake 命令行参数(CMake command-line arguments)或CMake 命令行选项(CMake command-line options)。这些参数或选项用于指定生成的构建系统、目标架构、构建类型等。它们控制 CMake 的行为,告诉 CMake 如何处理项目。
-
编写 CMakeLists.txt 文件时使用的语法称为CMake 语法(CMake syntax)或CMake 脚本语言(CMake scripting language)。CMake 语法用于编写 CMakeLists.txt 文件,指导 CMake 如何为项目生成构建系统。CMake 脚本语言包括指令、变量、函数、宏、控制结构等,用于组织和控制项目的构建过程。
CMakeLists.txt 是用于编写 CMake 构建脚本的文件。下面需要重点讲述CMake语法,其语法主要由以下几个部分组成:
- 注释
使用井号(#)开头的行是注释行,会被 CMake 忽略。
# 这是一个注释
- 变量
在 CMake 中,你可以使用set()
命令定义变量:
set(VARIABLE_NAME value)
读取变量的值时,使用 ${VARIABLE_NAME}
进行引用:
set(SOURCE_FILES main.cpp)
message("Source files: ${SOURCE_FILES}") # 输出:Source files: main.cpp
- 控制结构
CMake 提供了类似于其他编程语言的控制结构,如条件语句、循环语句等。
- 条件语句:
if(CONDITION)
# ...
elseif(OTHER_CONDITION)
# ...
else()
# ...
endif()
- 循环语句:
foreach(item IN LISTS some_list)
# ...
endforeach()
- 函数和宏
你可以定义自己的函数和宏,它们有类似的语法:
- 函数:
function(FUNCTION_NAME arg1 arg2)
# ...
endfunction()
- 宏:
macro(MACRO_NAME arg1 arg2)
# ...
endmacro()
- 常用命令
以下是一些常用的 CMake 命令:
-
project()
: 定义项目名称和版本。 -
cmake_minimum_required()
: 指定 CMake 的最低版本要求。 -
add_executable()
: 生成可执行文件。 -
add_library()
: 生成库文件。 -
target_link_libraries()
: 链接库文件。 -
include_directories()
: 添加头文件目录。 -
find_package()
: 寻找并加载外部库。 -
install()
: 定义安装规则。
这仅是 CMake 语法的简要概述,CMake 提供了丰富的功能和命令,具体内容可以参考官方文档:CMake官方文档。不过笼统的概述相信并不能让读者掌握CMake的使用,不用担心,接下来,我们会给出一个CMakeList的编写例子,让读者对CMake语法有初步的认识。在这之前,我们先给出项目的目录结构。
项目目录结构
MyApp/
├─ CMakeLists.txt
├─ src/
│ └─ main.cpp
├─ include/
│ ├─ static_lib/
│ │ └─ StaticLibHeader.h
│ └─ dynamic_lib/
│ └─ DynamicLibHeader.h
├─ libs/
│ ├─ static/
│ │ └─ libStatic.lib
│ └─ dynamic/
│ ├─ libDynamic.dll
│ └─ libDynamic.lib
├─ subproject/
│ ├─ CMakeLists.txt
│ ├─ src/
│ │ └─ subproject_main.cpp
│ └─ include/
│ └─ subproject/
│ └─ SubProjectHeader.h
└─ config.h.in
这个目录结构包含了以下组成部分:
-
根目录:包含主项目的
CMakeLists.txt
文件以及用于在构建时生成配置文件的config.h.in
文件。 -
src:存放项目源代码的目录,这里只有一个
main.cpp
文件作为示例。 -
include:包含头文件的目录。这里有两个子目录,一个是
static_lib
,包含静态库的头文件StaticLibHeader.h
;另一个是dynamic_lib
,包含动态库的头文件DynamicLibHeader.h
。 -
libs:存放静态库和动态库的目录。这里有两个子目录:
static
和dynamic
。static
目录中包含一个名为libStatic.lib
的静态库,dynamic
目录中包含一个名为libDynamic.dll
的动态库以及其导入库libDynamic.lib
。 -
subproject:一个子项目的目录,包含自己的
CMakeLists.txt
文件、源代码(subproject_main.cpp
)以及头文件(SubProjectHeader.h
)。
CMakeList脚本示例
# 设置 CMake 最低版本要求
cmake_minimum_required(VERSION 3.8)
# 定义项目名称和版本
project(MyApp VERSION 1.0.0 LANGUAGES CXX)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 定义用户可配置的选项
option(ENABLE_DEBUG "Enable debug output" ON)
if(ENABLE_DEBUG)
add_definitions(-DDEBUG_OUTPUT)
endif()
# 自定义宏:添加 MSVC 常用编译选项
macro(add_msvc_options target)
if(MSVC)
target_compile_options(${target} PRIVATE
/W4 # 设置警告级别为 4
/WX # 将警告视为错误
/MP # 启用多处理器编译
/permissive- # 禁用不严格的语言 conformance
/Zc:__cplusplus # 启用正确的 __cplusplus 宏值
/Zc:inline # 移除未使用的函数
/Gm- # 禁用最小生成(minimal rebuild)
/EHsc # 指定异常处理模型
)
endif()
endmacro()
# 添加源文件
set(SOURCE_FILES src/main.cpp)
# 生成可执行文件
add_executable(MyApp ${SOURCE_FILES})
# 调用自定义宏,为 MyApp 添加 MSVC 常用编译选项
add_msvc_options(MyApp)
# 为特定目标设置头文件目录
target_include_directories(MyApp PRIVATE include)
# 链接静态库
find_library(STATIC_LIB libStatic.lib PATHS "${CMAKE_SOURCE_DIR}/libs/static")
target_link_libraries(MyApp PRIVATE ${STATIC_LIB})
# 链接动态库
find_library(DYNAMIC_LIB libDynamic.dll PATHS "${CMAKE_SOURCE_DIR}/libs/dynamic")
find_library(DYNAMIC_LIB_IMPORT libDynamic.lib PATHS "${CMAKE_SOURCE_DIR}/libs/dynamic")
target_link_libraries(MyApp PRIVATE ${DYNAMIC_LIB_IMPORT})
# 使用 Windows 的 DLL delay-load 机制
set_target_properties(MyApp PROPERTIES LINK_FLAGS "/DELAYLOAD:libDynamic.dll")
# 根据目标架构定制编译选项和链接选项
if(CMAKE_GENERATOR_PLATFORM STREQUAL "Win32")
message("Building for Win32 (x86) architecture")
target_compile_options(MyApp PRIVATE /arch:SSE2)
elseif(CMAKE_GENERATOR_PLATFORM STREQUAL "x64")
message("Building for x64 architecture")
target_compile_options(MyApp PRIVATE /arch:AVX2)
else()
message(WARNING "Unknown architecture")
endif()
# 添加子项目
add_subdirectory(subproject)
# 在构建时生成配置文件
configure_file(config.h.in config.h @ONLY)
# 指定安装规则
install(TARGETS MyApp RUNTIME DESTINATION bin)
install(FILES "${CMAKE_SOURCE_DIR}/libs/dynamic/libDynamic.dll" DESTINATION bin)
接下来,需要在命令行中运行以下命令生成 Visual Studio 工程。首先,从项目的根目录创建一个新的目录,例如 build
,用于存放构建文件。接着,根据目标架构使用 -A
参数运行 CMake 命令:
- 对于 x86 架构:
cmake -G "Visual Studio 16 2019" -A Win32 ..
- 对于 x64 架构:
cmake -G "Visual Studio 16 2019" -A x64 ..
运行之后,应能在 build
目录中看到生成的 Visual Studio 工程文件。可以打开它并使用 Visual Studio 进行构建。
示例解析
- 设置 CMake 最低版本要求
# 设置 CMake 最低版本要求
cmake_minimum_required(VERSION 3.8)
cmake_minimum_required
指令用于指定项目所需的最低 CMake 版本。这个指令确保当前环境中的 CMake 版本满足项目的构建要求。如果 CMake 的版本低于指定的最低版本,CMake 会报错并终止构建过程。这个指令的主要作用是确保项目所使用的 CMake 功能和语法与当前 CMake 版本兼容。随着 CMake 的发展,有时会引入新功能、改进现有功能或者废弃旧功能。如果用户尝试使用 CMake 3.8 以下的版本来构建项目,将会收到错误消息。
# 定义项目名称和版本
project(MyApp VERSION 1.0.0 LANGUAGES CXX)
project
指令先是定义了项目名称,再使用VERSION
关键字并跟随具体版本号1.0.0,来指定当前项目版本,此处很好理解。而LANGUAGES
关键字以及后面的参数值CXX
,则代表C++语言,CMake 会根据当前操作系统、可用编译器和指定的编程语言自动选择合适的编译器。CMake 支持多种编译器,如 GCC、Clang、MSVC等。CMake 在选择编译器时会遵循一定的规则和优先级,匹配规则包括:
Unix-like 系统(如 Linux 和 macOS):对于 C++ 代码,默认情况下,CMake 会首先尝试使用系统上可用的 GCC 编译器。如果没有找到 GCC,CMake 会继续尝试查找其他可用的编译器,如 Clang。可以通过设置 CMAKE_CXX_COMPILER
变量来手动指定编译器。
Windows 系统:对于 C++ 代码,默认情况下,CMake 会尝试使用 MSVC作为编译器。如果没有找到MSVC,CMake 会继续尝试查找其他可用的编译器,如 MinGW 或 Clang。与 Unix-like 系统类似,也可以通过设置 CMAKE_CXX_COMPILER
变量来手动指定编译器。
set(CMAKE_CXX_COMPILER "/path/to/your/compiler")
某些情况下,CMake 可能无法自动检测到合适的编译器,或者需要使用特定版本的编译器,可以通过设置CMAKE_CXX_COMPILER
变量来实现。
- 设置 C++ 标准
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
这三行 CMake 指令用于设置 C++ 项目的编译选项。
set(CMAKE_CXX_STANDARD 17)
:这一行指令设置了项目使用的 C++ 标准版本。在这个例子中,我们选择了 C++17 标准。CMake 支持设置多种 C++ 标准版本,如 C++11、C++14、C++17、C++20 等。可以根据项目的需求,选择合适的 C++ 标准版本。
set(CMAKE_CXX_STANDARD_REQUIRED ON)
:这一行指令表示,如果编译器不支持指定的 C++ 标准,CMake 将报错并终止构建过程。如果将此选项设置为 OFF,则 CMake 会尽量使用所选 C++ 标准版本进行编译,但如果编译器不支持该版本,CMake 会自动降级到编译器支持的最接近的 C++ 标准版本。
set(CMAKE_CXX_EXTENSIONS OFF)
:这一行指令用于禁用编译器特定的 C++ 语言扩展。将此选项设置为 OFF 可确保项目遵循 C++ 标准,并具有更好的可移植性。如果将此选项设置为 ON,则 CMake 允许编译器使用其特定的 C++ 语言扩展,这可能导致项目在不同编译器之间的行为不一致。
- 定义用户可配置的选项
# 定义用户可配置的选项
option(ENABLE_DEBUG "Enable debug output" ON)
if(ENABLE_DEBUG)
add_definitions(-DDEBUG_OUTPUT)
endif()
option(ENABLE_DEBUG "Enable debug output" ON)
:此命令定义了一个名为 ENABLE_DEBUG
的用户可配置选项。option()
命令用于定义一个布尔型变量,可以在 CMake 生成构建系统时进行配置。命令的第二个参数是对可配置选项的描述。这个描述可以帮助其他开发者或用户理解这个选项的用途。当使用 CMake GUI 工具时,这个描述将作为提示显示在选项旁边。这个描述在命令行模式下不会出现。在这个例子中,ENABLE_DEBUG
的默认值为 ON
。用户可以通过 CMake 命令行参数或 GUI 工具来改变这个选项的值。
cmake -D ENABLE_DEBUG=OFF ..
if(ENABLE_DEBUG)
和 endif()
:这两个命令定义了一个条件语句。如果 ENABLE_DEBUG
选项为 ON
,则条件为真,执行语句块中的命令。否则,不执行这些命令。
add_definitions(-DDEBUG_OUTPUT)
:此命令仅在 ENABLE_DEBUG
为 ON
时执行。add_definitions()
命令用于添加编译器定义。在这个例子中,-DDEBUG_OUTPUT
添加了一个名为 DEBUG_OUTPUT
的预处理器宏定义。这个宏定义可以在源代码中使用,以便根据其值启用或禁用调试输出功能。例如,在 C++ 代码中,可以使用 #ifdef DEBUG_OUTPUT
和 #endif
来包裹调试输出相关的代码。
#ifdef DEBUG_OUTPUT
//只在debug模式下运行的逻辑
std::cout << "debug mode" << std::endl;
#endif
- 自定义宏:添加 MSVC 常用编译选项
# 自定义宏:添加 MSVC 常用编译选项
macro(add_msvc_options target)
if(MSVC)
target_compile_options(${target} PRIVATE
/W4 # 设置警告级别为 4
/WX # 将警告视为错误
/MP # 启用多处理器编译
/permissive- # 禁用不严格的语言 conformance
/Zc:__cplusplus # 启用正确的 __cplusplus 宏值
/Zc:inline # 移除未使用的函数
/Gm- # 禁用最小生成(minimal rebuild)
/EHsc # 指定异常处理模型
)
endif()
endmacro()
macro(add_msvc_options target)
定义了一个名为 add_msvc_options
的宏。在 CMake 中,宏用于封装一组命令,以便在多个地方重复使用。而add_msvc_options
宏接收一个参数 target
。在宏内部,${target}
会被替换为实际传递的目标名称。
target_compile_options()
命令用于为特定的目标(如可执行文件或库)添加编译选项。其中的${target}
是一个变量,表示调用宏时传递的项目名。而PRIVATE
关键字表示这些编译选项只对当前目标生效。在这个例子中,编译选项仅影响 ${target}
的构建。如果有其他目标依赖于 ${target}
,这些编译选项不会传递给那些依赖目标。
- 链接静态库和动态库
# 链接静态库
find_library(STATIC_LIB libStatic.lib PATHS "${CMAKE_SOURCE_DIR}/libs/static")
target_link_libraries(MyApp PRIVATE ${STATIC_LIB})
# 链接动态库
find_library(DYNAMIC_LIB libDynamic.dll PATHS "${CMAKE_SOURCE_DIR}/libs/dynamic")
find_library(DYNAMIC_LIB_IMPORT libDynamic.lib PATHS "${CMAKE_SOURCE_DIR}/libs/dynamic")
target_link_libraries(MyApp PRIVATE ${DYNAMIC_LIB_IMPORT})
find_library()
命令用于在指定的路径中查找库文件。在这个例子中链接静态库的部分,它查找名为 libStatic.lib
的静态库,并将其路径存储在变量 STATIC_LIB
中。PATHS
参数用于指定搜索库文件的目录,这里设置为 "${CMAKE_SOURCE_DIR}/libs/static"
。
target_link_libraries()
命令用于将库链接到指定的目标。这里将 STATIC_LIB
链接到 MyApp
目标。PRIVATE
关键字表示这个库仅对当前目标(MyApp
)可见,不会传递给其他依赖于 MyApp
的目标。
至于在链接动态库时,为什么使用 ${DYNAMIC_LIB_IMPORT}
而不是 ${DYNAMIC_LIB}
,原因是在 Windows 平台上,当链接到动态库时,需要链接到相应的导入库(.lib
文件),而不是直接链接到动态库(.dll
文件)。导入库包含了调用动态库函数所需的信息,编译器和链接器需要这些信息来正确生成可执行文件。运行时,可执行文件会自动加载相应的 .dll
文件。
- 修改链接标志,使用延迟加载机制
# 使用 Windows 的 DLL delay-load 机制
set_target_properties(MyApp PROPERTIES LINK_FLAGS "/DELAYLOAD:libDynamic.dll")
这个命令是用于设置目标(在这个例子中是 MyApp
)的属性。set_target_properties()
命令允许你修改一个目标的一些属性,例如链接标志、输出名称等。在这个例子中,我们修改了 MyApp
的链接标志。具体来说,LINK_FLAGS
属性表示要传递给链接器的标志。在这里,我们将 /DELAYLOAD:libDynamic.dll
添加到链接标志中。这个标志用于指示链接器启用 DLL 延迟加载机制。
延迟加载机制允许程序在运行时按需加载 DLL,而不是在程序启动时立即加载。这可以降低程序启动时间,并且在某些情况下可以避免因缺少 DLL 导致的程序启动失败。当程序首次调用 DLL 中的函数时,系统会自动加载 DLL。/DELAYLOAD:libDynamic.dll
标志告诉链接器,我们希望在运行时延迟加载 libDynamic.dll
。当程序需要使用 libDynamic.dll
中的函数时,才会加载这个 DLL。
- 根据目标架构定制编译选项和链接选项
# 根据目标架构定制编译选项和链接选项
if(CMAKE_GENERATOR_PLATFORM STREQUAL "Win32")
message("Building for Win32 (x86) architecture")
target_compile_options(MyApp PRIVATE /arch:SSE2)
elseif(CMAKE_GENERATOR_PLATFORM STREQUAL "x64")
message("Building for x64 architecture")
target_compile_options(MyApp PRIVATE /arch:AVX2)
else()
message(WARNING "Unknown architecture")
endif()
这段代码的目的是根据目标架构定制编译选项和链接选项。具体来说,它根据 CMAKE_GENERATOR_PLATFORM
变量的值来判断目标架构,并针对不同架构设置不同的编译选项。记得我们在输入cmake命令行时的-A
参数吗?这里即是设置了这个变量的值。
cmake -G "Visual Studio 16 2019" -A Win32 ..
if(CMAKE_GENERATOR_PLATFORM STREQUAL "Win32")
这个条件检查 CMAKE_GENERATOR_PLATFORM
变量是否等于 "Win32"。如果是,说明我们正在为 Win32 (x86) 架构构建项目。target_compile_options(MyApp PRIVATE /arch:SSE2)
告诉编译器为 x86 架构使用 SSE2 指令集。
cmake -G "Visual Studio 16 2019" -A x64 ..
若通过上面的CMake命令构建x64任务,则会走到elseif分支中,其中target_compile_options(MyApp PRIVATE /arch:AVX2)
命令告诉编译器为 x64 架构使用 AVX2 指令集。
- 添加子项目
# 添加子项目
add_subdirectory(subproject)
使用 add_subdirectory
指令时,CMake 会在指定的子目录中查找 CMakeLists.txt
文件,并执行其中的命令,让 CMake 构建系统继续构建子项目。这使得你可以将一个大型项目分解为多个较小的子项目,从而使项目的组织结构更加清晰。
在我们的示例中,这个命令将导致 CMake 在构建过程中进入 subproject
目录,并执行 subproject/CMakeLists.txt
文件中的命令。子项目可以包含它自己的源文件、库、可执行文件等,并可以与主项目共享变量、目标和属性。
- 生成配置文件
# 在构建时生成配置文件
configure_file(config.h.in config.h @ONLY)
configure_file(config.h.in config.h @ONLY)
命令的实际意义在于根据模板文件 config.h.in
生成配置文件 config.h
。在这个过程中,CMake 会将模板文件中的一些变量替换为其实际值。这对于生成项目中使用的配置文件非常有用,特别是当这些配置文件需要根据当前构建环境的某些属性进行调整时。
configure_file
命令的参数@ONLY
表示只替换 @VARIABLE_NAME@
形式的占位符。如果不使用这个选项,CMake 会尝试替换 ${VARIABLE_NAME}
形式的占位符。
举个例子,假设你有一个项目,其中的一些功能取决于编译时的选项。你可以使用 option()
命令定义这些选项,并使用 configure_file()
命令将这些选项的值写入一个配置文件。然后,在源代码中,你可以包含这个配置文件并根据选项值来启用或禁用某些功能。
config.h.in
文件内容示例:
#define ENABLE_FEATURE_X @ENABLE_FEATURE_X@
在 CMakeLists.txt
文件中,你可以使用以下命令为 ENABLE_FEATURE_X
定义一个可配置选项,并生成 config.h
文件:
option(ENABLE_FEATURE_X "Enable feature X" ON)
configure_file(config.h.in config.h @ONLY)
在这个例子中,当 ENABLE_FEATURE_X
选项被设置为 ON
时,生成的 config.h
文件将包含 #define ENABLE_FEATURE_X 1
。当选项设置为 OFF
时,config.h
文件将包含 #define ENABLE_FEATURE_X 0
。这样,源代码中就可以根据 ENABLE_FEATURE_X
的值来启用或禁用相关功能。
- 指定安装规则
# 指定安装规则
install(TARGETS MyApp RUNTIME DESTINATION bin)
install(FILES "${CMAKE_SOURCE_DIR}/libs/dynamic/libDynamic.dll" DESTINATION bin)
install
命令用于指定安装规则,它定义了在构建完成后如何将目标文件(可执行文件、库等)以及其他相关文件(如动态库、配置文件等)安装到指定的目录。这对于将构建好的项目打包成安装包或将项目部署到目标系统上非常有用。在我们的例子中,这两条 install
命令分别指定了将 MyApp
可执行文件和 libDynamic.dll
动态库安装到 bin
目录下。
以下是这两条 install
命令的详细解释:
-
install(TARGETS MyApp RUNTIME DESTINATION bin)
-
TARGETS MyApp
:指定要安装的目标,这里是MyApp
可执行文件。 -
RUNTIME
:表示我们要安装可执行文件的运行时组件。对于可执行文件,这通常指的就是可执行文件本身。 -
DESTINATION bin
:指定安装目标的目录。这里,MyApp
可执行文件将被安装到bin
目录下。
-
-
install(FILES "${CMAKE_SOURCE_DIR}/libs/dynamic/libDynamic.dll" DESTINATION bin)
-
FILES
:表示我们要安装的是文件,而不是目标。在这个例子中,我们要安装的文件是动态库libDynamic.dll
。 -
"${CMAKE_SOURCE_DIR}/libs/dynamic/libDynamic.dll"
:指定要安装的文件的路径。${CMAKE_SOURCE_DIR}
是一个变量,表示项目的根目录。 -
DESTINATION bin
:与第一个install
命令相同,这里指定将libDynamic.dll
安装到bin
目录下。
-
总结
本文介绍了 CMake 的基本概念、语法和一些常用命令。我们了解了 CMake 的工作原理和如何编写一个简单的 CMakeLists.txt 文件来构建一个包含可执行文件和动态库的项目。本文通过一个示例,详细解释了常用的 CMake 命令的作用,包括 cmake_minimum_required
、project
、add_executable
、add_library
、target_link_libraries
、include_directories
、link_directories
、find_package
、find_library
、find_file
、configure_file
以及 install
命令,相信读者以后会在开发工作中经常遇到。
通过阅读本文,读者应该对 CMake 有了一个基本的了解,并能够编写简单的 CMake 脚本来构建项目。CMake 是一个功能强大的构建工具,学会使用它将有助于提高项目的构建和部署效率。
网友评论