使用CMake组织C++项目

使用CMake组织C++项目

前言

如果你使用过 Visual Studio 或者其他 IDE,那么应该能体验到这些 IDE 在组织 C++ 项目源代码时的便利。使用 Visual Studio 创建的项目往往依赖于一个 sln 文件,只要用 Visual Studio 打开这个 sln 文件,就能打开一个文件结构清晰的 C++ 项目。哪些文件应该被包括到项目中,哪些排除在外,都被 Visual Studio 记录得很好。

大多数情况下,使用 Visual Studio 来组织 C++ 源代码很方便,但也有一些例外的情况。比如 sln 文件有自己的版本,使用新版 Visual Studio 创建的 sln 文件在旧版 Visual Studio 有可能打不开。直接分享 Visual Studio 项目的体验可能不会很好,因为不是所有人装上 Visual Studio 都能直接打开你的项目。

接下来我想介绍的是 CMake 。虽然 CMake 不是专门用来解决上面说的这个问题,但是借助 Visual Studio 或者其他 IDE 管理项目的方式,可以很快理解 CMake 是怎么组织 C++ 项目的。

CMake 是一个工具,它用一些命令(有点像函数)来描述一个项目的安装/编译过程。CMake 描述的内容包括有哪些头文件、源代码文件、依赖哪些第三方库等等。

Cmake 并不直接建构出最终的软件,而是产生标准的建构档(如 Unix 的 Makefile 或 Windows Visual C++ 的 projects/workspaces),然后再依一般的建构方式使用。

快速入门

下面借助 Hello World 项目来说明 CMake 是如何工作的。这个项目非常简单,只有一个源文件 main.cpp,没有头文件。使用 CMake 来描述编译 main.cpp 的过程,需要把描述编译过程的 CMake 命令放在 CMakeLists.txt 文件里。因此一个使用 CMake 管理的项目(以下简称 CMake 项目)可能会像这样:

1
2
3
.
├── CMakeLists.txt
└── main.cpp

main.cpp:

1
2
3
4
5
6
#include <iostream>
int main()
{
std::cout << "Hello World!" << std::endl;
return 0;
}

CMakeLists.txt:

1
2
project(hello-world)
add_executable(${PROJECT_NAME} main.cpp)

正如使用 Visual Studio 创建的项目一样,CMake 项目也有项目名称,项目类型等概念。在 CMakeLists.txt 文件中,命令 project(hello-world) 描述该 CMake 项目的名字是 hello-world。而命令 add_executable(${PROJECT_NAME} main.cpp),则描述该 CMake 项目有一个可执行文件,名字是 ${PROJECT_NAME},源文件有一个,是在当前目录下的 main.cpp

可以用变量的方式理解 ${PROJECT_NAME},因此它的内容就是由 project 命令定义的 hello-world

使用 Visual Studio 2019 (好像有的旧版 Visual Studio 也能打开 CMake 项目) 打开 CMake 项目所在的文件夹,按照 VS 的提示就可以编译运行了。

常用的 CMake 命令

以下介绍一些常见 CMake 命令的简单用法。之所以是简单的用法,是因为这些命令非常灵活,一篇文章难以介绍全面,了解他们的最好的方式是阅读 官方文档

  1. 创建可执行文件项目

使用 add_executable(<project name> <src>) 命令可以创建一个可执行程序项目。

使用方法:

1
2
3
4
5
6
# 简单写法
add_executable(hello-world main.cpp)
# 稍微复杂点的写法
add_executable(${PROJECT_NAME} main.cpp)
# 如果有多个源文件
add_executable(${PROJECT_NAME} a.cpp b.cpp c.cpp)
  1. 让CMake找到我的源文件

如果源文件太多了,可以把源文件都放到一个目录里。比如把所有的源文件都放在了 src 目录里。

使用 aux_source_directory(<src_dir> <var_name>) 命令把 src_dir 目录下的所有源文件都放到 var_name 变量里。

使用方法:

1
2
3
4
5
aux_source_directory(./src SRCS)
# 可执行程序
add_executable(${PROJECT_NAME} ${SRCS})
# 静态链接库
add_library(${PROJECT_NAME} STATIC ${SRCS})

注意:aux_source_directory 不会递归包含子目录,而且在源代码目录新增源文件后,要刷新 CMake 缓存才能生效。

  1. 让CMake找到我的头文件

include_directories(<dir> [dir2] [dir3] ...) 命令设置头文件目录,告诉 CMake 应该到哪些目录里寻找头文件。如果用 target_link_libraries() 让构建目标链接一个库,可以不对这个库的头文件目录使用这个命令,具体参考下文。

使用方法:

1
include_directories(./include)

  1. 创建库项目

使用 add_library(<project name> <type> <src>) 命令可以创建一个库项目。

使用 target_include_directories(<project name> <INTERFACE|PUBLIC|PRIVATE> <include_dir>) 设置库的头文件目录。为了让链接本库的项目能够正常使用,一般设置 PUBLIC 属性。

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
# 静态链接库
add_library(${PROJECT_NAME} STATIC a.cpp b.cpp c.cpp)
target_include_directories(${PROJECT_NAME} PUBLIC ./include)
# 或者:
add_library(${PROJECT_NAME} STATIC ${SRCS})
target_include_directories(${PROJECT_NAME} PUBLIC ./include)
# 动态链接库
add_library(${PROJECT_NAME} SHARED ${SRCS})
target_include_directories(${PROJECT_NAME} PUBLIC ./include)
# Header-Only 库
add_library(${PROJECT_NAME} INTERFACE)
target_include_directories(${PROJECT_NAME} PUBLIC ./include)

常用的项目结构

上面介绍了几个常用的 CMake 命令,接下来结合实际项目常用的结构,谈一谈 CMakeLists.txt 的写法。

  1. 简单的可执行文件项目

简单的可执行项目,包括一些头文件、一些 C++ 源文件,其文件结构大致如下:

1
2
3
4
5
6
.
├── CMakeLists.txt
├── include
│ └── hello.h
└── src
└── main.cpp

CMakeLists.txt:

1
2
3
4
5
6
7
8
# 项目名称 hello-world
project(hello-world)
# 从 src 目录搜索源文件
aux_source_directory(./src SRCS)
# 创建可执行文件项目
add_executable(${PROJECT_NAME} ${SRCS})
# 从 include 目录里查找头文件
include_directories(./include)
  1. 带有 examples 的库项目

对于库项目,我个人一般会写一些 examples。这样可以方便实时执行库代码,同时顺便写了使用样例,方便给别人参考。

1
2
3
4
5
6
7
8
9
10
.
├── CMakeLists.txt
├── examples
│ ├── CMakeLists.txt
│ ├── A.cpp
│ └── B.cpp
├── include
│ └── hello.h
└── src
└── hello.cpp

./CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
# 项目名称 hello-world
project(hello-world)
# 从 src 目录搜索源文件
aux_source_directory(./src SRCS)
# 创建静态库项目(也可以改成 SHARED 变成动态库)
add_library(${PROJECT_NAME} STATIC ${SRCS})
# 设置使用库文件所需要的头文件
target_include_directories(${PROJECT_NAME} PUBLIC ./include)

# 这个命令让 CMake 进入到指定子目录进行项目构建,这个目录也得有 CMakeLists.txt 文件
add_subdirectory(examples)

./examples/CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 项目名称,因为这个是库的样例项目,不是库的一部分,所以另取一个名字
project(hello-world-examples)

# 创建一个宏来快速创建可执行文件
macro(api_exe target)
add_executable(${PROJECT_NAME}-${target}
${target}.cpp
)
# 注意这里要设置对库的链接
target_link_libraries(${PROJECT_NAME}-${target} hello-world)
endmacro()

# 使用宏来快速创建可执行文件
api_exe(exampleA)
api_exe(exampleB)
api_exe(exampleC)

结语

这篇文章只是简略地总结了 CMake 的大概用法,没有细致地讲解 CMake 以及其命令。关于 CMake 更详细地用法,推荐读者结合官方文档以及其他文章慢慢探索。阅读开源项目的 CMakeLists.txt 也是个好做法,不过那些文件经过多年积累,内容多且复杂,读不懂也不要灰心(读不懂的还有我😭)。

作者

uint128.com

发布于

2020-03-03

更新于

2022-08-22

许可协议

评论