Makefile基本格式
make找到的第一项规则会被当做默认规则使用。
一个规则可分成三个部分:
targets : prerequisites
command
...
或是这样:
targets : prerequisites ; command
command
...
- targets: 是文件名,以空格分开,可以使用通配符。一般来说,我们的目标基本上是一个文件,但也有可能是多个文件。
- command: 是命令行,如果其不与“target:prerequisites”在一行,那么,必须以
[Tab]
键开头,如果和prerequisites在一行,那么可以用分号做为分隔。 - prerequisites: 也就是目标所依赖的文件(或依赖目标)。如果其中的某个文件要比目标文件要新,那么,目标就被认为是“过时的”,被认为是需要重生成的。这个在前面已经讲过了。
如果命令太长,你可以使用反斜框'\'
作为换行符。规则告诉make两件事,文件的依赖关系和如何成成目标文件。一般来说,make会以UNIX的标准Shell,也就是/bin/sh
来执行命令。
一个简单的例子
创建一个count_word.c和一个lexer.l,创建makefile文件内容为:
count_words: count_words.o lexer.o -lfl
gcc count_words.o lexer.o -lfl -o count_words
count_words.o: count_words.c
gcc -c count_words.c
lexer.o: lexer.c
gcc -c lexer.c
lexer.c: lexer.l
flex -t lexer.l > lexer.c
clean:
rm lexer.c lexer.o count_words.o count_words
直接输入make命令就可以生成可执行文件count_words了,如果要删除执行文件和中间的目标文件,那么就执行一下make clean。
注意:
- 1 当依赖关系定好后,下面一行就是如何生成目标文件的操作系统命令了,一定要以一个Tab键开头。 另外,make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期比targets文件新,或者targets不存在,那么make就会执行这下面一行的系统命令。
- 2 clean不是一个文件,它是一个动作名,冒号后面什么都没有,make就不会自动去找它的依赖性,也不会执行它后面的系统命令。因此,要执行clean就需要显式的指出make clean。
- 3 如果报错,可能需要先安装flex。
make如何工作
默认方式,直接输入make,则:
- 1 make会在当前的目录下找到名为“Makefile”或者“makefile”的文件。
- 2 如果找到,它会把文件中第一个target作为最终的目标文件(如上面例子中的count_words)。
- 2.1 首先,make会检查目标count_words的prerequisite文件count_words.o, lexer.o 和 -lfl。
- 2.2 count_words.o通过编译count_words.c生成
- 2.3 lexer.o通过编译lexer.c 生成,但是lexer.c 并不存在,因此会继续寻找lexer.c的生成方式,并找到了通过flex程序将lexer.l生成为lexer.c。
- 2.4 最后,make会检查-lfl,-l是gcc的一个命令选项,表示将系统库链接到程序。而”fl”对应的是libfl.a的库。(GNU make 可以识别这样的命令,当一个prerequisite是以这种-l
的形式表示出来的时候,make会自己搜索lib .so的库文件,如果没找到则继续搜索lib .a的库文件)。这里make找到的是/usr/lib/libfl.a文件,并将它与程序进行连接。
- 3 如果count_words文件不存在,或者count_words所依赖的后面的.o文件的修改时间比count_words本身更加新,那么,它会执行后面定义的命令来生成这个count_words文件。如果count_words所依赖的.o文件也不存在,那么make会继续按照前面的方式生成.o文件。
- 4 找到相应的.c和.h,用来生成.o,然后再用.o完成make的最终任务。
依赖关系
make会一层一层的去找文件的依赖关系,最终编译出第一个目标文件。
重新编译
只要任何prerequisite 比 target新,那么这个目标文件就会被下面的命令重新生成。每一个命令都会被传递到shell中,并在自己的子shell里面执行。
关于错误
如果在寻找过程中出现错误,如文件找不到,则make会直接退出并报错。对于所定义的命令错误或者编译不成功,make是不会理会的,它只负责文件的依赖性。
变量使用
为了让makefile更容易维护,在makefile中我们可以使用变量,或者更确切的说是一个字符串,类似c语言中的宏。例如:
CC = gcc
object = lexer.o count_words.o
count_words: $(object) -lfl
$(CC) $(object) -lfl -o count_words
count_words.o: count_words.c
$(CC) -c count_words.c
lexer.o: lexer.c
$(CC) -c lexer.c
lexer.c: lexer.l
flex -t lexer.l > lexer.c
clean:
rm lexer.c $(object) count_words
自动推导依赖关系
make可以根据.o文件的文件名自动推导出同名的.c文件并加入依赖关系。并且gcc -c也会被自动推导出来,这种方法也叫“隐式规则”,于是我们的makefile就变成了:
CC = gcc
object = lexer.o count_words.o
count_words: $(object) -lfl
$(CC) $(object) -lfl -o count_words
count_words.o:
lexer.o:
lexer.c: lexer.l
flex -t lexer.l > lexer.c
clean:
rm lexer.c $(object) count_words
关于Clean
一个好习惯是每个makefile都要写clean规则,这样不仅可以方便重编译,也有利于保持文件路径的清洁。一般的风格是:
clean:
rm lexer.c $(object) count_words
书写规则
规则包含两个部分,一个是依赖关系,一个是生成目标的方法。在 Makefile 中,规则的顺序是很重要的,因为,Makefile 中只应该有一个最终目标,其它的目标都是被这个目标所连带出来的,所以一定要让 make 知道你的最终目标是什么。一般来说,定义在 Makefile 中的目标可能会有很多,但是第一条规则中的目标将被确立为最终的目标。如果第一条规则中的目标有很多个,那么,第一个目标会成为最终的目标。 make所完成的也就是这个目标。
通配符
make支持的通配符与Bourne shell基本相同,包括~, *, ?, [...], [^...]
*.*
:表示了所有文件;?
:表示任意单个字符;[...]
:表示一个字符类;[^...]
:表示相反的字符类。~
:表示当前用户的/home路径,”~+用户名”可以表示该用户的/home路径。
注意: make会在读取makefile的时候就自动补充好通配符替代的内容,而shell则是在执行命令的时候才会进行通配符替代,在某些复杂情况,这两种方式会有极大的区别。
伪目标
make提供了一种特殊目标: “.PHONY”,用来表示目标文件不是真正的文件,即伪目标。clean命令可以被写作:
.PHONY: clean
clean:
rm -f *.o lexer.c
即使名为clean的文件存在,make也会执行clean后面的命令,不会造成误解。rm命令前面的减号则表示,不管出现什么问题都要继续做后面的事情。clean规则不要放在makefile的开头,不然就会变成make的默认目标了。
伪目标也可也作为makefile的默认目标,放在文件的最前端,由于伪目标的特性,他指出的所有prerequisite都会被重新编译。这样可以用来同时生成多个目标。另外,与普通目标文件一样,伪目标也可也使用依赖关系,例如:
.PHONY: clean cleano cleanc
clean: cleano cleanc
-rm $(program)
cleano:
-rm *.o
cleanc:
-rm lexer.c
这样就可以对不同类型的文件进行单独删除。
多目标
Makefile 的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。于是我们就能把其合并起来。
bigoutput littleoutput: text.g
generate text.g -$(subst output,,$@) > $@
上述规则等价于:
bigoutput: text.g
generate text.g -big > bigoutput
littleoutput: text.g
generate text.g -little > littleoutput
其中,-$(subst output,,$@)
这个函数是截取字符串的意思,“$@”
表示目标的集合,就像一个数组,“$@”
依次取出目标,并执于命令。
静态模式
静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活 。我们还是先来看一下语法:
<targets ...>: <target-pattern>: <prereq-patterns ...>
<commands>
....
- targets 定义了一系列的目标文件,可以有通配符,是目标的一个集合。
- target-parrtern 是指明了targets的模式,也就是的目标集模式。
- prereq-parrterns 是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。
看一个例子:
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
上面的例子中,指明了我们的目标从$object
中获取;目标集模式“%.o”
表明要所有以“.o”
结尾的目标,也就是“foo.o bar.o”
;依赖模式“%.c”
则取模式“%.o”
的“%”
,也就是“foo bar”,并为其加下“.c”的后缀,于是,依赖目标就是“foo.c bar.c”。
于是,上面的规则展开后等价于下面的规则:
foo.o : foo.c
$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(CC) -c $(CFLAGS) bar.c -o bar.o
查找文件
通常情况下,头文件会放到include下,源文件被放到src,将Makefile放在根目录下,为了让make能够找到相应的位置,需要在makefile开头添加VPATH参数,显式的指出源文件和头文件的路径,不仅make需要知道路径,gcc同样需要:
VPATH = src include
CPPFLAGS = -I include
注意:
1 VPATH变量可以包含一个路径列表,当make需要一个文件时会在其中搜索。这个列表既可以作为目标文件,也可作为关联文件的路径,但不能作为下面命令行程序中文件的路径。这正是为什么在命令行程序中使用自动化变量的原因,避免因为路径修改而导致的命令运行错误。
2 如果是因为make的相关路径配置错误,会使make停止运行;但如果是因为gcc的头文件路径配置错误,会使gcc编译停止。
3 在UNIX系统中,路径列表可以被空格或者冒号分隔开,在Windows中则是用空格或者分号。
4 make会在每次需要文件的时候搜索VPATH列表中的路径,如果有两个不同路径下文件重名,则make只会使用顺序查找到的第一个。
更加准确的方式是使用vpath变量,它的语法是:
vpath pattern directory-list
因此,上面makefile中的VPATH可以写做:
vpath %.c src
vpath %.l src
vpath %.h include
这样就告诉了make去src中寻找.c和.l文件,去include中寻找.h文件。
变量
变量最简单的形式就是 $(variable_name)
。变量可以包含几乎所有的字符包括标点符号。一般情况下,变量名需要被$()
所包裹,但是当变量名只有一个字符时,括号可以省略。如果要使用真实的“$”
字符,用“$$”
来表示。
变量赋值可以使用“=”或者“:=”,主要区别是,当用一个变量给别一个变量赋值值时,“:=”只能使用已经定义好的变量,而“=”可使用后面定义的变量。
obj = lexer.o
txt := $(obj)/txt
obj += count_words.o
自动变量
Makefile可以定义很多变量,但同时make本身也定义了一些自动变量。自动变量是make自动根据规则生成的,不需要用户显式的指出相应的文件或目标名称。以下是几个核心的自动变量:
$@
:目标文件的文件名;$%
:仅当目标文件为归档成员文件(.lib 或者 .a)时,显示文件名,否则为空;$<
:依赖(prerequisite)列表里面的第一个文件名;$?
:所有在prerequisite列表里面比当前目标新的文件名,用空格隔开;$^
:所有在prerequisite列表中的文件,用空格隔开; 如果有重复的文件名(包含扩展名),会自动去除重复;$+
:与$^相似,也是prerequisite列表中的文件名用空格隔开,不同的是这里包含了所有重复的文件名;$*
:显示目标文件的主干文件名,不包含后缀部分。
此外,上面的每个变量都带有两个不同的变种,用于适应不同种类的make。分别是在后面附加一个“D”或者“F”。例如,$(^D)
就是代表所有依赖文件的路径,$(<F)
表示依赖文件第一个的文件部分的值。使用上述内容前面的makefile可以重写为:
CC = gcc
object = lexer.o count_words.o
program = count_words
$(program): size $(object) -lfl
$(CC) $(object) -lfl -o $@
count_words.o: count_words.c
$(CC) -c $^
lexer.o: lexer.c
$(CC) -c $^
lexer.c: lexer.l
flex -t $^ > $@
size: count_words.o
size $^
touch size
.PHONY: clean cleano cleanc
clean: cleano cleanc
-rm $(program)
cleano:
-rm *.o
cleanc:
-rm lexer.c
目标变量
只在一个Target时才起作用,相当于局部变量。
如下示例:
- make: 输出Test
- make test: 依次输出Prog和Test
- make prog: 输出Prog
prog: A = prog
A = test
default:
@echo $(A)
test: prog
@echo $(A)
prog:
@echo $(A)
高级用法
这里介绍两种变量的高级使用方法。
- 1 变量值的替换
我们可以替换变量中的共有的部分,其格式是“$(var:a=b)”
或是“${var:a=b}”
,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束符”。一个示例:
foo:=a.o b.o c.o
bar:=$(foo:.o=.c)
这个示例中,我们先定义了一个“$(foo)”
变量,而第二行的意思是把“$(foo)”
中所有以“.o”字串“结尾”全部替换成“.c”,所以我们的“$(bar)”
的值就是“a.c b.c c.c”。
还有基于“静态模式”的变量替换:
foo:=a.o b.o c.o
bar:=$(foo:%.o=%.c)
这依赖于被替换字串中的有相同的模式,模式中必须包含一个“%”
字符,这个例子同样让$(bar)
变量的值为“a.c b.c c.c”。
- 2 把变量的值再当成变量
x = y
y = z
a := $($(x))
在这个例子中,$(x)
的值是“y”
,所以$($(x))
就是$(y)
,于是$(a)
的值就是“z”
。(注意,是“x=y”
,而不是“x=$(y)”
)
定义命令包
如果Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以“define”开始,以“endef”结束,如:
define run_yacc
yacc $(firstword$^)
mv y.tab.c $@
endef
这里,“run_yacc”是这个命令包的名字,不要和Makefile中的变量重名。
define本质上是定义一个多行变量,可以在call的作用下当作函数来使用,在其它位置使用只能作为多行变量的使用。其实单行变量也可以在call下当作函数使用,并且使用$(1)、$(2)
实现函数参数传递。
list_file = $(filter $(1),$(2))
bootfiles = $(call list_file,boot,*.c))
条件判断
条件表达式的语法为:
<conditional-directive>
<text-if-true>
endif
以及:
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif
其中<conditional-directive>
表示条件关键字,有四个:
- ifeq (
, ): 比较参数“arg1”和“arg2”的值是否相同。 - ifneq (
, ): 比较参数“arg1”和“arg2”的值是否相同,如果不同,则为真,和“ifeq”类似。 - ifdef
: 如果变量variable-name的值非空,那到表达式为真。否则,表达式为假。当然, 同样可以是一个函数的返回值。注意,ifdef只是测试一个变量是否有值,其并不会把变量扩展到当前位置。 - ifndef
: 和“ifdef”相反。
函数
在Makefile中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。函数调用后,函数的返回值可以当做变量来使用。函数调用,很像变量的使用,也是以“$”
来标识的,其语法如下:
$(<function> <arguments>)
或是:
${<function> <arguments>}
这里,function就是函数名,arguments是函数的参数,参数间以逗号“,”分隔,而函数名和参数之间以“空格”分隔。
字符串处理函数
- subst: 字符串替换
- patsubst: 模式字答串替换
- strip: 去除空格
- findstring: 查找字符串函数
- filter: 过滤函数
- filter-out: 反过滤函数
- sort: 排序函数
- word: 取单词函数
- wordlist: 取单词串函数
- words: 单词个数统计函数
- firstword: 首单词函数
文件名操作函数
- dir: 取目录函数
- notdir: 取文件函数
- suffix: 取后缀函数
- basename: 取前缀函数
- addsuffix: 加后缀函数
- addprefix: 加前缀函数
- join 连接函数
流程控制函数
- foreach函数
循环函数,它的语法是:
$(foreach<var>,<list>,<text>)
这个函数的意思是,把参数list中的单词逐一取出放到参数var所指定的变量中,然后再执行text所包含的表达式。每一次text会返回一个字符串,循环过程中,text的所返回的每个字符串会以空格分隔,最后当整个循环结束时,text所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。所以,var最好是一个变量名,list可以是一个表达式,而text中一般会使用var。
- if函数
if函数条件语句ifeq,if函数的语法是:
$(if<condition>,<then-part>)
或是:
$(if<condition>,<then-part>,<else-part>)
可见,if函数可以包含“else”部分,或是不含。condition参数是if的表达式,如果其返回的为非空字符串,那么这个表达式就相当于返回真,于是,then-part会被计算,否则else-part会被计算。而if函数的返回值是,如果condition为真(非空字符串),那个then-part会是整个函数的返回值,如果condition为假(空字符串),那么else-part会是整个函数的返回值,此时如果else-part没有被定义,那么,整个函数返回空字串。所以,then-part和else-part只会有一个被计算。
- call函数
call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中,你可以定义许多参数,然后你可以用call函数来向这个表达式传递参数。其语法是:
$(call <expression>,<parm1>,<parm2>,<parm3>...)
当make执行这个函数时,expression参数中的变量,如$(1)
,$(2)
,$(3)
等,会被参数parm1,parm2,parm3依次取代。而expression的返回值就是call函数的返回值。例如:
reverse = $(1) $(2)
foo = $(call reverse,a,b)
那么,foo的值就是“a b”。当然,参数的次序是可以自定义的,不一定是顺序的。
- origin函数
origin函数不像其它的函数,他并不操作变量的值,他只是告诉你你的这个变量是哪里来的。其语法是:
$(origin <variable>)
注意,variable是变量的名字,不应该是引用。所以你最好不要在variable中使用“$”
字符。origin函数会以其返回值来告诉你这个变量的“出生情况”,下面是origin函数的返回值:
返回值 | 定义 |
---|---|
“undefined” | variable从来没有定义过 |
“default” | variable是一个默认的定义,比如“CC”这个变量 |
“environment” | variable是一个环境变量,并且当Makefile被执行时,“-e”参数没有被打开 |
“file” | variable这个变量被定义在Makefile中 |
“command line” | variable这个变量是被命令行定义的 |
“override” | variable是被override指示符重新定义的 |
“automatic” | variable是一个命令运行中的自动化变量 |
- shell函数
shell函数参数应该就是操作系统Shell的命令。它和反引号“`”是相同的功能。这就是说,shell函数把执行操作系统命令后的输出作为函数返回。于是,我们可以用操作系统命令以及字符串处理命令awk,sed等等命令来生成一个变量,如:
contents := $(shell cat foo)
files := $(shell echo *.c)
- error函数:产生一个错误信息,同时make退出
- warning函数:输出一段警告信息,但不会让make退出,而是继续执行
附:%标记和系统通配符*
的区别
Makefile中的%标记和系统通配符*
的区别在于,*
是应用在系统中的,%是应用在这个Makefile文件中的。
(本文的测试环境是Windows7下使用MinGW提供的make.exe)
例如,如果你想编译一个文件夹下的所有.c文件,你可能会这样写:
%.o:%.c
gcc -o $@ $<
但是如果整个文件只有这两行的话,就会出现这样的错误:
Make: *** target not found. stop.
要知道原因,我们先来看看另一个makefile的运行过程,例如有Makefile如下:
test1.o:test1.c
gcc -o test1.o test1.c
test2.o:test2.c
gcc -o test2.o test2.c
all:test1.o test2.o
如果没有指定输出项目的时候Make会自动找到makefile中第一个目标中没有通配符的目标进行构造,所以步骤是:
- 1.构造all,发现需要test1.o和test2.o。
- 2.这个时候他就会在Makefile文件中找到目标能匹配test1.o和test2.o的规则。
- 3.找到test1.o的规则并且知道test1.c存在,运行下面的命令。
- 4.同步骤三构造出test2.o。
- 5.现在构造all的源文件已经齐全,构建all。
其中最重要的是第2步。
Makefile的通配符是在带着目的(如“寻找test1.o”)的时候才会把他要寻找的目标套用通配符%中。
所以通配符%的意思是:
我要找test1.o的构造规则,看看Makefile中那个规则符合。
然后找到了%.o:%.c,来套一下来套一下: %.o 和我要找的 test1.o 匹配,套上了,得到%=test1。
所以在后面的%.c就表示test1.c了。OK进行构造
而通配符*的意思是:
我不知道目标的名字,系统该目录下中所有后缀为.c的文件都是我要找的。
然后遍历目录的文件,看是否匹配。找出所有匹配的项目。
所以虽然连个符号的意思有点沾边,但是他们的工作方式时完全不一样。现在知道了为什么文件中只有
%.o:%.c
gcc -o $@ $<
会找不到目标了吧。因为没有-f参数时Make会自动找到makefile中第一个目标中没有通配符的目标进行构造,所以就等于找不到目标了。它的意思并不会自动把文件中所有的文件都编译。所以正确的代码应该是:
all:$(subst .c,.o,$(wildcard *.c))
%.o:%.c
gcc -o $@ $<
这才是把目录下所有文件都编译的命令。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 [ yehuohan@gmail.com ]