Makefile笔记(2)——学习汇总

一、Makefile基本用法

1. 早期的gcc全称为GNU C Compiler,它只负责处理C语言。GCC在发布之后很快就得到了扩展,支持C++/Fortran/Objective-C等一系列语言,后期改名为GNU Compiler Collection,指一套语言编译器,简写还是叫gcc。

2. makefile编译规则
规则是指编译生成一个目标的完整语句,通常包含目标,依赖和命令。标准的makefile中编译规则是这样的:

目标 … : 依赖 …
    命令
    …

目标:编译过程中需要生成的文件,一个目标同样可以是一个需要执行的命令。
依赖:编译目标时需要依赖的文件列表,以空格分隔
命令:被执行的指令,命令部分需要以tab开头,值得注意的是,命令部分的语句将会由makefile的规则做简单替换(变量替换,通配符的替换等等)之后被传递给shell,由shell解析,同时,命令部分并不一定需要重启一行,也可能出现在依赖文件列表行,但是并不建议这么做。通常,目标就是我们要生成的目标文件或中间文件。

#例1:
main:foo.c foo.h common.h
    gcc foo.c -o pp

clean:
    -rm pp

注:实测,依赖中不写 common.h,其内容变化后还是会重新生成pp,但是common.h内容变更后,main不会重新编译生成了。

3. make解析目标文件

当键入make指令之后,make就会在当前目录下寻找名为GNUmakefile,makefile、Makefile的文件,make默认读取这三种名称的文件并解析,当同时存在两种及以上的上述文件时,处理优先级为:GNUmakefile > makefile > Makefile。

即同时存在makefile和Makefile时,make只处理makefile文件而忽略Makefile。同时,用户可以使用 -f 参数来指定特定的Makefile文件,它可以是任意名称。例如:make -f file

4. 依赖文件
依赖文件的作用是:提供给make一个文件列表,当当前目标需要被指定编译时就会去检查这个文件列表中的文件是否有更新,如果有更新,就重新编译这个目标,否则不进行编译。

5. make解析目标
在上述例1中,存在两个目标:main 和 clean。
make的规则是:默认使用第一个不以"."(常用语伪目标)和"%"(常用于模式规则)开头命名的目标作为本条规则编译生成的结果。所以,上述示例中使用make编译的结果是生成 main。同时,make后可以跟一个目标名参数表示指定编译生成某个目标,所以在上述示例中,make等同于下面的指令:make main

这条指令的执行流程是:
(1) 检查依赖文件是否有更新,同时检查目标文件是否存在
(2) 如果依赖文件有更新或者没有目标文件,执行命令部分以重新生成目标
(3) 如果依赖文件没有更新,不重新编译生成目标
(4) 如果一个依赖文件是一个目标,使用相同的以上三条规则先处理依赖文件。


二、伪目标

例1中clean这个目标并没有依赖文件,只有目标和命令,这一类目标在makefile中叫做伪目标,普通目标对应着一个需要被生成的文件,而伪目标不对应具体的文件,它仅仅充当一个目标的标识,用作执行特定的功能,而不是执行编译过程。使用Makefile关键字".PHONY"来显式地定义一个伪目标。

不使用".PHONY"修饰的伪目标,make解析时会将其当成一个普通目标去解析。在检查依赖更新时make会调用隐式规则试图去解析它,尽管最后执行的结果是一样的,但是这样会影响makefile的执行效率,在大型工程的编译时需要注意。它的使用方式是这样的:

#例2:
.PHONY : clean
clean:
    -rm pp

这样显式地定义伪目标的好处有两点:
(1) 如果同时存在一个普通目标clean,非显示定义的伪目标将无法执行,显式定义伪目标可以解决这个问题,在出现同名普通目标时,它将覆盖普通目标得以执行,同时make将输出警告信息。
(2) 告诉make这就是一个伪目标,不要试图对其做其他处理,这样可以提高编译效率,减少编译时间。

但不是伪目标都是没有依赖且不生成对应目标文件。其实伪目标是可以有依赖的,同样地,伪目标对应的命令也可以生成一些文件(这取决于伪目标的命令部分),这并非makefile语法作的强制要求。

makefile的语法规定,伪目标不生成对应的目标文件,每次调用伪目标时都会重新执行一次伪目标的命令。所以不要将伪目标作为其他普通目标的依赖,这会导致对应普通目标每次都被重新编译(因为伪目标每次都需要被重新编译)。当然,很多实际工程案例专门利用这一点,如下:

#例3:
main: foo.o bar.o main.c common.h gcc foo.o bar.o main.c -o main foo.o : foo.c foo.h common.h gcc -c foo.c -o foo.o bar.o : bar.c bar.h common.h FORCE gcc -c bar.c -o bar.o clean: rm -rf *.o main .PYHTON : FORCE FORCE: # must a <tab> follow FORCE, else will report no rules to make FORCE.

每次 make main 都会执行如下命令,无论文件是否有修改。
# make
gcc -c bar.c -o bar.o
gcc foo.o bar.o main.c -o main

若没有FORCE,首次编译出main后若是无修改,则不会再进行任何编译,只会有如下打印:
# make
make: `main' is up to date.


3. 命令

makefile目标生成规则三要素中的命令在即将开始执行的时候,make对其的操作是将命令中的变量按照Makefile规则进行替换后,再将其传递给shell,而不是由makefile的语法来解析。即命令部分由shell解析,遵循shell的语法。

认识到这一点是非常有必要的,因为makefile的处理语法与shell的处理语法有一些区别,至于具体的区别完全取决于使用的shell,混淆这个概念会导致Makefile执行出错。


三、生成中间文件,分布式编译

如例3,此时若修改了foo.c只会重新编译foo.o和main.o,不会重新编译其它文件,提交编译效率。


四、自动推导

1. 在编译foo.o和bar.o时,我们并不需要添加编译目标的命令,因为make会对目标进行隐式推导:make为foo.o自动寻找foo.c文件,并将foo.c编译成foo.o,如下例4:

#例4:
main:foo.o bar.o common.h
    gcc foo.o bar.o main.c -o main
foo.o:common.h foo.h
bar.o:common.h bar.h
clean:
    rm -rf *.o  main

#----------------
# make
cc    -c -o foo.o foo.c
cc    -c -o bar.o bar.c
gcc foo.o bar.o main.c -o main

需要注意的是,隐式规则可以通过给出的.o文件而自动地去寻找并编译对应的同名.c文件并编译,所以在写依赖文件列表的时候可以省略相应的.c文件。但是不允许省略对应的.h文件,否则将使得.h文件的更新不会导致重新编译而出错(想想依赖文件列表的作用)。

2. makefile支持的自动推导语言

本示例中仅仅以C源代码为例讲解makefile的自动推导规则,事实上,makefile的语法支持很多中语言:
C++: 从 .cc 或者 .cpp 文件推导 .o
Pascal: 从 .p 文件推导 .o
Fortran: 从 .r或者 .f 文件推导 .o
...


五、变量使用

1. 变量可以是单个目标也可以是多个空格隔开的目标列表,变量的值同时也可以是其他变量。

2. 使用变量时,需要在变量前使用"$",否则都应该用()或者{}将其包含。

3. $@是makefile中的内置变量,它的值是目标文件,例5中是${TARGET}

#例5
TARGET = main
OBJ = foo.o bar.o

${TARGET}:${OBJ} common.h
    cc ${OBJ} main.c -o $@
foo.o:common.h
bar.o:common.h bar.h
clean:
    rm -rf ${OBJ} ${TARGET}

#--------------------
# make
cc    -c -o foo.o foo.c
cc    -c -o bar.o bar.c
cc foo.o bar.o main.c -o main

 

六、变量的赋值方式

1. makefile中总体的变量名赋值的规则:
(1) 以变量名开头,后面接赋值操作符
(2) 变量名后的空格以及复制操作符后的空格将被忽略
(3) 变量名没有长度限制
(4) 没有被赋值的变量视为空字符串,但是在makefile中有一些内置变量是自带初始值或者在某个处理阶段被自动赋值。

2. 几种赋值方式
Makefile中有几种赋值方式有几种,分别为"=","?=",":=","::=","+=","!=",赋值方式分别为:
"=" 普通的赋值符,将右值赋给左值
"?=" 如果没有初始化该变量,就给它赋上默认值,属于条件赋值符
":=" 直接赋值,不过在变量展开上与"="不同(见下文).
"::=" 这种赋值符等效于":="
"+=" 追加赋值符,在原变量的值上追加赋值
"!=" 这个赋值符比较特殊,右值为一条shell命令,shell命令的返回值赋给左边的变量.

注:"!="这种赋值实测无效,"::="已经无效了。

变量的扩展方式
变量在使用$符号对其进行引用(通俗来说就是取变量值)时,叫做变量的扩展,变量在扩展完成之后才真正确定了变量的值。值得注意的是,变量在赋值的时候并不直接确定变量的值,变量的扩展有两种:循环递归扩展和简单扩展。

(1)循环递归扩展
在变量被赋值时并不直接确定了当前变量的值,如果这个变量引用了其它变量,make会先确定被引用变量的值之后再确定该变量的值。也就是说,在整个Makefile被make读取完成之后,再确定该变量的值,而非简单地使用当前值进行扩展。若是重复赋值,就会递归解析最后一次的。

#例6:
var1 = ${var2}
var2 = "hello"

main:
    @echo ${var1}

#---------------
# make
hello

注:若是本目录下有文件名为main的文件,且时间戳没有更新,执行make时就会报“make: `main' is up to date.”,而不是执行任何命令。对于赋值描述符而言,"=" 和 "?=" 都属于循环递归扩展的变量类型。

(2) 简单扩展变量
与循环递归扩展不一样的是,这种方式就是在赋值的时候就确定了变量的值,不管它是否引用了其它变量。

#例7:
var1 := ${var2}
var2 := "hello"

main:
    @echo ${var1}

#-------------
# make

输出结果为空。对于赋值描述符而言,":="、"::="、"!=" 都属于简单扩展的变量类型。

(3) "+=" 并不属于循环递归扩展也不属于简单扩展变量,它属于墙头草系列。如果左侧变量被提前设置为简单赋值变量,则 "+=" 操作的就是简单赋值变量,否则就是循环扩展赋值(包括新定义变量)变量。

#例8:
var1 += ${var2}
var2 := "hello"
var2 += "baby"

var3 := "world"
var3 += ${var4}
var4 = "!!!"

main:
    @echo "var1 :" ${var1}
    @echo "var2 :" ${var2}
    @echo "var3 :" ${var3}

#-------------------
# make
var1 : hello baby
var2 : hello baby
var3 : world

可以看到,在例8的第1行直接使用"+="定义了一个新的变量,明显地,make的解析规则对 var1 的值进行了递归地扩展。在第4-6行,"+="作用于一个已经被初始化为简单扩展变量上,所以并不会进行递归扩展,此 var4 还是空,因此保持原来的值"world"。其实不难理解,"+="赋值符除了初始化赋值,它更多的应用场合是追加,既然是追加,那肯定是客随主便,原来是什么类型就按照什么类型的方式来操作,即左边的变量定义时是何种赋值方式就使用何种追加方式。

3. 创建私有变量
在默认的情况下,makefile中的变量都是全局变量。但是,如果在某个目标构建的规则中,你想使用某个变量名,但是却不想继承变量原来的值,这时候就可以使用 private 关键词,它表示私有变量,与全局同名变量相互独立。使用时也需要加上private。

#例9:
#file: aa/Makefile
private var3 := "dog"

my:
    @echo ${private var3}


#file: Makefile
include aa/Makefile

var3 := "world"

main: my
    @echo "var3 :" ${var3}

#------------------
# make
dog
# make main
dog
var3 : world

4. 变量的删除
使用 unset 命令来删除一个变量,语法为:unset variable... 被删除之后,引用该变量的值结果为空。

#例10:
var3 := "world"

unset var3

main:
    @echo "var3 :" ${var3}

#--------------------------
# make
Makefile:3: *** missing separator.  Stop.

注:实测,Makefile中似乎已经不存在这个关键字了,make报错。

5. 內建变量
在makefile有一些特殊的內建变量,如下:

目标:依赖列表
    命令

"$@":表示需要被编译的目标
"$<":依赖列表中第一个依赖文件名
"$^":依赖列表中所有文件
"$?": 依赖文件列表中所有有更新的文件,以空格分隔
"~"或者"./":用户的家目录,如果"~"后接字符串,表示/home/+字符串,比如~ubuntu,展开为/home/ubuntu/。

#例11:
main: main.c foo.c bar.c
    @echo $@
    @echo $<
    @echo $^
    @echo $?
    @echo ~
    @echo ./
    touch main

#---------------------
# make
make: `main' is up to date.
# touch foo.c
# make
main
main.c
main.c foo.c bar.c
foo.c
/root
./
touch main

 

七、Makefile中的通配符

1. 主要使用的通配符有"*","?"。"*"表示匹配所有任何符合条件的。"?" 通常在依赖文件列表中使用,匹配所有有更新的目标。例如:
*.o 表示所有的.o文件
*.c 表示所有的.c文件
* 表示所有的文件

make在编译目标时,会去检查目标的依赖文件列表是是否有文件更新,"$?"表示当前依赖列表中已经更新的依赖文件。

#例12:
main:foo.c bar.c
    @echo $?
    touch main

#----------------
# touch foo.c 
# make
foo.c
touch main

若是文件是第一次编译的,会同时输出foo.c bar.c,因为在目标没有被生成的时候,make工具会将所有的依赖文件视为已更新的文件,从而重新编译生成目标。之后touch哪个才会输出哪个。
值得注意的是,在shell中,"$?"表示上一条指令的执行结果,这里需要做相应区分。

2. 通配符的转义
若是想"*.c"表达就是名字为"*.c"的这个文件,而不是所有以".c"结尾的文件时,需要对"*"号进行转义,方法为使用"\*"代替"*"。

3. 通配符的赋值

#例13:
OBJ = *.c
main:
    @echo ${OBJ}

#----------------
# make
bar.c foo.c main.c
#例14:
OBJ = *.o
main:
    @echo ${OBJ}

#-------------
# make
*.o

从原理上来说,当你在赋值时指定通配符匹配时,如果通配符表达式匹配不到任何合适的对象,通配符语句本身就会被赋值给变量,为了解决这个问题定义了wildcard这个通配符函数。

4. 通配符函数

#例15:
OBJ = ${wildcard *.o}
main:
    @echo ${OBJ}

#---------------
# make

若没有匹配到任何合适的文件,${OBJ}的内容为空,而并非是错误的"*.o"。

5. 内建变量通配符就是上面介绍的"$@"、"$<"、"$^"、"$?"、"~"、"./"。

 

八、模式规则

1. 普通模式规则
模式规则类似于普通规则。只是在模式规则中,目标名中需要包含有模式字符"%",包含有模式字符"%"的目标被用来匹配一个文件名,"%" 可以匹配任何非空字符串。规则的依赖文件中同样可以使用"%",其取值情况由目标中的"%"来决定。
例如:对于模式规则"%.o : %.c",它表示的含义是:所有的.o文件依赖于对应的.c文件。由所有的.c文件生成对应的.o文件:

#例16:
OBJ = foo.o bar.o main.o

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

main: $(OBJ)
    gcc $(OBJ) -o main

#----------------------
# make
cc -c  foo.c -o foo.o
cc -c  bar.c -o bar.o
cc -c  main.c -o main.o
gcc foo.o bar.o main.o -o main

根据这个模式规则,makefile提供了隐式推导规则。同时,模式规则的依赖可以不包含"%",当依赖不包含"%"时代表的是所有与模式匹配的目标都依赖于指定的依赖文件。
注:要想被编译到,需要将其指定为依赖才行!第7行也可以写成"gcc *.o -o main"。第6行的"main:"这个目标若换成是"all:",则每次执行make第7行都会编译,因为目标"all"文件不存在,所以每次都会编译。

2. 静态模式规则
静态模式可以更加容易地定义多目标的规则,它的语法:

目标 ...: 目标模式 : 依赖的模式
        命令
        ...

相对于普通的模式规则,静态模式规则则显得更加地灵活,作为模式规则的一种,仍然使用"%"来进行模式的匹配,例如当前目录下的文件:foo.c foo.h bar.c bar.h main.c,makefile内容:

#例17:
OBJ = foo.o bar.o
main: ${OBJ}
    cc ${OBJ} main.c -o main
${OBJ}: %.o : %.c
    cc -c $^

#---------------
# make
cc -c foo.c
cc -c bar.c
cc foo.o bar.o main.c -o main

make时,发现目标main依赖于foo.o bar.o这两个文件,make就会在当前目录下找foo.o bar.o两个文件,又发现没有这两个文件,所以就需要寻找生成这两个依赖文件的规则。
第4行就是生成foo.o bar.o的规则,先被执行,这一行使用了静态模式规则,对于存在于${OBJ}中的每个.o文件,使用对应的.c文件作为依赖,调用命令部分,生成.o文件。

可以看到,相比于普通的模式规则,静态模式规则更加地灵活。

注:例17这样写有个弊端,make后,touch main.c,然后再make,执行命令如下。说明目标存在的情况下,检查更新时只会检查依赖文件是否更新,不会检查编译命令中使用到的文件是否更新。因此即使main.c被改动了也不会重新编译。例16中写法更好一些,因为任何一个.c文件都是依赖,只要有一个更新了目标就会重新编译生成。例17的补救可以在第3行命令后加一个伪目标FORCE强制每次都编译。

# make
make: `main' is up to date.

3. 另一种常用的语法
在模式规则时还有另一种常用的语法,是这样的:${OBJ:pre-pattern=pattern},举个例子:

${OBJ:%.c=%.o}

含义为将OBJ中所有.c后缀文件替换成.o后缀文件的,会自动生成.o后缀的文件。

#例18
SRC = foo.c bar.c main.c

OBJ = ${SRC:%.c=%.o}

main: $(OBJ)
    gcc $(OBJ) -o main

clean:
    rm -rf *.o main

#------------------------
# make
cc    -c -o foo.o foo.c
cc    -c -o bar.o bar.c
cc    -c -o main.o main.c
gcc foo.o bar.o main.o -o main

此规则保留了完整的依赖,并且增加一个文件直接添加的是.c文件名而不是.o目标文件,比较直观方便。注意第三行,大括号中的":"和"="两边都不能加空格,否则make就变成只执行一条 "gcc foo.c bar.c main.c -o main" 了。

4. 通配符与模式规则区别
模式匹配对应的是生成规则,规则对应:目标、依赖和命令,与普通规则不同的是,它并不显示地指定具体的规则,则是自动匹配。而通配符对应的是目标,表示寻找所有符合条件的目标,通常代表一个集合。一个是针对执行规则,一个是针对目标文件,自然是不同的。模式规则和函数中的模式匹配也是不同的。


九、函数使用

1. 语法
函数的使用语法是这样的:

$(function arguments)
或者
${function arguments}

参数之间用逗号","分隔,单个参数可以是以空格分隔的列表。

2. 一些常见內建文本操作函数
(1) 文本(文本)替换

$(subst from,to,text)

函数作用:
对目标文本(或列表)text执行文本替换,将主文本中的from替换成to,并返回替换后的新文本。
参数:
from: 将要被替换的子文本
to: 替换到文本text的新子文本
text: 被操作的主文本
返回:返回替换之后的新文本

#例19
TEXT = "hello world"
FROM = hello
TO   = HELLO
RESULT = $(subst $(FROM),$(TO),$(TEXT))

all:
    @echo $(RESULT)

#-------------
# make
HELLO world

 

(2) 文本(文本)模式替换

$(patsubst pattern,replacement,text)

函数作用:
对目标文本(或列表)text执行文本替换,以模式替换的形式进行替换。
参数:
pattern: 将要被替换的模式匹配方式.
replacement: 替换后的模式匹配方式.
text: 被操作的文本
返回:返回替换后的新文本

#例20
TEXT = foo.c bar.c
RESULT = $(patsubst %.c,%.o,${TEXT})

all:
    @echo $(RESULT)

#-------------
# make
foo.o bar.o

同样的,TEXT可以是列表,这个操作可以使用一个更加简单的模式匹规则语法来操作,相当于:${TEXT : %.c=%.o} 结果是一样的。

(3) 文本精简

$(strip string)

函数作用:将文本中前导和尾随的空格删除,在文本中存在多个连续空格时,使用一个空格替换。
参数参数:
string:目标文本
返回值:精简完的文本

#例21
RESULT=$(strip "Hello   world, nihao  ")

all:
    @echo $(RESULT)

#-------------
# make
Hello world, nihao 

(4) 在主文本中寻找子文本

$(findstring find,in)

函数作用 :
这是文本查找函数,在in中寻找是否有find文本。
参数:
find: 子文本
in:主文本
返回值:如果in中存在find,返回find,否则返回""(空文本)。

#例22
RESULT=$(findstring main.c, "foo.c main.c bar.c")

all:
    @echo $(RESULT)

#-------------
# make
main.c

#例22-2
TEXT=foo.c main.c bar.c
RESULT=$(findstring main.c bar.c, ${TEXT})

all:
    @echo $(RESULT)

#-------------
# make
main.c bar.c

注:例22中的第一个参数main.c不能加引号,否则就找不到,返回空!

(5) 过滤作用

$(filter pattern…,text) 和 $(filter-out pattern…,text)

函数作用:
过滤作用,将符合模式规则的text中的文本挑选出来。
参数:
pattern: 过滤的模式规则
text: 将要处理的文本
返回值:返回符合模式规则的文本

#例23
TEXT := foo.c bar.c foo.h bar.h
RESULT = $(filter %.c,$(TEXT))

all:
    @echo $(RESULT)

#--------------
# make
foo.c bar.c


#例23-2
TEXT := foo.c bar.c foo.h bar.h
RESULT = $(filter-out %.c,$(TEXT))

all:
    @echo $(RESULT)

#--------------
# make
foo.h bar.h

这里的pattern不一定是带 % 的模式匹配,也可以是文件列表。filter(filter-out)函数返回的是text文本中符合(不符合)条件的项目:

#例24
TEXT := foo.c
TEXT_P := foo.c bar.c main.c
RESULT = $(filter $(TEXT_P),$(TEXT))
RESULT_OUT = $(filter-out $(TEXT_P),$(TEXT))
RESULT_R1 = $(filter $(TEXT), $(TEXT_P))
RESULT_OUT_R1 = $(filter-out $(TEXT), $(TEXT_P))

all:
    @echo $(RESULT)
    @echo $(RESULT_OUT)
    @echo $(RESULT_R1)
    @echo $(RESULT_OUT_R1)

#------------------
# make
foo.c

foo.c
bar.c main.c

(6) 按照首字母字典排序

$(sort list)

函数作用:
将给定的list(通常是以空格分隔的文件列表)按照首字母字典排序。
参数:
list:目标列表
返回值:返回排序后的列表。

#例25
TEXT := main.c foo.c bar.c
RESULT = $(sort $(TEXT))

all:
    @echo $(RESULT)

#------------------
# make
bar.c foo.c main.c

(7) 返回列表中的某个元素

$(word n,text)

函数作用:
返回text列表中第n个元素,通常来说,这个text为文件或文本列表,元素为单个的文件名或文本
参数:
n:第n个元素,比较特殊的是,元素列表的计数从1开始。
text: 文件列表
返回值:返回第n个元素

#例26
TEXT := main.c foo.c bar.c
RESULT = $(word 2, $(TEXT))

all:
    @echo $(RESULT)

#------------------
# make
foo.c

(8) 返回列表的子列表

$(wordlist s,e,text)

函数作用:
返回text列表中指定的由s(start)开始由e(end)结尾的列表,s和e均为数字。
参数:
s:截取开始的地方,s从1开始
e:截取文本结束的地方。
text:目标文件列表
返回值:返回截取的文本

#例27
TEXT := main.c foo.c bar.c hello.c ku.c gou.c
RESULT = $(wordlist 2, 4, $(TEXT))

all:
    @echo $(RESULT)

#------------------
# make
foo.c bar.c hello.c

如果s大于text的最大列表数量,返回空文本,如果e大与text的最大列表数量,返回从s开始到结尾的列表,如果s大于e,返回空。

(9) 列表中的元素数量

$(words text)

函数作用:
返回text列表中的元素数量
参数:
text:目标列
返回值:返回text列表中的元素数量

#例28
TEXT := main.c foo.c bar.c hello.c ku.c gou.c
RESULT = $(words $(TEXT))

all:
    @echo $(RESULT)

#------------------
# make
6

(10) 返回列表第一个或最后一个元素

$(firstword names…) 和 $(lastword names…)

函数作用:返回names列表中的第一个元素或最后一个元素
参数:
names:目标列表
返回值:返回names列表中的第一个元素

#例29
TEXT := main.c foo.c bar.c hello.c ku.c gou.c
RESULT_F = $(firstword $(TEXT))
RESULT_L = $(lastword $(TEXT))

all:
    @echo $(RESULT_F)
    @echo $(RESULT_L)

#------------------
# make
main.c
gou.c

3. 一些常见內建文件与目录操作函数

(1) 截取文件路径名中的目录部分或文件名部分

$(dir names…) 和 $(notdir names…)

函数作用:截取文件路径中的目录部分,如:/home/usb/file.c,截取/home/usb/,目标可以是列表
参数:
names:目标文件,可以是列表
返回值:返回目录,如果目标是列表,返回以空格分隔的目录。

#例30
TEXT := /home/usb/main.c /home/usb/foo.c /home/tx/bar.c /home/rx/hello.c
RESULT_D = $(dir $(TEXT))
RESULT_N = $(notdir $(TEXT))

all:
    @echo $(RESULT_D)
    @echo $(RESULT_N)

#------------------
# make
/home/usb/ /home/usb/ /home/tx/ /home/rx/
main.c foo.c bar.c hello.c

(2) 获取和去除文件后缀

$(suffix names…) 和 $(basename names…)

函数作用 :获取文件列表中的后缀部分。
参数:
names:目标文件,可以是列表
返回值:返回文件列表中的后缀部分,如:".c",".o"

#例31
TEXT := /home/usb/main.c foo.h /home/tx/bar.h hello.c
RESULT_S = $(suffix $(TEXT))
RESULT_B = $(basename $(TEXT))

all:
    @echo $(RESULT_S)
    @echo $(RESULT_B)
#------------------
# make
.c .h .h .c
/home/usb/main foo /home/tx/bar hello

(3) 为列表添加后缀

$(addsuffix suffix,names…)

函数作用:为目标文件列表添加后缀
参数:
suffix:添加的后缀内容
names:目标列表
返回值:返回添加完后缀的列表

#例32
TEXT := foo bar
RESULT := ${addsuffix .o , ${TEXT}}

all:
    @echo $(RESULT)

#------------------
# make
foo.o bar.o

(4) 两个列表元素逐个衔接

$(join list1,list2)

函数作用 :逐个地将list2中的元素链接到list1。
参数:
list1:链接后元素在前的列表
list2:链接后元素在后的列表
返回值:返回链接后的链表

#例33
LIST1 := foo bar
LIST2 := .c .h
RESULT := ${join ${LIST1} , ${LIST2}}

all:
    @echo $(RESULT)

#------------------
# make
foo.c bar.h

当list1和list2不等长时,视为与空文本的链接,例如list1为3个元素,list2有两个元素,那么list1的第三个元素对应空文本。
list中的空格将始终被当做一个空格处理。

(5) 获取绝对路径

$(realpath names…)

函数作用:对names中的每个文件,求其绝对路径,当目标为链接时,将解析链接。
参数:
names:目标文件名(列表)
返回值:目标文件名对应的绝对路径(列表)。

#例34
LIST := foo.c mf
RESULT := ${realpath ${LIST}}

all:
    @echo $(RESULT)

#------------------
root@ubuntu:/work/11.Makefile/2# make
/work/11.Makefile/2/foo.c /work/11.Makefile/4.pattern_rules/Makefile

注:mf是个软链接文件,这个是真的会去遍历目录,不仅仅是字符串列表操作。

(6) abspath

$(abspath names…)

函数介绍:作用与realpath()函数相似,唯一的不同是不解析链接,将链接也当前一个普通文件。

#例35
LIST := foo.c mf
RESULT := ${abspath ${LIST}}

all:
    @echo $(RESULT)

#------------------
root@ubuntu:/work/11.Makefile/2# make
/work/11.Makefile/2/foo.c /work/11.Makefile/2/mf

4. 其他常用函数

(1) 对列表单个元素进行文本操作

$(foreach var,list,text)

函数作用:对list中的每个var,调用text命令。
参数:
var:被操作的目标元素
list:目标元素列表
text:对目标元素执行的操作。
返回值:返回执行操作后的文本.

例36
TEXT := foo.c bar.c
RESULT := ${foreach file, ${TEXT}, /home/${file}pp}

all:
    @echo $(RESULT)

#------------------
# make
/home/foo.cpp /home/bar.cpp

注:单纯的文本操作,可以个列表价一些前缀或后缀。

(2) file 函数

$(file op filename[,text])

函数作用 :向文件执行文本的输入输出
参数:
op:要对文件进行的操作,支持:>(覆盖写) >>(追加写) <(读)
filename:文件名
text:如果op为写,此参数表示要写入的文本。
返回值:返回值无意义

#例37
TEXT := "hello world"
RESULT := ${file >, test, ${TEXT}}

all:
    @echo ${RESULT}

注:测试失败,读写都不会有任何动作。也没查到相关资料。

(3) 调用自定义函数和自定义函数

$(call variable,param,param,…)

函数作用:call函数在makefile中是一个特殊的函数,因为它的作用就是创建一个新的函数。你可以使用call函数创建各种实现复杂功能
的函数。
参数:
variable:函数名
param:函数参数,是被传递给自定义的variable函数。
param ...
返回值:返回定义函数的结果.

当call函数被调用时,make将展开这个函数,函数的参数会被赋值给临时参数$1,$2,$0则代表函数名本身,参数没有上限也没有下限。

对于call函数需要注意:当定义的函数名与内建函数名同名时,总是调用内建函数。call函数在分配参数之前扩展它们,意味着对具有特殊扩展规则的内置函数的引用的变量值可能不会正常执行。

#例38
func = $1.$2
foo = $(call func,a,b)
all:
    @echo $(foo)

#---------------
# make
a.b


#例38-2
define func1
    @echo "my name is $(0)"
endef

define func2
    @echo "my name is $(0)"
    @echo "param1 => $(1)"
    @echo "param2 => $(2)"
endef

all:
    $(call func1)
    $(call func2, hello, world)

#-----------------------
# make
my name is func1
my name is func2
param1 =>  hello
param2 =>  world


#例38-3
define func_my
    gcc -c $(1)
    gcc -c $(2)
endef

all:
    $(call func_my, bar.c, foo.c)

#---------------
# make
gcc -c  bar.c
gcc -c  foo.c

define是定义变量的一种方式,这种定义变量的特殊之处在于它可以定义带有换行的变量,所以它可以定义一系列的命令,在这里就可以定义一个函数。

(4) 获取未展开的变量值

$(value variable)

函数作用:获取未展开的变量值。
参数:
variable:目标变量,不需要添加"$"
返回值:返回变量的未展开的值.

#例39
FOO = $PATH
all:
    @echo $(FOO)
    @echo $(PATH)
    @echo $(value FOO)

#----------------------------
# make
ATH
/usr/local/sbin:/usr/local/bin:.../usr/sbin:/usr/bin:/sbin:/bin:/usr/games
/usr/local/sbin:/usr/local/bin:.../usr/sbin:/usr/bin:/sbin:/bin:/usr/games

打印系统环境变量。第一个结果${FOO}为ATH是因为,make将$P解析成makefile中的变量,而不是将(PATH)作为一个整体来解析。

(5) 使函数中能执行编译指令

$(eval text)

函数作用:eval在makefile中是一个非常特别的函数,它允许此函数新定义一个makefile下的结构,包含变量、目标、隐式或者显示的规则。eval函数的参数会被展开,然后再由makeifle进行解析。也就是说,eval函数会被make解析两次,第一次是对eval函数的解析,第二次是make对eval参数的解析。
参数:
text:一个makefile结构
返回值:返回值无意义.

#例40
define func
foo:
    gcc -c foo.c -o foo 
endef 

$(eval $(call func))

# make
gcc -c foo.c -o foo 


#例40-2
define func
$1:
    gcc -c $2 -o $$@
endef

$(eval $(call func,main,main.c))

all:
    @echo hello

#-----------------
# make
gcc -c main.c -o main

不要"all:" make也可以执行,此时就算有"all:",但是make也不会执行它!

例40-2的func函数返回值依旧是一个makefile规则的表达式,这时候如果需要执行这个规则,那就要使用makefile的规则对其进行再一次地解析,就需要用到eval()函数,eval()函数通常与call()一起使用。

例40-2第三行中出现的"$$",在makefile的语法中,$是一个特殊字符,通常与其他符号结合表示特定的含义,如果我们单纯的就想打出"$"字符,我们需要使用"$$"表示"$"符号,就像C语言中的转义符号'\',如果我们要使用真实的'\'符号,我们就得使用'\'来进行
转义。

(6) 判断变量的来源

$(origin variable)

函数作用:这个函数与其他函数的区别在于,它不超作变量的值,而是返回变量的定义信息,告诉你变量是哪来的,告诉你变量的出生。
参数:
variable:被操作的目标变量,注意是变量的名字,不是引用,所以不要加“$”字符
返回值:返回变量的定义信息。这些定义信息是一些枚举值:

undefined:变量未定义
default:变量被默认定义,就像CC.
environment: 传入的环境变量
environment override:本来在makefile中被定义为环境变量,但是新定义的变量覆盖了环境变量。
file:在makefile中被定义
command line:在命令行中被定义
override:使用override定义的变量
automatic:在规则的命令部分被定义的自动变量,也可以理解为临时变量.
#例41
RESULT=false
ifeq "$(origin PATH)" "environment"
RESULT=true
endif

VAL := 1

override USER = ZhangShan

all:
    @echo $(RESULT)
    @echo $(origin SHELL)
    @echo $(origin CC)
    @echo $(origin VAL)
    @echo $(origin CmdVal) # passed when make
    @echo $(origin USER)
    @echo $(origin @)

#-----------------------------------------
# make CmdVal=hello
true
file
default
file
command line
override
automatic

# make
true
file
default
file
undefined
override
automatic

PATH是个环境变量,因此打印为true; make时传参CmdVal了,就是命令行参数类型,若没有传参就是undefined; 将环境变量USER在makefile中使用override定义的变量进行覆盖,因此是override; 如果是自动化变量(如 @,< 等),那么返回automatic。

(7) 判断变量的展开方式

$(flavor variable)

函数作用: 与origin的属性类似,这个函数返回变量的展开方式,在之前章节有提到过,变量展开方式有两种:循环递归扩展和简单扩展。
参数:
variable:目标变量
返回值:返回变量的展开方式,这是一个枚举值:

undefined:变量未定义
recursive:循环递归扩展
simple:简单扩展
#例42
foo:=foo
bar=bar

all:
    @echo $(flavor $(foo))
    @echo $(flavor $(bar))

#------------------
# make
simple
recursive

5. 控制执行流的函数

(1) 返回错误信息并终止执行

$(error text…)

函数作用: 生成一个错误,并返回错误信息text。
参数:
text:将要打印的错误信息。
返回值:返回值无意义,makefile执行终止。

只要调用了这个函数,错误信息就会生成,但是如果将其作为一个循环递归变量的右值,它会被最后扩展的时候才被调用,或者放在规则的命令部分,它将被shell解析,可能结果并不是你想要的。

#例43
all:
    $(error there is an error!)
    @echo "won't be excute!"

#-------------------
# make
Makefile:2: *** there is an error!.  Stop.

(2) 打印警告和提示信息

$(warning text…) 和 $(info text…)

这两个函数属性与error相似,但是这两个函数并不会导致makefile的执行中断,而是警告和提示,警告会打印出行号,而info不会。

#例44
all:
    $(warning just a warning test)
    $(info just a info test)
    @echo "excute!"

#---------------------
# make
Makefile:2: just a warning test
just a info test
excute!

6. shell函数

(1) makefile中以shell方式执行函数

$(shell command...)

函数作用: shell函数与其他函数不一样的地方在于,它将它的参数当做一条shell命令来执行,shell命令是不遵循makefile的语法的,
也就是说不由make解析,将传递给shell,这一点和规则中的命令部分是一样的。
参数:
command:命令部分
返回值:返回shell命令执行结果

值得注意的是,make会对最后的返回值进行处理,将所有的换行符替换成单个空格。在makefile中可以使用另一种做法来调用shell命令,就是用"`xxx`"(键盘左上角ESC下的键)将命令包含。

#例45
KDIR1 := /lib/modules/`uname -r`/build
KDIR2 := /lib/modules/$(shell uname -r)/build

all:
    @echo $(KDIR1)
    @echo $(KDIR2)

#--------------------
# make
/lib/modules/4.2.0-27-generic/build
/lib/modules/4.2.0-27-generic/build

 

十、makefile多目录处理

1. 在makefile的语法中,支持像C/C++一样直接包含其他文件,include语法:

include filename

这个include指令告诉make,挂起读取当前makefile的行为,先进到其他文件中读取include后指定的一个或者多个Makefile,然后再恢复处理当前的makefile。include指令同时支持通配符,和变量的使用,例如:

bar = bar.mk
include *.c $(bar)

被包含的文件不需要采用makefile默认的名称(GNUmakefile,makefile或Makefile)。通常情况下,使用include的场景是:多个目录下的文件编译由各个makefile分布式处理,需要使用同一组变量或者模式规则。

举一个简单的示例,同目录下存在四个文件:Makefile foo.c bar.c inc.mk,inc.mk为被包含的文件,内容为:SRC += bar.c,Makefile为主makefile文件,内容为:SRC = foo.c include inc.mk,定义一个SRC变量,并包含inc.mk文件中的操作,最后$(SRC)的结果为:foo.c bar.c.

表明文件的包含关系*享变量,我们可以直接简单地理解为将被包含文件中的数据添加到主文件中,这和C/C++中的include操作是一致的。

#例46
inc.mk:
SRC += foo.c bar.c

Makefile:
SRC := main.c
-include inc.mk
all:
    @echo $(SRC)

#-----------------
# make
main.c foo.c bar.c

2. 被include包含的目标的循环处理
如果被包含的文件不存在,将会发生什么事呢。首先,Makefile在检测到include的指令时,尝试寻找被包含的文件,发现目标文件不存在。扫描完整个Makefile之后,寻找是否有生成目标文件的规则,如果有,则生成该被包含的目标文件,第二次执行Makefile的扫描时该文件就Makfile搜索到并成功包含进来。如果在第二次扫描之后没有发现生成被包含文件的规则,程序报错,且退出当前Makefile的执行。
在某些情况下,如果我们不确定被包含文件是否存在,且不希望在不存在时报错,可以使用下面的包含指令,即:在include添加"-",就可以忽略报错而继续运行Makefile。

-include file

3. 指定文件搜索目录
在执行多目录下的工程编译时,make默认在当前目录下搜索文件,如果需要指导make去指定搜索其它目录,我们需要使用"-I"或者 "--include-dir" 参数来指定。
事实上,执行编译的过程是makefile的规则中的命令部分,这一部分是由shell进行解析的,-I 其实是shell下gcc的编译指令。通常,用法是这样的:

-I. -I../dir/

除了使用gcc的参数"-I",还可以使用makefile中自带的变量"VPATH",通过环境变量"VPATH"指定的目录会被make添加到目录搜索中。在VPATH中添加的目录,即使是文件处于其他目录,我们也可以像操作当前目录一样操作其他目录的文件,例如:

#例47
VPATH += src #test.c位于当前目录下的src目录下
test: test.c
    gcc $^ -o $@

#-----------------
# make
gcc src/test.c -o test

等效于:

test:src/foo.c
    gcc $^ -o $@

但是写成下面这样是不行的:

VPATH += src
test:
    gcc foo.c -o $@

这是因为 VPATH 是makefile中的语法规则,而命令部分是由shell解析,所以shell并不会解析VPATH,而是将其当成编译当前目录下的foo.c。
注:好像-I只能用来包含头文件。

4. 切换目录并编译
若需要切换到其他目录下进行编译,这时候我们就需要使用到make的"-C"选项,需要注意的是,"-C"选项只支持大写,不支持小写:

make -C dir

make将进入对应目录,并搜索目标目录下的makefile并执行,执行完目标目录下的makefile,make将返回到调用断点处继续执行makefile。利用这个特性,对于大型的工程,我们完全可以由顶层makefile开始,递归地遍历整颗目录树,完成所有目录下的编译。

5. 多目录makefile共享环境变量
与shell类似,makefile同样支持环境变量,环境变量分为两种:
(1) 对应运行程序
(2) 对应程序运行参数
下面是常用环境变量的列表(针对C/C++编译,其他语言不列出):

(1) 对应运行程序的环境变量:
AR:打包程序,默认值为ar,对目标文件进行打包,封装静态库。
AS:汇编程序,默认值为as,将汇编指令编译成机器指令。
CC:c编译器,默认值为cc,通常情况下,cc是一个指向gcc的链接,负责将c程序编译成汇编程序。
CXX:c++编译器,默认值为g++。
CPP:预处理器,默认值为"$(CC) -E",注意这里的CPP不是C++,而是预处理器。
RM:删除文件,默认值为"rm -f",-f表示强制删除。

(2) 对应程序运行参数的环境变量:
ARFLAGS:指定$(AR)运行时的参数,默认值为"ar"。
ASFLAGS:指定$(AS)运行时的参数,无默认值。
CFLAGS:指定$(CC)运行时的参数,无默认值。
CXXFLAGS:指定$(CXX)运行时的参数,无默认值。
CPPFLAGS:指定$(CPP)运行时的参数,无默认值,注意这里的CPP不是C++,而是预处理器。
LDFLAGS:指定ld链接器运行时的参数,无默认值。
LDLIBS:指定ld链接器运行时的链接库参数,无默认值。

这些默认的环境变量将在执行make时传递给makefile。

同样的,也可以使用 export 指令将特定的变量添加到环境变量中,但是,通过 export 指定添加的环境变量只作用于当前makefile以及递归调用的子makefile中,对于同目录下或者非递归调用的其他目录下的makefile是不起作用的。

当前目录下Makefile:

#例48
AAA += XYZ
export AAA
AAA += LMN

all:
    make -C dir/
    @echo main makefile

all:
    make -C dir/
    @echo main makefile


dir目录下的Makefile
all:
    @echo $(AAA)

#---------------------
# make
make -C dir/
make[1]: Entering directory `/work/11.Makefile/2/dir'
XYZ LMN
make[1]: Leaving directory `/work/11.Makefile/2/dir'
main makefile

若没有export,dir目录下的Makefile是感知不到AAA变量的,就会打印空。
使用export指令,可以实现多个目录下的makefile共享同一组变量设置,对于多目录下的编译提供了很大的便利。同时需要注意的是,不同makefile中,即使有调用关系,变量是不共享的(注意调用与包含的区别)。而且,即使在makefile中修改了环境变量,不使用export更新环境变量,在其调用的子makefile下不会共享其修改,还是默认的环境变量。如果想共享不同makefile中的变量,可以通过"include filename"的方式来实现。

6. 链接库的目录搜索

在makefile的目标编译规则中,依赖文件同样也可以是库文件,库文件与普通文件不一样。当其作为依赖文件或者是参数命令的编译时,使用-lxxx来指定对应的库,如果库不存在于当前目录下,还需要为其指定搜索路径,使用"-L"参数指定。一般而言,动态库会存在于系统目录(/usr/local/lib,/lib/,/usr/lib)中,并不需要指定相应的目录,而静态库存在于工程目录中,则可能需要使用-L参数来指定。下面示例中,使用-L.指定库搜索目录为当前目录,使用-l指示链接mylib库。

#例49
all: foo.c -lmylib
    cc  $< -L . -lmylib -o main

 

 

 

 

参考:
makefile官方文档:https://www.gnu.org/software/make/manual/make.html

https://zhuanlan.zhihu.com/p/362640343

 

补充:

试验篇

1. 依赖中要考虑头文件

SRC = ${wildcard *.c} #将所有.c文件赋值给SRC,以空格分隔,SRC=foo.c bar.c main.c
MAIN_SRC = main.c
TARGET = main
RAW_OBJ = ${patsubst %.c, %.o, ${SRC}} #将SRC中所有.c后缀文件转换成.o后缀文件,RAW_OBJ=foo.o bar.o main.o
OBJ = ${filter-out main%, ${RAW_OBJ}} #过滤掉main.o,OBJ=foo.o bar.o

${TARGET}:${OBJ}
    cc $^ ${MAIN_SRC} -o ${TARGET} #cc foo.o bar.o main.c -o main

${OBJ}:%.o : %.c %.h common.h #表示foo.o依赖于foo.c foo.h common.h,生成foo.o。bar同理
    cc -c $^

clean:
    rm -rf *.o *.gch ${TARGET}

编译测试:

/work/11.Makefile/2# make //全编译
cc -c bar.c bar.h common.h
cc -c foo.c foo.h common.h
cc bar.o foo.o main.c -o main #cc foo.o bar.o main.c -o main
/work/11.Makefile/2# 
/work/11.Makefile/2# touch common.h
/work/11.Makefile/2# make //所有文件都依赖common.h,都全编译
cc -c bar.c bar.h common.h
cc -c foo.c foo.h common.h
cc bar.o foo.o main.c -o main #cc foo.o bar.o main.c -o main
/work/11.Makefile/2# 
/work/11.Makefile/2# touch foo.h //只有依赖foo.h文件的才会编译
/work/11.Makefile/2# make
cc -c foo.c foo.h common.h
cc bar.o foo.o main.c -o main #cc foo.o bar.o main.c -o main
/work/11.Makefile/2# 
/work/11.Makefile/2# touch bar.c //只有依赖bar.c文件的才会编译
/work/11.Makefile/2# make
cc -c bar.c bar.h common.h
cc bar.o foo.o main.c -o main #cc foo.o bar.o main.c -o main

需要特别强调的一点是依赖文件,很多人总是会忽略依赖文件的作用,因为依赖文件在编译的时候并不提供任何帮助,但是make需要靠依赖文件来判断文件的更新和判断目标是否需要更新,如果忽略依赖的头文件,仅仅添加%.c依赖的话,*.h common.h中的修改将不会导致重新编译,这样明显不是用户想要的。

 

2. 总结:总结make时Makefile的编译规律可知,在make时会先去检查目标文件是否存在,若目标文件不存在,则就会触发目标下面的编译规则去编译。若将编译目标写成"all:",但编译命令中又不会生成all文件,则每次make都会执行目标下的命令进行编译!这就降低了编译效率。若目标为"main:",编译又生成了main这个文件,那么再次make时是否重新编译将取决于其依赖有没有更新,因此Makefile中需要准确完整的指出其依赖,以免修改文件后不会重新生成目标文件!

简而言之,make时会先判断目标文件是否存在,若不存在则编译生成它。若已经存在了,就要看其依赖有没有更新,若有更新则重新编译其依赖并编译重新生成目标文件。否则什么也不做。

 

上一篇:开放英语2


下一篇:蟒蛇书学习笔记——Chapter 09 Section 02 使用类和实例