Makefile
零、概述
Makefile是一种用于构建和管GNU软件项目的自动化构建工具make的配置文件,定义了目标、依赖和构建规则的概念。
- 目标就是要做的事、要产出的内容、可执行文件等。
- 依赖就是产出目标的原料、源文件、条件等等。
- 构建规则就是用依赖产出目标的过程或是指令。
很多IDE软件中的编译按键,实际上也是调用的Makefile作为配置文件。一般在Linux中编译文件使用的是,在命令行中输入 make 命令来编译工程,那么这期间做了什么事呢?
- make 命令会在当前目录下查找是否存在“Makefile”这个文件,也可以make的时候指定
make -f your.mk
。 - 存在则按照Makefile 中定义的编译方式进行编译
- 文件改动后make,会对改动的自动编译,会对比目标和依赖的时间,依赖时间超前于目标的部分,就会重新进行编译。
一、Makefile特性
1 格式
Makefile遵循一定的格式,一般如下:
1 | 目标文件:依赖文件 |
这边的
其中指令部分也就是编译过程中要执行的命令,常见的就是gcc xxx
1 | .PHONY:all |
运行如下:
可以看到不经将指令结果运行出来了,还将该条指令打印了出来,如果不想打印指令本身,可以在前面添加一个@
来屏蔽指令本身,如下:
这样就只会打结果了。
2 特性
2.1 变量
1 | A = xxx // 延时变量 |
假设定义了一个变量val,可以通过以下方式引用:
- $(val)
- ${val}
在shell中是通过$( )
来引用变量的。
2.1.1 立即变量
立即变量的意思就是在解析赋值语句的时候就将变量值赋值到对应的变量上。
1 | .PHONY: all |
运行如下:
可以看到old_a的值立即赋值到A上,所以在后续old_a改变了,A的是还是之前赋值的。
2.1.2 延时变量
延时赋值,可以理解成用到再赋值,在解析语句的时候没有立即赋值,而是在真正使用到的时候才将值赋值进去,有点全局的意思。
1 | .PHONY: all |
还是上一个例子,只是将立即赋值改成延时赋值,运行如下:
可以看到这边的值,是在使用的才赋值上去,而在此之前被新的值覆盖了。更有说服力的例子如下:
1 | .PHONY: all |
上述 Makefile 中,变量 A 的值在执行时才确定,它等于 test,是延时变量。如果使用“ A := $@”,这是立即变量,这时$@为空,所以 A 的值就是空。
运行如下:
2.1.3 追加变量
追加变量常见于编译参数的追加,如下:
1 | .PHONY:all |
运行如下:
可以看到CFLAGS
最终是追加后的结果。
2.1.4 变量替换
将一个变量中的内容进行替换
1 | .PHONY: all |
运行如下:
2.1.5 环境变量
在make的时候会将一些环境变量引入到Makefile中,例如CFLAGS、SHELL、MAKE等变量。
1 | .PHONY:all |
运行如下:
这个是原先就有的环境变量,当然也可以通过export
来定义的临时环境变量。
1 | .PHONY:all |
运行如下:
2.1.6 递归传递变量
在使用递归传递变量前,需要了解的是,递归运行Makefile,也就是可以在顶层目录中调用下一个目录的Makefile,结构如下:
1 | . |
内容如下:
运行如下:
可以看到在./中的Makefile中调用了./src/下的Makefile文件。
现在来看变量的递归使用
就是在顶层Makefile中定义一个变量,然后在下一级Makefile中调用该变量,运行如下:
可以看到,在./src/Makefile中正确获取到顶层Makefile中定义的变量para
。
2.1.4 override变量
现在假设有一个变量你不希望被其他人轻易改变,或者说被无意修改,就可以给该变量加上override
前缀,修改上个例子中的src下的Makefile:
1 | .PHONY:all |
运行如下:
可以看到,para变量可能中上一层Makefile递归下来,但是有些时候不希望被修改就可以这么用。
那如果一定想把override修饰的变量进行修改的话,就需要也加上override,但是不建议这么干,除非你清楚地知道自己在干嘛:
1 | .PHONY:all |
运行如下:
可以看到确实是被新的赋值给覆盖了。
2.1.5 通配符(自动变量)
1 | 1.通配符 pattan |
2.1.6 后缀替换
变量是文件列表的时候用于后缀替换
1 | .PHONY:all |
运行如下:
2.2 条件判断
有时候需要根据不同的宏来做不同的编译动作,例如当前编译的是调试版本还是发布版本,格式如下:
1 | ifeq ($(xxx),yyy) #取到xxx变量的值,对比是不是等于yyy |
举例如下:
1 | .PHONY:all |
运行如下:
二、常用函数
1 文本处理
用于处理文本的函数,例如后缀替换、空格去除、内容过滤等等。
1.1 subst函数
可以将变量中的内容替换成其他的,例如将变量中的.c
替换成.o
,如下:
1 |
|
运行如下:
这个是用于替换文本,不一定得是后缀,也可以是其他得,例如将o换成6:
1.2 patsubst函数
这个函数和上述得$(subst )区别是:以空格为分隔符,可以利用通配符来完成替换,符合条件即可替换,如下:
1 |
|
运行如下:
1.3 strip函数
strip函数可以将多余的空格进行剔除,确保源文本中内容之间的空格只有一个,如:在main.c
和hello.c
之间加入大量的空格。
1 |
|
运行如下:
1.4 findstring函数
findstring函数可以在源文本中找到符合条件的文本信息(字符串),如下:
1 |
|
运行如下:
1.5 filter 函数
filter函数可以用来过滤出来符合条件的文本,可以是单纯的文本,也可以是通配符例如:
1 |
|
运行如下:
1.6 filter-out 函数
filter-out函数和fliter函数类中,不同的是用来过滤剔除符合条件的文本,可以是单纯的文本,也可以是通配符例如:
1 |
|
运行如下:
可以看到,符合条件的文本被剔除了,只剩下不符合条件的文本信息。
1.7 sort函数:单词排序
sort函数会将源文本中的每一个文件按文件名首字母进行排序,删除重复的单词,例如:
1 |
|
运行如下:
加入重复文件名后:
1.8 word函数:取单词
word函数可以取源文本中的第n个单词信息,例如:
1 |
|
运行如下:
如图所示,分别取第1,2,3个单词。
1.9 wordlist函数:取字串
wordlist函数用来从一个源文本中取出从n到m之间的一个单词串,例如:
1 |
|
运行如下:
如图取出第1到第3的单词。
1.10 words函数:统计单词数目
words函数可以统计源文本中包含多少个单词,单词之间是通过空格分隔的,例如:
1 |
|
运行如下:
1.11 firstword函数:取首个单词
可以取到整个源文本中最开头的单词,例如:
1 |
|
运行如下:
2 文件处理
用于处理文件相关操作,文件名替换、前缀后缀处理、目录处理等。
2.1 dir函数:取路径名的目录
dir函数用来从一个路径名中截取目录的部分,例如:
1 |
|
运行如下:
如果路径是目录的就会取前一级目录路径,因为文本而言并不知道这个是不是目录,如果最后是/结尾的,那么结果将是空。
2.2 notdir函数:取文件名
notdir函数顾名思义,就是提出目录部分只取文件名,例如:
1 |
|
运行如下:
2.3 suffix函数:取文件名后缀
suffix函数可以取文件名后缀,假设有main.cpp就会取到.cpp,例如
1 |
|
运行如下:
2.4 basename函数:取文件名前缀
basename函数可以取文件名前缀,假设有main.cpp就会取到main,例如
1 |
|
运行如下:
2.5 addsuffix函数:给文件名加后缀
addsuffix函数可以为文本变量添加后缀,例如:
1 |
|
运行如下:
2.6 addprefix函数:给文件名加前缀
addprefix函数可以给文件名加指定的前缀信息,例如:
1 |
|
运行如下:
2.7 wildcard函数:列出所有符号匹配模式的文件(很常用)
这个函数非常经常用,可以按格式列出符合条件的文件名,例如:
1 |
|
运行如下:
Linux中一般都会先通过wildcard函数获取到需要编译的源文件,在进行处理,例如替换后缀为需要的目标文件等等的操作。
3 其他函数
3.1 if函数
Makefile中的 if 函数提供了在一个函数上下文中实现条件判断的功能,类似于ifeq关键字,if函数的使用格式如下:
1 | $(if CONDITION,THEN-PART) |
举例如下:
1 |
|
运行如下:
可以看到当dir
有定义的时候,就输出dir
变量的值,当dir
没有定义的时候就会使用后面的值。
3.2 call函数
后面自定义函数中使用,格式如下:
1 | $(call <expression>,<parm1>,<parm2>,<parm3>...) |
parm1
等变量会被带入到函数中,在函数中体现为对应的$(1)
等。
3.3 origin函数
顾名思义,origin函数的作用就是告诉你,你所关注的一个变量是从哪里来的。函数的使用格式为:
1 | $(origin <variable>) |
主要有以下选项:
- default:变量是一个默认的定义,比如 CC 这个变量
- file:这个变量被定义在Makefile中
- command line:这个变量是被命令行定义的
- override:这个变量是被override指示符重新定义过的
- automatic:一个命令运行中的自动化变量
- undefined:未定义
举例:
1 |
|
运行如下:
3.4 shell 函数
可以在makefile中运行shell命令,然后将获取的值赋值到变量中,如下:
1 |
|
运行如下:
3.5 foreach函数
如果想做一些循环或遍历操作时,可以使用foreach
函数:
1 | $(foreach VAR,LIST,TEXT) |
foreach
函数的工作过程是:把LIST中使用空格分割的单词依次取出并赋值给变量VAR,然后执行TEXT表达式。重复这个过程,直到遍历完LIST中的最后一个单词。函数的返回值是TEXT多次计算的结果。
举例:
1 |
|
运行如下:
这只是举例而已,虽然加后缀的功能在本例中直接使用$(addsuffix .666,$(SRC))
就可以完成,但是这只是举例$(foreach )
可以遍历处理任务。
4 自定义函数
在实际使用中,常常可能因为便捷性的考虑,会新建一些函数,用于复用,然后通过$(call functionname, para1, para2,...)
来调用,例如:
1 |
|
运行如下:
二、debug手段
打印变量
例如想查看某个变量是否被正确赋值,例如C文件搜索是否正确,或是CFLAGS是否符合要求,可以通过打印查看。以下三种打印区别就是:
$(info ):仅提示
$(warning ):报一个warning的提示,一般使用这个
$(error ):作为一个报错,同时停止运行,大型项目中可以更快定位
使用方式:
SRC := $(wildcard *.c)
$(warning CZR_Prefix-$(SRC))
注意:
添加CZR\_Prefix
是为了更好地在console输出中定位文本信息。