自动生成依赖关系(十)
我们在之前的 makefile 学习中,其目标文件(.o)只依赖于源文件(.c)。那么如果在源文件中还包含有头文件,此时编译器如何编译源文件和头文件呢?我们来看看编译行为带来的缺陷:1、预处理器将头文件中的代码直接插入源文件;2、编译器只通过预处理后的源文件产生目标文件;3、规则中以源文件为依赖,命令就可能无法执行。
成都创新互联主要从事做网站、网站建设、网页设计、企业做网站、公司建网站等业务。立足成都服务景谷,10年网站建设经验,价格优惠、服务专业,欢迎来电咨询建站服务:028-86922220
我们来看看下面的 makefile 有没有问题
makefile 源码
OBJS := func.o main.o hello.out : $(OBJS) @gcc -o $@ $^ @echo "Target File ==> $@" $(OBJS) : %.o : %.c @gcc -o $@ -c $^
func.h 源码
#ifndef _FUNC_H_ #define _FUNC_H_ #define HELLO "Hello D.T." void foo(); #endif
func.c 源码
#include#include "func.h" void foo() { printf("void foo() : %s\n", HELLO); }
main.c 源码
#include#include "func.h" int main() { foo(); return 0; }
我们来看看编译结果
我们看到已经正确实现了字符串的打印,那么我们接下来在 func.h 源文件中想要改掉这个字符串为 Software 呢?试试看能不能修改成功
我们看到在重新编译的时候,它并没有因为头文件的改变而改变,我们在 makefile 中又没有进行头文件的相关添加,改掉头文件中的内容肯定是不动的。下来我们在模式规则中加上头文件,在 %.c 后加上 func.h,再来看看编译结果
我们看到直接添加之后,编译出错了。因为 -c 后面的目标中含有头文件,所以不能直接进行编译。我们可以只编译 %.o 后面的第一依赖 %.c,这样就不会去编译 func.h 头文件了,将下面的 $^ 改为 $< ,我们来看看效果
我们看到已经正确改过来了。经过上面的实验,我们看到:头文件作为依赖条件出现于每个目标对应的规则中,当头文件改动时,任何源文件都将会被重新编译(编译低效);当项目中头文件巨大时,makefile 将很难维护。那么我们的头脑中不禁会冒出这么个想法:通过命令对自动生成对头文件的依赖;将生成的依赖自动包含进 makefile 中;当头文件改动后,自动确认需要重新编译的文件。那么此时我们还需要知道一个命令,Linux 中的 sed 命令。sed 是一个流编辑器,用于流文本的修改(增、删、查、改);它可用于流文本中的字符串替换,其字符串替换方式为:sed 's:src:des:g',具体格式如下
sed 同样也支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果。格式如下
下来我们以代码为例来看看 sed 命令是如何使用的
再来看看 gcc 关键编译选项,获取目标的完整依赖关系:gcc -M test.c;获取目标的部分依赖关系:gcc -MM test.c。makefile 如下
.PHONY : test test : gcc -M main.c
编译结果如下
我们看到 -M 是获取了它的所有依赖关系,再来试试 -MM 呢
我们看到 -MM 后,它只依赖与 main.c func.h。我们可以拆分目标的依赖,即将目标的完整依赖差分为多个部分依赖。格式如下
我们来做个实验
.PHONY : a b c test : a b test : b c test : @echo "$^"
我们来打印看看目标 test 的依赖都有哪些,编译结果如下
那么我们思考下:如何将 sed 和 gcc -MM 用于 makefile,并自动生成依赖关系呢?
我们再来看看 makefile 中的 include 关键字,它类似于 C 语言中的 include,是将其它文件的内容原封不动的搬入当前文件。make 对 include 关键字的处理方式是在当前目录下搜索或指定搜索目标文件。如果搜索一成功,便将文件内容搬入当前 makefile 中;如果搜索失败,将会产生警告,以文件名作为目标查找并执行对应规则,当文件名对应的规则不存在时,最终产生错误。格式如下
下来还是以代码为例来进行说明
.PHONY : test include test.txt all : @echo "this is $@" test.txt : @echo "test.txt" @touch test.txt
我们在第 3 行包含 test.txt,可是当前目录下并没有 test.txt,然后触发 test.txt 的规则。因而会打印出 test.txt,然后再创建 test.txt,我们来看看编译结果
我们看到确实是创建了一个 test.txt 文件。那么在 makefile 中命令的执行是:1、规则中的每个命令默认是在一个新的进程中执行(Shell);2、可以通过接续符(;)将多个命令组合成一个命令;3、组合的命令依次在同一个进程中被执行;4、set -e 指定发生错误后立即退出执行。那么我们看看下面的代码会实现想要的功能吗?
.POHONY : all all : mkdir test cd test mkdir subtest
我们来看看编译结果
我们看到在当前目录下创建了目录,但是 subtest 目录却不是在 test 目录下创建的,这是怎么回事呢?在第一条命令执行时创建了目录 test,此时这个进程已经关闭了;在第二条命令执行时,执行的是另一个进程,虽然它已经进入到目录 test 中,但是随着这个进程的关闭,又回到了当前目录;第三个进程是重新创建了目录 subtest。那么如何解决这个问题呢?直接利用 set -e 和 接续符来解决
.PHONY : test all : set -e; \ mkdir test; \ cd test; \ mkdir subtest
看看编译结果
那么我们之前思考问题的初步思路是:1、通过 gcc -MM 和 sed 得到 .dep 依赖文件(目标的部分依赖),技术点是规则中命令的连续执行;2、通过 include 指令包含所有的 .dep 依赖文件。技术点是当 .dep 依赖文件不存在时,使用规则自动生成。下面我们来看看解决方案是怎样的
ONY : all clean MKDIR := mkdir RM := rm -fr CC := gcc SRCS := $(wildcard *.c) DEPS := $(SRCS:.c=.dep) include $(DEPS) all : @echo "all" %.dep : %.c @echo "Creating $@ ..." @set -e; \ $(CC) -MM -E $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@ clean : $(RM) $(DEPS)
我们来看看编译结果
我们先来分析下,在执行 make all 前,它先通过 include 包含 $(DEPS),通过 $(DEPS) 触发模式规则,进而创建文件夹。我们看到在前面出现两个没有文件夹的信息,其实这条信息是可以隐藏的。我们在 include 前面加上 - 就 OK,来看看效果
我们看到并没打印出前面的两条信息了。那么我们再来思考下:如何组织依赖文件相关的规则与源码编译相关的规则,进而形成功能完整的 makefile 程序呢?我们如何在 makefile 中组织 .dep 文件到指定目录呢?初步想法是当 include 发现 .dep 文件不存在时:1、通过规则和命令创建 deps 文件;2、将所有 .dep 文件创建到 deps 文件夹;3、.dep 文件中记录目标文件的依赖关系。
我们下来看看初步的代码设计是怎样的
.PHONY : all clean MKDIR := mkdir RM := rm -rf CC := gcc DIR_DEPS := deps SRCS := $(wildcard *.c) DEPS := $(SRCS:.c=.dep) DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS)) include $(DEPS) all : @echo "all" $(DIR_DEPS) : $(MKDIR) $@ $(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c @echo "Creating $@ ..." @set -e; \ $(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@ clean : $(RM) $(DIR_DEPS)
我们来看看编译结果,是不是都将所有的 .dep 文件放入一个 deps 文件中
我们看到已经实现效果了。我们仔细看看 make 有一个警告,说 main.dep 被修改了,也就是说 main.dep 被重新创建了。那么我们来分析下,为什么一些 .dep 依赖文件会被重复创建多次呢?deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,于是乎就触发相应的规则重新解析和执行命令。那么我们知道了原因,此时这个方案该如何优化呢?我们可以使用 ifeq 动态决定 .dep 目标的依赖,具体 makefile 如下
.PHONY : all clean MKDIR := mkdir RM := rm -fr CC := gcc DIR_DEPS := deps SRCS := $(wildcard *.c) DEPS := $(SRCS:.c=.dep) DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS)) all : @echo "all" ifeq ("$(MAKECMDGOALS)", "all") -include $(DEPS) endif ifeq ("$(MAKECMDGOALS)", "") -include $(DEPS) endif $(DIR_DEPS) : $(MKDIR) $@ ifeq ("$(wildcard $(DIR_DEPS))", "") $(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c else $(DIR_DEPS)/%.dep : %.c endif @echo "Creating $@ ..." @set -e; \ $(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@ clean : $(RM) $(DIR_DEPS)
我们再次编译看看
我们看到它还是报了这样的错误,有可能是编译器的优化造成的。思路是正确的。下来我们来看看 include 的一些鲜为人知的秘密。
A、 使用减号(-)不但关闭了 include 发出的警告,同时将关闭了错误;当错误发生时 make 将忽略这些错误!以代码为例来进行分析说明
.PHONY : all include test.txt all : @echo "this is all" test : @echo "creating $@ ..." @echo "other : ; @echo "this is other" " > test.txt
我们来编译看看
我们看到不但发出警告,而且报错了。下来我们来在 include 前面加上 - 试试
这样它也不报错了,直接就通过了,我们还以为 makefile 写的对着呢。这便是第一个暗黑操作。下来看看第二个暗黑操作
B、如果 include 触发规则创建了文件,之后还会发生什么?以代码为例来进行分析说明
.PHONY : all include test.txt all : @echo "this is all" test.txt : @echo "creating $@ ..." @echo "other : ; @echo "this is other" " > test.txt
看看编译结果
我们进行直接 make 的时候,发现它输出的 this is other,并不是我们所期望的 this is all。这是为什么呢?因为在 include 的时候,直接将 test.txt 铺开在这,此时会触发规则。makefile 就变成了下面这样
.PHONY : all other : @echo "creating $@ ..." @echo "this is other" all : @echo "this is all"
我们在直接 make 的时候,它默认执行的是第一个目标,因此便会输出 this is other,只有当我们 make all 的时候才会输出 this is all。这便是 include 的第二个暗黑操作了,下面继续看看第三个
C、如果 include 包含的文件存在,之后会发生什么呢?以代码为例来进行分析说明
.PHONY : all -include test.txt all : @echo "this is all" test.txt : b.txt @echo "this is $@"
在当前目录下创建一个 b.txt 文件,看看编译结果
我们看到同样也执行了 test.txt 的相应的规则。看看下面这个 makefile 将会输出什么
.PHONY : all -include test.txt all : @echo "$@ : $^" test.txt : b.txt @echo "creating $@ ..." @echo "all : c.txt" > test.txt
看看结果
我们看到它最后输出的 all 的依赖是 c.txt,不应该觉得奇怪吗?我们明明在 all 后面没有依赖啊。再来看看生成的 test.txt 文件,它的内容是 all : c.txt,因此输出的结果是我们意想不到的。那么我们关于 include 便有了这几条总结:1、当目标文件不存在时,以文件名查找规则并执行;2、当目标文件不存在时且查找到的规则中创建了目标文件,将创建成功的目标文件包含进当前的 makefile 中;3、当目标文件存在,将目标文件包含进当前 makefile,以目标文件名查找是否有相应规则,YES 的话则比较规则的依赖关系来决定是否执行规则的命令,NO 的话则 NULL(无操作)。4、当目标文件存在且目标名对应的规则被执行,规则中的命令更新了目标文件,make 重新包含目标文件,替换之前包含的内容。目标文件未被更新,便是 NULL(无操作)。
经过了这么多的知识点的探索,此时已经具备实现之前的想法的能力了。想要实现的具体格式如下
下面我们就根据这个来编写相关的 makefile。
func.h 源码
#ifndef FUNC_H #define FUNC_H #define HELLO "hello Makefile" #endif
func.c 源码
#include#include "func.h" void foo() { printf("void foo() : %s\n", HELLO); }
main.c 源码
#include#include "func.h" int main() { foo(); return 0; }
makefile 源码
.PHONY : all clean MKDIR := mkdir RM := rm -rf CC := gcc DIR_DEPS := deps DIR_OBJS := objs DIR_EXES := exes DIRS := $(DIR_DEPS) $(DIR_EXES) $(DIR_OBJS) EXE := app.out EXE := $(addprefix $(DIR_EXES)/, $(EXE)) SRCS := $(wildcard *.c) OBJS := $(SRCS:.c=.o) OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS)) DEPS := $(SRCS:.c=.dep) DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS)) all : $(DIR_OBJS) $(DIR_EXES) $(EXE) ifeq ("$(MAKECMDGOALS)", "all") -include $(DEPS) endif ifeq ("$(MAKECMDGOALS)", "") -include $(DEPS) endif $(EXE) : $(OBJS) $(CC) -o $@ $^ @echo "Success! Target => $@" $(DIR_OBJS)/%.o : %.c $(CC) -o $@ -c $^ $(DIRS) : $(MKDIR) $@ ifeq ("$(wildcard $(DIR_DEPS))", "") $(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c else $(DIR_DEPS)/%.dep : %.c endif @echo "Creating $@ ..." @set -e; \ $(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@ : ,g' > $@ clean : $(RM) $(DIRS)
编译结果如下
我们看到已经自动生成了,并且最后的结果也是我们想要的,那么我们如果在 func.h 中改变字符串,看看结果是否也会改变
我们看到在编译的时候报错了,原因是只能编译 .c 文件,.h 头文件不参与编译,这时我们便要用到预定义函数 filter 了。因此我们需要在 makefile 第37 行将它改为 $(CC) -o $@ -c $(filter %.c, $^);再来看看效果
我们看到也成功的替换掉了。这时我们基本上已经完成我们之前的想法了,那么在实际开发中,肯定需要时不时的添加头文件,我们再来在 func.h 中包含一个头文件 define.h,在 define.h 文件中定义字符串 hello-makefile,看看结果是否会跟着改变
我们看到字符串并没有发生改变,再来看看 func.dep 和 main.dep 中是否包含了 define.h
也没有包含,按理说不应该,因为我们在 func.h 中包含了 define.h,那么在 func.c 和 main.c 中肯定也就包含了 define.h。下来我们来分析下这个,当 .dep 文件生成后,如果动态的改变头文件间的依赖关系,那么 make 可能无法检测到这个改变,进而做出错误的编译决策。解决方案便是:1、将依赖文件名作为目标加入自动生成的依赖关系中;2、通过 include 加载依赖文件时判断是否执行规则;3、在规则执行时重新生成依赖关系文件;4、最后加载新的依赖文件。解决方法是在 sed 命令后加上 $@,看看编译效果,顺便我们再来加上 rebuild。
我们看到已经正确实现了,我们来看看在 deps 文件下的 .dep 文件是否包含 define.h 呢?
确实是包含了 define.h,我们再来加上 new.h,看看是否还会有效
我们看到 new.h 同样也包含进去了。通过对综合示例的学习,总结如下:1、makefile 中可以将目标的依赖拆分写到不同的地方;2、include 关键字能够触发相应规则的执行;3、如果规则的执行导致依赖更新,可能导致再次解释执行相应规则;4、依赖文件也需要依赖于源文件得到正确的编译决策;5、自动生成文件间的依赖关系能够提高 makefile 的移植性。
欢迎大家一起来学习 makefile,可以加我QQ:243343083。
网站名称:自动生成依赖关系(十)
URL地址:http://scjbc.cn/article/pcgpoo.html