最近工作编译程序一直在用别人写的Makefile,但是没有系统的学习过,趁着放假学一波
0x00 Makefile 概述
一个企业级项目,通常会有很多源文件,有时也会按功能、类型、模块分门别类的放在不同的目录中,有时候也会在一个目录里存放了多个程序的源代码。
这时,如何对这些代码的编译就成了个问题。Makefile 就是为这个问题而生的,它定义了一套规则,决定了哪些文件要先编译,哪些文件后编译,哪些文件要重新编译。
整个工程通常只要一个 make 命令就可以完成编译、链接,甚至更复杂的功能。可以说,任何一个 Linux 源程序都带有一个Makefile 文件。
0x01 Makefile 的优点
管理代码的编译,决定该编译什么文件,编译顺序,以及是否需要重新编译;节省编译时间。如果文件有更改,只需重新编译此文件即可,无需重新编译整个工程;一劳永逸。Makefile 通常只需编写一次,后期就不用过多更改。
0x02 编译知识
Makefile最初是为了编译C/C++而诞生的, 所以它里面的很多隐藏规则都是针对 C/C++的。在讲 Makefile 之前有必要对 C/C++的编译有一点了解
预处理器
:将.c
文件转化成 .i
文件,使用的 gcc 命令是:gcc –E
,对应于预处理命令 cpp;
编译器
:将.c/.h
文件转换成.s
文件,使用的 gcc 命令是:gcc –S
,对应于编译命令 cc –S;
汇编器
:将.s
文件转化成 .o
文件,使用的 gcc 命令是:gcc –c
,对应于汇编命令是 as;
链接器
:将.o
文件转化成可执行程序,使用的 gcc 命令是: gcc
,对应于链接命令是 ld;
加载器
:将可执行程序加载到内存并进行执行,loader 和 ld-linux.so。
0x03 Makefile规则
Target...: Prerequsites...
Command
Command
...
或
Targets: Prerequisites;Command
Command
...
下面会称 Target
为目标, Prerequisites
为目标依赖, Command
为规则的命令行
Command 必须以[Tab]开始, Command 可以写成多行,通过来继行,但行尾的后不能有空格。
规则包含了文件之间的依赖关系和更新此规则 target 所需要的 Command
targets 可以使用通配符, 如果格式是"A(M)
"表示档案文件(.a
)中的成员“M
”
在需要用本义的时候,使用两个$$来表示。
当规则的 target 是一个文件,它的任何一个依赖文件被修改后,在执行 make <target>时这个目标文件都会被重新编译或重新连接。如果有必要此 target 的一个依赖文件也会被先重新编译。
0x04伪目标
Makefile 中把那些没有任何依赖只有执行动作的目标称为“伪目标
“(Phony targets
)
.PHONY : clean
clean :
-rm edit $(objects
通过.PHONY 将 clean 声明为伪目标,避免当目录下有名为“clean”文件时,clean 无法执行
这样的目标不是为了创建或更新程序,而是执行相应动作。
0x05自动推导规则
在使用 make 编译.c 源文件时,编译.c 源文件规则的命令可以不用明确给出。这是因为 make 本身存在一个默认的规则,能够自动完成对.c 文件的编译并生成对应的.o 文件。它执行命令“cc -c”来编译.c 源文件。在 Makefile 中我们只需要给出需要重建的目标文件名(一个.o 文件),make 会自动为这个.o 文件寻找合适的依赖文件(对应的.c 文件。对应是指:文件名除后缀外,其余都相同的两个文件),而且使用正确的命令来重建这个目标文件。
例如, 现在有三个文件 test.cpp
, my.cpp
, my.h
- test.cpp
#include <iostream>
#include "my.h"
int main(int argc, char * argv[]) {
int a = 100, b = 101;
std::cout << "this code is for test makefile" << std::endl;
std::cout << xadd(a, b) << std::endl;
}
- my.h
#ifndef _MY_H_
#define _MY_H_
int xadd(const int x, const int y);
#endif
- my.cpp
#include "my.h"
int xadd(const int x, const int y)
{
return x + y;
}
对于上边的例子,此默认规则就使用命令“gcc -c test.cpp -o test.o”来创建文件“main.o”。对一个目标文件是“N.o”,倚赖文件是“N.c”的规则,完全可以省略其规则的命令行,而由 make 自身决定使用默认命令。此默认规则称为 make 的隐含规则。
test: test.cpp my.o
gcc -c -o test test.cpp
my.o: my.cpp my.h
gcc -c -o my.o my.cpp
clean :
rm test my.o
也可以用隐式规则
test: test.cpp my.o
my.o: my.cpp my.h
clean :
rm test my.o
效果是一样的
这里要说明一点的是, clean 不是一个文件,它只不过是一个动作名字,有点像c语言中的label一 样,其冒号后什么也没有,那么,make就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。 要执行其后的命令,就要在make命令后明显得指出这个label的名字。这样的方法非常有用,我们可以在一 个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。
0x06 规则书写建议
书写规则建议的方式是:单目标,多依赖。就是说尽量要做到一个规则中只存在一个目标文件,可有多个依赖文件。尽量避免使用多目标,单依赖的方式。
0x07 makefile 工作原理文和件搜索顺序
在默认的方式下,也就是我们只输入 make
命令。那么,
- 首先会搜索目录下的
GNUmakefile
,makefile
,Makefile
文件,或者make -f
从指定文件读取
2.找到makefile后首先从第一个target
开始,如果生成target
依赖别的目标就递归从依赖开始
例如:上面的例子中,首先准备编译生成目标test
,发现依赖my.o
没有生成,就向下找my.o的生成,发现my.o的资源my.cpp
,my.h
已经就绪了,就先编译出my.o
,回到test
,发现test.cpp
和my.o
全部就绪,使用规则Command
生成目标test
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在 找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所 定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系 之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。
通过上述分析,我们知道,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命 令将不会被自动执行,不过,我们可以显示要make执行。即命令—— make clean
,以此来清除所有 的目标文件,以便重编译。
0x08 makefile中使用变量
我们可以看到 .o 文件的字符串被重复了两次,如果我们的工程需要加入一个新的 .o 文件, 那么我们需要在两个地方加(应该是三个地方,还有一个地方在clean中)。
当然,我们的makefile并不复 杂,所以在两个地方加也不累,但如果makefile变得复杂,那么我们就有可能会忘掉一个需要加入的地方, 而导致编译失败。所以,为了makefile的易维护,在makefile中我们可以使用变量。makefile的变量也 就是一个字符串,理解成C语言中的宏可能会更好。
比如,我们声明一个变量 obj,表示所有obj文件,在makefile的一开始就定义
obj = my.o
maincpp = test.cpp
于是,我们就可以很方便地在我们的makefile中以 $(obj)
的方式来使用这个变量了,于是 我们的改良版makefile就变成下面这个样子:
obj = my.o
maincpp = test.cpp
test :$(maincpp) $(obj)
my.o: my.cpp my.h
clean:
rm $(obj)
于是如果有新的 .o 文件加入,我们只需简单地修改一下 obj 变量就可以了。
关于变量更多的话题,我会在后续给你一一道来。
0x09 另类风格的makefiles
既然我们的make可以自动推导命令,那么我看到那堆.o
和 .h
的依赖就有点不爽,那么多的重复的 .h
,能不能把其收拢起来,好吧,没有问题,这个对于make来说很容易,谁叫它提供了自动 推导命令和文件的功能呢?来看看最新风格的makefile吧。
obj = my.o
maincpp = test.cpp
test :$(maincpp) $(obj)
$(obj): my.h
clean:
rm $(obj) test
这种风格,让我们的makefile变得很简单,但我们的文件依赖关系就显得有点凌乱了。鱼和熊掌不可兼得。 还看你的喜好了。我是不喜欢这种风格的,一是文件的依赖关系看不清楚,二是如果文件一多,要加入几个 新的.o
文件,那就理不清楚了。
0x10 清空目标文件的规则
每个Makefile中都应该写一个清空目标文件( .o
和执行文件)的规则,这不仅便于重编译,也很 利于保持文件的清洁。这是一个“修养”(呵呵,还记得我的《编程修养》吗)。一般的风格都是:
clean:
rm test $(obj)
更为稳健的做法是:
.PHONY: clean
clean:
rm test $(obj)
0x11 Makefile的文件名
默认的情况下,make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、 “makefile”、“Makefile”的文件,找到了解释这个文件。在这三个文件名中,最好使用“Makefile” 这个文件名,因为,这个文件名第一个字符为大写,这样有一种显目的感觉。最好不要用“GNUmakefile”, 这个文件是GNU的make识别的。有另外一些make只对全小写的“makefile”文件名敏感,但是基本上来说, 大多数的make都支持“makefile”和“Makefile”这两种默认文件名。
当然,你可以使用别的文件名来书写Makefile,比如:“Make.Linux”,“Make.Solaris” ,“Make.AIX”等,如果要指定特定的Makefile,你可以使用make的-f
和--file
参数
make -f Makefile.Linux
make -f Makefile.mac
0x12 引用其他的Makefile
在Makefile使用include
关键字可以把别的Makefile包含进来,这很像C语言的 #include
,被包含的文件会原模原样的放在当前文件的包含位置。 include
的语法是:
include <filename>
filename 可以是当前操作系统Shell的文件模式(可以包含路径和通配符)。
在include
前面可以有一些空字符,但是绝不能是 Tab
键开始。 include
和 <filename>
可以用一个或多个空格隔开。举个例子,你有这样几个Makefile
: a.mk
、 b.mk
、 c.mk
,还有一个文件叫 foo.make
,以及一个变量 $(bar)
,其包含 了 e.mk
和 f.mk
,那么,下面的语句:
include foo.make *.mk $(bar)
等价于
include foo.make a.mk b.mk c.mk e.mk f.mk
- 如果make执行时,有
-I
或--include-dir
参数,那么make就会在这个参数所指定的目 录下去寻找。
2.如果目录 <prefix>/include
(一般是: /usr/local/bin
或 /usr/include
)存在的话,make也会去找。
如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的 文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是 不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以 在include前加一个减号“-”。如:
-include <filename>
0x13 环境变量MAKEFILES
如果你的当前环境中定义了环境变量 MAKEFILES
,那么,make会把这个变量中的值做一个类似于 include
的动作。这个变量中的值是其它的Makefile,用空格分隔。只是,它和 include
不 同的是,从这个环境变量中引入的Makefile的“目标”不会起作用,如果环境变量中定义的文件发现 错误,make也会不理。
但是在这里我还是建议不要使用这个环境变量,因为只要这个变量一被定义,那么当你使用make时, 所有的Makefile都会受到它的影响,这绝不是你想看到的。在这里提这个事,只是为了告诉大家,也许 有时候你的Makefile出现了怪事,那么你可以看看当前环境中有没有定义这个变量。
0x14 变量定义及赋值:
变量直接采用赋值的方法即可完成定义,如:
INCLUDE = ./include/
变量取值:
用括号括起来再加个美元符,如:
`FOO = $(OBJ)`
系统自带变量:
通常都是大写,比如 CC
、PWD
、CFLAG
,等等。
有些有默认值,有些没有。比如常见的几个:
CPPFLAGS
: 预处理器需要的选项 如:-I
CFLAGS
:编译的时候使用的参数 –Wall –g -c
LDFLAGS
:链接库使用的选项 –L -l
变量的默认值可以修改,比如 CC
默认值是 cc
,但可以修改为 gcc:CC=gcc
0x15 函数
Makefile 也为我们提供了大量的函数,同样经常使用到的函数为以下两个。需要注意的是,Makefile 中所有的函数必须都有返回值。在以下的例子中,假如目录下有 main.c、func1.c、func2.c 三个文件。
通配符:
用于查找指定目录下指定类型的文件,跟的参数就是目录+文件类型,比如:
src = $(wildcard ./src/*.c)
这句话表示:找到 ./src 目录下所有后缀为 .c 的文件,并赋给变量 src。
命令执行完成后,src 的值为:main.c func1.c fun2.c。
patsubst:
匹配替换,例如以下例子,用于从 src 目录中找到所有 .c 结尾的文件,并将其替换为 .o 文件,并赋值给 obj。
obj = $(patsubst %.c ,%.o ,$(src))
命令执行完成后,obj 的值为 main.o func1.o func2.o。
特别地,如果要把所有 .o 文件放在 obj 目录下,可用以下方法:
obj = $(patsubst ./src/%.c, ./obj/%.o, $(src))
更多可以参考https://seisman.github.io/how-to-write-makefile/overview.html