概述在学习Makefile之前先有个宏观的认识。Makefile执行分两个阶段第一阶段解析ParseMake 首先把整个 Makefile 从头到尾读一遍这时候变量定义ifeq/ifneqincludedefineevalwarninginfo都会被处理这些Make语法只能在 Make 解析阶段执行不能直接放进 recipe 里执行。第二阶段构建Build考虑用户要构建什么目标比如用户输入make check于是找到对应的目标依赖命令执行对应recipe。下面这个例子让你对Makefile的两个执行阶段有个直观感受$(warning begin)check: echo hello$(warning end)执行make check后输出为Makefile:1: begin Makefile:6: end hello1. Makefile规则这就是Makefile中的规则# 在行前使用#表示注释target1, target2...:depend1, depend2,...#需要顶格可以没有依赖没有目标称为伪目标clean常用伪目标comand1#每个命令前必须有一个Tab缩进comand2#每个命令前必须有一个Tab缩进target(目标)执行command后可以生成一个和目标同名的文件也可以不生成文件因为command可能只是一个动作这样的目标为伪目标depend(依赖)生成目标target所需要的文件被成为依赖command(命令)生成目标需要执行的命令也被成为生成规则的配方(配方recipe)。(规定行首必须需要一个tab缩进)只有“配方recipe行”才会被作为 shell 命令执行每一行配方命令由 一个新的 shell 进程执行行与行之间不共享 shell 状态变量、cd 的当前目录等如果你想要共享shell状态可以把命令放到同一行。或者使用 .ONESHELL: 特殊目标。【注】①target可以有多个因为comand也可以有多个②要对规则目标依赖命令(配方)四个词语及概念烂熟于心。例#多个目标多个命令app1,app2:a.c b.c c.c gcc-oapp1 a.c b.c gcc-oapp2 b.c c.c1.1 Makefile 控制打印调试使用Makefile 会默认打印出编译过程中执行的命令本身如果不想打印命令可以通过在那行命令前加上‘’但命令任然会执行。如下no_output:echonot displayechowill display执行结果如下$make no_output not display echowill displaywill display2. Makefile工作原理红字很重要2.1 规则的执行先从一个例子开始# makefile# 规则之间的嵌套# 规则1app:a.o b.o c.o gcc a.o b.o c.o-oapp# 规则2a.o:a.c gcc-ca.c# 规则3b.o:b.c gcc-cb.c# 规则4c.o:c.c gcc-cc.c总结一下结论执行make后会去查找Makefile文件从中找到第一条规则且规则中命令不能有通配符第一条规则中的命令执行的前提是依赖全都存在。如果第一条规则中的依赖不存在他就会去找哪条命令能生成需要的依赖直到所有的依赖都有了如果还是找不到直接生成依赖所需的规则就会去找有通配符的规则进行匹配。如果有依赖会从第一条规则开始检查规则是否满足“目标比依赖新”的要求如果不满足则递归的寻找依赖的生成规则并如此递归下去。如果要执行Makefile中非第一条规则那在make编译时就要把要那条规则的目标写到 make 后面比如make 。2.2 文件的时间戳(标记文件的更新时间)还是看这个例子# makefile # 规则之间的嵌套 # 规则1 app:a.o b.o c.o gcc app.o a.o b.o c.o -o app # 规则2 a.o:a.c gcc -c a.c # 规则3 b.o:b.c gcc -c b.c # 规则4 c.o:c.c gcc -c c.c如果先执行make后修改了文件中的b.c然后再执行make那么这时候就会先执行规则3然后再执行规则1。问题第二次执行make的时候明明第一条规则的目标实现比依赖要新按理说不应该去执行规则2了呀自己写个小程序实验目标是实现以下效果Makefile与上述例子相同。在执行完一遍make后。我更改b.c再次make后执行结果如下可以验证Make在检查第一条规则时看当前的依赖是否有依赖如果有就会递归的检查生成第一条规则依赖的规则是否满足“目标比依赖新”的要求。2.3 自动推导Makefile会会使用默认的编译规则编译.c文件所以在写依赖时只需要写*.oMakefile就会为这个.o寻找合适的.c源文件。2.4 伪目标clean常用于伪目标即不会生成任何文件没有依赖只是执行命令。为了防止目录中已经存在名为clean的文件而造成冲突需要使用.PHONY声明clean不是真正的文件举例如下# 添加规则, 删除生成文件 *.o 可执行程序# 声明clean为伪文件.PHONY:clean clean:# shell命令前的 - 表示强制这个指令执行, 如果执行失败也不会终止-rm$(obj)$(target)echoClear obj and target done!3. 变量Makefile 中有三种分别为自定义变量预定义变量自动变量(自动化变量)。最后看变量的赋值方法。3.1 自定义变量使用方法定义方法变量名变量值变量取值方法$(变量名)小括号必须要带注意偶尔看到有人使用${变量名}的方法对makefile中的变量取值虽然也行但小括号是标准写法。3.2 预定义变量Makefile中有些变量是预定义的一般都是大写的用户可以直接使用比如CFLAGS编译的时候使用的参数,-Wall -g -cAR生成静态库库文件的程序名称CCC 语言编译器的名称CXXC 语言编译器的名称PWDMakefile所在绝对路径详情请参考爱编程的大丙博客。下面是其中的一个例子objadd.o div.o main.o mult.o sub.otargetcalcCFLAGS-O3# 代码优化$(target):$(obj)$(CC)$(obj)-o$(target)$(CFLAGS)你可能有疑惑上面说预定义代码不用定义但上面的例子不也是定义了一遍CFLAGS嘛答因为Makefile中代码的定义和赋值是在一起的。3.3 自动变量(自动化变量)自动变量 很重要用于表示规则中的目标和依赖文件只能在某条规则的命令中使用。variableimplication$*表示不包含文件扩展名的目标文件的名称$表示可包含文件扩展名的目标文件的名称如.o .c .out等$表示依赖项中第一个依赖文件的名称$?表示依赖项中比目标文件时间戳晚了的依赖文件空格分隔$^表示依赖项中不重复的依赖文件空格分隔$表示依赖项中可能有重复按先后顺序排列的的依赖文件空格分隔下面是一个小例子calc:add.o div.o main.o mult.o sub.o gcc $^-o $3.4 变量的赋值重要赋值符‘’使用赋值符“”设置的变量,它的值在运行时才能确定赋值符‘:’这种赋值行为立即生效被赋值的变量为即时变量。赋值符‘?’如果之前这个变量没有被设置过才会生效。赋值符‘’Makefile中如果变量是字符串我们想要追加一些字符串进去时使用很常用。例如src./aa.c ./bb.c src./sub.c 最后src的值为./aa.c ./bb.c ./sub.c【其中?:有什么区别】?的设计初衷为变量提供 “默认值”同时允许用户通过环境变量或命令行参数如 make CCclang灵活覆盖适合需要定制化构建的场景。:的设计初衷强制固定变量值避免外部环境干扰确保每次构建的依赖如编译器一致适合对构建稳定性要求高的场景。递归赋值与:类似但赋值时机是 “延迟到变量被使用时”而非解析时。不过在现代 Make 中和:的行为在简单赋值场景下差异不大但:的 “立即赋值” 特性更直观推荐优先使用。3.4 变量的取值使用小括号将变量括起来加美元符号与shell脚本中不同。FOO$(DIR)4. 模式匹配(模板)如果文件中有多个文件写成大概本文“2.2 文件时间戳”小节那样似乎没什么不妥但是如果文件多达10个以上会导致文件非常臃肿我们可以将这一系列类似的规则使用百分号%写成一个模板如下只是可读性下降了# 模式匹配-通过一个公式,代表若干个满足条件的规则 # 依赖有一个,后缀为.c,生成的目标是一个.o 的文件,%是一个通配符,匹配的是文件名%.o:%.c gcc $-c-o $问题这条规则会去哪里寻找.c源文件问题问的就有毛病这条规则是其他规则需要的时候才被调用的。如果一条规则在执行过程发现自己的依赖文件中需要某.o文件但是又找不到就会用该匹配规则来生成对应的.o文件5. 函数Makefile中的函数的主要目的是帮助开发者快速获取想要得到的信息格式如下返回值接收变量$(函数名 参数1参数2…)且Makefile中的函数都有返回值。主要讲解我用到过的几个函数从函数定义函数参数函数返回值用法举例四方面介绍。5.1wildcard函数定义return$(wildcard PATTERN), 该函数只有一个参数但该参数可以分为由空格分隔的多个部分。用于从给定目录找到与PATTERN样式匹配的文件名并返回这些文件的路径。注意给定目录的子目录不在扫面范围。函数参数PATTERN可以有多个部分可以指定路径要匹配的文件用*表示类似*.c *,o。如果不指定路径则默认为当前路径。返回值返回匹配到的文件的路径含文件名以空格分隔。用法举例|-a.c|-b.c|-test/|-aa.c|-bb.c 比如从./*.c和./test/*.c中找到所有的符合*.c要求的空格分隔的文件路径。rte$(wildcard *.c ./*.c ./test/*.c)返回值rtea.c b.c ./a.c ./b.c ./test/aa.c ./test/bb.c 可见*.c与./*.c是重复了有点5.2patsubst函数定义return$(patsubst pattern, replacement, text), 该函数有 3 个参数由逗号分隔。用于将text中给定的文件名列表中找到与pattern符合的文件名并将其都替换成replacement指定的文件名样式。函数参数pattern : 模式字符串text中要被替换的名称的样式通配符的形式表示 [通配符是 %]比如%.c表示.c后缀的都要被替换掉。replacement : 模式字符串想要被替换成什么通配符的形式表示。比如%.o表示原来的后缀被替换成.o我猜测可以有多个模式由空格分隔但没啥用处text : 等待被替换的文件名称列表的原始数据返回值按照规则替换后的字符串。用法举例若srcaa.c bb.c test/aa.c test/bb.cboj$(patsubst *.c, *.o,$(src))返回值obja.o b.o test/aa.o test/bb.o 只是字符串的变换没有真正执行编译操作。5.3filter函数定义return$(figure pattern, text), 该函数有 2 个参数由逗号分隔。用于从text中给定的文件名列表中过滤出与pattern符合的文件名并返回。函数参数pattern : 模式字符串可以通配符表示多个之间空格分隔通配符的形式表示 [通配符是 %]要留下的名称样式。text : 等待被过滤的文件名称列表的原始数据返回值按照规则过滤后的字符串。用法举例若objaa.o bb.o ff.o gg.o ee.o 若mainsff.o gg.oboj$(figure$(mains)ee.o,$(obj))返回值objff.o gg.o ee.o 只是字符串的变换没有真正执行编译操作。5.4filter-out函数定义return$(figure-out pattern, text), 该函数有 2 个参数由逗号分隔。用于将text中给定的文件名列表中去除与pattern符合的文件名并返回。函数参数pattern : 模式字符串text中要被去除的文件名也可用通配符表示通配符的形式表示 [通配符是 %]一般直接指定要去除的文件。text : 等待被去除的文件名称列表的原始数据返回值按照规则去除后的字符串。用法举例若objaa.o bb.o ff.o gg.o 若mainsff.o gg.oboj$(figure-out$(mains),$(obj))返回值objaa.o bb.o 只是字符串的变换没有真正执行编译操作。5.5eval函数作用把call或其他函数展开后得到的文本再当成 Makefile 代码重新解析并执行。直接看下面的Makefile源码define pkg_dirs$(1)_LIST$(2)/demo_a$(2)/demo_b$(1)_COUNT :2endef demos: echolist[$(demos_LIST)]echocount$(demos_COUNT)# call 加 eval$(eval$(call pkg_dirs,demos,apps/gopoint))上述代码中define用于定义可复用Makefile代码模板call函数负责给模板传参展开生成文本eval函数负责把生成的文本当成真正的 Makefile 代码执行。如果没有eval函数call展开的也只是一堆无关文本而不是Makefile变量。所以必须要用eval函数可以手动执行make demos查看该段代码输出。5.6strip函数作用字符串洗白函数用于把变量中多余的空格去掉。直接看下面的Makefile源码# Makefileempty :space :$(empty)$(empty)AB$(space)$(space)Chello Dhello world ifeq($(A),)$(warning A is empty)endif ifeq($(B),)$(warning B is empty)endif test: echoA[$(A)]echo echoB[$(B)]echostrip(B)[$(strip$(B))]echo echoC[$(C)]# 可以看到在变量赋值时前导空格被丢弃了echo echoD[$(D)]echostrip(D)[$(strip$(D))]ifeq($(strip$(B)),)$(warning B is empty after strip func, Haha)endif执行make test的输出log:Makefile:12: A is empty Makefile:33: B is empty after strip func, Haha A[] B[ ] strip(B)[] C[hello] D[hello world] strip(D)[hello world]6. 简单习题练习文件结构如下├── Makefile//你需要写出来的 ├── include │ └── head.h //存放计算函数的声明 ├── main.c //调用head.h调用计算函数 └── src ├── add.c //分别实现一个计算函数 ├── div.c ├── mult.c └── sub.cMakefile实现结果如下# Copyright xxxxxxx.com# 需要在环境变量声明编译器路径CC$(CROSS_COMPILE)gccCFLAGS-Wall-O2# 增加宏定义CFLAGS-DENABLE_FLAG# 最终的目标名 apptargettest.out# 搜索头文件include./include# 搜索当前项目目录下的源文件src$(wildcard ./*.c ./src/*.c)# 后缀替换obj$(patsubst %.c, %.o,$(src))# 第一条规则# 依赖中都是 xx.o yy.o zz.o# gcc命令执行的是链接操作$(target):$(obj)$(CC)$(CFLAGS)$^-o$(target)%.o:%.c# gcc $ -c -I $(include) -o $$(CC)$(CFLAGS)-I$(include)-c$-o$#声明clean并非文件.PHONY:clean clean:-rm$(obj)$(target)echoClear obj and target done习题反思在写$(CC) $(CFLAGS) -I $(include) -c $ -o $这条命令时我忽略了-o $导致生成的.o文件都在当前目录了其他规则反而找不到了。其实是gcc编译命令使用不扎实。在写patsubst函数时对使用%还是*混淆了导致错误。参考链接爱编程的大丙跟着学习一遍大佬牛逼。参考链接Makefile中的匹配符%-忘尘通配符不是make要找的第一条命令呦。7. Makefile 中的条件表达式7.1. Makefile 中的条件表达式作用条件语句可以根据一个变量的值来控制make执行或者忽略Makefile的特定部分废话条件语句可以是两个不同变量或者是常量和变量之间的比较。Makefile 的条件判断属于解析阶段语法而不是 Shell 命令。它们不能放在配方recipe里当成命令执行它们内部应该是合法的 Makefile 内容例如变量赋值、规则定义、再嵌套条件、包含等而不是 Shell 命令。在条件表达式中不能使用自动化变量自动变量(自动化变量)在规则命令执行时才有效。下面是 Malefile 条件判断中使用到的一些关键字关键字功能ifeq判断参数是否相等相等为 true不相等为 false (常用于判断变量是否为空)ifneq与ifeq相反ifdef判断是否有值有值为 true没有值为 falseifndef与ifndef相反7.2. ifeq 和 ifneq条件判断的使用方式如下ifeq (first, second) ifeq first second ifeq first second ifeq first second ifeq first second示例first $(CXX) second g ifeq ($(first), $(second)) # if与后方()必须有空格 mycmd pwd # 这里面只能放变量赋值、规则定义、再嵌套条件、包含等不能直接放类似echo等shell命令 else mycmd echo complication is not correct endif all: $(mycmd)执行make之后的效果跟在 shell 中执行pwd命令一样。条件语句中使用到三个关键字ifeq、else、endif。其中ifeq表示条件语句的开始并指定一个比较条件相等。括号和关键字之间要使用空格分隔两个参数之间要使用逗号分隔。参数中的变量引用在进行变量值比较的时候被展开。ifeq后面的是条件满足的时候执行的条件不满足忽略else表示当条件不满足的时候执行的部分不是所有的条件语句都要执行此部分endif是判断语句结束标志Makefile 中条件判断的结束都要有其实ifneq和ifeq的使用方法是完全相同的。不过多介绍。7.3. ifdef 和 ifndef使用方式如ifdef VARIABLE_NAME它的主要功能是判断变量是否被定义了示例如下展示了ifeq最常用的方式判断变量是否为空b 123 ifdef a def_a echo yes a else def_a echo no a endif ifeq (,$(a)) val_a echo a is empty else val_a echo a not_empty endif ifdef b def_b echo yes b else def_b echo no b endif ifeq (,$(b)) val_b echo b is empty else val_b echo b not_empty endif all: $(def_a) $(val_a) $(def_b) $(val_b)执行make后结果如下no a a is emptyyesb b not_empty