自动生成依赖关系
1、编译行为带来的缺陷
- 预处理器将头文件中的代码直接插入源文件
- 编译器只通过预处理后的源文件产生目标文件
- 因此,规则中以源文件为依赖,命令可能无法执行
示例1观察以下makefile文件是否正确:当修改func.h中宏HELLO的内容后,执行make命令发现,编译器无法更新main.c和func.c,进而无法更新执行的结果:原因在于func.h中更新的内容无法自动更新到func.c和main.c文件中,进而导致编译的hello.out文件结果无任何变化。
func.h
#ifndef FUNC.H
#define FUNC.H
#define HELLO "hello makefile"
void foo();
#endif
func.c
#include "stdio.h"
#include "func.h"
void foo()
{
printf("void foo():%s\n",HELLO);
}
main.c
#include "stdio.h"
#include "func.h"
extern void foo();
int main()
{
foo();
return 0;
}
makefile
OBJS := func.o main.o
hello.out := $(OBJS)
@gcc -o $@ $^
@echo "Target File => $@"
$(OBJS) : %.o : %.c
@gcc -o $@ -c $^
解决方法1:修改makefile文件--将头文件作为依赖条件出现于每个目标对应的规则中
OBJS := func.o main.o
hello.out := $(OBJS)
@gcc -o $@ $^
@echo "Target File => $@"
$(OBJS) : %.o : %.c func.h
@gcc -o $@ -c $<
优点:
头文件的更改会更新到相关的源文件中,并更新到最终的目标文件
缺点:
- 当头文件改动,任何源文件都将被重新编译(编译低效)
- 当项目中头文件数量巨大时,makefile将很难维护
解决方案2:
- 通过命令自动生成对头文件的依赖
- 将生成的依赖自动包含进makefile中
- 当头文件改动后,自动确认需要重新编译的文件
针对解决方案2需要使用的技术:
(1) linux的sed命令
sed是一个流编辑器,用于流文本的修改(增/删/改/查)
sed可用于流文本中的字符串替换
-
sed的字符串替换方式为:sed 's:src:des:g'
例如执行下列语句:
echo "test=>abc+abc+abc" | sed 's:abc:xyz:g'
test的内容将变为xyz+xyz+xyz
sed的正则表达式支持
- 在sed中可以用正则表达式匹配替换目标
- 并且可以使用匹配的目标生成替换结果
例如
sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
示例2--sed用法
(2) 编译器依赖生成选项gcc -MM(gcc -M)
- gcc -M des
获取目标des的完整依赖关系 -
gcc -MM des
获取目标des的部分依赖关系
(-E 仅对依赖关系做初步解析)
(3)makefile中的include关键字
- 类似C语言中的include
- 将其他文件的内容原封不动的搬入当前文件
- 语法:include filename
例如:
include foo.make
include *.mk
include $(var)
make对include关键字的处理方式
在当前目录搜索或指定目录搜索目标文件
- 搜索成功:将文件内容搬入当前makefile中
- 搜索失败:产生警告
-- 以文件名作为目标查找并执行相应规则
-- 当文件名对应的规则不存在时,最终产生错误
示例3-1--include用法--目标文件不存在,目标规则不存在--无操作,直接报错
.PHONY : all
include test.txt
all :
@echo "this is $@"
示例3-2--include用法--当目标文件不存在,目标规则存在----以文件名查找规则,并执行
.PHONY : all
include test.txt
all :
@echo "this is $@"
test.txt :
@echo "this is $@"
示例3-3--include用法--目标文件存在,目标规则存在
.PHONY : all
include test.txt
all :
@echo "this is $@"
test.txt :
@echo "creating $@"
@echo "other : ; @echo "this is other" " > test.txt
--执行make命令,将执行makefile中的第一条规则
--执行make all命令将执行all规则
include 暗黑操作一:
-
使用减号(-)不但关闭了include发出的警告,同时关闭了错误;当错误发生时make将忽略做这些错误!
示例3-5-1--include不使用(-)会报告所有的错误和警告
.PHONY : all
include test.txt
all :
@echo "this is $@"
示例3-5-2--include(-)关闭了include发出的警告和错误
.PHONY : all
include test.txt
all :
@echo "this is $@"
include 暗黑操作二:
-
如果include触发规则创建了文件,之后还会执行规则中的命令,然后重新执行include后的命令
示例3-6-1-include执行的规则中不存在依赖;则会将规则直接包含进makefile
.PHONY : all
-include test.txt
all :
@echo "this is $@"
test.txt :
@echo "creating $@ ..."
@echo "other"
示例3-6-2--include执行时先判断规则是否存在,如果存在会再检查依赖是否是最新的,如果依赖比当前规则要新,会直接执行依赖;否则直接执行规则
示例3-6-2
makefile
.PHONY : all
-include test.txt
all :
@echo "this is $@"
test.txt : b.txt
@echo "creating $@ ..."
@echo "all : c.txt" > test.txt
test.txt
all : a.txt
示例3-6-2 当规则文件比依赖文件内容要新--test.txt比b.txt时间戳更新,执行make all结果如下:
示例3-6-2 当依赖文件比规则文件内容要新--b.txt比test.txt时间戳更新,执行make all结果如下:
include的总结:
-
当目标文件不存在----以文件名查找规则,并执行
-
当目标文件不存在,且查找到的规则中创建了目标文件----将创建成功的目标文件包含进当前makefile
-
当目标文件存在,将目标文件包含进当前makefile;并以目标文件名查找是否有相应规则--如果有,比较规则的依赖关系,决定是否执行规则的命令;否则,无操作。
-
当目标文件存在,且目标名对应的规则被执行
如果规则中的命令更新了目标文件--make重新包含目标文件,替换之前包含的内容
如果目标文件未被更新,无操作。
(4) makefile中命令的执行机制
- 规则中的每个命令默认是在一个新的进程中执行(Shell)
- 可以通过接续符(;)将多个命令组合成一个命令
- 组合的命令依次在同一个进程中被执行
- set -e指定发生错误后立即退出执行
示例4-1--规则中命令的执行--无接续符--规则中的每个命令默认是在一个新的进程中执行
.PHONY : all
all :
mkdir test
cd test
mkdir subtest
示例4-2--规则中命令的执行--接续符--组合的命令依次在同一个进程中被执行
.PHONY : all
all :
set -e;\
mkdir test;\
cd test;\
mkdir subtest
解决方案2的初步思路
- 通过gcc -MM和sed得到.dep依赖文件(目标的部分依赖)
技术点:规则中命令的连续执行 - 通过include指令包含所有的.dep依赖文件
技术点:当.dep依赖文件不存在时,使用规则自动生成 - 当include发现.dep文件不存在时
通过规则和命令创建deps文件
将所有.dep文件创建到deps文件夹
.dep文件中记录目标文件的依赖关系
代码部分设计如下:
$(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' > $@
观察上述代码,(DIR_DEPS) %.c 语句存在问题:当第一次执行时会创建deps文件夹和对应的.dep文件,而第二次deps文件夹内容会被新的.dep文件进行更新,导致第一次生成的.dep文件因为依赖更新而会被重复执行。
解决方法如下:使用ifeq动态决定.dep目标的依赖,实现代码如下:
ifeq ("$(wildcard $(DIR_DEPS))","")
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
示例5
func.h
#ifndef FUNC_H
#define FUNC_H
#define HELLO "hello world"
void foo();
#endif
func.c
#include "stdio.h"
#include "func.h"
void foo()
{
printf("void foo():%s\n",HELLO);
}
main.c
#include "stdio.h"
#include "func.h"
extern void foo();
int main()
{
foo();
return 0;
}
makefile
.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))
ifeq ("$(MAKECMDGOALS)","all")
-include $(DEPS)
endif
ifeq ("$(MAKECMDGOALS)","")
-include $(DEPS)
endif
all :
@echo "$@"
$(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加载依赖文件时判断是否执行规则
- 在规则执行时重新生成依赖关系文件
- 最后加载新的依赖文件
示例6--最终代码实现:
define.h
#ifndef DEFINE_H
#define DEFINE_H
#define HELLO "hello world"
#endif
func.h
#ifndef FUNC_H
#define FUNC_H
#include "define.h"
void foo();
#endif
func.c
#include "stdio.h"
#include "func.h"
void foo()
{
printf("void foo():%s\n",HELLO);
}
main.c
#include "stdio.h"
#include "func.h"
extern void foo();
int main()
{
foo();
return 0;
}
makefile
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_DEPS := deps
DIR_EXES := exes
DIR_OBJS := objs
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 $(filter %.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)
小结:
- makefile中可以将目标的依赖拆分写到不同的地方
- include关键字能够触发相应规则的执行
- 如果规则的执行到政治依赖更新,可能导致再次解释执行相应规则
- 依赖文件也需要依赖于源文件得到正确的编译决策
- 自动生成文件间的依赖关系能够提高makefile的移植性