如果我们将同一个目标的命令拆分的写到不同地方,会发生什么呢?我们来看看下面的代码
.PHONY : all all : @echo "command-1" VAR := test all : @echo "all : $(VAR)"
我们来分析下,这份代码中有两个目标 all,那么我们在执行 make 的时候。它到底是执行哪个呢?一个可能是两个都执行,另一个就是执行第一个,因为默认的是执行第一个目标。下来我们来看看执行结果
我们看到它说 all 重复了,便忽略了前面的 all 命令。最终执行的是最后一个 all 命令。因此,当 makefile 中出现同名目标时,会将所有的依赖合并在一起,成为目标的最终依赖;当多处出现同一目标的命令时,make 发出警告,所有之前定义的命令被最后定义的命令取代。注意:当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只要依赖,没有命令;否则,同名目标的命令将被覆盖!我们还是来看看,再建立一个新的 makefile.1
makefile.1 源码
all : @echo "this is command from makefile.1"
makefile 源码
.PHONY : all VAR := test all : @echo "all : $(VAR)" include makefile.1
我们来看看编译结果,看看打印出的是不是 all : test
我们看到输出的是 "this is command from makefile.1,并不是我们所期望的 all : test。其实也不难理解,因为我们包含的 makefile.1 中也包含了 all 命令,因此将上面的 all 给替换了。换句话说,这个现象有可能会给我们带来意想不到的结果。那么这也就属于 makefile 中的一条隐式规则了,什么是隐式规则呢?在 make 中,它提供了一些常用的,例行的规则实现;当相应目标的规则未提供时,make 尝试使用隐式规则。那么我们来看看下面这个 makefile 能编译成功吗?
.PHONY : all SRCS := $(wildcard *.c) OBJS := $(SRCS:.c=.o) app.out : $(OBJS) $(CC) -o $@ $^ $(RM) $^ @echo "Target ==> $@"
我们看看编译的结果,是否会报错?
我们看到已经正确的编译了,而且结果也是对的。那么我们并没有在里面定义相应的规则啊,为什么就能正确编译呢?我们按照它的格式写个规则,再将 cc 换成 gcc 试试,看看结果是否会相同?
.PHONY : all CC := gcc SRCS := $(wildcard *.c) OBJS := $(SRCS:.c=.o) app.out : $(OBJS) $(CC) -o $@ $^ $(RM) $^ @echo "Target ==> $@" %.o : %.c @echo "my rule" $(CC) -c -o $@ $^
编译结果如下
结果和之前是一样的,只不过是输出了我们自定义的语句,将 cc 换成了 gcc。那么 cc 是什么呢?为何能编译源文件呢?cc 第一个 c 是 C 语言,第二个是 compiler 编译器的意思。那么这个 cc 编译器是哪来的呢?在原来的 Unix 系统中是有 cc 编译器的,因为它是商业版的,需要收费,因此在 Linux 系统中也是要支持 cc 的,不过此 cc 非彼 cc,我们来看看 cc 最后的原型是什么
我们看到 cc 最后指向的是 gcc,因此使用 cc 进行编译工作其实也是和使用 gcc 进行编译是一样的。那么上面的模式规则是谁来实现的呢?这个便是 makefile 中的隐式规则了。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作;改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不再使用隐式规则。当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推导可能需要的源文件,如下
既然隐式编译这么强大,我们是不是就不用自己编写相关规则了呢?其实不是的,根据前辈们的经验总结。在实际的项目中,我们还是有必要禁止 makefile 中的隐式规则的,因为隐式规则有副作用。具体表现在:a> 编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为;b> 编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。
那么我们下来来看看隐式规则链。当依赖的目标不存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!如:需要名为 N.o 的目标:N.y –> N.c –> N.o。我们还是以代码为例来进行分析说明
main.c 源码
#include <stdio.h> extern void greeting(); int main() { greeting(); return 0; }
func.p 源码(这只是一个测试的代码,用的是 Pascal 语言)
unit Func; interface procedure Greeting(); attribute (name = 'greeting'); implementation procedure Greeting(); begin WriteLn('Hello, Pascal!'); end; end.
makefile 源码
app.out : main.o func.o $(CC) -lstdc++ -o $@ $^
那么我们此时想要用 func.c 实现某个功能,但是现在没有 func.c,所以编译时肯定会出错
我们看到 func.o 竟然会利用 func.p 来生成,不过最终还是出错了。出现这样的错误,我们是不是很纳闷呢?明明是没有对应的源文件,但是却报的是没有 pc 命令。这个便是 makefile 中的隐式规则了,那么 make 究竟提供了多少隐式规则?应如何查看查看隐式规则呢?查看隐式规则的方法是:查看所有的是 make -p;查看具体的规则是 make -p | grep "XXX"。下来我们来看看 make 中的隐式规则
因为所有的规则非常多,我们只截取了其中的一段,下来看看 %.o 对应的规则
我们看到它默认的是支持好多格式的,其中就包括 .c 和 .p 文件,因此它会将将我们之前的测试代码当成源文件进行编译了。那么我们应该如何避免它的隐式规则呢?在局部禁用的话,是直接在 makefile 中自定义规则或者在 makefile 中定义模式(如:%.o : %.p);全局禁用的话则使用 make -r。下来我们先来使用下局部禁用的方式,直接在 makefile 中定义模式,但是不做具体处理。
它直接就报错了,说没有相应的源文。我们再来看看使用全局禁用的方式
我们看到使用全局禁用的方式后,连 main.o 的文件也不能生成了。下来我们来说说后缀规则,它是旧式的“模式规则”,可以通过后缀描述的方式自定义规则。格式如下
后缀规则分为双后缀规则和单后缀规则。双后缀规则是指定义一对文件后缀(依赖文件后缀和目标文件后缀),如:.cpp.o <==> %.o : %.cpp;单后缀规则是指定义单个文件后缀(源文件后缀),如:.c <==> % : %.c。后缀规则有这么几个注意事项:1、后缀规则中不允许有依赖;2、后缀规则必须有命令,否则无意义;3、后缀规则将逐步被模式规则所取代。
下来我们还是以代码为例来进行分析说明,我们新建一个 func.c 文件用于说明问题
func.c 源码
#include <stdio.h> void greeting() { printf("void greeting : %s/n", "hello makefile!"); }
makefile 源码
app.out : main.o func.o $(CC) -lstdc++ -o $@ $^ .c.o : @echo "my suffix rule" $(CC) -o $@ -c $^ .c : @echo "my suffix rule" $(CC) -o $@ -c $^
我们来看看编译结果
我们看到已经正确实现了。不过在现在的工程项目中,我们一般都会摒弃掉后缀规则,采用的都是模式规则。通过对 makefile 中隐式规则的学习,总结如下:1、当多处出现同一目标的命令时,只有最后定义的命令才有效;2、make 提供了一系列的隐式规则可使用,当 makefile 中未定义相关规则时,将尝试使用隐式规则;3、隐式规则中可能使用 make 中的预定义变量,改变预定义变量可部分改变预定义规则的行为;4、隐式规则可能造成意想不到的编译行为,在实际工程项目中尽量不使用隐式规则;5、后缀规则是一种旧式的模式规则,它正逐步被模式规则所取代。
欢迎大家一起来学习 makefile,可以加我QQ:243343083。
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/182989.html