一、为什么要用到 make 命令和 makefile 文件
在 Linux 下编写一个程序,每次编译都需要在命令行一行一行的敲命令。如果是一个很小的程序还好说,命令不怎的复杂,编译速度也挺快,但是对于大型程序来说,这样无疑很麻烦,且不说可能会敲错命令,有时候仅仅改动了一个小地方,却需要将整个程序全部重新编译一遍,显然很浪费时间。Linux 提供了 make 命令来解决上述问题,它会在必要时重新编译所有受改动影响的源文件。同时,还提供了一个 makefile 文件,它告诉 make 命令如何构建应用程序。这里用一个简单的例子提前演示一下:
/* hello.c */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h> int main()
{
printf("hello world!\n");
exit();
}
/* Makefile */
hello: hello.c
gcc -o hello.s -S hello.c
gcc -o hello.o -c hello.s
gcc -o hello hello.o
clean:
-rm hello hello.s hello.o
这里提供了两段代码,第一段代码是一个简单的 HelloWorld 程序,第二段代码是为这个程序编写的一个 makefile 文件。此时,只需要在命令行输入 make 命令,就可以对源文件 hello.c 进行编译,如下:
执行 make 命令时,make 命令会读取 makefile 文件,并按照 makefile 文件中给出的命令来创建文件,同时会在执行时将命令打印到标准输出。执行完 make 命令后,源文件所在目录下多了三个文件:hello、hello.o 和 hello.s,其中 hello 是可执行文件,使用命令 ./hello 即可查看程序的输出结果。这和直接在命令行使用 gcc 命令所得到的结果是一样的,而且,当你修改了源文件时,也只需要再次使用 make 命令即可重新编译,十分方便。
二、make 命令
make 命令用于从一个名为 makefile 的文件中获得构建一个程序的依赖关系。make 命令会根据 makefile 文件来确定目标文件的创建顺序以及正确的规则调用顺序。
make 命令的一些常用参数
1)-k 参数:
使用 -k 参数可以让 make 命令在发现错误时仍然继续执行,而不是在检测到第一个错误时就停下来。利用这个选项可以在一次操作中发现所有未编译成功的源文件;
2)-n 参数:
使用 -n 参数,让 make 命令输出将要执行的操作步骤,而不是真正执行这些操作;
3)-f 参数:
使用 -f 参数,后面可以接一个文件名,用于指定一个文件作为 makefile 文件。如果没有使用 -f 选项,则 make 命令会在当前目录下查找名为 makefile 的文件,如果该文件不存在,则查找名为 Makefile 的文件。
三、makefile 文件
makefile 文件由一组依赖关系和规则构成。每个依赖关系都由一个目标(即将要创建的文件)和一个该目标所依赖的源文件组成;规则描述了如何通过这些依赖文件创建目标。简单的来说,makefile 文件的写法如下:
target: prerequisites
command1
command2
...
其中,target 是即将要创建的目标(通常是一个可执行文件),target 后面紧跟一个冒号,prerequisite 是生成该目标所需要的源文件(依赖),一个目标所依赖的文件可以有多个,依赖文件与目标之间以及各依赖文件之间用空格或制表符 Tab 隔开,这些元素组成了一个依赖关系。随后的命令 command 就是规则,也就是 make 需要执行的命令,它可以是任意的 shell 命令。另外,makefile 文件中,注释以 # 号开头,一直延续到该行的结束。
3.1 依赖关系
依赖关系定义了最终应用程序里的每个文件与源文件之间的关系。一个依赖关系列表由目标和该目标的零个或多个依赖组成,语法是:先写目标,然后接一个冒号,再用一个空格或制表符隔开,最后是用空格或制表符隔开的依赖文件列表,如下:
target: prerequisite1 prerequisite2 prerequisite3 ...
依赖关系表明了这样一件事:目标文件 target 依赖于文件 prerequisite1、prerequisite2、prerequisite3 ...,即,要生成 target,需要有这几个依赖文件的存在,而且,若其中一个依赖文件发生了改变,则需要重新生成 target。目标所依赖的文件可以有一个或多个,也可以没有依赖文件 —— 该目标总被认为是过时的,在执行 make 命令时,若指定了该目标,则该目标所对应的规则将总被执行(如目标 clean)。
makefile 文件中可以有很多个目标,每个目标都有自己对应的规则。make 命令默认创建的是 makefile 文件中的第一个目标。也可以自己指定一个目标让 make 命令去创建,只需要将该目标的名字作为参数放到 make 命令之后即可(如常用的 make clean)。实际上,更好的做法是,将 makefile 文件中的第一个目标定义为 all,然后再 all 后面列出其他从属目标,这将告诉 make 命令,在未指定特定目标时,默认情况下将创建哪个目标。此外,使用目标 all ,还可以使 make 命令一次性创建多个文件,这取决于 all 后面所接的从属目标的个数。
举个例子说明一下文件与文件之间的依赖关系:
/* sum.c */
#include <stdio.h>
#include <stdlib.h> extern int add(int i,int j); int main()
{
printf("%d\n",add(,));
exit();
} /* add.c */
#include <stdio.h> int add(int i,int j)
{
int k;
k = i + j;
return k;
}
这是一个简单的加法程序,包含两个文件:sum.c 和 add.c,其中,sum.c 中的 main 函数调用了 add.c 中的 add 函数。这个程序的依赖关系表如下:
sum: sum.o add.o
sum.o: sum.c stdio.h stdlib.h
add.o: add.c stdio.h
其中,最终所需要的目标文件是 sum,sum.o 和 add.o 是依赖 —— 要生成目标文件 sum ,需要先生成 sum.o 和 add.o。同样的,作为目标的 sum.o 依赖于 sum.c、stdio.h 和 stdlib.h;add.o 依赖于 add.c 和 stdio.h。这组依赖关系形成了一个层次结构,它显示了源文件之间的关系。
可以看出来,如果 add.c 发生了改变,那么就需要重新编译 add.o,而由于 add.o 发生了改变,目标文件 sum 也需要被重新创建,同时,由于 add.c 的改变并没有影响到 sum.o(sum.o 不依赖于 add.c),因此,sum.o 并不需要被重新编译。也就是说,通过使用 makefile 文件和 make 命令,我们可以实现,只重新编译所有受到改动影响的源文件,没有受到影响的源文件不必重新编译。这比把整个程序全部重新编译一遍显然要快上很多,尤其是对于大型程序。
3.2 规则
makefile 文件里另一部分内容是规则,它们定义了目标的创建方式。 规则的内容可以是任意的 shell 命令。关于规则,有以下两点需要注意:
1)规则所在行必须以制表符 tab 开头,不能用空格;
2)规则所在行最好不要以空格结尾,可能会导致 make 命令执行失败;
3)如果一行不足以写下所有内容,需要在每行代码的结尾加上一个反斜杠符 “\”,以让所有的命令在逻辑上处于同一行。
两个特殊字符 - 和 @:
1)在规则中,若命令之前加上了符号 “-”,则表明 make 命令将忽略该命令产生的所有错误;
2)若在命令之前加上了符号“@”,则表明 make 在执行该命令前,不会将该命令显示在标准输出上。
/* Makefile */
all: sum sum: sum.o add.o
gcc -o sum add.o sum.o
sum.o: sum.c
gcc -c sum.c
add.o: add.c
gcc -c add.c
clean:
-rm sum sum.o add.o
这是 3.1 中 sum.c 程序的 makefile 文件。其中 gcc 、rm 命令等行就是规则,它们告诉了 make 命令将如何去创建目标。
两个特殊的目标:clean 和 install
目标 clean 和 install 是两个特殊的目标,它们并不用于创建文件,而是有其他用途。
目标 clean 在前面已经提到过,它使用 rm 命令来删除目标文件。rm 命令通常以减号 - 开头,表示让 make 命令忽略该命令的执行结果,这意味着,即使由于文件不存在而导致 rm 命令返回错误,命令 make clean 也能成功执行。
目标 install 用于按照命令的执行顺序将应用程序安装到指定的目录,还是用上面的 sum.c 程序来演示一下目标 install 的用法:
all: sum # 安装目录
INSTDIR = /tmp sum: sum.o add.o
gcc -o sum add.o sum.o
sum.o: sum.c
gcc -c sum.c
add.o: add.c
gcc -c add.c
clean:
rm sum sum.o add.o install: sum
@if [ -d $(INSTDIR) ];\
then\
cp sum $(INSTDIR);\
chmod a+x $(INSTDIR)/sum;\
chmod og-w $(INSTDIR)/sum;\
echo "Installed in $(INSTDIR)";\
else\
echo "The directory $(INSTDIR) dose not exist!";\
fi
使用这个 makefile 文件,make 命令将会把 sum 安装到目录 /tmp 下(实际上,应用程序一般是安装在 /usr/local/bin 下的,这里为了方便就放到 /tmp 下了) 。执行 make install 命令,将得到如下结果:
输出结果显示 sum 已被成功安装到了 /tmp 目录下(实际上就是把可执行文件 sum 复制到 /tmp 目录下)。再进入 /tmp 目录查看,可以看到可执行文件 sum,其文件权限是 rwxr-xr-x,与 makefile 文件中所设置的一致。
3.3 makefile 文件中的宏
在 makefile 文件中定义一个宏很简单,如下:
MACRONAME=value
这里定义了一个宏 MACRONAME,引用宏的方法是使用 $(MACRONAME) 或 ${MACRONAME} 。使用宏定义,可以让 makefile 文件的可移植性更强。除了自己定义一些宏以外,make 命令还内置了一些特殊的宏定义,使得 makefile 文件变得更加简洁:
宏 | 说明 |
$? | 当前目标所依赖的文件列表中比当前目标文件还要新的文件 |
$@ | 当前目标的名字 |
$< | 当前依赖文件的名字 |
$* | 不包括后缀名的当前依赖文件的名字 |
除了在 makefile 文件里面定义宏以外,还可以调用 make 命令时,在命令行上给出宏定义。命令行上的宏定义将 覆盖在 makefile 文件中的宏定义。需要注意的是,在 make 命令后接宏定义时,宏定义必须以单个参数的形式传递,因此,需要避免在宏定义中使用空格或加引号。
参考资料:
《Linux 程序设计 第四版》
https://www.ibm.com/support/knowledgecenter/zh/ssw_aix_71/com.ibm.aix.cmds3/make.htm