make:只在需要时重新构建
本节阅读量:make 帮我们做两件事:
- 把长编译命令写进
Makefile,以后用一个make代替。 - 比较目标和依赖的修改时间,判断要不要重新编译。
第二件事尤其重要:make 不会简单地把 Makefile 里的命令全部执行一遍,而是先检查目标和依赖,再决定哪些命令需要执行。
先理解目标、依赖和命令
一条 Makefile 规则的基本形状是:
|
|
例如:
|
|
可以读成:
|
|
命令行前面必须是一个 Tab,不能用普通空格代替。
运行 make 时,它会比较目标和依赖的修改时间,而不是分析文件内容:
|
|
所以,修改并保存 hello.cpp 后再次运行 make,会得到新的 hello;什么都没改时再次运行,make 会直接告诉你目标已经是最新的。
课程项目也是同一套逻辑:目标是 mini,依赖是参与构建的 .cpp 和 .h 文件。只要其中一个依赖比 mini 更新,再运行 make 就会重新编译。
课程使用的 Makefile
第一章的 Makefile 是:
|
|
先看几个名字:
| 名字 | 含义 |
|---|---|
SOURCES |
src/ 各子目录里的 .cpp 文件 |
HEADERS |
src/ 各子目录里的 .h 文件 |
all |
默认入口 |
mini |
要生成的命令行程序 |
clean |
删除生成文件 |
这两行使用 wildcard 自动收集文件:
|
|
例如,SOURCES 会包含 src/front/parser.cpp、src/interp/interpreter.cpp 等文件,HEADERS 会包含 src/front/parser.h、src/compile/ir.h 等文件。
默认目标 all 依赖 mini:
|
|
因此直接运行 make 时,它会先检查 mini 是否需要重新生成。
为什么头文件也要写进依赖
.cpp 会通过 #include 使用头文件:
|
|
修改 ast.h 会改变最终程序。如果规则只写:
|
|
那么所有 .cpp 都没变化时,make 可能误以为现有的 mini 已经是最新的,接下来运行的仍是旧程序。
正确规则把两类文件都列为依赖:
|
|
这样修改任意 .cpp 或 .h 后,再次运行 make 都会触发编译。
HEADERS 不需要出现在 c++ 命令里。编译器从 .cpp 出发,会沿着 #include 读取头文件;把 .h 写进依赖,是为了让 make 知道什么时候需要重新执行命令。
.PHONY 表示伪目标
mini 是一个真实文件,all 和 clean 却只是操作入口,不需要生成同名文件。这种目标叫伪目标。下面这行告诉 make,每次请求它们时都按规则处理,不要检查目录里是否恰好存在名为 all 或 clean 的文件:
|
|
于是 make clean 每次都会执行:
|
|
平时不必先清理再构建;依赖没有漏写时,普通的 make 会自行判断哪些变化需要重新编译。
这一章的 Makefile 有意保持简单:只要任意一个 .cpp 或 .h 更新,就重新运行同一条 c++ 命令,构建整个 mini。它还没有把每个 .cpp 分别编译成目标文件;对目前几百行的教学项目来说,这样更容易看清完整命令。
编译命令
真正生成 mini 的命令是:
|
|
$(SOURCES) 是 Makefile 变量,不是直接输入终端的文字。执行规则前,make 会先把它展开成实际的 .cpp 文件列表,再把整条命令交给 shell。
| 部分 | 作用 |
|---|---|
c++ |
调用 C++ 编译器 |
-std=c++17 |
使用 C++17 标准 |
-Wall -Wextra -pedantic |
打开较严格的编译警告和标准检查 |
-Isrc |
把 src 加入头文件搜索路径 |
$(SOURCES) |
传入所有 .cpp 文件 |
-o mini |
把输出程序命名为 mini |
有了 -Isrc,源码可以写:
|
|
不必根据当前 .cpp 所在目录拼很长的相对路径。
实际使用
进入某一章代码目录后构建:
|
|
第一次没有 mini,所以会执行编译命令。之后可以直接运行:
|
|
修改源码或头文件后,再运行一次 make 即可。若没有任何依赖发生变化,通常会看到类似:
|
|
这不是错误,只表示现有的 mini 已经是最新的。
需要删除生成文件时再运行:
|
|
到这里,只要能读懂“目标、依赖、命令”,并知道依赖文件更新会触发规则重新执行,就足够完成后续章节。