makefile-- 自动生成依赖关系 示例

自动生成依赖关系


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)/%.dep : $(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的移植性

猜你喜欢

转载自blog.csdn.net/qq_28944301/article/details/82958170