XSwitch通信百科
Makefile 极速入门
小樱桃在项目文件中大量使用 Makefile,如果你不了解 Makefile,或者你不知道怎么给别人解释 Makefile,看这篇文章,或者分享给他人。
什么是 Makefile
- Makefile 是一个工程文件,最初是为了帮助 C 语言写的工程项目编译用的
- Makefile 由
make
程序解释并执行 - Makefile 最初是在 UNIX 操作系统上出现的,后来被移植到 Linux 和 Windows 等其它系统上
- Makefile 很好用
最简单的 Makefile
下面,我们先来看一个最简单的 Makefile。
all: gcc -o test test.c
上面的文件可以存成Makefile
或makefile
,但一般前者居多。当在命令行上执行make
命令时,它就会找到该文件并解释执行里面的指令。
其中,all
表示一个目标(Target,目标后面总是有一个冒号),不带参数的make
命令会执行第一个目标下的指令,在此它会编译当前目录下的test.c
,并生成test
程序。然后执行./test
会在终端上打印Hello World!
。
test.c
内容如下:
#include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { printf("Hello World!\n"); }
在上述例子中,每次执行make
命令都会编译文件。
特别注意:目标后面的命令(如上述gcc
命令)行要以制表符(Tab)开头,而不是空格,否则,会出现Makefile:4: *** missing separator. Stop.
之类的错误。
依赖
在大型项目中通常会包含很多源文件,如果每次修改一个文件都会重复编整个项目,这显然费时费力。Makefile 的一个好处就是可以指定依赖关系,以免重复编译。如,我们把 Makefile 改成如下的样子。
all: test test: test.c gcc -o test test.c
其中,目标后面的内容称为依赖。连续执行两次make
,就会发现,第二次显示“无事可做”。
$ make gcc -o test test.c $ make make: Nothing to be done for `all'.
这是因为,要完成all
这个目标,它依赖于test
。而test
这个目标又依赖于test.c
。在执行第一次make
以后,生成了test
这个文件,因而第二次就不会重复编译了。
如果你修改了test.c
,则再次执行make
,由于test.c
比test
新,因此会重新编译。这就是 Makefile 要解决的最根本的问题。
当然,太简单的示例看不到太多好处,我们看另一个比较复杂的例子。
all: test test: test1.o test2.o test3.o gcc -o test test.c test1.o test2.o test3.o test1.o: test1.c gcc -c -o test1.o test1.c test2.o: test3.c gcc -c -o test2.o test2.c test3.o: test3.c gcc -c -o test3.o test3.c
在上述示例中。test
依赖于test.c
以及三个.o
的目标文件,而三个文件又分别依赖于三个.c
源文件。gcc
命令使用-c
参数可以生成目标文件。这样,在执行make
命令时,它会先生成三个目标文件,最后再生成test
可执行程序。如果修改了任何一个.c
文件,只有依赖它的目标会触发再次编译,而没修改过的文件不会重复编译。
目标
我们在前面已经讲过目标。不带参数的make
会执行它找到的第一个目标。除此之外,也可以把目标作为参数放到命令行上,如在前面的例子中,可以执行make test
,或make test1.o
、make test2.o
等直达目标。
在 C 语言项目中,通常还会有如下目标。
install: cp test /usr/local/bin/ clean: rm test *.o
其中,install
一般用于安装程序,此处,我们直接把生成的test
可执行程序复制到/usr/local/bin
目录下。在 UNIX/Linux 系统上,该目录是非系统程序(自己制作的程序)的默认安装路径。另外,clean
目标通用用于清除编译过程中产生的文件(保持源代码目录干净)。
变量
在复杂的项目中也使用变量,如下面的例子中,SRC
就是一个变量,可以在后面用$(SRC)
引用它。
SRC = test.c test1.c test2.c test3.c test: $(SRC) gcc -o test $(SRC)
除了这种明显的变量外,也有一些特殊的变量,如,上述 Makefile 也可以写成如下的形式,其中的$^
代表该目标所依赖的所有文件,也就是test:
后面的部分,即$(SRC)
引用的内容。
SRC = test.c test1.c test2.c test3.c test: $(SRC) gcc -o test $^
甚至,可以写成如下形式:
SRC = test.c test1.c test2.c test3.c test: $(SRC) gcc -o $@ $^
看不懂了吧?上述的$@
代表目标文件,也就是test
。下面,是几个常见变量的列表(为了保持简洁,此处没有列出所有可能的变量)。
$@
:表示目标文件。$^
:表示所有的依赖文件,但去除重复的。$<
:表示第一个依赖文件。$?
:表示比目标还要新的依赖文件列表。$+
:类似$^
,也是所有依赖目标的集合,但不会去除重复的。
如果你觉得这些变量不容易记,忘记它们。
.PHONY
通过上面学习,可以看到,目标是跟文件名是有对应关系的。可以这么理解:如果目标对应的文件名已经存在,并且比它依赖的源文件还新,下次就无须重复执行它对应的指令。
但有时候,目标会与本地的文件或目录重名,而又想在每次执行make
时都重新执行对应的目标时该怎么办?这样说起来比较抽象,举个具体一点的例子。好多项目中会有一个build
目录,而 Makefile 中会有一个build
目标,如:
build: echo Hello
这时候,无论怎么执行make build
,都会出现make: build' is up to date.
,导致echo Hello
无法正常执行。这时候,可以使用.PHONY
目标。如:
.PHONY: build build: gcc -o ...
通过给build
加上.PHONY
标记,告诉make
程序不要检查本地与目录重名的文件或目录。
小樱桃的 Makefile
小樱桃常用的 Makefile 中会有如下内容:
all: @echo OK setup: .env .env: env.example cp env.example .env
此处,@echo
与上一节中的echo
的主要区别是,前者不会在输出中回显命令行本身,而后面会回显命令行。读者可以自行实验查看其区别。
setup
目录依赖.env
这个文件。而这个文件默认是不存在的。因而,当执行到该目标时,就会执行对应的指令,把env.example
复制为.env
。
小樱桃通常将源文件存放到 Git 仓库中。.env
文件通常用于存放 Docker 中的环境变量。而每个人使用的环境变量通常是不同的,所以该文件不应该被放到 Git 仓库中。因此,我们在 Git 仓库中放了一个env.example
文件,用户第一次 Clone Git 仓库后,执行make setup
命令初始化项目,产生一个.env
文件,然后按需修改。该文件一般也会记到.gitignore
文件中,因而不会被签入 Git 仓库。make setup
仅需在项目初始化时执行一次,不过,即使执行多次也不会覆盖你本地的.env
,因为,只要你的.env
比env.example
新,它就不会被覆盖(后面的命令根本不会执行)。当然,此处有一个风险是你修改了env.example
再执行make setup
,它还是会被覆盖的。但我们在实际使用时从来没遇到过问题。
小樱桃会有一些通用的目标,除了上面的setup
,还有build
、run
、start
、stop
、test
、up
、down
等,不管使用任何语言写的程序,这些目标都很容易记住,在多个项目间切换时也不用花太多时间查阅文档,虽然它们大多数时候只是简单的映射到如npm run
、go build
等指令。当需要查阅文档时,Makefile 本身就是文档——所有的目标以及对应的命令行都整整齐齐的在 Makefile 中,很容易知道哪些目标都实际执行了哪些指令。
现在,你理解 Makefile 了吗?
参考文档
看看别人是怎么教你理解 Makefile 的。