Firefly开源社区

Makefile简介

600

积分

21

威望

25

贡献

技术大神

Rank: 3Rank: 3

积分
600

活跃会员

发表于 2014-10-22 19:55:59     
本帖最后由 linjc 于 2014-10-22 19:57 编辑

一、Makefile 的规则
target ... : prerequisites ...
command
.....
target 是目标文件,prerequisites 依赖文件,command 也就是make 需要执行的命令
test:main.o
    gcc -o test main.o
main.o:main.c
    gcc -c main.c -o main.o
clean:
    @rm -vf main.o test
使用:定义依赖关系;依赖的下行是要执行的编译或连接命令,必须以TAB开头;在clean其冒号后什么也没有,那么,make 就不会自动去找文件的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在 make 命令后明显得指出这个lable 的名字。这样的方法非常有用,我们可以在一个 makefile 中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。在默认的方式下,也就是我们只输入 make 命令。那么,
1、make 会在当前目录下找名字叫“Makefile”或“makefile”的文件。
2、如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“test”这个文件,并把这个文件作为最终的目标文件。
3、如果 test文件不存在,或是 test所依赖的后面的 .o 文件的文件修改时间要比test这个文件新,那么,他就会执行后面所定义的命令来生成 test这个文件。
4、如果 test所依赖的.o 文件也存在,那么 make 会在当前文件中找目标为.o 文件的依赖性,如果找到则再根据那一个规则生成.o 文件。(这有点像一个堆栈的过程)
5、当然,你的 C 文件和 H 文件是存在的啦,于是 make 会生成 .o 文件,然后再用 .o 文件生命 make 的终极任务,也就是执行文件 edit 了。

这 就是整个 make 的依赖性,make 会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么 make 就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make 根本不理。make 只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。通过上述分析,我们知道,像 clean 这种,没有被第一个目标文件直接或间接关联,
那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要 make 执行。即命令——“make clean”,以此来清除所有的目标文件,以便重编译。GNU 的 make 很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为,我们的 make 会自动识别,并自己推导命令。只要 make 看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果 make 找到 一 个 whatever.o , 那么whatever.c , 就会是whatever.o 的 依 赖 文 件 。并且 cc -cwhatever.c 也会被推导出来。
二、Makefile详述
Makefile 里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。
1、显式规则。显式规则说明了,如何生成一个或多的个目标文件。这是由 Makefile 的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
2、隐晦规则。由于我们的 make 有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写 Makefile,这是由 make 所支持的。
3、变量的定义。在Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点像C语言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
4、 文件指示。其包括了三个部分,一个是在一个 Makefile 中引用另一个 Makefile,就像C 语言中的 include 一样;另一个是指根据某些情况指定 Makefile 中的有效部分,就像C语言中的预编译#if 一样;还有就是定义一个多行的命令。引 用其它的 Makefile:在 Makefile 使 用 include 关 键 字 可以 把 别 的 Makefile包 含 进 来 , 这 很 像 C 语 言 的#include,被包含的文件会原模原样的放在当前文件的包含位置。include 的语法是:
include <filename>
filename 可以是当前操作系统 Shell 的文件模式(可以保含路径和通配符)在 include 前面可以有一些空字符,但是绝不能是[Tab]键开始。include 和<filename>可以用一个或多个空格隔开。
5、注释。Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用“#”字符,这个就像 C/C++中的“//”一样。如果你要在你的 Makefile 中使用“#”字符,可以用反斜框进行转义,如:“\#”。
最后,还值得一提的是,在 Makefile 中的命令,必须要以[Tab]键开始。这样,make的执行过程如下:
1、读入所有的 Makefile。
2、读入被 include 的其它 Makefile。
3、初始化文件中的变量。
4、推导隐晦规则,并分析所有规则。
5、为所有的目标文件创建依赖关系链。
6、根据依赖关系,决定哪些目标要重新生成。
7、执行生成命令。
三、使用通配符
如果我们想定义一系列比较类似的文件,我们很自然地就想起使用通配符。make 支持三各通配符:“*”,“?”和“[...]”。这是和 Unix 的 B-Shell 是相同的。
波浪号(“~”)字符在文件名中也有比较特殊的用途。如果是“~/test”,这就表示当前用户的$HOME 目录下的 test 目录。而“~hchen/test”则表示用户 hchen的宿主目录下的test 目录。(这些都是 Unix 下的小知识了,make 也支持)而在Windows 或是 MS-DOS下,用户没有宿主目录,那么波浪号所指的目录则根据环境变量“HOME”而定。通配符代替了你一系列的文件,如“*.c”表示所以后缀为 c 的文件。一个需要我们注意的是,如果我们的文件名中有通配符,如:“*”,那么可以用转义字符“\”,如“\*”来表示真实的“*”字符,而不是任意长度的字符串。好吧,还是先来看几个例子吧:
clean:
    rm -f *.o
上面这个例子我不不多说了,这是操作系统 Shell 所支持的通配符。这是在命令中的通配符。
print: *.c
lpr -p $?
touch print
上面这个例子说明了通配符也可以在我们的规则中,目标 print 依赖于所有的[.c]文件。其中的“$?”是一个自动化变量,表示被修改的文件。
objects = *.o
上面这个例子表示了通符同样可以用在变量中。并不是说[*.o]会展开,不!objects的值就是“*.o”。Makefile 中的变量其实就是 C/C++中的宏。如果你要让通配符在变量中展开,也就是让 objects 的值是所有[.o]的文件名的集合,那么,你可以这样:objects := $(wildcard *.o),这种用法由关键字“wildcard”指出。
四、路径的自动搜索
在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件分类,并存放在不同的目录中。所以,当 make 需要去找寻文件的依赖关系时,你可以在文件前加上路径,但最好的方法是把一个路径告诉 make,让 make 在自动去找。Makefile 文件中的特殊变量“VPATH”就是完成这个功能的,如果没有指明这个变量,make 只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make 就会在当当前目录找不到的情况下,到所指定的目录中去找寻文件了。
VPATH = src:../headers
上面的的定义指定两个目录,“src”和“../headers”,make 会按照这个顺序进行搜索。目录由“冒号”分隔。(当然,当前目录永远是最高优先搜索的地方)
五、伪目标
我们提到过一个“clean”的目标,这是一个“伪目标”,
clean:
正像我们前面例子中的“clean”一样,即然我们生成了许多文件编译文件,我们也应该提供一个清除它们的“目标”以备完整地重编译而用。 (以“make
clean”来使用该目标)因为,我们并不生成“clean”这个文件。“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以 make 无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了 “伪目标”的意义了。当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”,向 make 说明,不管是否有这个文件,这个目标就是“伪目标”。只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有“makeclean”这样。于是整个过程可以这样写:
.PHONY: clean
clean:
rm *.o temp
六、定义命令包
如果 Makefile 中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以“define”开始,以“endef”结
束,如:
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
这里,“run-yacc”是这个命令包的名字,其不要和 Makefile 中的变量重名。在“define”和“endef”中的两行就是命令序列。这个命令包中的第一个命令是运行 Yacc 程序,因为Yacc 程序总是生成“y.tab.c”的文件,所以第二行的命令就是把这个文件改改名字。还是把这个命令包放到一个示例中来看看吧。
foo.c : foo.y
$(run-yacc)
我们可以看见,要使用这个命令包,我们就好像使用变量一样。在这个命令包的使用中,命令包“run-yacc”中的“$^”就是“foo.y”,“$@”就 是“foo.c”(有关这种以“$”开头的特殊变量,我们会在后面介绍),make 在执行命令包时,命令包中的每个命令会被依次独立执行。
七、使用函数
函数调用,很像变量的使用,也是以“$”来标识的,其语法如下:
$(<function> <arguments> )
或是
${<function> <arguments>}
这里,<function>就是函数名,make 支持的函数不多。<arguments>是函数的参数,参数间以逗号“,”分隔,而函数名和参数之间以“空格”分隔。函数调用以“$”开头, 以圆括号或花括号把函数名和参数括起。感觉很像一个变量,是不是?函数中的参数可以使用变量,为了风格的统一,函数和变量的括号最好一样,如使用 “$(subst a,b,$(x))”这样的形式,而不是“$(subst a,b,${x})”的形式。因为统一会更清楚,也会减少一些不必要的麻烦。还是来看一个示例:
comma:= ,
empty:=
space:= $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
在这个示例中,$(comma)的值是一个逗号。$(space)使用了$(empty)定义了一个空格,$(foo)的值是“a b c”,$(bar)的定义用,调用了函数“subst”,这是一个替换函数,这个函数有三个参数,第一个参数是被替换字串,第二个参数是替换字串,第三个参数是替换操作作用的字串。这个函数也就是把$(foo)中的空格替换成逗号,所以$(bar)的值是“a,b,c”。
$(subst <from>,<to>,<text> )
名称:字符串替换函数——subst。
功能:把字串<text>中的<from>字符串替换成<to>。
返回:函数返回被替换过后的字符串。
$(patsubst <pattern>,<replacement>,<text> )
名称:模式字符串替换函数——patsubst。
功能:查找<text>中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换。这里,<pattern>可以包括通配符“%” ,表示任意长度的字串。如果<replacement> 中也包含“%” ,那么,<replacement>中的这个“%”将是<pattern>中的那个“%”所代表的字串。(可以用“\”来转义,以“\%”来表示真实含义的“%”字符)
返回:函数返回被替换过后的字符串。
$(strip <string> )
名称:去空格函数——strip。
功能:去掉<string>字串中开头和结尾的空字符。
返回:返回被去掉空格的字符串值。
$(findstring <find>,<in> )
名称:查找字符串函数——findstring。
功能:在字串<in>中查找<find>字串。
返回:如果找到,那么返回<find>,否则返回空字符串。
过滤函数——filter
反过滤函数——filter-out
排序函数——sort
取单词函数——word
取单词串函数——wordlist
单词个数统计函数——words
首单词函数——firstword
取目录函数——dir
取后缀函数——suffix
加后缀函数——addsuffix
连接函数——join
foreach 函数,if函数
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 命令开始时,会把找寻include 所指出的其它Makefile,并把其内容安置在当前的位置。
八、隐式规则
1、编译 C 程序的隐含规则。
“<n>.o”的依赖文件会自动推导为“<n>.c”,并且其生成命令“$(CC) –c $(CPPFLAGS) $(CFLAGS)”
2、编译 C++程序的隐含规则。
“<n>.o”的依赖目标会自动推导为“<n>.cc”或“<n>.C”,并且其生成命令是“$(CXX) –c $(CPPFLAGS) $(CFLAGS)”。(建议使用“.cc”作为 C++源文件的后缀,而不是“.C”)
3、编译 Pascal 程序的隐含规则。
“<n>.o”的依赖目标会自动推导为“<n>.p”,并且其生成命令是“$(PC) –c $(PFLAGS)”
4、编译 Fortran/Ratfor 程序的隐含规则。
“<n>.o”的依赖目标会自动推导为“<n>.r”或“<n>.F”或“<n>.f”,并且其生成命令是:
“.f” “$(FC) –c $(FFLAGS)”
“.F” “$(FC) –c $(FFLAGS) $(CPPFLAGS)”
“.f” “$(FC) –c $(FFLAGS) $(RFLAGS)”
5、预处理 Fortran/Ratfor 程序的隐含规则。
“<n>.f”的依赖目标会自动推导为“<n>.r”或“<n>.F”。这个规则只是转换Ratfor或有预处理的 Fortran 程序到一个标准的 Fortran 程序。其使用的命令:
“.F” “$(FC) –F $(CPPFLAGS) $(FFLAGS)”
“.r” “$(FC) –F $(FFLAGS) $(RFLAGS)”
6、编译 Modula-2 程序的隐含规则。
“<n>.sym”的依赖目标会自动推导为“<n>.def”,并且其生成命令是:“$(M2C) $(M2FLAGS) $(DEFFLAGS)”。
“<n.o>” 的目标的依赖目标会自动推导为“<n>.mod”,并且其生成命令是:“$(M2C) $(M2FLAGS) $(MODFLAGS)”。
7、汇编和汇编预处理的隐含规则。
“<n>.o” 的目标的依赖目标会自动推导为“<n>.s”,默认使用编译“as”,并且其生成命令是:“$(AS) $(ASFLAGS)”。
“<n>.s” 的目标的依赖目标会自动推导为“<n>.S”,默认使用 C 预编译器“cpp”,并且其生成命令是:“$(AS) $(ASFLAGS)”。
8、链接 Object 文件的隐含规则。
“<n>”目标依赖于“<n>.o”,通过运行 C 的编译器来运行链接程序生成(一般是“ld”),其生成命令是:“$(CC) $(LDFLAGS) <n>.o $(LOADLIBES) $(LDLIBS)”。这个规则对于只有一个源文件的工程有效,同时也对多个 Object 文件(由不同的源文件生成)的也有效。例如如下规则:
x : y.o z.o并且“x.c”、“y.c”和“z.c”都存在时,隐含规则将执行如下命令:
cc -c x.c -o x.o
cc -c x.c -o x.o
cc -c y.c -o y.o
cc -c z.c -o z.o
cc x.o y.o z.o -o x
如果没有一个源文件(如上例中的 x.c)和你的目标名字(如上例中的 x)相关联,那么,你最好写出自己的生成规则,不然,隐含规则会报错的。
9、Yacc C 程序时的隐含规则。
“<n>.c”的依赖文件被自动推导为“n.y”(Yacc生成的文件) ,其生成命令是 :
“$(YACC) $(YFALGS)”
10、Lex C 程序时的隐含规则。
“<n>.c”的依赖文件被自动推导为“n.l”(Lex 生成的文件),其生成命令:“$(LEX)$(LFALGS)”。
11、Lex Ratfor 程序时的隐含规则。
“<n>.r”的依赖文件被自动推导为“n.l”(Lex 生成的文件),其生成命令是:“$(LEX) $(LFALGS)”。
12、从 C 程序、Yacc 文件或 Lex 文件创建 Lint 库的隐含规则。
“<n>.ln” (lint生成的文件)的依赖文件被自动推 导 为“n.c”其生成命令 是:“$(LINT) $(LINTFALGS) $(CPPFLAGS) -i”。
对于“<n>.y”和“<n>.l”也是同样的规则。
九、自动化变量
$@
表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,"$@"就是匹配于目标中模式定义的集合。
$%
仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是"foo.a(bar.o)",那么,"$%"就是"bar.o","$@"就 是"foo.a"。如果目标不是函数库文件(Unix下是[.a],Windows 下是[.lib]),那么,其值为空。
$<
依赖目标中的第一个目标名字。如果依赖目标是以模式(即"%")定义的,那么"$<"将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
$?
所有比目标新的依赖目标的集合。以空格分隔。
$^
所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。
$+
这个变量很像"$^",也是所有依赖目标的集合。只是它不去除重复的依赖目标。
$*
这个变量表示目标模式中"%"及其之前的部分。如果目标是"dir/a.foo.b",并且目标的模式是"a.%.b",那么,"$*"的值就是"dir /a.foo"。这个变量对于构造有关联的文件名是比较有较。如果目标中没有模式的定义,那么"$*"也就不能被推导出,但是,如果目标文件的后缀是 make 所识别的,那么"$*"就是除了后缀的那一部分。例如:如果目标是"foo.c",因为".c"是 make 所能识别的后缀名,所以,"$*"的值就是"foo"。这个特性是 GNU make 的,很有可能不兼容于其它版本的 make,所以,你应该尽量避免使用"$*",除非是在隐含规则或是静态模式中。如果目标中的后缀是 make 所不能识别的,那么"$*"就是空值。
下面给出一个完整的Makefile
十、例子
1.相关文件:
func1.c 文件
#include "head.h"
#include "head1.h"
func2.c 文件
#include "head.h"
#include "head2.h"
main.c 文件
#include "head.h"

2.最普通的写法:
exe: main.o func1.o func2.o
    gcc -o exe main.o func1.o func2.o
main.o:main.c head.h
    gcc -c main.c
func1.o:func1.c head.h head1.h
    gcc -c func1.c
func2.o:func2.c head.h head2.h
    gcc -c func2.c
clean:
    rm -f *.o exe

3.使用变量:
OBJS = main.o func1.o func2.o
exe: $(OBJS)
    gcc -o exe $(OBJS)
main.o:main.c
    gcc -c main.c
func1.o:func1.c
    gcc -c func1.c
func2.o:func2.c
    gcc -c func2.c
clean:
    rm -f $(OBJS) exe

4.最终:
OBJS = main.o func1.o func2.o
exe: $(OBJS)
    gcc -o exe $(OBJS)
clean:
    rm -f $(OBJS) exe


回复

使用道具 举报

63

积分

0

威望

0

贡献

技术小白

积分
63
发表于 2014-10-23 10:56:44     
不错,学习了。:lol
回复

使用道具 举报

发表于 2014-10-23 11:19:15     
支持原创技术贴!
暴走的创客!
回复

使用道具 举报

48

积分

0

威望

0

贡献

技术小白

积分
48
发表于 2016-11-23 20:53:27     
谢谢分享啊
回复

使用道具 举报

58

积分

0

威望

0

贡献

技术小白

积分
58
发表于 2016-11-28 15:30:18     
不错,顶给有需要的朋友
回复

使用道具 举报

58

积分

0

威望

0

贡献

技术小白

积分
58
发表于 2016-11-28 15:30:37     
不错,顶给有需要的朋友
回复

使用道具 举报

79

积分

0

威望

0

贡献

技术小白

积分
79
发表于 2017-2-4 10:03:33     
学习
回复

使用道具 举报

116

积分

0

威望

0

贡献

技术小白

积分
116
发表于 2017-7-30 22:28:49     
不错,顶给有需要的朋友
回复

使用道具 举报

12

积分

0

威望

0

贡献

游客

积分
12
发表于 2018-4-4 16:00:47     
写的很详细
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

友情链接 : 爱板网 电子发烧友论坛 云汉电子社区 粤ICP备14022046号-2
快速回复 返回顶部 返回列表