feisky

云计算、虚拟化与Linux技术笔记
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Makefile全解析

Posted on 2009-11-16 22:44  feisky  阅读(3783)  评论(0编辑  收藏  举报

什么是 makefile

或许很多 Winodws 的程序员都不知道这个东西,因为那些 Windows的 IDE 都为你做了这 个工作,但我觉得要作一个好的和 professional 的程序员,makefile 还是要懂。这就好像现在有这么多的 HTML 的编辑器,但如果你想成为一个专业人士,你还是要了解 HTML 的标识的含义。特别在 Unix 下的软件编译,你就不能不自己写 makefile 了,会不会写 makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。

程序的编译连接

源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成 Object File。而在链接程序时,链接器会在所有的 ObjectFile 中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error),在 VC下,这种错误一般是:Link 2001 错误,意思说是说,链接器未能找到函数的实现。你
需要指定函数的 Object File.

Makefile 的规则

target ... : prerequisites ...
command
...
...

target 也就是一个目标文件,可以是 Object File,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,在后续的“伪目标”章节中会有叙述。
prerequisites 就是,要生成那个 target 所需要的文件或是目标。
command 也就是 make 需要执行的命令。(任意的 Shell 命令)如果命令太长,你可以使用反斜框(‘\’)作为换行符。一般来说,make 会以 UNIX 的标准 Shell,也就是/bin/sh 来执行命令。

一个简单Makefile的解析

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 也会被推导出来。

使用变量

本例中只是用到了一个收入文件,如果以来文件比较多,为了便于输入和引用,可以使用定义变量,如SRC=main.c,这样以后就可以使用$(SRC)来代替main.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 说明,不管是否有这个文件,这个目标就是“伪目标”。
.PHONY : clean
只要有这个声明,不管是否有“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) $(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函数

常有的隐含规则

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 y.c -o y.o
cc -c z.c -o z.o
cc x.o y.o z.o -o x
rm -f x.o
rm -f y.o
rm -f z.o
如果没有一个源文件(如上例中的 x.c)和你的目标名字(如上例中的 x)相关联,那
么,你最好写出自己的生成规则,不然,隐含规则会报错的。
9、Yacc C 程序时的隐含规则。
“<n>.c” 的 依 赖 文件 被 自动 推 导 为 “ n.y” ( Yacc 生 成的 文件 ) ,其 生成 命 令是 :
“$(YACC) $(YFALGS)”。(“Yacc”是一个语法分析器,关于其细节请查看相关资料)
10、Lex C 程序时的隐含规则。
“<n>.c”的依赖文件被自动推导为“n.l”(Lex 生成的文件),其生成命令是:“$(LEX)
$(LFALGS)”。(关于“Lex”的细节请查看相关资料)
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

 

一般项目文件的Makefile:

EXEC = file
OBJS = file.o
SRC  = file.c

CC = gcc
LD = ld
CFLAGS += -O2 -Wall
LDFLAGS += 

all:$(EXEC)

$(EXEC):$(OBJS)
 $(CC) $(LDFLAGS) -o $@ $(OBJS)

%.o:%.c
 $(CC) $(CFLAGS) -c $< -o $@

clean:
 @rm -vf $(EXEC) *.o *~


设备驱动的Makefile:

EXEC = key.ko 
OBJS = key.o 
SRC  = key.c 

INCLUDE = /usr/src/kernel/include
USEINC =.
CC = arm-linux-gcc
LD = arm-linux-ld
MODCFLAGS = -O2 -Wall -D__KERNEL__ -DMODULE -I$(INCLUDE) -I$(USEINC) -march=armv4t -c -o
LDFLAGS = -r

all: $(EXEC)

$(EXEC): $(OBJS)
 $(LD) $(LDFLAGS) -o $@ $(OBJS)

%.o:%.c
 $(CC) $(MODCFLAGS) -mapcs -c $< -o $@

clean:
 -rm -f $(EXEC) *.o *~ core
无觅相关文章插件,快速提升流量