本文将演示如何使用 CMake 管理一个中等复杂度的项目,从创建项目到编译和运行的整个过程,涵盖了从基本配置到高级特性的实际应用。
实战内容如下:
- 创建 CMakeLists.txt 文件:定义项目、库、可执行文件和测试。
- 编写源代码和测试:编写代码和测试文件。
- 创建构建目录:保持源代码目录整洁。
- 配置项目:生成构建系统文件。
- 编译项目:生成目标文件。
- 运行可执行文件:执行程序。
- 运行测试:验证功能正确性。
- 使用自定义命令和目标:执行额外操作。
- 跨平台和交叉编译:支持不同平台和架构。
构建一个简单的 C++ 项目
假设我们有一个项目,包含一个主程序和一个库,库中有两个不同的功能模块。
项目结构如下:
MyProject/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ ├── lib/
│ │ ├── module1.cpp
│ │ ├── module2.cpp
│ ├── include/
│ └── mylib.h
│ └── CMakeLists.txt
└── tests/
├── test_main.cpp
└── CMakeLists.txt
1. 创建 CMakeLists.txt 文件
1.1 根目录 CMakeLists.txt 文件
cmake_minimum_required(VERSION 3.10) # 指定最低 CMake 版本
project(MyProject VERSION 1.0) # 定义项目名称和版本
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 包含头文件路径
include_directories(${PROJECT_SOURCE_DIR}/src/include)
# 添加子目录
add_subdirectory(src)
add_subdirectory(tests)
解释:
cmake_minimum_required(VERSION 3.10)
: 确保使用的 CMake 版本至少是 3.10,以支持后续使用的 CMake 特性。project(MyProject VERSION 1.0)
: 声明项目名称为MyProject
并指定版本号为 1.0。CMake 会创建一些与项目相关的变量,如PROJECT_NAME
和PROJECT_VERSION
。set(CMAKE_CXX_STANDARD 11)
和set(CMAKE_CXX_STANDARD_REQUIRED ON)
: 将 C++ 标准设置为 C++11,并强制要求使用该标准。include_directories(${PROJECT_SOURCE_DIR}/src/include)
: 将src/include
目录添加到包含目录列表中,这样编译器可以找到相应的头文件。add_subdirectory(src)
和add_subdirectory(tests)
: 指示 CMake 处理src
和tests
子目录下的 CMakeLists.txt 文件。
1.2 src 目录 CMakeLists.txt 文件
# 创建库目标
add_library(MyLib STATIC
lib/module1.cpp
lib/module2.cpp
)
# 指定库的头文件
target_include_directories(MyLib PUBLIC ${CMAKE_SOURCE_DIR}/src/include)
# 创建可执行文件目标
add_executable(MyExecutable main.cpp)
# 链接库到可执行文件
target_link_libraries(MyExecutable PRIVATE MyLib)
解释:
add_library(MyLib STATIC lib/module1.cpp lib/module2.cpp)
: 创建一个名为MyLib
的静态库,使用module1.cpp
和module2.cpp
作为源文件。target_include_directories(MyLib PUBLIC ${CMAKE_SOURCE_DIR}/src/include)
: 将src/include
目录添加到MyLib
的公共包含目录中,这意味着任何链接MyLib
的目标都可以使用这些头文件。add_executable(MyExecutable main.cpp)
: 创建一个名为MyExecutable
的可执行文件,使用main.cpp
作为源文件。target_link_libraries(MyExecutable PRIVATE MyLib)
: 将MyLib
库链接到MyExecutable
可执行文件,并且该链接关系是私有的,意味着MyExecutable
的依赖不会传递给其他依赖MyExecutable
的目标。
1.3 tests 目录 CMakeLists.txt 文件
# 查找 GTest 包
find_package(GTest REQUIRED)
include_directories(${GTEST_INCLUDE_DIRS})
# 创建测试目标
add_executable(TestMyLib test_main.cpp)
# 链接库和 GTest 到测试目标
target_link_libraries(TestMyLib PRIVATE MyLib ${GTEST_LIBRARIES})
解释:
find_package(GTest REQUIRED)
: 查找 Google Test 测试框架包,REQUIRED
表示如果找不到该包将导致 CMake 报错。include_directories(${GTEST_INCLUDE_DIRS})
: 将 Google Test 的包含目录添加到包含目录列表中。add_executable(TestMyLib test_main.cpp)
: 创建一个名为TestMyLib
的可执行测试程序,使用test_main.cpp
作为源文件。target_link_libraries(TestMyLib PRIVATE MyLib ${GTEST_LIBRARIES})
: 将MyLib
库和 Google Test 库链接到TestMyLib
测试可执行文件,同样是私有的链接关系。
2. 编写源代码和测试
2.1 src/main.cpp 文件代码
#include <iostream>
#include "mylib.h"
int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}
解释:
- 包含了
iostream
用于标准输入输出,mylib.h
应该包含了MyLib
库的相关声明。 main()
函数输出一条消息并返回 0。
2.2 src/lib/module1.cpp 文件代码
#include "mylib.h"
// Implementation of module1
解释:
- 包含
mylib.h
头文件,这是MyLib
库的一部分,应该在此文件中实现module1
的具体功能,但这里只是一个占位。
2.3 src/lib/module2.cpp 文件代码
#include "mylib.h"
// Implementation of module2
解释:
- 类似
module1.cpp
,包含mylib.h
并应该实现module2
的具体功能,这里未给出具体实现。
2.4 src/include/mylib.h 文件代码
#ifndef MYLIB_H
#define MYLIB_H
// Declarations of module functions
#endif // MYLIB_H
解释:
- 这是一个典型的头文件保护宏,避免头文件被重复包含。在
#ifndef
和#endif
之间应该包含MyLib
库中函数的声明,但这里没有给出具体声明。
2.5 tests/test_main.cpp 文件代码
#include <gtest/gtest.h>
// Test cases for MyLib
TEST(MyLibTest, BasicTest) {
EXPECT_EQ(1, 1);
}
解释:
- 包含 Google Test 的头文件
gtest/gtest.h
。 - 使用
TEST
宏定义了一个名为MyLibTest
的测试套件,其中包含一个名为BasicTest
的测试用例,该测试用例使用EXPECT_EQ
断言来验证1
是否等于1
,这是一个非常简单的测试,通常可以在此基础上添加更多复杂的测试逻辑。
3. 创建构建目录
mkdir build
cd build
解释:
- 创建一个名为
build
的目录,这是一个良好的实践,可以将生成的构建文件与源代码分离,使源代码目录保持整洁。 - 进入
build
目录,后续的 CMake 操作将在此目录中进行。
4. 配置项目
cmake..
解释:
- 运行
cmake..
命令,..
表示 CMake 会查找上一级目录中的 CMakeLists.txt 文件,并根据该文件生成构建系统文件(如 Makefile 或 Visual Studio 项目文件等)。
5. 编译项目
make
解释:
- 使用
make
命令进行编译,该命令将根据build
目录中生成的构建文件(如 Makefile)编译源文件,生成目标文件(可执行文件和库文件)。
6. 运行可执行文件
./MyExecutable
解释:
- 执行生成的可执行文件
MyExecutable
,将输出Hello, CMake!
。
7. 运行测试
./TestMyLib
解释:
- 执行测试可执行文件
TestMyLib
,将运行test_main.cpp
中定义的测试用例,这里只有一个简单的测试,用于验证1
是否等于1
。
8. 使用自定义命令和目标
8.1 自定义命令
add_custom_command(
TARGET MyExecutable
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Build complete!"
)
解释:
add_custom_command
用于添加一个自定义命令,这里添加到MyExecutable
目标的POST_BUILD
阶段。COMMAND ${CMAKE_COMMAND} -E echo "Build complete!"
表示在构建完成MyExecutable
后,使用 CMake 的-E
选项执行echo
命令输出Build complete!
。
8.2 自定义目标
add_custom_target(run
COMMAND ${CMAKE_BINARY_DIR}/MyExecutable
DEPENDS MyExecutable
)
解释:
add_custom_target
创建一个名为run
的自定义目标。COMMAND ${CMAKE_BINARY_DIR}/MyExecutable
表示该目标的命令是执行MyExecutable
可执行文件。DEPENDS MyExecutable
表示run
目标依赖于MyExecutable
,当MyExecutable
被更新时,run
目标会重新执行。
9. 跨平台和交叉编译
9.1 指定平台
cmake -DCMAKE_SYSTEM_NAME=Linux..
解释:
- 通过
-DCMAKE_SYSTEM_NAME=Linux
命令行选项告诉 CMake 要构建的目标系统是 Linux 系统,CMake 会根据这个信息调整生成的构建文件。
9.2 使用工具链文件
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
解释:
- 创建一个工具链文件
toolchain.cmake
,设置CMAKE_SYSTEM_NAME
为Linux
和CMAKE_SYSTEM_PROCESSOR
为arm
,用于指定构建的系统是 Linux 且处理器是 ARM 架构。 cmake -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake..
命令使用该工具链文件进行构建,这对于交叉编译非常有用,例如在 x86 主机上为 ARM 平台编译代码。