---- 整理自狄泰软件唐佐林老师课程
文章目录
1. 编译行为带来的缺陷
1.1 问题
- 目标文件(.o)是否只依赖于源文件(.c)?
- 编译器如何编译源文件和头文件?

1.2 缺陷
- 预处理器将头文件中的代码直接插入源文件
- 编译器只通过预处理后的源文件产生目标文件
- 因此,规则中以源文件为依赖,命令可能无法执行
1.3 实验

#include <stdio.h>
#include "func.h"
void foo()
{
printf("foo(): %s\n", HELLO);
}
#ifndef FUNC_H
#define FUNC_H
#define HELLO "Hello Makefile"
void foo();
#endif
#include "func.h"
int main()
{
foo();
return 0;
}
OBJS := func.o main.o
hello.out: $(OBJS)
@gcc -o $@ $^
@echo "Target File ==> $@"
$(OBJS):%.o:%.c
@gcc -o $@ -c $^
1.4 结果
1.4.1 新的问题
执行 make,可以得到正确的可执行文件,修改 func.h 中的宏定义,将 “HELLO” 宏改为 “Hello Makefile”,此时,再次执行 make,提示文件是最新的。并不是我们想要的结果。

1.4.2 初步解决方案
- make 解释器根据文件新旧关系判断出并不需要重新编译,但是,这并不是我们想要的结果
- 将 func.h 加入到依赖关系中去,这样头文件的改变,make 就能感知到。
OBJS := func.o main.o
hello.out: $(OBJS)
@gcc -o $@ $^
@echo "Target File ==> $@"
$(OBJS):%.o:%.c func.h
@gcc -o $@ -c $<

1.4.3 初步方案问题
- 头文件作为依赖条件出现在每个目标对应的规则中(即使这个目标文件与该头文件没有任何关系)
- 当头文件改动时,任何源文件都将被重新编译(编译低效)
- 当项目中头文件数量巨大时,Makefile 很难维护
1.5 目标
解决方案:
- 通过命令自动生成对头文件的依赖
- 讲生成的依赖自动包含进 Makefile 中
- 当头文件改动后,自动确认需要重新编译的文件
2. 预备工作
2.1 sed 命令
- sed 是一个流编辑器,用于流文本的修改(增删改查)
- sed 可用于流文本中的字符串替换
- sed 字符串替换方式为:
sed 's:src:dst:g',其中,s 和 g 是固定格式,src 为待替换的字符串,dst 为替换后的字符串。


- sed 还支持正则表达式,可以使用匹配的目标生成替换结果


\(.*\)\.o[ :]*匹配以.o结尾的文件名,包括它的路径结构,后边的[ :]表示文件名后有任意空格或者冒号(:)也可以进行匹配。替换部分则表示在匹配到的文件名前加上前缀objs/。
2.2 gcc 关键编译选项
- 生成依赖关系
-M和-MM
-M:获取目标的完整依赖关系
-MM:获取目标的部分依赖关系

- 将 gcc 的这个特殊选项和 sed 命令结合在一起

- 拆分目标的依赖:将目标的完整依赖拆分为多个部分依赖


2.3 include 关键字
- 类似于 C 语言中的 include 关键字
- 将其它文件的内容原封不动的搬到当前文件
- make 对 include 关键字的处理,在当前目录搜索或指定目录搜索目标文件:
- 搜索目标文件成功:将文件内容搬入当前 makefile 中
- 搜索目标文件失败:产生警告
- 以文件名作为目标查找并执行对应规则
- 当文件名对应的规则不存在时,最终产生错误

- make 解释器在解析 makefile 时会试图包含test.txt,发现 test.txt 文件并不存在,于是,make 以 test.txt 为目标查找并执行对应的规则,所以首先输出的是 test.txt。如果不存在 test.txt 文件也不存在以 test.txt 为目标对应的规则,则 make 直接报错。
- 通过在 include 前加上"
-",消除文件不存在时产生的警告,如:-include filename,但这种做法也容易将一些 bug 隐藏。

make 通过 include 将 test.txt 的内容原封不动的搬到了 makefile 中,other 对应的规则也成了第一条规则对应的目标,make 默认情况下寻找第一个目标执行,所以输出 this is other。

make 解释器一开始发现当前目录下没有 test.txt 文件,便去寻找以 test.txt 为目标的规则,找到之后,执行对应的命令,生成了一个 test.txt 文件,并向该文件中写入了 other : ;@echo this is other 这条规则,然后,make 解释器再次将这个新生成的文件内容原封不动的加载到 makefile 中,other 目标对应的规则成了第一条规则,make 默认情况下执行这个目标下的命令,因此,最终输出了 this is other。
2.4 makefile 中命令的执行机制
- 规则中的每个命令默认是在一个新的进程中执行(shell)
- 可以通过接续符(
;)将多个命令组合成一个命令 - 组合的命令依次在同一个进程中被执行
set -e指定发生错误后立即退出执行
2.4.1 问题
- 下面的代码想要实现的功能?有什么问题?


- 在执行 mkdir test 命令时,make 会创建一个进程,执行完这条命令,进程终止。
- 接着该执行下一条命令 cd test 了,同样是先创建一个进程,然后执行完命令,进程终止。
- 最后,make又创建一个进程,执行 mkdir subtest 命令,但是这个进程的工作目录是当前目录,而没有进入到 test 目录,因此,在当前目录下创建了文件夹 subtest。而我们希望的是 subtest 文件夹是建立在 test 目录下。
2.4.2 加上连接符

3. 自动生成依赖关系的解决方案
3.1 初步方案
- 通过 gcc -MM 和 sed 得到 .dep 依赖文件(目标的部分依赖)
规则中命令的连续执行 - 通过 include 指令包含所有的 .dep 依赖文件
当 .dep 依赖文件不存在时,使用规则自动生成

- 如何在 makefile 中将 .dep 文件组织到指定目录?
- 当 include 发现 .dep 文件不存在时:
- 通过规则和命令创建 deps 文件夹。
- 将所有 .dep 文件创建到 deps 文件夹。
- .dep 文件中记录目标文件的依赖关系。

3.2 存在的问题
- 为什么一些 .dep 依赖文件会被重复创建多次?
- 问题本质分析:
- deps 文件夹的时间属性会因为依赖文件创建而发生改变
- make 发现 deps 文件夹比对应的目标更新
- 触发相应的规则重新解析和命令的执行
(对于 func.c 来说就是 deps/func.dep : deps func.c,make 记录下该规则后便去执行规则下的命令,生成依赖文件,接着再去推倒出 main 相关的规则并执行命令。当生成 deps/main.dep 时,deps 文件夹的时间戳也被更新了,因此,make 根据记录下来的规则发现,应该重新生成 func.dep,这就是为什么执行结果中出现了创建两次 func.dep的原因)
3.3 优化方案
使用 ifeq 动态决定 .dep 目标的依赖


3.4 进一步优化
- 当 .dep 文件生成后,如果动态的改变头文件间的依赖关系,那么 make 可能无法检测到这个改变,进而做出错误的编译决策。
- 基于上一个实验代码,先 make 得到上一个实验的结果
- 接着新建 new.h,在 func.h 中包含 new.h 文件

- 优化



被折叠的 条评论
为什么被折叠?



