值得思考的问题
目标文件 (.o) 是否只依赖于源文件 (.c)?
编译器如何编译源文件和头文件?
编译行为带来的缺陷
预处理器将头文件中的代码直接插入源文件
编译器只通过预处理后的源文件产生目标文件
因此,
- 规则中以源文件为依赖,命令可能无法执行
下面的 makefile 有没有问题?
问题的提出
makefile
TARGET := hello.out
CC := gcc
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
$(OBJS) : %.o : %.c
$(CC) -o $@ -c $<
当前的目录文件
当我们修改了 foo.h 中的内容后,结果如下
并没有再次进行编译,这导致最终可执行文件没有重新生成。
实验中的解决方案
头文件作为依赖条出现于每个目标对应的规则中
当头文件改动,任何源文件都将被重新编译 (编译低效)
当项目中头文件数量巨大时,makefile 将很难维护
疯狂的想法
通过命令自动生成对头文件的依赖
将生成的依赖自动包含进 makefile 中
当头文件改动后,自动确认需要重新编译的文件
预备工作 (原材料)
Linux 命令 sed
编译器依赖生成选项 gcc -MM (gcc -M)
Linux 中的 sed 命令
sed 是一个流编辑器,用于流文件的修改 (增/删/查/改)
sed 可用于流文本中的字符替换
sed 的字符串替换方式为 sed 's:src:des:g'
sed 的正则表达式支持
在 sed 中可以用正则表达式匹配替换目标
并且可以使用匹配的目标生成替换结果
gcc 关键编译选项
生成依赖关系
- 获取目标的完整依赖关系
gcc -M test.c
- 获取目标的部分依赖关系
gcc -MM test.c
小技巧:拆分目标的依赖
将目标的完整依赖拆分为多个多个部分依赖
预备工作
makefile
.PHONY : test a b c
test : a b
test : b c
test :
@echo "$^"
运行结果如下所示:
我们将 test 目标的依赖拆分成两部分,和直接将 test 目标的依赖写成一起是一样的。
makefile 中的 include 关键字
类似 C 语言中的 include
将其它文件的内容原封不动的搬入当前文件
make 对 include 关键字的处理
在当前目录搜索或指定目录搜索目标文件
- 搜索成功:将文件搬入当前 makefile 中
- 搜索失败:产生警告
- 以文件名作为目标查找并执行对应规则
- 当文件名对应的规则不存在时,最终产生了错误
初探 include 关键字
makefile
.PHONY : all
include test.txt
all :
@echo "this is all"
test.txt :
@echo "this is test.txt"
@touch test.txt
test.txt
other :
@echo "this is $@"
执行结果如下所示:
首先 make 会查找当前目录中是否存在 test.txt 文件,当前目录中存在该文件,所以 make 会将 test.txt 中的内容插入到 include 的地方,other 成为了顶层目标,所以我们在执行 make 的时候,其实是在执行 other 所对应的命令。
我们将当前目录下的 test.txt 文件删除,再次执行 make,运行结果如下所示:
此时 make 查找不到对应的文件,所以首先会产生一个警告,然后去查找当前 makefile 中是否存在对应的目标,当前 makefile 中存在 test.txt 目标,所以会执行目标所对应的命令,这时顶层的目标为 all,所以又会去执行 all 所对应的命令。
makefile 中命令的执行机制
规则中的每个命令默认是在一个新的进程中执行 (Shell)
可以通过接续符 (;) 将多个命令组合成一个命令
组合的命令依次在同一个进程中被执行
set -e 指定发生错误后立即退出执行
下面的代码想要实现功能?有没有问题?
每个命令都是 Shell 开启一个新的进程执行的,进程之间不会相互影响,所以 subtest 是创建在当前目录下的,而不是 test 目录下的。
makefile 的命令执行
makefile
.PHONY : all
all :
set -e; \
mkdir test; \
cd test; \
mkdir subtest
set -e 指定发生错误后立即退出执行,并通过接续符将四个命令组合成一个命令,Shell 此时只会创建一个进程去执行这些命令,所以 subtest 是创建在 test 目录下的。
解决方案的初步思路
通过 gcc -MM 和 sed 得到 .dep 依赖文件 (目标的部分依赖)
- 技术点:规则中命令的连续执行
通过 include 指令包含所有的 .dep 依赖文件
- 技术点:当 .dep 依赖文件不存在时,使用规则自动生成
解决方案原型
makefile
.PHONY : all clean
CC := gcc
MKDIR := mkdir
RM := rm -rf
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
-include $(DEPS)
all :
@echo "this is $@"
%.dep : %.c
@echo "creating $@ ..."
@set -e; \
$(CC) -MM -E $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DEPS)
.dep 依赖文件记录了一个目标文件所对应的依赖关系;如果对应的依赖文件不存在,则通过 include 去执行目标所对应的命令,gcc -MM 生成目标文件的依赖关系,通过 sed 将这些依赖关系都添加 objs/ 前缀,并重定向到对应的依赖文件中。