make 简介

本节阅读量:

这一节专门解释 makeMakefile

make 是什么

make 可以先理解成:

1
按照 Makefile 里的规则,把代码编译出来。

你在第一章目录里运行:

1
make

它会读取当前目录下的:

1
Makefile

然后执行里面写好的编译命令,生成:

1
mini

mini 就是这一章的小命令行程序。

一个最小 Makefile

先不看课程代码,先看一个 Hello World。

假设有一个文件叫 hello.cpp

1
2
3
4
5
6
#include <iostream>

int main() {
    std::cout << "hello\n";
    return 0;
}

如果手动编译,可以运行:

1
c++ hello.cpp -o hello

这条命令的意思是:

1
2
用 c++ 编译 hello.cpp。
输出程序叫 hello。

如果写成 Makefile,可以是:

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

这里有两行最重要。

第一行:

1
hello: hello.cpp

可以读成:

1
2
我要生成 hello。
hello 依赖 hello.cpp。

第二行:

1
	c++ hello.cpp -o hello

可以读成:

1
如果要生成 hello,就执行这条编译命令。

注意:第二行前面必须是一个 Tab,不是普通空格。这是 Makefile 一个比较古老但必须遵守的规则。

现在运行:

1
make

make 会找到第一个目标 hello,然后执行:

1
c++ hello.cpp -o hello

最后运行:

1
./hello

会看到:

1
hello

所以 Makefile 最核心的形状就是:

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

本课程后面的 Makefile 只是这个形状的扩展。

为什么不用每次手写编译命令

第一章真正的编译命令大概长这样:

1
2
3
4
5
6
7
8
9
c++ -std=c++17 -Wall -Wextra -pedantic -Isrc \
    src/cli/main.cpp \
    src/front/ast.cpp \
    src/front/lexer.cpp \
    src/front/parser.cpp \
    src/interp/interpreter.cpp \
    src/compile/ir.cpp \
    src/compile/assembly.cpp \
    -o mini

这条命令很长,所以我们把它写进 Makefile。以后只要运行:

1
make

就等于让 make 帮你执行那条长命令。

Makefile 里最重要的几行

第一章的 Makefile 大概是:

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

all: mini

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

clean:
	rm -f mini out out.s

先不用学完整的 Makefile 语法。第一遍只看懂几件事:

1
2
3
4
SOURCES    要编译的 .cpp 文件
all        默认目标
mini       要生成的程序
clean      删除生成出来的文件

还有一行:

1
.PHONY: all clean

可以先读成:

1
all 和 clean 是命令名字,不是真的文件名。

为什么要说这个?

make 默认会把目标名当成“要生成的文件名”。比如:

1
mini: $(SOURCES)

这里的 mini 确实是一个要生成的文件。

但:

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

这里的 clean 不是要生成一个叫 clean 的文件。它只是一个命令入口,意思是“清理生成文件”。

所以写:

1
.PHONY: clean

就是告诉 make

1
clean 不是文件名,每次运行 make clean 都执行它下面的命令。

all 也一样。它只是默认入口,不是真的要生成一个叫 all 的文件。

这里的:

1
$(wildcard src/*/*.cpp)

意思是:

1
找出 src/ 下面各个子目录里的 .cpp 文件。

所以:

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

可以读成:

1
SOURCES 包含 src/cli、src/front、src/interp、src/compile 等目录下的 .cpp 文件。

所以这一行:

1
mini: $(SOURCES)

可以读成:

1
mini 依赖这些 .cpp 文件。

这一行:

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

可以读成:

1
用 c++ 和这些编译选项编译所有源码文件,生成 mini。

编译选项是什么意思

把这行命令拆开看:

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

c++ 是 C++ 编译器命令。它负责把 .cpp 源文件编译成可以运行的程序。

-std=c++17 表示使用 C++17 标准。C++ 有不同版本,比如 C++11、C++14、C++17、C++20。本课程使用 C++17,是因为它足够现代,也足够普遍。

-Wall 会让编译器多提醒你一些常见问题。这些提醒不一定会让编译失败,但通常值得看一眼。

-Wextra 会再多打开一些提醒。它比 -Wall 更严格一点,能帮助我们更早发现可疑代码。

-pedantic 表示更严格地按 C++ 标准检查代码。有些编译器支持自己的扩展写法,代码可能能编过,但不一定是标准 C++。加上这个选项后,编译器会尽量提醒你哪些写法不够标准。

-Isrc 表示把 src 目录加入头文件搜索路径。这样代码里就可以写:

1
#include "front/parser.h"

而不是写成更长的相对路径。

$(SOURCES) 是 Makefile 变量,前面已经用:

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

把它设置成所有要编译的 .cpp 文件。

-o mini 表示输出文件叫 mini。如果没有这个选项,编译器可能会生成默认名字,例如 a.out

所以整条命令可以读成:

1
2
3
4
用 C++17 编译 src 下的所有源码。
开启比较严格的警告。
把 src 作为头文件搜索目录。
最后生成 mini 这个可执行文件。

0.2 命令是怎么进入代码的

上一节

1.0 合抱之木,生于毫末

下一节