make
是一种自动构建目标文件的工具,最早应用于 C 语言的编译过程,现在也用于 node.js 等工程中。其语法独特而复杂,上手有一定的难度。这篇文章中我会以一个 C++ 工程为例,展示如何编写一个通用的 Makefile 文件。
Makefile 的基本语法是
1 | TARGETS: DEPENDENCIES |
每个 Makefile 文件都要指定一个终极目标。make
工具会查看这个终极目标的依赖关系,将它分解成多个子目标,然后再自底向上地执行子目标的操作,在完成子目标的基础上实现终极目标。
当程序足够简单的时候,我们的 Makefile 可能只有一个目标。现在我们来设想一个比较复杂的情况。有这样一个 C++ 的工程:
1 | demo |
源文件、头文件和测试文件分别放置在三个文件夹中,如何顺利地编译这个工程呢?
如果你已经熟悉了 Makefile 的编写,你应该看得懂下面的操作:
1 | .SUFFIXES: |
这个工程的终极目标all
依赖于三个目标文件。而每个文件夹下的目标文件分别由一条静态模式指令生成。在这个例子中,静态模式%.o
匹配目标中的所有*.o
文件,并设定其依赖文件为对应的%.cpp
。对所有匹配成功的组合,将.cpp
的源文件(用$<
表示)编译成.o
的目标文件(用$@
表示),这样就实现了目标的编译。如果想要删除编译产生的文件,只需要调用伪目标clean
即可。
不过上面的 Makefile 显然还不够完美,有两个地方值得改进。其一是封装编译的参数,当编译的参数需要修正时,我们只用修改一处,而不必逐行修改。其二是自动获取目标文件名,即使工程中有上百个源文件,Makefile 依旧会简洁明了,而不是充斥着各种文件的名称。
实现第一点并不困难,使用make
的宏扩展功能即可:
1 | .SUFFIXES: |
虽然 Makefile 变长了,但它的语义却更加清晰。如果我们要添加新的目标文件,只需要修改变量$(OBJS)
的值即可。
不过这还是不够完美。有没有一种办法,可以不用输入目标文件的名字,只要是文件夹下符合要求的文件(例如所有的.cpp
文件),统统拿来编译呢?这也不困难,只要运用通配符和有关的字符串函数就行了:
1 | .SUFFIXES: |
这个 Makefile 比上一个更长了。不过我们已经看不到文件名了。就像你想到的那样,我们将目标文件的文件名存到了两个变量中
1 | $(SRCOBJS) == "src/demo.o src/main.o" |
这是通过make
的两个内置函数wildcard
和patsubst
实现的。wildcard
返回所有符合给定模式的匹配。在上面的例子中,我们要匹配所有处于$(SRCDIR)
和$(TESTDIR)
目录下的.cpp
文件,并将其路径作为变量传入另一个函数patsubst
,它会将每个路径中的.cpp
替换成.o
,最后存入我们指定的变量中。
有了如此逆天的功能,妈妈再也不用担心我们会写出又长又臭的 Makefile 了……