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


九、函数使用

转到:Makefile笔记(3)——函数汇总

十、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逻辑处理

1. makefile中的逻辑处理部分包括:条件判断以及运行时运行时参数指定。

2. ifeq/ifneq

格式:

ifeq (var1,var2)
...
else
...
endif

注意:ifeq与后面接的条件判断式之间必须有空格。执行语句部分可以有空格和tab键。

ifeq是最常用的条件判断语句之一,除了使用()来放置参数,它还支持下面几种语法,即单引号和双引号在判断中表示同样的含义。

ifeq 'var1' 'var2'
ifeq "var1" "var2"
ifeq 'var1' "var2"
ifeq "var1" 'var2'

ifneq 是 ifeq 相反的判断。

#例1
foo := 1
bar := 2

ifeq ($(foo), $(bar))
    result = true
else
    result = false
endif

all:
    @echo $(result)

#--------------------
# make
false

3. ifdef/ifndef

用于测试一个变量是否有值,值为空则返回假,值为非空则返回真,即使是值为false或False之类的,也是返回真。需要注意的是,ifdef指令针对循环扩展类型的变量时,只判断变量是否有值,并不会对其进行扩展

#例3
foo =
bar = $(foo)
ifdef bar
    foobar = yes
else
    foobar = no
endif

all:
    @echo $(foobar)

#------------------
# make
yes

下面这个示例就不一样了,唯一的差别是一个赋值时使用了循环递归扩展变量,一个使用了简单扩展变量。

#例3-2
foo :=
bar := $(foo)
ifdef bar
    foobar = yes
else
    foobar = no
endif

all:
    @echo $(foobar)

#------------------
# make
no

ifndef 的语法与 ifdef 一致,返回值与ifdef相反。

4. 变量种类

(1) makefile内变量定义
(2) makefile环境变量
(3) 命令行执行make时参数传递

5. 变量定义方式

(1) 使用 "=" 定义的循环递归扩展变量。
(2) 使用 ":=" 定义的简单扩展变量。
(3) 使用 define 定义的变量,它的特点是支持定义带换行的变量,扩展方式为简单扩展。
(4) 使用 override 定义的变量,顾名思义,就是覆盖其他变量内容,扩展方式为简单扩展。

6. 变量优先级

(1) override的优先级最高,使用override定义的变量在整个makefile解析期间始终不会被改变,除非存在另一个override对其进行修改。
(2) 用户传递的命令行参数其次,这个特性被使用的频率是非常高的,用命令行传入参数的方式覆盖makefile中定义的变量或者环境变量,由用户指定当前的参数设置。在makefile中对此变量使用"+="符号追加的值也会被忽略。
(3) 使用"define",":=","="定义的变量。
(4) 优先级最低的,就是环境变量,默认值总是优先级最低的,这个也符合常理。

makefile在执行时存在的默认变量,对应一系列默认值。在执行make时,make允许用户传递参数值到makefile中。

 

 

 

 

 

 

 

 

 

参考:
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时会先判断目标文件是否存在,若不存在则编译生成它。若已经存在了,就要看其依赖有没有更新,若有更新则重新编译其依赖并编译重新生成目标文件。否则什么也不做。

 

posted on 2022-02-21 01:13  Hello-World3  阅读(715)  评论(0编辑  收藏  举报

导航