CMake 前置知识 - 在你学习 CMake 之前
CMake 前置知识 - 在你学习 CMake 之前
前言
这篇文章不是用来介绍 CMake 的使用方式的,作者也并没有深入了解过 CMake,甚至没有自己写过独立项目。写下这篇文章只是为了指引像一两年前我的人:
- 刚刚走出 devc++,初次接触命令行、多文件编译
- 第一次部署 C++ 项目,看到复杂的项目构造和不熟悉的构建过程而不知所措
- 热切地希望通过学习 CMake 来掌握项目开发能力
那时候的我像渴望飞翔的雏鹰,看到大家都用 CMake 自己也想学,希望能提高自己的能力。然而询问学长得到的答案基本都是 “这种东西用到再查就行了”、“CMake 不用学习,会用就行”,自己查则很多都看不懂,用起来也遇到很多配置错误很难完成部署。
如果我能回去指导那时候的我,我希望能有这样一篇文章,铺平从 devc++ 到 CMake 的路,或者至少给出一张更加清晰的地图,告诉我不要着急,迷雾会在未来扫清。
C++ 编译过程简述
这部分知识会在 编译原理 课程中系统介绍,也可以通过阅读 CSAPP 自学,B站 up 主 九曲阑干 将该书制作成视频,可以借此快速入门。这里我会简单过一遍。
在 IDE 如 devc++ 中,我们写下 hello.cpp
源文件后,需要先对它进行编译生成 hello.exe
文件后才能运行。但事实上从高级语言到机器语言的 编译 操作并非一步到位的,而是至少包含预处理、编译、汇编和链接四个步骤。
预处理阶段负责对输入的源文件标准化。C++ 对程序的写法是比较宽松的,你可以在各种地方加入任意空格,可以反斜杠换行,还可以用诸如 #define MAX((A),(B)) ...
这类宏指令,他们都需要被预编译器修剪、展开,以方便编译阶段处理。
编译阶段会将预处理过的源代码一步步解释成程序执行流,并在这过程中进行优化和信息提取,最终生成汇编代码。这部分对应编译原理的词法分析、语法分析、语义分析、中间代码生成等等内容,因此不会在此展开。你只需要知道最后生成的汇编代码依然是人类可读的,只是不再包含层次结构,每一条语句也只会执行最基础的操作即可。
汇编阶段会将编译出的汇编文件转换成目标文件,这是一种人类不可读的二进制代码,你可以理解为可执行文件的一部分。注意到这里为止都是对单个源代码文件来说的,也就是 hello.cpp
预处理为 hello.i
,编译为 hello.s
,汇编为 hello.o
。
链接阶段会将可执行文件的不同部分组合起来,输出最终的可执行文件。不同的部分可能包括自己写的多个源代码目标文件(比如这里的 hello.cpp
、标准库文件(比如包含 iostream
的动态链接库)、用户库文件,它们会以或静态或动态的方式组合在一起。这一步骤经常发生错误,但对平常只写单文件的开发者又很陌生,遇到时往往会不知所措。所以以后当你遇到以 链接失败
之类开头的错误,或者错误信息是“找不到函数”“变量名重复”之类的,你就知道是怎么回事了。
以上这些编译过程一般都会由编译器(如gcc、clang、msvc)按默认流程一步执行到位,我们也可以选择性地执行到某个位置,而这正是 构建
的基础。
什么是构建工具
构建就是将项目编译打包成可执行文件的过程,而构建工具源于以下需求:
- 文件很多,手动编译费时费力
- 如果单纯写一个脚本全部编译一遍,那没修改过的源文件就会被重复编译,很浪费时间
- 希望简化编译脚本,方便地提供配置、编译、测试、清理、安装等功能
可见,如果我们只有一个源文件,大可以直接调用编译器生成可执行文件,后面的 make 和 cmake 啥的就没有必要了。
但很多软件是有以上需求的,于是 Makefile、ninja 等工具应运而生。他们会识别没编译过的或被修改过的源代码,将他们编译为目标文件,再链接起来。以 Makefile 为例,我们加入另一个源文件 some_func.cpp
,然后写一个简单的编译脚本:
1 | hello.exe: hello.o some_func.o |
将这个脚本和源文件一起保存在同一目录,命名为 makefile
,即可用 make 完成构建。
可以看到 makefile
的格式很简单,就是一组组的文件生成规则列下来,每个文件都可能依赖其他文件,会标注在冒号后面,然后下面用 Tab
缩进接生成文件所需要的命令。写好之后运行 make 程序,他就会默认生成第一个文件,根据依赖递归地执行命令。make 会根据文件的修改时间判断哪些是不需要重新生成的,从而减少构建时间。
为什么需要 CMake
初步使用过 CMake 后就会发现,CMake 其实是用来生成 Makefile 等构建配置文件的工具,那为什么有了构建工具还需要 CMake 这样套娃呢?据我了解有以下原因:
- 跨构建工具:构建工具有很多种,比如 makefile、ninja、msbuild等等,我们希望用一个统一的配置文件生成不同构建工具的配置
- 跨编译器:编译器也有很多种,比如 gcc、clang、msvc等等,我们希望用一个配置文件方便地生成使用不同编译器的构建文件
- 依赖管理:大型软件一般会产生诸多依赖库(不是文件依赖),而编写构建过程的人并不清楚开发者将依赖放在哪里,所以需要有一个系统找到各种依赖来完成编译链接过程
- 灵活配置:构建工具是以文件作为生成任务的,但有时我们需要更复杂的配置,比如区分 Debug、Release 的程序,比如允许让程序选择性地包含某些功能模块等等
总的来说,cmake 让构建过程实现了跨平台、自动化、灵活配置。如果你不需要这些功能,而仅仅是包含多个文件、运行在本地、没有什么依赖的个人项目,使用构建工具就已经足够了。
如果你已经做好准备,要开创一个大事业,那就准备好使用 CMake 吧。
P.S. CMake 的依赖管理好像只负责找到依赖,而不具备包管理器应有的下载、安装、罗列、卸载等操作,你需要其他如 conan 的工具来做到这一点。。所以说 rust 的 cargo 真是一个很先进的工具。
接下来要学习什么
看完这一切后,如果你决定使用 CMake,你只需要知道下面知识就行:
- 一般先创建 build 文件夹来存放构建产生的文件,在此目录下运行构建指令。如果你忘了,你会发现仓库根目录下一片狼藉。。
- 确保一切工具准备到位,包括编译工具(推荐 clang)、构建工具(推荐 ninja)、CMake,并且配好了环境变量
- 在 build 目录下运行
cmake ..
,其中..
是包含CMakeLists.txt
的目录。CMake 会尝试寻找合适的工具链并生成构建配置,第一次操作请检查工具链选择是否符合你的想法,因为 CMake 可能会找不到工具链或选择了错误的工具链。 - 运行
cmake --build .
完成构建。CMake 会根据配置调用构建工具完成构建
如果你还想编写 CMake 文件,我可能提供不了太多帮助,可以参考下面的内容:
- 快速入门:CMake 教程 | 菜鸟教程
- 一些书(中文,现搜的,算是mark一下):
- 官方教程(要求英文阅读):官方教程
另外,请确保你已经学习过 软件工程,这能解答很多为什么这样设计的困惑。