一、Makefile的语法
一个基本的 makefile 主要由目标对象、依赖文件、变量和命令四部分组成,目标对象是 make 命令最终需要生成的文件,通常为目标文件或可执行程序;依赖文件是生成目标对象所依赖的文件,通常为目标文件或源代码文件;使用变量保存与引用一些常用值可以增强 makefile 文件的简洁性、灵活性跟可读性,一处定义,多处使用,通常还可以对其内容进行赋值或追加;目标对象通常对应着依赖文件而成为一条规则,如“hello.o:hello.c hello.h”,而对应这条规则,通常跟随着一些命令,这些命令的格式跟 Shell 终端的格式一致,如“rm -f *.o”或“$(CC) -c hello.c -o hello.o”,注意每条命令语句前面必须加上制表符 tab 键,否则 make 命令将提示错误,不论是规则语句还是命令语句,都可以引用变量,如“$(CC)”,make 命令在执行这些语句之前都会先将变量替换为它对应的值。
一个 makefile 文件中可以有多个目标对象,要生成特定的目标对象,在执行 make 命令的时候跟上目标对象名即可,如“make hello.o”,倘若不指定,make 命令自动将 makefile 文件中的第一个目标对象作为默认对象来生成。
通常 makefile 文件中的一条规则的编写形式如下:
targets …:dependent_files … command …
targets 表示目标对象,dependent_files 表示依赖文件,command 表示命令行,“(tab)”表示制表符,“…”表示数量有一个或者多个。下面给出了一个最基本的编译 hello 程序的 makefile 文件。
#This is a example for describing makefile hello:hello.o gcc hello.o -o hello hello.o: hello.s gcc -c hello.s -o hello.o hello.s:hello.i gcc -S hello.i -o hello.s hello.i:hello.c hello.h gcc -E hello.c > hello.i .PHONY: clean clean: rm -f hello.i hello.s hello.o hello
以上 makefile 文件详细描述了 gcc 生成二进制文件 hello 所经过的预处理、编译、汇编和链接等四个过程,并使用 clean 对象来实现生成文件清除操作。
二、Makefile与命令
makefile 中的命令由一些 shell 命令行组成,这些命令被一条条地执行,除了第一条紧跟在依赖关系后面的命令需要使用分号隔开以外,其他每一行命令行必须以制表符 tab 开始。多个命令行之间可以有空行或者注释行,它们在 makefile 执行时被忽略。每条命令行中可以使用注释,“#”字符出现处到行末的内容将被忽略。
1、命令回显
默认情况下,每执行一条 makefile 中的命令之前,Shell 终端都会显示出这条命令的具体内容,除非该命令用分号分隔而紧跟在依赖关系后面。我们称之为“回显”,就好比我们在命令行中执行一样。如果不想显示命令的具体内容,我们可以在命令的开头加上“@”符号,这种情况通常用于 echo 命令。
make 执行 “echo ‘开始编译XXX模块’”语句的输出结果是:
echo '开始编译XXX模块' 开始编译XXX模块
而 make 执行“@echo ‘开始编译XXX模块’”语句的输出结果是:
开始编译XXX模块
2、命令的执行
当 makefile 中的目标需要被重建时,此条目标对应的依赖关系后面紧跟的命令将会被执行,如果有多行命令,那么 make 将为每一行命令独立分配一个子 Shell 去执行,因此多行命令之间的执行是相互独立的,也不存在依赖关系。
需要注意的是,在一条依赖关系下的多个命令行中,前一行中的 cd 命令改变目录后不会对后面的命令行产生影响,也就是说后续命令行的执行目录不会是之前使用 cd 命令进入的那个目录。而 Makefile 中处于同一行、用分号分隔的多个命令属于同一个子 Shell,前面 cd 命令的目录切换动作可以影响到分号后面的其他命令,如:
hello:src/hello.c src/hello.h cd src/ ; gcc hello.c -o hello
如果需要将一个完整的 Shell 命令行书写在多行上,可是使用反斜杠“\”来处理多行命令的连接,表示反斜杠前后的两行属于同一个命令行,如上例也可以这样书写:
hello:src/hello.c src/hello.h cd src/ ; \ gcc hello.c -o hello
3、并发执行命令
GNU make 可以同时执行多条命令,默认情况下,make 在同一时刻只只执行一个命令,后一个命令的执行依赖前一个命令的完成,为了同时执行多条命令,可以在执行 make 命令时添加“-j”选项来指定同时执行命令条数的上限。
如果选项“-j”之后跟一个整数,其含义是 make 在同一时刻允许执行的最多命令条数;如果选项“-j”后面不跟整数,则表示不限制同时执行的命令条数,即每条依赖关系后有多少条命令就同时执行多少条;如果不加选项“-j”,则默认单步依次执行多条命令。
三、Makefile与变量
变量是在 Makefile 中定义的名字,用来代替一个文本字符串,该文本字符串称为该变量的值。变量名是不包括“:”、“#”、“=”结尾空格的任何字符串。同时,变量名中包含字母、数字以及下划线以外的情况应尽量避免,因为它们可能在将来被赋予特别的含义。变量名是大小写敏感的,例如变量名“foo”、“FOO”、和“Foo”代表不同的变量。推荐在 Makefile 内部使用小写字母作为变量名,预留大写字母作为控制隐含规则参数或用户重载命令选项参数的变量名。在具体要求下,这些值可以代替目标体、依赖文件、命令以及 Makefile 文件中的其他部分,在 Makefile 文件中引用变量 VAR 的常用格式为“$(VAR)”。在 Makefile 中的变量定义有两种方式:一种是递归展开方式,另一种是简单方式。
递归展开方式的定义格式为:VAR=var。递归展开方式定义的变量是在引用该变量时进行替换的,即如果该变量包含了对其他变量的应用,则在引用该变量时一次性将内嵌的变量全部展开,虽然这种类型的变量能够很好地完成用户的指令,但是它也有严重的缺点,如不能在变量后追加内容(因为语句“CFLAGS = $(CFLAGS) -o”在变量扩展过程中可能导致无穷循环)。
为了避免上述问题,简单扩展型变量的值在定义处展开,并且只展开一次,因此它不包含任何对其他变量的引用,从而消除变量的嵌套引用。简单扩展方式的定义格式为:VAR:=var。
下面给出一个使用了变量的 Makefile 例子,这里用 OBJS 代替 main.o 和 add.o,用 CC 代替 gcc,用 CFLAGS 代替“-Wall -O –g”。这样在以后修改时,就可以只修改变量定义,而不需要修改下面的引用实体,从而大大简化了 Makefile 维护的工作量。
OBJS=main.o add.o CC=gcc CFLAGS = -Wall -O -g add:$(OBJS) $(CC) $(OBJS) -o add main.o:main.c $(CC) $(CFLAGS) -c main.c -o main.o add.o:add.c $(CC) $(CFLAGS) -c add.c -o add.o clean: rm *.o
可以看到,此处变量是以递归展开方式定义的。
Makefile 中的变量分为用户自定义变量、预定义变量、自动变量及环境变量。如上例中的 OBJS 就是用户自定义变量,自定义变量的值由用户自行设定,而预定义变量和自动变量为通常在 Makefile 都会出现的变量,其中部分有默认值,也就是常见的设定值,当然用户可以对其进行修改。
预定义变量包含了常见编译器、汇编器的名称及其编译选项。下表列出了Makefile中常见预定义变量及其部分默认值。
AR:库文件维护程序的名称,默认值为ar AS:汇编程序的名称,默认值为as CC:C编译器的名称,默认值为cc CPP:C预编译器的名称,默认值为$(CC) –E CXX:C++编译器的名称,默认值为g++ FC:FORTRAN编译器的名称,默认值为f77 RM:文件删除程序的名称,默认值为rm –f ARFLAGS:库文件维护程序的选项,无默认值 ASFLAGS:汇编程序的选项,无默认值 CFLAGS:C编译器的选项,无默认值 CPPFLAGS:C预编译的选项,无默认值 CXXFLAGS:C++编译器的选项,无默认值 FFLAGS:FORTRAN编译器的选项,无默认值
可以看出,上例中的 CC 和 CFLAGS 是预定义变量,其中由于 CC 没有采用默认值,因此,需要把“CC=gcc”明确列出来。
由于常见的 GCC 编译语句中通常包含了目标文件和依赖文件,而这些文件在 Makefile 文件中依赖关系的一行已经有所体现,因此,为了进一步简化 Makefile 的编写,便引入了自动变量。自动变量通常可以代表编译语句中出现的目标文件和依赖文件等,并且具有本地含义(即下一语句中出现的相同变量代表的是下一语句的目标文件和依赖文件)。下表列出了 Makefile 中常见自动变量。
$*:不包含扩展名的目标文件名称 $+:所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件
lt;:第一个依赖文件的名称 $?:所有时间戳比目标文件晚的依赖文件,并以空格分开 $@:目标文件的完整名称 $^:所有不重复的依赖文件,以空格分开 $%:如果目标是归档成员,则该变量表示目标的归档成员名称
自动变量的书写比较难记,但是在熟练了之后会非常的方便,请大家结合下例中的自动变量改写的 Makefile 进行记忆。
OBJS=main.o add.o CC=gcc CFLAGS = -Wall -O -g add:$(OBJS) $(CC) $^ -o $@ main.o:main.c $(CC) $(CFLAGS) -c
lt; -o $@ add.o:add.c $(CC) $(CFLAGS) -c
lt; -o $@ clean: rm *.o
另外,在 Makefile 中还可以使用环境变量。使用环境变量的方法相对比较简单,make 在启动时会自动读取系统当前已经定义了的环境变量,并且会创建与之具有相同名称和数值的变量。但是,如果用户在 Makefile 中定义了相同名称的变量,那么用户自定义变量将会覆盖同名的环境变量。
四、Makefile与条件语句
Makefile 中的条件语句可以根据变量的值执行或忽略 Makefile 文件中的一部分脚本。条件语句可以将一个变量与其它变量的值相比较,或将一个变量与一个字符串常量相比较。条件语句用于控制 make 实际看见的 Makefile 文件部分,而不能用于在执行时控制 shell 命令。
1、条件语句的例子
如下条件语句的例子告诉 make 如果变量 CC 的值是“gcc”时使用一个链接库,如不是则使用其它链接库。它可以根据变量 CC 值的不同来链接不同的函数库。
libs_for_gcc = -lgnu normal_libs = foo: $(objects) ifeq ($(CC),gcc) $(CC) -o foo $(objects) $(libs_for_gcc) else $(CC) -o foo $(objects) $(normal_libs) endif
该条件语句使用三个指令:ifeq、else和endif。ifeq 指令是条件语句的开始,并指明条件。它包含两个参数,被逗号分开,并被扩在圆括号内。运行时首先对两个参数变量替换,然后进行比较。在 Makefile 中跟在 ifeq 后面的行是符合条件时执行的命令,如果前面的条件失败,else 指令将导致跟在其后面的命令执行。在上述例子中,意味着当第一个选项不执行时,和第二个选项连在一起的命令将执行。在条件语句中,else 指令是可选的。endif 指令结束条件语句,任何条件语句必须以endif指令结束,后跟 Makefile 文件中的正常内容。
当变量 CC 的值是 gcc,上例的效果为:
foo: $(objects) $(CC) -o foo $(objects) $(libs_for_gcc)
当变量 CC 的值不是 gcc 而是其它值的时候,上例的效果为:
foo: $(objects) $(CC) -o foo $(objects) $(normal_libs)
相同的结果也能使用另一种方法获得,即先将变量的赋值条件化,然后再使用变量:
libs_for_gcc = -lgnu normal_libs = ifeq ($(CC),gcc) libs=$(libs_for_gcc) else libs=$(normal_libs) endif foo: $(objects) $(CC) -o foo $(objects) $(libs)
2、条件语句的语法
对于没有else指令的条件语句的语法为:
conditional-directive text-if-true endif
“text-if-true”可以是任何文本行,在条件为真时它被认为是 Makefile 文件的一部分;如果条件为假,将被忽略。完整条件语句的语法为:
conditional-directive text-if-true else text-if-false endif
如果条件为真,使用“text-if-true”;如果条件为假,使用“text-if-false”。“text-if-false”可以是任意多行的文本。
关于“conditional-directive”的语法对于简单条件语句和复杂条件语句完全一样。有四种不同的指令用于测试不同的条件。下面是指令表:
ifeq (arg1, arg2) ifeq 'arg1' 'arg2' ifeq "arg1" "arg2" ifeq "arg1" 'arg2' ifeq 'arg1' "arg2"
扩展参数 arg1、arg2 中的所有变量引用,并且比较它们,如果它们完全一致,则使用“text-if-true”,否则使用“text-if-false”(如果存在的话)。我们经常要测试一个变量是否有非空值,当经过复杂的变量和函数扩展得到一个值,该值实际上有可能由于包含空格而被认为不是空值,由此可能造成混乱,然而,我们可以使用 strip 函数来避免空格作为非空值的干扰。例如:
ifeq ($(strip $(foo)),) text-if-empty endif
上例的 $(foo) 中即使全为空格,也被当作空值处理。
ifdef variable-name text-if-true else text-if-false endif
如果变量“variable-name”从没有被定义过则变量是空值,注意 ifdef 仅仅测试变量是否被定义,它无法判断变量是否有非空值,因而,使用 ifdef 测试所有定义过的变量都返回真,但那些像“foo=”的情况除外,测试空值请使用ifeq($(foo),)。例如:
bar = foo = $(bar) ifdef foo frobozz = yes else frobozz = no endif
设置“frobozz”的值为“yes”, 而:
foo = ifdef foo frobozz = yes else frobozz = no endif
设置“frobozz” 为“no”。
ifndef variable-name text-if-true else text-if-false endif
ifndef 跟 ifdef 的功能恰好相反,ifdef 用来判断变量是否已经被定义,ifndef 则用来判断变量是否没有被定义。
在条件语句中另两个有影响的指令是 else 和 endif。这两个指令以一个单词的形式出现,没有任何参数。在指令行前面允许有多余的空格,空格和 Tab 可以插入到行的中间,以“#”开始的注释可以在行的结尾。
条件语句影响 make 使用的 Makefile 文件。如果条件为真,make 将读入“text-if-true”包含的行;如果条件为假,make 则读入“text-if-false”包含的行(如果存在的话);Makefile 文件的语法单位,例如规则,可以跨越条件语句的开始或结束。
为了避免不可忍受的混乱,在一个 Makefile 文件中开始一个条件语句,而在另外一个 Makefile 文件中结束该语句的情况是不允许的。如果您试图引入包含不中断条件语句的 Makefile 文件,可以在条件语句中使用 include 指令将该 Makefile 文件包含进来。
五、Makefile与函数
在 Makefile 中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。make 所支持的函数也不算很多,不过已经足够我们的操作了。函数调用后,函数的返回值可以当做变量来使用。介于篇幅的限制,这里只简单介绍几个最常用的函数,在介绍这些函数之前,我们先来了解一下调用函数的语法。
1、 函数的调用语法
函数调用,很像变量的使用,也是以“$”来标识的,其语法如下:
$(<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”。
2、 subst
$(subst <from>,<to>,<text> ) 名称:字符串替换函数。 功能:把字串<text>中的<from>字符串替换成<to>。 返回:函数返回被替换过后的字符串。
示例:
$(subst ee,EE,feet on the street),
把“feet on the street”中的“ee”替换成“EE”,返回结果是“fEEt on the strEEt”。
3、strip
$(strip <string> ) 名称:去空格函数。 功能:去掉 <string> 字串中开头和结尾的空字符。 返回:返回被去掉空格的字符串值。
示例:
$(strip a b c )
把字串“a b c ”去到开头和结尾的空格,结果是“a b c”。
4、dir
$(dir <names…> ) 名称:取目录函数——dir。 功能:从文件名序列<names>中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部分。如果没有反斜杠,那么返回“./”。 返回:返回文件名序列<names>的目录部分。
示例: $(dir src/foo.c hacks)返回值是“src/ ./”。
5、join
$(join <list1>,<list2> ) 名称:连接函数。 功能:把<list2>中的单词对应地加到<list1>的单词后面。如果<list1>的单词个数要比<list2>的多,那么,<list1>中的多出来的单词将保持原样。如果<list2>的单词个数要比<list1>多,那么,<list2>多出来的单词将被复制到<list1>中。 返回:返回连接过后的字符串。
示例:$(join aaa bbb , 111 222 333)返回值是“aaa111 bbb222 333”。
6、shell
Shell 函数,顾名思义,它的参数应该就是操作系统 Shell 的命令,它和反引号“`”是相同的功能。Shell 函数将执行系统命令后的输出作为返回值,我们可以用系统命令以及字符串处理命令 awk、sed 等等命令来生成一个变量如:
contents := $(shell cat foo) files := $(shell echo *.c)
注意,该函数将新开一个 Shell 来执行命令,需要注意其运行性能,如果 Makefile 中有一些比较复杂的规则,并大量使用了该函数,对于系统性能是有害的。特别是 Makefile 的隐晦的规则可能会让 Shell 函数执行的次数比想像的多得多。
除非注明,文章均为CppLive 编程在线原创,转载请注明出处,谢谢。
是不是要学linux都要会makefile的使用啊
不会的,Makefile只是一个方便编译程序的文件而已~
@cpplive
最近改了主题,把你的链接移到了内页,我很惭愧,希望你不要介意。http://www.roading.org/?page_id=73 😛
完全木有意见的,因为工作与一点私事的缘故,博客最近很少更新,也希望Adoo兄表介意~