摘要,本文将以深入优先,搜索迷宫为例,讲解makefile的用法,基本规则与隐含规则,模式规则;makefile的处理过程与原理;变量的定义,如何自动生成头文件依赖关系等。
本文来源:http://blog.youkuaiyun.com/trochiluses/article/details/11179191
1.为什么要写makefile
现在,我们有6个C文件和h文件,它们的详细内容可以参考这里,也可以在我的资源里进行打包下载实验,它们之间的依赖关系如下:
/* main.c */ #include <stdio.h> #include "main.h" #include "stack.h" #include "maze.h"
/* main.h */
#ifndef MAIN_H
#define MAIN_H
typedef struct point { int row, col; } item_t;
#define MAX_ROW 5
#define MAX_COL 5
#endif
/* stack.c */ #include "stack.h"
/* stack.h */ #ifndef STACK_H #define STACK_H #include "main.h" /* provides definition for item_t */ extern void push(item_t); extern item_t pop(void); extern int is_empty(void); #endif
/* maze.c */ #include <stdio.h> #include "maze.h"
/* maze.h */ #ifndef MAZE_H #define MAZE_H #include "main.h" /* provides defintion for MAX_ROW and MAX_COL */ extern int maze[MAX_ROW][MAX_COL]; void print_maze(void); #endif为了编译这个程序,我们每次都要输出如下命令
$ gcc -c main.c $ gcc -c stack.c $ gcc -c maze.c $ gcc main.o stack.o maze.o -o main问题:但是,如果这些文件之间的依赖关系有所变化,如果我仅仅更改了maize.h,那么我需要更新哪些文件呢?
解决方法:写一个makefile,表明这些文件之间的依赖关系,从而仅仅更新必要的文件,makefile的内容如下:
main: main.o stack.o maze.o gcc main.o stack.o maze.o -o main main.o: main.c main.h stack.h maze.h gcc -c main.c stack.o: stack.c stack.h main.h gcc -c stack.c maze.o: maze.c maze.h main.h gcc -c maze.c
2.基本规则
2.1问题:如果写makefile,它的基本语法?
答案:
main: main.o stack.o maze.o gcc main.o stack.o maze.o -o main
main是这条规则的目标(Target),main.o、stack.o和maze.o是这条规则的条件(Prerequisite)。目标和条件之间的关系是:欲更新目标,必须首先更新它的所有条件;所有条件中只要有一个条件被更新了,目标也必须随之被更新。所谓“更新”就是执行一遍规则中的命令列表,命令列表中的每条命令必须以一个Tab开头,注意不能是空格,Makefile的格式不像C语言的缩进那么随意,对于Makefile中的每个以Tab开头的命令,make会创建一个Shell进程去执行它。
对于上面这个例子,make执行如下步骤:
-
尝试更新Makefile中第一条规则的目标
main,第一条规则的目标称为缺省目标,只要缺省目标更新了就算完成任务了,其它工作都是为这个目的而做的。由于我们是第一次编译,main文件还没生成,显然需要更新,但规则说必须先更新了main.o、stack.o和maze.o这三个条件,然后才能更新main。 -
所以
make会进一步查找以这三个条件为目标的规则,这些目标文件也没有生成,也需要更新,所以执行相应的命令(gcc -c main.c、gcc -c stack.c和gcc -c maze.c)更新它们。 -
最后执行
gcc main.o stack.o maze.o -o main更新main。
2.2clean规则
通常Makefile都会有一个clean规则,用于清除编译过程中产生的二进制文件,保留源文件:
clean: @echo "cleanning project" -rm main *.o @echo "clean completed"
把这条规则添加到我们的Makefile末尾,然后执行这条规则:
$ make clean cleanning project rm main *.o clean completed
如果在make的命令行中指定一个目标(例如clean),则更新这个目标,如果不指定目标则更新Makefile中第一条规则的目标(缺省目标)。
clean
目标不依赖于任何条件,并且执行它的命令列表不会生成
clean
这个文件,刚才说过,只要执行了命令列表就算更新了目标,即使目标并没有生成也算。在这个例子还演示了命令前面加
@和-字符的效果:如果make执行的命令前面加了@字符,则不显示命令本身而只显示它的结果;通常make执行的命令如果出错(该命令的退出状态非0)就立刻终止,不再执行后续命令,但如果命令前面加了-号,即使这条命令出错,make
也会继续执行后续命令。通常
rm
命令和
mkdir
命令前面要加
-
号,因为
rm
要删除的文件可能不存在,
mkdir
要创建的目录可能已存在,这两个命令都有可能出错,但这种错误是应该忽略的。
如果存在clean这个文件,clean目标又不依赖于任何条件,make就认为它不需要更新了。而我们希望把clean当作一个特殊的名字使用,不管它存在不存在都要更新,可以添一条特殊规则,把clean声明为一个伪目标:
.PHONY: clean
这条规则没有命令列表。类似.PHONY这种make内建的特殊目标还有很多,各有不同的用途,详见[GNUmake]。在C语言中要求变量和函数先声明后使用,而Makefile不太一样,这条规则写在clean:规则的后面也行,也能起到声明clean是伪目标的作用:
clean: @echo "cleanning project" -rm main *.o @echo "clean completed" .PHONY: clean2.3makefile的处理过程与原理:(附录2)
3.隐含规则和模式规则
3.1问题:main.o依赖于main.c是一个common sense,有无方法来来解决这个冗余?
答案:
如果一个目标拆开写多条规则,其中只有一条规则允许有命令列表,其它规则应该没有命令列表,否则make会报警告并且采用最后一条规则的命令列表。
这样我们的例子可以改写成:
main: main.o stack.o maze.o gcc main.o stack.o maze.o -o main main.o: main.h stack.h maze.h stack.o: stack.h main.h maze.o: maze.h main.h main.o: main.c gcc -c main.c stack.o: stack.c gcc -c stack.c maze.o: maze.c gcc -c maze.c clean: -rm main *.o .PHONY: clean
这不是比原来更繁琐了吗?现在可以把提出来的三条规则删去,写成:
main: main.o stack.o maze.o gcc main.o stack.o maze.o -o main main.o: main.h stack.h maze.h stack.o: stack.h main.h maze.o: maze.h main.h clean: -rm main *.o .PHONY: clean
这就比原来简单多了。可是现在main.o、stack.o和maze.o这三个目标连编译命令都没有了,怎么编译的呢?试试看:
$ make cc -c -o main.o main.c cc -c -o stack.o stack.c cc -c -o maze.o maze.c gcc main.o stack.o maze.o -o main
现在解释一下前三条编译命令是怎么来。如果一个目标在Makefile中的所有规则都没有命令列表,make会尝试在内建的隐含规则(Implicit Rule)数据库中查找适用的规则。make的隐含规则数据库可以用make -p命令打印,打印出来的格式也是Makefile的格式,包括很多变量和规则,其中和我们这个例子有关的隐含规则有:
# default
OUTPUT_OPTION = -o $@
# default
CC = cc
# default
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
%.o: %.c
# commands to execute (built-in):
$(COMPILE.c) $(OUTPUT_OPTION) $<
$@和$<是两个特殊的变量,$@的取值为规则中的目标,$<的取值为规则中的第一个条件。%.o: %.c是一种特殊的规则,称为模式规则(Pattern Rule)。现在回顾一下整个过程,在我们的Makefile中以main.o为目标的规则都没有命令列表,所以make会查找隐含规则,发现隐含规则中有这样一条模式规则适用,main.o符合%.o的模式,现在%就代表main(称为main.o这个名字的Stem),再替换到%.c中就是main.c。所以这条模式规则相当于:
main.o: main.c cc -c -o main.o main.c
4.变量的语法规则
1)= 延迟定义
foo = $(bar) bar = Huh? all: @echo $(foo)输出:Huh?
2):=立即展开
y := $(x) bar
x := foo
all:
@echo $(y) ouput: bar
3)?= 条件赋值
foo ?= $(bar)的意思是:如果foo没有定义过,那么?=相当于=,定义foo的值是$(bar),但不立即展开;如果先前已经定义了foo,则什么也不做,不会给foo重新赋值。
+=运算符可以给变量追加值,例如:
objects = main.o objects += $(foo) foo = foo.o bar.o
object是用=定义的,+=仍然保持=的特性,objects的值是main.o $(foo)(注意$(foo)前面自动添一个空格),但不立即展开,等到后面需要展开$(objects)时会展开成main.o foo.o bar.o。
再比如:
objects := main.o objects += $(foo) foo = foo.o bar.o
object是用:=定义的,+=保持:=的特性,objects的值是main.o $(foo),立即展开得到main.o (这时foo还没定义),注意main.o后面的空格仍保留。
如果变量还没有定义过就直接用+=赋值,那么+=相当于=。
-
$@,表示规则中的目标。 -
$<,表示规则中的第一个条件。 -
$?,表示规则中所有比目标新的条件,组成一个列表,以空格分隔。 -
$^,表示规则中的所有条件,组成一个列表,以空格分隔。
5.自动处理头文件的依赖关系
问题:这样,书写头文件的依赖关系还是太繁琐,有自动处理头文件依赖关系的方法?
答案:使用 gcc -MM *.c
$ gcc -MM *.c main.o: main.c main.h stack.h maze.h maze.o: maze.c maze.h main.h stack.o: stack.c stack.h main.h
接下来的问题是怎么把这些规则包含到Makefile中,GNU make的官方手册建议这样写:
all: main main: main.o stack.o maze.o gcc $^ -o $@ clean: -rm main *.o .PHONY: clean sources = main.c stack.c maze.c include $(sources:.c=.d) %.d: %.c set -e; rm -f $@; \ $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$关于上述语法的含义,可以参见附录3
附录
附录3:
sources变量包含我们要编译的所有.c文件,$(sources:.c=.d)是一个变量替换语法,把sources变量中每一项的.c替换成.d,所以include这一句相当于:
include main.d stack.d maze.d
类似于C语言的#include指示,这里的include表示包含三个文件main.d、stack.d和maze.d,这三个文件也应该符合Makefile的语法。如果现在你的工作目录是干净的,只有.c文件、.h文件和Makefile,运行make的结果是:
$ make Makefile:13: main.d: No such file or directory Makefile:13: stack.d: No such file or directory Makefile:13: maze.d: No such file or directory set -e; rm -f maze.d; \ cc -MM maze.c > maze.d.$$; \ sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \ rm -f maze.d.$$ set -e; rm -f stack.d; \ cc -MM stack.c > stack.d.$$; \ sed 's,\(stack\)\.o[ :]*,\1.o stack.d : ,g' < stack.d.$$ > stack.d; \ rm -f stack.d.$$ set -e; rm -f main.d; \ cc -MM main.c > main.d.$$; \ sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.$$ > main.d; \ rm -f main.d.$$ cc -c -o main.o main.c cc -c -o stack.o stack.c cc -c -o maze.o maze.c gcc main.o stack.o maze.o -o main
一开始找不到.d文件,所以make会报警告。但是make会把include的文件名也当作目标来尝试更新,而这些目标适用模式规则%.d: %c,所以执行它的命令列表,比如生成maze.d的命令:
set -e; rm -f maze.d; \ cc -MM maze.c > maze.d.$$; \ sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \ rm -f maze.d.$$
注意,虽然在Makefile中这个命令写了四行,但其实是一条命令,make只创建一个Shell进程执行这条命令,这条命令分为5个子命令,用;号隔开,并且为了美观,用续行符\拆成四行来写。执行步骤为:
-
set -e命令设置当前Shell进程为这样的状态:如果它执行的任何一条命令的退出状态非零则立刻终止,不再执行后续命令。 -
把原来的
maze.d删掉。 -
重新生成
maze.c的依赖关系,保存成文件maze.d.1234(假设当前Shell进程的id是1234)。注意,在Makefile中$有特殊含义,如果要表示它的字面意思则需要写两个$,所以Makefile中的四个$传给Shell变成两个$,两个$在Shell中表示当前进程的id,一般用它给临时文件起名,以保证文件名唯一。 -
这个
sed命令比较复杂,就不细讲了,主要作用是查找替换。maze.d.1234的内容应该是maze.o: maze.c maze.h main.h,经过sed处理之后存为maze.d,其内容是maze.o maze.d: maze.c maze.h main.h。 -
最后把临时文件
maze.d.1234删掉。
不管是Makefile本身还是被它包含的文件,只要有一个文件在make过程中被更新了,make就会重新读取整个Makefile以及被它包含的所有文件,现在main.d、stack.d和maze.d都生成了,就可以正常包含进来了(假如这时还没有生成,make就要报错而不是报警告了),相当于在Makefile中添了三条规则:
main.o main.d: main.c main.h stack.h maze.h maze.o maze.d: maze.c maze.h main.h stack.o stack.d: stack.c stack.h main.h
如果我在main.c中加了一行#include "foo.h",那么:
1、main.c的修改日期变了,根据规则main.o main.d: main.c main.h stack.h maze.h要重新生成main.o和main.d。生成main.o的规则有两条:
main.o: main.c main.h stack.h maze.h
%.o: %.c
# commands to execute (built-in):
$(COMPILE.c) $(OUTPUT_OPTION) $<
第一条是把规则main.o main.d: main.c main.h stack.h maze.h拆开写得到的,第二条是隐含规则,因此执行cc命令重新编译main.o。生成main.d的规则也有两条:
main.d: main.c main.h stack.h maze.h %.d: %.c set -e; rm -f $@; \ $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$
因此main.d的内容被更新为main.o main.d: main.c main.h stack.h maze.h foo.h。
2、由于main.d被Makefile包含,main.d被更新又导致make重新读取整个Makefile,把新的main.d包含进来,于是新的依赖关系生效了。
附录2:make处理makefile的过程
make处理Makefile的过程也分为两个阶段:
-
首先从前到后读取所有规则,建立起一个完整的依赖关系图,例如:
-
然后从缺省目标或者命令行指定的目标开始,根据依赖关系图选择适当的规则执行,执行Makefile中的规则和执行C代码不一样,并不是从前到后按顺序执行,也不是所有规则都要执行一遍,例如
make缺省目标时不会更新clean目标,因为从上图可以看出,它跟缺省目标没有任何依赖关系。
clean目标是一个约定俗成的名字,在所有软件项目的Makefile中都表示清除编译生成的文件,类似这样的约定俗成的目标名字有:
-
all,执行主要的编译工作,通常用作缺省目标。 -
install,执行编译后的安装工作,把可执行文件、配置文件、文档等分别拷到不同的安装目录。 -
clean,删除编译生成的二进制文件。 -
distclean,不仅删除编译生成的二进制文件,也删除其它生成的文件,例如配置文件和格式转换后的文档,执行make distclean之后应该清除所有这些文件,只留下源文件。
附录一:makefile更新目标的过程
make会自动选择那些受影响的源文件重新编译,不受影响的源文件则不重新编译,这是怎么做到的呢?
-
make仍然尝试更新缺省目标,首先检查目标main是否需要更新,这就要检查三个条件main.o、stack.o和maze.o是否需要更新。 -
make会进一步查找以这三个条件为目标的规则,然后发现main.o和maze.o需要更新,因为它们都有一个条件是maze.h,而这个文件的修改时间比main.o和maze.o晚,所以执行相应的命令更新main.o和maze.o。 -
既然
main的三个条件中有两个被更新过了,那么main也需要更新,所以执行命令gcc main.o stack.o maze.o -o main更新main。
现在总结一下Makefile的规则,请读者结合上面的例子理解。如果一条规则的目标属于以下情况之一,就称为需要更新:
-
目标没有生成。
-
某个条件需要更新。
-
某个条件的修改时间比目标晚。
在一条规则被执行之前,规则的条件可能处于以下三种状态之一:
-
需要更新。能够找到以该条件为目标的规则,并且该规则中目标需要更新。
-
不需要更新。能够找到以该条件为目标的规则,但是该规则中目标不需要更新;或者不能找到以该条件为目标的规则,并且该条件已经生成。
-
错误。不能找到以该条件为目标的规则,并且该条件没有生成。
执行一条规则A的步骤如下:
-
检查它的每个条件P:
-
如果P需要更新,就执行以P为目标的规则B。之后,无论是否生成文件P,都认为P已被更新。
-
如果找不到规则B,并且文件P已存在,表示P不需要更新。
-
如果找不到规则B,并且文件P不存在,则报错退出。
-
-
在检查完规则A的所有条件后,检查它的目标T,如果属于以下情况之一,就执行它的命令列表:
-
文件T不存在。
-
文件T存在,但是某个条件的修改时间比它晚。
-
某个条件P已被更新(并不一定生成文件)
-
Makefile实战详解

本文通过一个迷宫搜索程序实例,详细介绍了Makefile的基本语法、规则应用、变量定义及头文件依赖关系的自动化处理等内容。
137

被折叠的 条评论
为什么被折叠?



