上一篇:makefile学习笔记(三)介绍了makefile的多目标规则、多规则目标、静态模式规则和伪目标。本节将介绍makefile的更多内容。
1. 命令
- 命令的回显
通常,make在执行命令行之前会把要执行的命令行进行输出,这就是命令的回显。如以下makefile所示:
all:
echo "Hello csdn!"
在执行make时输出:
$ make
echo "Hello csdn!"
Hello csdn!
有时候命令的回显看起来会很烦人。幸运的是,它是可以被关闭的。关闭命令回显有以下几种方式:
- 每个需要关闭回显的命令行前加上"@"字符;
- 执行make时带上参数 -s 或 –slient 禁止所有执行命令的显示;
- 在Makefile中使用没有依赖的特殊目标 .SILENT 也可以禁止所有命令的回显。
- 命令的执行
在Makefile中,书写在同一行中的多个命令属于一个完整的shell命令行,书写在独立行的一条命令是一个独立的shell命令行。所以需要注意:在一个规则的命令中,命令行cd改变目录不会对其后的命令的执行产生影响。就是说其后的命令执行的工作目录不会是之前使用cd进入的那个目录。如果要实现这个目的,就不能把cd和其后的命令放在两行来书写,而应该把这两条命令写在一行上,用分号分隔。这样它们才是一个完整的shell命令行。
下面用一个名为cmd.mk的makefile对比一下两个规则的输出:
target1:
@echo "target1"
@cd ~
@pwd
target2:
@echo "target2"
@cd ~; pwd
演示如下:
- 命令执行的错误处理
通常情况下,规则中的每一条命令在运行结束后,make都会检测命令执行的返回状态,如果返回成功,就执行下一条命令;命令出错(返回状态非0),make就会放弃对当前规则的执行,或者终止对当前makefile的解析执行。
在一些情况下,规则中的一个命令的执行失败并不代表规则执行的错误。为了忽略一些无关紧要的命令执行失败的情况,我们可以在命令之前加一个减号-,来告诉make忽略此命令的执行失败检查。
2. 内嵌函数
make的内嵌函数为我们提供了处理文件名、变量、文本和命令的方法,使我们的makefile更为灵活和健壮。我们可以在需要的地方调用函数来处理指定的文本(参数),函数在调用它的地方被替换为它的处理结果。函数调用(引用)的展开和变量引用的展开方式类似。
函数调用方式1:
$(FUNCTION ARGUMENTS)
函数调用方式2:
${FUNCTION ARGUMENTS}
- 内嵌函数的分类
内嵌函数的内嵌函数有很多个,如下图所示:
下面以文件名处理函数中的wildcard函数作为例子。
- 使用wildcard函数改进project_demo2项目的makefile
wildcard函数的使用范式如下:
$(wildcard PATTERN)
函数名称:wildcard
函数功能:列出当前目录下所有符合模式“PATTERN”格式的文件名。
返回值:空格分割的、存在当前目录下的所有符合模式“PATTERN”的文件名。
函数说明:“PATTERN”使用shell可识别的通配符,包括?(单字符)、*(多字符)等。
举一个最简单的例子:
$(wildcard *.c)
上述函数的返回值为当前目录下所有.c源文件列表。
有了wildcard函数,就可以改进project_demo2项目的makefile,使其能够自动扫描当前目录下特定后缀的源文件:
# 定义可执行文件变量
executbale := hello
# 新修改:引进wildcard函数扫描源文件,定义列表变量,简化代码
sources := $(wildcard *.c)
# 使用变量的引用替换,定义objects文件列表
objects := $(sources:.c=.o)
# 定义编译命令变量
CC := gcc
RM := rm -rf
# 终极目标规则,生成hello可执行文件
$(executbale): $(objects)
# 使用自动化变量改造我们的编译命令
$(CC) $^ -o $@
# 子规则, main.o和hello.o的生成规则,使用静态模式规则
$(objects):%.o:%.c
$(CC) -c $< -o $@
# clean规则
.PHONY: clean
clean:
$(RM) $(executbale) $(objects)
3. 自动生成依赖关系
至此,project_demo2项目的makefile已经完成的比较完善了,但还存在一个问题:更新头文件hello.h的内容后,执行make时,项目并不会重新编译。这是因为项目的makefile还没能精确的反映整个项目工程的依赖关系。项目的原依赖关系如下图所示:
可以看到依赖关系图中并没有hello.h。hello.h是以头文件的形式被包含在了hello.c文件和main.c文件中中,然后再让hello.o和main.o分别依赖hello.c和main.c,而并没有直接依赖hello.h。在这种情况下,就算是修改了hello.h的内容,项目的依赖关系也依旧没有变化,生成的可执行文件也不会有变化。所以要解决这个问题,就应该在项目的依赖关系中加入对于头文件的依赖。加入的语句如下:
$(objects): hello.h
上述规则加入虽然能解决我们的问题,但是对于大型复杂的项目,像这样逐个去分析头文件被依赖的关系,几乎是不可能的。所以我们需要工具来帮我们做这个事。下面将介绍如何实现自动生成依赖关系。
gcc为我们提供了这样的功能,帮助我们分析一个文件对其他文件的依赖关系列表。当我们在执行gcc时带上 -MM 选项,gcc工具就会为我们列出指定文件对其他文件的依赖关系列表。看下列例子:
接下来要考虑的问题就是如何将gcc的输出导入到makefile中了。
makefile支持使用sinclude关键字将指定文件导入到当前的makefile当中,它的作用与C语言的 #inlucde 预处理命令是一样的。使用方式为:sinclude <other_makefiles>
。因此,我们可以将gcc对于源文件的依赖关系分析输出到依赖描述文件当中,该类文件一般与源文件同名,但以 .d 作为结尾。然后再将依赖描述文件导入到makefile中。这部分操作的代码如下:
# 使用变量的引用替换,定义依赖描述文件列表
deps := $(sources:.c=.d)
# 导入依赖描述文件列表
sinclude $(deps)
当我们使用sinclude关键字向当前makefile导入文件时,如果所导入的文件不存在,make会试图去执行可以产生导入文件的规则去生成被导入的文件,然后再执行导入。因此我们可以使用静态模式规则,让make在执行时,去调用gcc生成依赖关系文件。可以这么写:
$(deps):%.d:%.c
# > 为重定向符号,表示将项目中对 main.c和 hello.c的依赖关系分别
#输出到 main.d和 hello.d文件中
gcc -MM $< > $@
经过改进后,最终版的project_demo2项目makefile文件如下:
# 定义可执行文件变量
executbale := hello
# wildcard函数扫描源文件,定义列表变量
sources := $(wildcard *.c)
# 使用变量的引用替换,定义object文件列表
objects := $(sources:.c=.o)
# 使用变量的引用替换,定义依赖描述文件列表
deps := $(sources:.c=.d)
# 定义编译命令变量
CC := gcc
RM := rm -rf
# 终极目标规则,生成 hello可执行文件
$(executbale): $(objects)
# 使用自动化变量改造我们的编译命令
$(CC) -o $@ $^
# 子规则, main.o和 hello.o的生成规则,使用静态模式规则
$(objects):%.o:%.c
$(CC) -o $@ -c $<
# clean规则
.PHONY: clean
clean:
$(RM) $(executbale) $(objects) $(deps)
# 自动规则依赖
sinclude $(deps)
$(deps):%.d:%.c
$(CC) -MM $< > $@
演示结果如下:
至此,makefile笔记系列完美收官。
总结
这一系列笔记介绍了 makefile 的编写和 make 工具的使用,主要包括以下的内容:
- makefile 规则的概念
- makefile 变量的使用
- makefile 函数的使用
- makefile 如何实现自动生成依赖