章节目录

make:只在需要时重新构建

本节阅读量:

make 帮我们做两件事:

  • 把长编译命令写进 Makefile,以后用一个 make 代替。
  • 比较目标和依赖的修改时间,判断要不要重新编译。

第二件事尤其重要:make 不会简单地把 Makefile 里的命令全部执行一遍,而是先检查目标和依赖,再决定哪些命令需要执行。

先理解目标、依赖和命令

一条 Makefile 规则的基本形状是:

1
2
目标: 依赖
	生成目标的命令

例如:

1
2
hello: hello.cpp
	c++ hello.cpp -o hello

可以读成:

1
2
3
目标      hello
依赖      hello.cpp
命令      用 hello.cpp 生成 hello

命令行前面必须是一个 Tab,不能用普通空格代替。

运行 make 时,它会比较目标和依赖的修改时间,而不是分析文件内容:

1
2
3
hello 还不存在             执行命令
hello.cpp 比 hello 更新     重新执行命令
hello 比 hello.cpp 更新     不需要执行

所以,修改并保存 hello.cpp 后再次运行 make,会得到新的 hello;什么都没改时再次运行,make 会直接告诉你目标已经是最新的。

课程项目也是同一套逻辑:目标是 mini,依赖是参与构建的 .cpp 和 .h 文件。只要其中一个依赖比 mini 更新,再运行 make 就会重新编译。

课程使用的 Makefile

第一章的 Makefile 是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
SOURCES := $(wildcard src/*/*.cpp)
HEADERS := $(wildcard src/*/*.h)

.PHONY: all clean

all: mini

mini: $(SOURCES) $(HEADERS)
	c++ -std=c++17 -Wall -Wextra -pedantic -Isrc $(SOURCES) -o mini

clean:
	rm -f mini out out.s

先看几个名字:

名字 含义
SOURCES src/ 各子目录里的 .cpp 文件
HEADERS src/ 各子目录里的 .h 文件
all 默认入口
mini 要生成的命令行程序
clean 删除生成文件

这两行使用 wildcard 自动收集文件:

1
2
SOURCES := $(wildcard src/*/*.cpp)
HEADERS := $(wildcard src/*/*.h)

例如,SOURCES 会包含 src/front/parser.cpp、src/interp/interpreter.cpp 等文件,HEADERS 会包含 src/front/parser.h、src/compile/ir.h 等文件。

默认目标 all 依赖 mini:

1
all: mini

因此直接运行 make 时,它会先检查 mini 是否需要重新生成。

为什么头文件也要写进依赖

.cpp 会通过 #include 使用头文件:

1
#include "front/ast.h"

修改 ast.h 会改变最终程序。如果规则只写:

1
mini: $(SOURCES)

那么所有 .cpp 都没变化时,make 可能误以为现有的 mini 已经是最新的,接下来运行的仍是旧程序。

正确规则把两类文件都列为依赖:

1
mini: $(SOURCES) $(HEADERS)

这样修改任意 .cpp 或 .h 后,再次运行 make 都会触发编译。

HEADERS 不需要出现在 c++ 命令里。编译器从 .cpp 出发,会沿着 #include 读取头文件;把 .h 写进依赖,是为了让 make 知道什么时候需要重新执行命令。

.PHONY 表示伪目标

mini 是一个真实文件,all 和 clean 却只是操作入口,不需要生成同名文件。这种目标叫伪目标。下面这行告诉 make,每次请求它们时都按规则处理,不要检查目录里是否恰好存在名为 all 或 clean 的文件:

1
.PHONY: all clean

于是 make clean 每次都会执行:

1
2
clean:
	rm -f mini out out.s

平时不必先清理再构建;依赖没有漏写时,普通的 make 会自行判断哪些变化需要重新编译。

这一章的 Makefile 有意保持简单:只要任意一个 .cpp 或 .h 更新,就重新运行同一条 c++ 命令,构建整个 mini。它还没有把每个 .cpp 分别编译成目标文件;对目前几百行的教学项目来说,这样更容易看清完整命令。

编译命令

真正生成 mini 的命令是:

1
c++ -std=c++17 -Wall -Wextra -pedantic -Isrc $(SOURCES) -o mini

$(SOURCES) 是 Makefile 变量,不是直接输入终端的文字。执行规则前,make 会先把它展开成实际的 .cpp 文件列表,再把整条命令交给 shell。

部分 作用
c++ 调用 C++ 编译器
-std=c++17 使用 C++17 标准
-Wall -Wextra -pedantic 打开较严格的编译警告和标准检查
-Isrc 把 src 加入头文件搜索路径
$(SOURCES) 传入所有 .cpp 文件
-o mini 把输出程序命名为 mini

有了 -Isrc,源码可以写:

1
#include "front/parser.h"

不必根据当前 .cpp 所在目录拼很长的相对路径。

实际使用

进入某一章代码目录后构建:

1
2
cd code/01_numbers
make

第一次没有 mini,所以会执行编译命令。之后可以直接运行:

1
./mini run examples/add.lang

修改源码或头文件后,再运行一次 make 即可。若没有任何依赖发生变化,通常会看到类似:

1
make: Nothing to be done for 'all'.

这不是错误,只表示现有的 mini 已经是最新的。

需要删除生成文件时再运行:

1
make clean

到这里,只要能读懂“目标、依赖、命令”,并知道依赖文件更新会触发规则重新执行,就足够完成后续章节。


0.2 命令是怎么进入代码的

上一节

1.0 合抱之木,生于毫末

下一节