摘要,本文将以深入优先,搜索迷宫为例,讲解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已被更新(并不一定生成文件)
-