选择正确的构建系统可以决定项目的成功与否。从处理依赖项到确保正确编译和链接代码,正确的构建系统可以节省 你时间并避免潜在的麻烦。
在众多可用选项中,CMake 构建系统脱颖而出,可以处理复杂的跨平台项目。
本教程介绍了 CMake 的受欢迎程度、基本设置和最佳实践,以及使用它时的常见陷阱。我们还探讨了代码生成器和自定义目标等高级主题。最后,你将会更加有效地利用 CMake,助力项目取得长期成功。
为什么选择 CMake?
CMake 不仅仅是另一个构建系统,它还是一个用于管理构建源代码过程的工具,可提供无与伦比的灵活性和可扩展性。对于中级软件开发人员来说,了解 CMake 是一项必不可少的技能。
为什么它是许多开发人员和组织的首选:
- 跨平台兼容性:CMake 抽象了特定于平台的详细信息,允许定义一次构建配置,并为多个受支持的操作系统或 IDE 生成本机构建脚本。
- 灵活性: 无论 Linux、Windows 还是 macOS,CMake 都能帮助开发人员启动和运行构建系统。
- 可扩展性: CMake 可以扩展以满足需求,无论项目规模如何。
- 广泛采用: CMake 的受欢迎程度意味着开发人员享有广泛的社区支持,以及众多框架/解决方案集成。
在 CMake 上设置新项目
下面,我们将讨论如何为 Linux、macOS 和 Linux 设置 CMake。
Linux
打开终端,然后使用 sudo apt update 命令更新软件包列表。
接下来,使用以下命令安装 CMake:sudo apt install cmake。根据所使用的编程语言,可能还需要安装编译器。例如,如果使用 C++ 开发程序,则必须使用以下命令安装 g++ 编译器:
sudo apt install g++.
通过 Homebrew 的 macOS
如果你尚未安装适用于 macOS 的 Homebrew 包管理器,则需要安装。然后运行命令:
/bin/bash-c’’curl-fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)”
完成后,通过运行以下命令安装 CMake:brew install cmake。
Windows 安装 3
在 CMake 官方网站上,下载适用于 Windows 的最新稳定版本(使用 .msi 安装程序)。
接下来,运行安装程序并按照安装向导进行操作。在此过程中,将 CMake 添加到当前用户或所有用户的系统 PATH 中。
示例项目
让我们来看一个基本 C++ CMake 项目示例,该项目用于配置了简单 CMakeLists.txt 文件的 Hello World C++ 程序。
首先,在名为 CppHelloWorld 的空目录中创建一个包含以下内容的 main.cpp 文件:
// main.cpp
#include <iostream>
int main() {
std::cout << “Hello, World!” << std::endl;
return 0;
}
这将在标准输出中输出 “Hello, World!”。
现在,将 CMakeLists.txt 文件添加到 CppHelloWorld 目录,其中包含以下内容:
# CMakeLists.txt
# Minimum CMake version required
cmake_minimum_required(VERSION 3.5)
# Project name
project(HelloWorld)
# Add the executable
add_executable(HelloWorld main.cpp)
在这里,该文件指定了最低 CMake 版本、项目名称和源文件。
要在 Windows 计算机上构建 CMake 项目,请在 Windows 搜索栏中搜索 cmd exe 文件,打开命令提示符。然后,在命令提示符下,执行以下命令:
C:\CppHelloWorld>mkdir build
C:\CppHelloWorld>cd build
C:\CppHelloWorld\build>cmake ..
C:\CppHelloWorld\build>cmake –build .
C:\CppHelloWorld\build>.\Debug\HelloWorld.exe
要在 Linux 计算机上构建 CMake 项目,请打开终端应用程序,然后在命令提示符下执行以下命令:
ubuntu@ubuntu/CppHelloWorld$mkdir build && cd build
ubuntu@ubuntu/CppHelloWorld/build$cmake ..
ubuntu@ubuntu/CppHelloWorld$make
ubuntu@ubuntu/CppHelloWorld$./HelloWorld
输出:
Hello, World!
以下是正在发生的事情:
- mkdir build 会创建一个单独的 build 目录,cd 会进入 build 目录
- cmake ..通过为系统生成适当的构建文件来配置项目。
- cmake –build .或 make 编译项目并链接可执行文件。
在此之后,build 目录的 debug 文件夹中应该有一个可执行文件。
CMake 项目设置的最佳实践
通过遵循以下最佳实践, 你可以创建更易于跨不同平台维护、扩展和移植的 CMake 项目,此外,这对促进协作也有好处。
使用适当的文件夹结构以实现可扩展性
在构建、扩展和管理跨平台 C++ 项目时,组织得当的文件夹结构是必不可少的。它将确保 项目易于导航,使新开发人员能够快速掌握布局并了解代码库的结构。
目录结构示例:
MyProject/
CMakeLists.txt # Top-level CMake configuration file
src/ # Source files for the project
main.cpp
include/ # Header files (public APIs)
my_module.h
tests/ # Unit tests and testing framework configurations
test_main.cpp
在上面的代码中:
- src/ 包含实现细节,而 include/ 仅公开公共标头,从而创建自然的关注点分离。
- tests/ 确保单元测试是隔离的并清晰组织起来的。
- build/ 包含构建工件和配置文件。
利用现代 CMake 功能
现代 CMake 强调目标和属性,而不是变量和命令。许多新功能还增强了 CMake 项目的管理。
target_compile_features() 设置语言标准和编译器功能:
add_library(my_library)
target_compile_features(my_library PUBLIC cxx_std_17)
这种方法意味着任何链接到 my_library 的目标都将自动继承 C++17 要求,从而简化项目配置。
target_include_directories() 命令允许 你为目标指定包含目录,从而更好地控制可见性:
target_include_directories(my_libraryPUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includePRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src )
在这里,关键字 PUBLIC 使 include 目录可用于针对 my_library 进行链接的目标,而 PRIVATE 则将其保留在库内部。
target_compile_definitions() 命令将预处理器定义添加到目标中:
target_compile_definitions(my_library
PRIVATE
MY_LIBRARY_INTERNAL
PUBLIC
MY_LIBRARY_API_VERSION=2
)
最后,target_sources() 命令允许在创建目标后将源文件添加到目标中:
add_library(my_library)
target_sources(my_library
PRIVATE
src/implementation1.cpp
src/implementation2.cpp
)
可以在其文档页面上探索 CMake 的许多此类功能。
管理第三方依赖项
有效的依赖项管理对于项目稳定性至关重要。例如,find_package() 可以定位外部库,target_link_libraries() 可以链接它们:
find_package(BoostREQUIREDCOMPONENTSfilesystem) target_link_libraries(my_executable PRIVATE Boost::filesystem)
此方法将依赖项解析与目标定义完全分开,使 CMakeLists.txt 更具可读性和可维护性。
正确实施版本控制
如果你使用 Git 或其他版本控制系统,请考虑以下做法以进行 CMakeLists.txt:
- 始终将 txt 文件提交到版本控制。
- 在 txt 中使用相对路径以确保可移植性。
- 避免对绝对路径或特定于计算机的设置进行硬编码。
- 考虑使用 CMake 的 configure_file() 命令从 Git 标签生成版本信息。
- 记录 CMake 约定并使用 CI/CD 管道来实施它们,确保所有贡献者都有统一的构建过程。
最后一颗子弹是关键,因为在整个团队中保持一致的 CMake 实践至关重要。
注意跨平台差异
CMake 擅长管理跨平台构建。例如,if/elseif/endif 关键字可用于分隔自定义平台命令:
if(WIN32)
target_compile_definitions(my_library PRIVATE WIN32_LEAN_AND_MEAN)
elseif(UNIX)
target_link_libraries(my_library PRIVATE pthread)
endif()
尽可能利用 CMake 的内置变量和命令,努力最大限度地减少特定于平台的代码。这将增强便携性并降低维护开销。
请记住,CMake 是一个强大的工具,掌握其现代功能可以显着改善开发工作流程。随着项目的发展,请定期重新访问和优化 CMake 设置,以最好地满足开发需求。
将代码生成器与 CMake 集成
代码生成器已成为非常有价值的工具,尤其是对于大型项目。这些实用程序根据预定义的模板或其他规范自动创建重复代码,从而提高工作效率,减少人为错误,并保持整个代码库的一致性。
代码生成器特别适用于:
- 创建样板代码
- 从数据模型生成接口
- 从接口定义语言 (Interface Definition Languages, IDL) 生成代码
- 自动生成重复模式
将 CMake 代码生成器与 CMake 自定义目标相结合,将通过自动生成所需的文件并确保正确管理依赖项来简化构建过程。
示例代码生成命令
CMake 提供了两个用于集成代码生成器的关键命令: add_custom_command() 和 add_custom_target() 。
第一个 add_custom_command() 指定如何生成文件。
例如:
# Generate a simple text file
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/hello.txt
COMMAND ${CMAKE_COMMAND} -E echo “Hello, World!” >
${CMAKE_CURRENT_BINARY_DIR}/hello.txt
COMMENT “Generating hello.txt”
)
在此示例中,我们将在 build 目录中生成一个名为 hello.txt 的文件。我们使用 CMake 的内置命令行工具 (${CMAKE_COMMAND} -E) 将 “Hello, World!” 回显到文件中。COMMENT 是在构建过程中执行此命令时会看到的内容。
第二个 add_custom_target() 创建一个依赖于生成的文件的目标。
例如:
# Create a custom target that depends on hello.txt
add_custom_target(generate_hello ALL
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/hello.txt
)
在这里,我们创建了一个名为 generate_hello 的自定义目标。ALL 关键字表示在构建项目时将默认构建此目标。CMake 将确保在认为此目标完成之前生成文件 “hello.txt”。
此外,自定义命令的一个常见用例是使用 Doxygen 等工具生成 API 文档。
代码生成器提供:
- 一致性:生成的代码遵循预定义的模式,确保整个项目的一致性。
- 效率:自动执行重复性任务可以节省开发人员的时间并降低人为错误的可能性。
- 可维护性:对代码生成过程的更改可以在整个项目中使用。
- 版本控制: 可以对生成器及其输入进行版本控制,而不是对生成的代码本身进行版本控制。
常见陷阱以及如何避免它们
虽然 CMake 是管理构建过程的强大工具,但它也带来了一些挑战。
遵循正确的 CMake 安装步骤并仔细配置环境将为开发团队节省大量时间和挫败感,尤其是在大型项目中。
以下是一些常见的错误配置,尽管并非详尽无遗:
- 不正确的版本:一个常见问题是指定的最低 CMake 版本不正确或使用指定版本中不可用的功能。
- 解决方案: 始终在根 txt 文件的顶部指定所需的最低 CMake 版本。
- 库链接问题:库链接不当可能会导致未定义的引用或运行时错误。
- 解决方案:将 target_link_libraries() 与正确的可见性说明符一起使用。
- 路径处理不正确:硬编码路径或不正确的路径变量可能会导致不同系统出现问题。
- 解决方案:确保利用 CMake 的内置变量和函数进行路径处理。
- 特定于平台的代码:使用特定于平台的命令或路径可能会中断其他系统上的构建。
- 解决方案:使用 CMake 的独立于平台的命令和变量。
- 编译器标志:不同的编译器可能不支持相同的标志。
- 解决方案:有条件地使用特定于编译器的标志。
- 文件系统区分大小写:Windows 文件系统通常不区分大小写,而类 Unix 系统区分大小写。
- 解决方案:始终在文件名和 CMake 命令中使用一致的大小写。
此外,以下操作将帮助你调试上述一些问题:
- 将 cmake-gui 用于图形界面,以配置 CMake 项目并帮助识别配置问题。
- 启用 verbose output 以查看在构建期间执行的确切命令。
- 对于 Makefile 生成器,实现 CMake 的 message 命令以输出调试信息。
- 在调试模式下运行 CMake 以获得更详细的输出。
- 遵循现代 CMake 实践,例如,目标和属性,而不是全局变量。
- 利用 find_package() 查找依赖项,而不是设置手动路径。
- 对 txt 文件实施版本控制;将它们视为与源代码一样重要。
- 定期更新 CMake 版本并重构 你脚本以使用更新、更高效的功能。
- 记录 CMake 设置,尤其是对于复杂的配置或自定义函数。
- 了解典型问题并知道如何解决它们, 你可以为项目创建更强大、可移植且可维护的基于 CMake 的构建系统。
安全注意事项
在构建过程中,绝不应忽视安全性,但在使用 CMake 时,开发人员还需要牢记一些其他因素:
- 文件权限:确保构建文件和脚本具有适当的权限,以避免未经授权的修改。
- 敏感信息:对 txt 中的敏感数据(例如密码、API 密钥)使用环境变量或外部配置文件。不要对其进行硬编码。
- 跨平台安全标志:当面向不同的平台时,请确定特定于每个操作系统的安全标志。例如,在 Linux 上, 可以启用与位置无关的可执行文件 (PIE) 以提高安全性。
结论
CMake 是一个非常灵活和强大的工具。如果实施得当,它将大大简化构建复杂 C++ 项目的过程。通过遵循最佳实践、了解常见陷阱以及探索代码生成器和自定义目标等高级功能, 你可以为项目设置以实现长期成功。
随着 你对 CMake 的熟悉程度越来越高,请随时探索更高级的主题,例如交叉编译、复杂的依赖项管理和高级工具链。通过练习。你可以掌握 CMake 并自信地处理最具挑战性的 C++ 项目。
引用
CMake 文档:CMake 官方文档
https://cmake.org/cmake/help/latest/manual/cmake.1.html
编程前瞻:如何构建基于 CMake 的项目
https://preshing.com/20170511/how-to-build-a-cmake-based-project
CMake 命令:
https://cmake.org/cmake/help/latest/manual/cmake-commands.7.html
CLion 文档:从头开始创建新项目
https://www.jetbrains.com/help/clion/creating-new-project-from-scratch.html
CMake find_package 命令
https://cmake.org/cmake/help/latest/command/find_package.html