无论是C、C++、还是pas,首先要把源文件编译成中间代码文件,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。
链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。
总结一下,源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成Object File。而在链接程序时,链接器会在所有的Object File中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error)。
target:规则的目标。可以是 Object File,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,在后续的“伪目标”章节中会有叙述。
prerequisites:规则的依赖。生成规则目标所需要的文件名列表。通常一个目标依赖于一个或者多个文件。(.c文件 .h文件 所有的.o文件既是依赖(相对于可执行程序edit)又是目标(相对于.c和.h文件)。)
command:规则的命令行。是规则所要执行的动作(任意的 shell 命令或者是可在shell 下执行的程序)。它限定了 make 执行这条规则时所需要的动作。
一个规则可以有多个命令行,每一条命令占一行。 注意:每一个命令行必须以[Tab]字符开始,[Tab] 字符告诉 make 此行是一个命令行。make 按照命令完成相应的动作。这也是书写 Makefile中容易产生,而且比较隐蔽的错误。
命令就是在任何一个目标的依赖文件发生变化后重建目标的动作描述。
说白一点就是说,prerequisites中如果有一个以上的文件比 target 文件要新的话,command 所定义的命令就会被执行。这就是 Makefile 的规则。也就是Makefile 中最核心的内容
例如:
all:
echo "Hello World"
如上图所示的Makefile中,all就是一个目标,目标名是放在“:”前面的,名字可以由字母和下划线组成。这里的all目标是一个抽象的概念,在此应将其理解为“在终端上打印‘Hello World’”这一行为。" echo "Hello World""就是生成目标的命令。生成目标的命令可以是操作系统命令行中的命令或者make所定义的函数。在Linux操作系统中,echo命令的用处是将字符串打印到终端上,他与C语言中的printf()函数的功能很相似。
注明:
1)一个Makefile中可以定义多个目标。
2)调用make命令时,得告诉它我们希望它构建的目标是什么,即要它干什么。当没有指明具体的目标时,make将以文件中定义的第一个目标作为这次运行的目标。“第一个”目标,也被称为默认目标。
3)当make得到目标后,先找到构建目标的对应规则,然后运行规则中的命令来达到构建目标的目的。目前的Makefile中每个规则中都只有一条命令,实际上,一个规则中可以根据需要存在多条命令。
例如:
当不带参数运行make时,输出为:
Just for test !
Hello World
命令行输入make test,输出为:
Just for test !
从输出结果可以发现,当不带参数运行make时,构建all目标之前test目标也被构建了。
所以现在引入Makefile中的依赖关系这一概念。
上例中all目标后面的test是告诉make,all目标依赖于test目标,这个依赖目标又被称为(all目标的)先决条件(prerequisite)。
一个规则是由目标、先决条件以及命令组成的。需要指出的是,目标和先决条件之间表达的就是依赖关系(dependency),这种依赖关系指明在构建目标之前,必须保证先决条件先满足(即先被构建)。
一个规则可以定义多个目标,当一个规则中存在多个目标且这个规则是Makefile中的第一个规则时,如果运行make命令不带任何目标,那么规则中的第一个目标将被视为默认目标。
书写时,可以将一个较长行使用反斜线(\)来分解为多行,这样可以使我们的Makefile书写清晰、容易阅读理解。但需要注意:反斜线之后不能有空格(这也是大家最容易犯的错误,错误比较隐蔽)。
命令行必需以[Tab]键开始,以和Makefile其他行区别。 就是说所有的命令行必需以[Tab] 字符开始,但并不是所有的以[Tab] 键出现行都是命令行。但make 程序会把出现在第一条规则之后的所有以[Tab] 字符开始的行都作为命令行来处理。
(记住:make程序本身并不关心命令是如何工作的,对目标文件的更新需要你在规则描述中提供正确的命令。“make”程序所做的就是当目标程序需要更新时执行规则所定义的命令)。
Makefile中把那些没有任何依赖只有执行动作的目标称为“伪目标”(phony targets)。
对这些.o 文件为目标的规则处理有下列三种情况:
1. 目标.o 文件不存在,使用其描述规则创建它;
2. 目标.o 文件存在,目标.o 文件所依赖的.c 源文件、.h 文件中的任何一个比目标.o
文件“更新”(在上一次 make 之后被修改)。则根据规则重新编译生成它;
3. 目标.o 文件存在,目标.o 文件比它的任何一个依赖文件(的.c 源文件、.h 文件)
“更新”(它的依赖文件在上一次 make 之后没有被修改),则什么也不做。
编写Makefile的第一步,不是一个猛子扎进去试着写一个规则并对之调试,而应该先采用面向依赖关系的思考方法勾勒出makefile要表达怎样的依赖关系,这一点尤为重要。通过不断地练习这种思考方法,才可能达到流畅地编写makefile的能力。
上例的一个依赖关系类图为:
表示simple可执行文件是通过main.c 和 foo.c 编译生成的。通过这个依赖关系图就可以写出一个Makefile来了,但根据这个依赖关系所写出来的Makefile在显示项目中的可维护性很差。
怎样的依赖关系能使我们写出更具维护性的Makefile呢?下图实现了对依赖关系的更精确的表达,其中可以看到目标文件的身影。通过添加源程序文件所对应的目标文件,将有助于写出表达能力更强的Makefile,这也意味着所获得的Makefile更具有可维护性。
通过这个依赖关系图编写Makefile为:
1 all:main.o foo.o
2 gcc -o app main.o foo.o
3 main.o:main.c
4 gcc -o main.o -c main.c
5 foo.o:foo.c
6 gcc -o foo.o -c foo.c
7 clean:
8 rm app main.o foo.o
下图为依赖关系与规则间的映射。
分别编写好两个源文件之后,执行效果如下:
为什么第二次make还是又生成了app这个可执行文件呢?
make是通过文件的时间戳来判定哪些文件需要编译的,所以如果电脑系统时间变更,将可能影响make的执行。由于第二次make的时候,虽然foo和main的目标文件已经生成了,但all目标在编译过程中并不生成,所以在此执行make,又生成了app,更改makefile(红色部分为更改部分)如下:
1 app:main.o foo.o 2 gcc -o app main.o foo.o 3 main.o:main.c 4 gcc -o main.o -c main.c 5 foo.o:foo.c 6 gcc -o foo.o -c foo.c 7 clean: 8 rm app main.o foo.o
这样再有多次执行make并且文件时间戳没有改变时,make会提示你:
make: 'app' is up to date.//app已经是最新的。
我们对foo.c做点改动,看make是否可以发现我们的改动并重新编译,注意make是按照时间戳来判定文件是否有改动的,touch命令可以改变文件的时间戳,这相当于对文件做了一次修改。
touch:将文件的访问及修改时间都更新成目前时间,如果文件不存在,则创建一个字节数为0的空文件。
可以看到touch之后时间戳改变,然后我们make试试。
果然,make察觉到了foo.c的更改,并重新构建了。
现在的makefile虽然能够工作,但不够灵活,下面的文章让makefile更专业。
让你的makefile更专业。
在上一个Makefile所在目录下通过touch命令创建一个clean文件,然后执行make clean,将发现make总是提示clean文件是最新的,而不是按我们期望的那样对项目文件进行清楚操作。
make这样的行为,是因为它将clean当做文件来处理,在当前目录下找到了clean文件,而且clean目标没有任何先决条件,所以当我们要求make构建clean目标时它会认为clean文件是最新的,从而拒绝我们真正的文件清除操作。
出现这种情形,是因为我们对clean目标的定义与make所理解的有出入。目录文件名与makefile的目标名重名在现实项目中是难免的,假目标(phony target)概念的提出正是为了解决这种问题的。
假目标采用 .PHONY关键字来定义,注意必须是大写字母。运用假目标之后,更改makefile并运行如下:
.PHONY: clean app:main.o foo.o gcc -o app main.o foo.o main.o:main.c gcc -o main.o -c main.c foo.o:foo.c gcc -o foo.o -c foo.c clean: rm -rf app main.o foo.o
Linux下一切皆为文件。
采用.PHONY关键字声明一个目标之后,make并不会将其当做一个文件来处理。可以想象,由于假目标并不与文件关联,所以每次构建假目标时它所在规则中的命令一定会被执行。拿这里的clean目标做比方,即使多次执行make clean,make每次都会执行文件清楚操作。
运用变量提高可维护性:
编写专业的makefile离不开变量,通过使用变量可以使得makefile更具可维护性。
运用变量改写第一个makefiel。
.PHONY: clean CC = gcc RM = rm EXE =simple OBJS =main.o foo.o $(EXE): $(OBJS) $(CC) -o $(EXE) $(OBJS) main.o:main.c $(CC) -o main.o -c main.c foo.o:foo.c $(CC) -o foo.o -c foo.c clean: $(RM) -rf $(EXE) $(OBJS)
引入变量之后,如果需要更改编译器,只需要更改赋值变量的地方,其实相当于C语言宏定义的作用,便于更改移植。
上面的makefile,存在目标名和先决条件名在规则中重复出现,如果目标名或先决条件发生了改变,那么得在相应的命令中跟着更改这个很麻烦,为了省去这种麻烦,我们借助于如下一些自动变量:
除了这三个自动变量外,在makefile中还可以使用其他的自动变量,后面需要使用到的时候再提及。目前simple项目用这三个变量就足够了。
用上面的变量测试上面的Makefile。
.PHONY: all all:first second third @echo "\$$@ = $@" @echo "$$^ = $^" @echo "$$< = $<" first second third:
在Makefile中,“$”具有特殊的意思,如果想采用echo输出“$”,则必须用两个连着的“$”;
“$@” 对于Bash Shell 也有着特殊的意思,需要在“$$@”之前加一个反斜杠“\”(引号不包括在内)。
最后一行是一个只有目标的规则,如果除去它会有什么问题呢?读者可以自己试试。
注释(makefile中用#表示注释,需要注释多行,在注释行的末尾加上反斜杠"\",下一行也会被注释)最后一行之后报错如上图。显示没有规则创建上述目标。因为all的先决条件决定了构建all目标之前必须先构建first ,而first如果不存在,报错也是应该的。
采用自动变量之后运行结果的Makefile如下所示:
.PHONY: clean CC = gcc RM = rm EXE =simple OBJS =main.o foo.o $(EXE): $(OBJS) $(CC) -o $@ $^ main.o:main.c $(CC) -o $@ -c $^ foo.o:foo.c $(CC) -o $@ -c $^ clean: $(RM) -rf $(EXE) $(OBJS)
特殊变量:
在Makefile中,有两个特殊变量会经常用到:MAKE和MAKECMDGOALS。MAKE变量表示的是当前处理Makefile的命令名是什么。当需要在Makefile中运行另一个Makefile时,需要用到这个变量。
1 .PHONY: all 2 all: 3 @echo "MAKE = $(MAKE)"
MAKECMDGOALS变量表示的是当前构建的目标名。
1 .PHONY: all clean 2 all clean: 3 @echo "\$$@ = $@" 4 @echo "MAKECMDGOALS =$(MAKECMDGOALS)"
运行结果为:
从测试结果来看,MAKECMDGOALS变量指的是用户输入的目标,当只运行make命令且不带参数时,虽然根据Makefile的语法规则,Makefile中的第一个目标即为默认目标,即all目标,但MAKECMDGOALS在这里例外,MAKECMDGOALS此时是空而不是all目标,这一点需要注意。
运行make时可以同时指定多个目标。make在获得了多个目标后,将以从左到右的顺序逐个地构建目标。
CURDIR:是make的内嵌变量,指当前目录。
echo $(CURDIR)可以测试makefile的当前目录。