0%

深入了解CMAKE

前言

在 ros 中经常使用cmake文件进行编译,其中进行了可执行文件添加和相关库的添加但是没有深入了解cmake的具体操作所以今天进行学习。

cmake 操作内容如下

  1. 创建 CMakeLists.txt 文件:定义项目、库、可执行文件和测试。

  2. 编写源代码和测试:编写代码和测试文件。

  3. 创建构建目录:保持源代码目录整洁。

  4. 配置项目:生成构建系统文件。

  5. 编译项目:生成目标文件。

  6. 运行可执行文件:执行程序。

  7. 运行测试:验证功能正确性。

  8. 使用自定义命令和目标:执行额外操作。

  9. 跨平台和交叉编译:支持不同平台和架构。

构建一个简单的 C++ 项目

假设我们有一个项目,包含一个主程序和一个库,库中有两个不同的功能模块。

项目结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

MyProject/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ ├── lib/
│ │ ├── module1.cpp
│ │ ├── module2.cpp
│ ├── include/
│ └── mylib.h
└── tests/
├── test_main.cpp
└── CMakeLists.txt

创建 CMakeLists.txt 文件

根目录 CMakeLists.txt 文件

在 MyProject 根目录下创建一个 CMakeLists.txt 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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)

src 目录 CMakeLists.txt 文件

在 src 目录下创建一个 CMakeLists.txt 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 创建库目标
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)

tests 目录 CMakeLists.txt 文件

在 tests 目录下创建一个 CMakeLists.txt 文件:

1
2
3
4
5
6
7
8
9
10
11

# 查找 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})

编写源代码和测试

以下是各个文件的代码:

src/main.cpp 文件代码

1
2
3
4
5
6
7
8
9

#include <iostream>
#include "mylib.h"

int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}

src/lib/module1.cpp 文件代码

1
2
3
4
5

#include "mylib.h"

// Implementation of module1

src/lib/module2.cpp 文件代码

1
2
3
4
#include "mylib.h"

// Implementation of module2

src/include/mylib.h 文件代码

1
2
3
4
5
6
7
8

#ifndef MYLIB_H
#define MYLIB_H

// Declarations of module functions

#endif // MYLIB_H

tests/test_main.cpp 文件代码

1
2
3
4
5
6
7
8

#include <gtest/gtest.h>

// Test cases for MyLib
TEST(MyLibTest, BasicTest) {
EXPECT_EQ(1, 1);
}

创建构建目录

在项目根目录下创建一个构建目录:

1
2
mkdir build
cd build

配置项目

在构建目录中运行 CMake 以配置项目:

1
cmake ..

编译项目

1
make

运行可执行文件

编译完成后,可以运行生成的可执行文件:

1
./MyExecutable

运行测试

使用生成的测试目标进行测试:

1
./TestMyLib

使用自定义命令和目标

自定义命令

在 src/CMakeLists.txt 文件中添加自定义命令:

1
2
3
4
5
6
7

add_custom_command(
TARGET MyExecutable
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Build complete!"
)

自定义目标

在 src/CMakeLists.txt 文件中添加自定义目标:

1
2
3
4
5
6

add_custom_target(run
COMMAND ${CMAKE_BINARY_DIR}/MyExecutable
DEPENDS MyExecutable
)

运行自定义目标:

1
make run

跨平台和交叉编译

指定平台

如果需要指定平台进行构建,可以在运行 CMake 时指定平台:

1
cmake -DCMAKE_SYSTEM_NAME=Linux ..

使用工具链文件

创建一个工具链文件 toolchain.cmake:

1
2
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

使用工具链文件进行构建:

1
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake ..

实践操作

完全按照教程中的配置进行cmake编译完全没有办法生成makefile从而无法make

从简单的开始实践:

单个文件使用外部库的情况

1
2
3
4
5
6
7
8
9
cmake_minimum_required (VERSION 3.10)

project (jsoncpp)

find_package(jsoncpp REQUIRED)

add_executable(jsoncpp readFromStream.cpp)

target_link_libraries(jsoncpp PRIVATE jsoncpp_lib)

多个文件包含personal.h,personal.cpp,main.cpp

src/ # 源代码目录
├── CMakeLists.txt # src目录的CMake配置文件
├── include/ # 头文件目录
│ └── personal.h # 自定义头文件
├── personal.cpp # 自定义源文件
└── main.cpp # 主程序文件

1
2
3
4
5
6
7
cmake_minimum_required (VERSION 2.8)

project (personaldata)

include_directories(${PROJECT_SOURCE_DIR}/include)

add_executable(personaldata main.cpp personal.cpp)

关于诺干源文件一起编译

如果同一目录下有无穷多源文件,那么一个一个添加就很慢了。此时可以使用cmake中的函数存储这些源文件,aux_source_directory(dir var) 他的作用是把dir目录中的所有源文件都储存在var变量中,然后需要用到源文件的地方用 变量var来取代此时 CMakeLists.txt 可以这样优化。

1
2
3
4
5
6
7
8
9
cmake_minimum_required (VERSION 2.8)

project (personaldata)

include_directories(${PROJECT_SOURCE_DIR}/include)

aux_source_directory(. SRC_FILES) # 收集当前目录所有源文件 .为当前目录 SRC_FILE为储存的名称

add_executable(personaldata ${SRC_FILE}) # 使用收集的文件创建可执行目标

但是不推荐这样使用因为:

不包含头文件:

  • 只收集源文件(.cpp/.c),不包含头文件(.h)

  • 头文件需要单独使用 include_directories() 处理

不递归子目录

  • 只扫描指定目录本身,不会进入子目录

  • 如果需要子目录文件,需在每个子目录使用此命令或使用 add_subdirectory()

虽然方便,但官方文档不推荐使用 `aux_source_directory`: 当添加新文件时,CMake 不会自动检测,需要手动重新运行 CMake 可能包含不需要的文件(如备份文件、测试文件等) 更好的做法是显式列出文件:
1
2
3
4
5
6
set(SRC_FILES
main.cpp
personal.cpp
# 明确列出所有需要的文件
)
add_executable(MyExecutable ${SRC_FILES})
### 指定头文件文件夹 对于集中的头文件,CMake提供了一个很方便的函数`include_directories ( dir )`他的作用是 自动去dir目录下寻找头文件,相当于 gcc中的 `gcc -I dir`此时 CMakeLists.txt 可以这样优化
1
2
3
4
5
6
7
8
9
10
11
12
13
cmake_minimum_required (VERSION 2.8)

project (personaldata)

include_directories(./lamnotinclude)

set(SRC_FILES
main.cpp
personal.cpp
# 明确列出所有需要的文件
)

add_executable(personaldata ${SRC_FILES})
## 生成动态库和静态库(重点)