4.1多目标
Makefile 的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。于是我们就能把其合并起来。当然,多个目标的生成规则的执行命令是同一个,这可能会可我们带来麻烦,不过好在我们的可以使用一个自动化变量“$@”(关于自动化变量,将在后面讲述),这个变量表示着目前规则中所有的目标的集合,这样说可能很抽象,还是看一个例子吧。
big output littleoutput :text.g
generate text.g -$(subst output,,$@)>$@
这个等价于:
bigoutput :text.g
generate text.g -big >bigoutput
littleoutput :text.g
generate text.g -little >littleoutput
ps:这些是书上的内容,不太清楚这个的运用场景,有大佬的请指点下。
4.2静态模式
静态模式可以更加容易地定义多目标的规则,先看下静态规则的语法
<targets...>:<target-pattern>:<prereq-patterns...>
<commands>
targets 定义了一系列的目标文件,可以有通配符。是目标的一个集合。
target-parrtern 是指明了 targets 的模式,也就是的目标集模式。
prereq-parrterns 是目标的依赖模式,它对 target-parrtern 形成的模式再进行一次依赖目标的定义。
假如<target-parrtern>定义成“%.o”,意思是我们的集合中都是以“.o”结尾的,而如果我们的<prereq-parrterns>定义成“%.c”,意思是对
<target-parrtern>所形成的目标集进行二次定义,其计算方法是,取<target-parrtern>模式中的“%”(也就是去掉了[.o]这个结尾),并为其加上[.c]这个结尾,形成的新集合。
所以,我们的“目标模式”或是“依赖模式”中都应该有“%”这个字符,如果你的文件名中有“%”那么你可以使用反斜杠“\”进行转义,来标明真实的“%”字符。
还是用之前的测试小例子
项目结构图如下
makefile文件内容如下
CC = gcc
OBJ =main.o fun.o
CFLAGS = -I headers
#VPATH = src:headers
vpath %.h headers
vpath %.c src
all:main
.PHONY:all
main:$(OBJ)
$(CC) -o $@ $(OBJ)
$(OBJ):%.o:%.c
$(CC) -c $(CFLAGS) $< -o $@
.PHONY:cleanall cleanobj cleantarget
cleanall:cleanobj cleantarget
cleanobj:
rm $(OBJ) -f
cleantarget:
rm main
$(OBJ):%.o:%.c
$(CC) -c $(CFLAGS) $< -o $@
使用的就是静态模式,其中指明了我们的目标从$OBJ 中获取,“%.o”表明要所有以“.o”结尾的目标,也就是“main.o fun.o”,
也就是变量$OBJ 集合的模式,而依赖模式“%.c”则取模式“%.o”的“%”,也就是“main fun”,并为其加下“.c”的后缀,
于是,我们的依赖目标就是“main.c fun.c”。而命令中的“$<”和“$@”则是自动化变量,“$<”表示所有的依赖目标集(也
就是“main.c fun.c”),“$@”表示目标集(main.o fun.o”)。
而将依赖规则展开的话
main.o:main.c
$(CC) -c $(CFLAGS) main.c -o main.o
fun.c:fun.o
$(CC) -c $(CFLAGS) fun.c -o fun.o
这里看一下Makefile中的 filter 函数用法。将$(OBJ):%.o:%.c换成$(filter%.o,$(OBJ)):%.o:%.c,表示只要其中模式为“%.o”的内容。
4.3自动生成依赖性
在 Makefile 中,我们的依赖关系可能会需要包含一系列的头文件,比如,如果我们的 main.c 中有一句“#include"defs.h"”,那么我们的依赖关系应该是:
main.o:main.c defs.h
一个比较大型的工程,你必需清楚哪些 C 文件包含了哪些头文件,并且,你在加入或删除头文件时,也需要小心地修改 Makefile,这是一个很没有维护性的工作。为了避免这种繁重而又容易出错的事情,我们可以使用 C/C++编译的一个功能。大多数的 C/C++编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。例如,如果我们执行下面的命令:
cc -M main.c
其输出是:
main.o:main.cdefs.h
使用 GNU 的 C/C++编译器,得用“-MM”参数,不然,“-M”参数会把一些标准库的头文件也包含进来。
GNU 组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,即为每一个“name.c”的文件都生成一个“name.d”的 Makefile 文件,[.d]文件中就存放对应[.c]文件的依赖关系。
利用一个模式规则来产生[.d]文件
%.d:%.c
@set -e;rm -f $@;\
$(CC) -MM $(CFLAGS) $< > $@.$$$$;\
sed 's,\($*\)\.o[ :]*,\1.o $@:,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
第一个命令@set -e
。@关键字告诉make不输出该行命令;set -e的作用是,当后面的命令的返回值非0时,立即退出。
“rm-f$@”的意思是删除原先所有的目标,也就是[.d]文件
第二条命令$(CC) -M $(CPPFLAGS) $< > $@.$$$$; 为每个依赖文件“$<”,也就是[.c]文件,“$@”表示模式“%.d”文件。$$$$
为字符串"$$"
,由于makefile中所有的$字符都是特殊字符(即使在单引号之中!),要得到普通字符$,需要用$$
来转义; 而$$
是shell的特殊变量,它的值为当前进程号;使用进程号为后缀的名称创建临时文件,是shell编程常用做法,这样可保证文件唯一性。
该条命令作用是生成一个main.d.$$$$(一个进程编号的随机数)的临时文件
第三条命令
sed 's,\($*\)\.o[ :]*,\1.o $@:,g' < $@.$$$$ > $@; \
-
sed是一个流编辑器,用于流文本的修改(增/删/改/查)
-
sed可用于流文本中的字符串替换
-
sed的字符串替换方式为:sed 's:src:des:g'
-
例如执行下列语句:
echo "test=>abc+abc+abc" | sed 's:abc:xyz:g'
test的内容将变为xyz+xyz+xyz
sed中的s符号告诉sed命令,这次要做一个替换的任务。s符号的格式为:[address[,address]] s,pattern-to-find,replacement-pattern,[g p w n]。 下面来匹配上面的示例:
[address[,address]]:是指要处理的行的范围,在这次的操作中采用的是默认值。
pattern-to-find等价于\($*\)\.o[ :]*
replacement-pattern等价于\1.o $@ :
因此正则表达式 \($*\)\.o[ :]*,为匹配目标,正则表达式\1.o $@ : ,为更换目标
$*,表示的是target的除去了后缀后的filename,也就是%.d当中的%部分。
以上面main.c源文件为例。
此时\($*\)为 \(main\),作用是创建一个字符标签,即正则表达式中的“分组模式”,给后面的replacement-pattern使用。如\1.o 正则表达式“后向引用”,展开后就是main.o
\. 在正则表达式中‘.’作用是匹配一个字符。所以需要使用转义元字符‘\’来转义。
[ :] 匹配一组字符里的任意字符 。
*匹配0个或多个前一字符
再来看整体的命令
第一步执行的是sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$。将临时文件$@.$$$$中的内容重定向到sed中更改内容。
即通过sed的正则表达式,输入的main.o:main.c defs.h被替换成了main.o main.d : main.c defs.h。
第二步执行的操作> $@ 将main.o main.d : main.c defs.h内容重定向到$@
第四条命令使用删除临时文件
此时[.d]文件也会自动更新了,并会自动生成了在这个[.d]文件中加入的不只是依赖关系,包括生成的命令也可一并加入,让每个[.d]文件都包含一个完赖的规则。接下来,就要把这些自动生成的规则放进我们的主 Makefile 中使用 Makefile 的“include”命令,来引入别的 Makefile 文件,例如:
sources=foo.c bar.c
include $(sources:.c=.d)
上述语句中的“$(sources:.c=.d)”中的“.c=.d”的意思是做一个替换,把变量$(sources)所有[.c]的字串都替换成[.d]。
在这里,笔者用之前的项目实例来做一个示范。工程结构如下:
首先看一下不使用头文件自动依赖的效果
fun.h中的头文件为
#ifndef _FUN_H
#define _FUN_H
#define PRINT "hello world!"
int fun(void);
#endif
fun.c的源文件内容为
#include"fun.h"
#include<stdio.h>
int fun(void)
{
printf(PRINT);
return 0;
}
main.c的内容为
#include<stdio.h>
#include"fun.h"
int main(){
fun();
}
makefile的内容为
CC = gcc
OBJ =main.o fun.o
CFLAGS = -I headers
#VPATH = src:headers
vpath %.h headers
vpath %.c src
all:main
.PHONY:all
main:$(OBJ)
$(CC) -o $@ $(OBJ)
$(OBJ):%.o:%.c
$(CC) -c $(CFLAGS) $< -o $@
.PHONY:cleanall cleanobj cleantarget
cleanall:cleanobj cleantarget
cleanobj:
rm $(OBJ) main1.o -f
cleantarget:
rm main
生成的可执行文件main,打印出来时为hello world!。然而将fun.h中的PRINT修改为hello lujw!时再次make时,显示所有文件都为最新不用更新,是错误的。
将自动依赖规则加入到makefile文件中。
CC = gcc
OBJ =main.o fun.o
CFLAGS = -I headers
#VPATH = src:headers
vpath %.h headers
vpath %.c src
all:main
.PHONY:all
main:$(OBJ)
$(CC) -o $@ $(OBJ)
$(OBJ):%.o:%.c
$(CC) -c $(CFLAGS) $< -o $@
%.d:%.c
@set -e;rm -f $@;\
$(CC) -MM $(CFLAGS) $< > $@.$$$$;\
sed 's,\($*\)\.o[:]*,\1.o $@:,g' < $@.$$$$ > $@;\
rm -f $@.$$$$\
-include $(OBJ:.o=.d)
.PHONY:cleanall cleanobj cleantarget
cleanall:cleanobj cleantarget
cleanobj:
rm $(OBJ) *.d *.d.* -f
cleantarget:
rm main
此时再次尝试时,头文件中的修改也能被自动依赖进目标列表了。