Makefile-自动生成依赖-示例分析

Makefile-自动生成依赖-示例分析

参考:

一、背景

Linux下,编译C/C++项目时,往往先将.c文件编译生成.o文件。下面是一个简单的示例。

OBJS := func.o main.o

main : $(OBJS)
    @gcc -o $@ $^
    @echo "Target File => $@"

$(OBJS) : %.o : %.c
    @gcc -o $@ -c $^

如果func.c文件中include一个头文件func.h,那么只修改func.h文件,重新执行make命令将不会触发编译。
因为Makefile文件中.o文件的生成只依赖于.c文件,只要.c文件没有修改,便不会触发重新编译,这不是我们想要的。

为了解决这个问题,可以将头文件加入到依赖中。

OBJS := func.o main.o

main : $(OBJS)
    @gcc -o $@ $^
    @echo "Target File => $@"

$(OBJS) : %.o : %.c func.h
    @gcc -o $@ -c $^

这样导致的缺点是:

  • 上面只是简单的例子,实际上,不同的.c文件有不同的头文件,需要为不同.o文件的生成规则添加不同的头文件依赖。
  • 当代码头文件发生改动时,需要手动逐个规则进行维护,非常麻烦。
  • 项目比较大时,头文件包含非常复杂,人工很难判断头文件包含情况。

于是自动生成依赖应运而生,这也是本文说明的内容。

二、自动生成依赖示例分析

以一个例子进行说明,最终实现一个比较完备的Makefile

1. 例子文件结构

  • bin
  • dep
  • include
    • fun.h
    • st_work.h
  • obj
  • src
    • fun.c
    • main.c
    • st_work.c
  • Makefile

具体代码参考Github上的代码:https://github.com/zzh-wisdom/Makefile-learn/tree/master/demo_06_%E8%87%AA%E5%8A%A8%E7%94%9F%E6%88%90%E4%BE%9D%E8%B5%96

2. 实现的目标功能

  • 通过gcc -MM和sed得到每个.c文件的依赖情况,形成依赖规则放到对应的.d文件中。
  • 通过include关键字包含所有的.d依赖文件。若依赖文件不存在,可以自动成。
  • 当文件目录bin dep obj 不存在时,可以自动创建。
  • 生成的可执行文件放到bin目录中,.d文件放到dep目录中,.o文件放到obj目录中。

3. GCC编译器依赖生成选项 -MM(-M)

  • gcc -M des

    获取目标文件des的完整依赖关系,包括系统的头文件。由于系统的头文件一般是不变的,不需要加入到依赖关系中,所以这个命令不是我们想要的。

  • gcc -MM des

    获取目标文件des的依赖关系,但不包括系统的头文件。

注意:由于我们将头文件放置到include目录中,与.c文件不在同一个目录,需要为gcc指定头文件搜索的路径,使用-I选项。例如:

gcc -Iinclude -M src/main.c
gcc -Iinclude -MM src/main.c

注意:-I选项与其值include可以连在一起写,也可以用空格隔开。如:

gcc -I include -M src/main.c

执行结果为:

zzh@zzhdeMacBook-Pro demo_06_自动生成依赖 % gcc -Iinclude -M src/main.c
main.o: src/main.c \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/stdio.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_stdio.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_symbol_aliasing.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_posix_availability.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/Availability.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/AvailabilityInternal.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_types.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/machine/_types.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/i386/_types.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_types.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_va_list.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/machine/types.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/i386/types.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_int8_t.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_int16_t.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_int32_t.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_int64_t.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int8_t.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int16_t.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int32_t.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int64_t.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_intptr_t.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_uintptr_t.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_size_t.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_null.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/stdio.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/_ctermid.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_off_t.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/_types/_ssize_t.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/secure/_stdio.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/secure/_common.h \
  include/st_work.h \
  /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/assert.h \
  include/fun.h
zzh@zzhdeMacBook-Pro demo_06_自动生成依赖 % gcc -Iinclude -MM src/main.c
main.o: src/main.c include/st_work.h include/fun.h
zzh@zzhdeMacBook-Pro demo_06_自动生成依赖 % 
  • 实际上,include目录下往往只是放一些通用的头文件,与c文件紧密相关的头文件,还是放到src目录下的,这里这样放只是为了说明问题。

4. Makefile中的include关键字

  • 语法:include filename

    例如:
    include fun.d
    include *.mk
    include $(var)

  • 功能:

    类似C语言中的include,将其他文件的内容原封不动的搬入当前文件。

  • include的执行机制

      1. 若目标文件不存在,以文件名为目标查找规则,若找到,执行生成目标文件,再包含到Makefile。否则,产生错误,停止运行。
      1. 若目标文件存在,将目标文件包含进当前makefile。并以文件名为目标查找规则,若找到,比较依赖文件和目标文件的最新关系,决定是否执行命令;若没找到,无操作。
      1. 若目标文件存在,且目标文件名的规则找到并执行。Makefile会将最新生成的目标文件重新include进去。

5. Makefile的命令执行机制

  • 规则中的每个命令默认是在一个新的进程中执行。
  • 可以通过接续符;将多个命令组合成一个命令,注意使用反斜杠\将不同行的命令连接起来。
  • 组合的命令依次在同一个进程中被执行。
  • 命令set -e可以指定发生错误后,立即退出当前进程。因此,这个命令往往使用分号与其他命令组合成一个来执行。

6. sed命令的简介

  • sed是一个流编辑器,用于流文本的修改(增/删/改/查)。

  • sed可用于流文本中的字符串替换,替换方式为:sed ‘s,src,des,g’。

  • 在sed中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果。

    例如:

    sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
    

    其中使用括号()括起来的部分成为一个子表达式,以方便后续使用,括号() 是特殊字符,需要使用反斜杠\进行转义。

示例:

zzh@zzhdeMacBook-Pro ~ % echo "<h1>header</h1>" | sed 's,h[1-9],div,g'
<div>header</div>

另外,命令结果可以使用符号>将执行的结果打印到文件中。如:

echo "<h1>header</h1>" | sed 's,h[1-9],div,g' > temp.html

7. Makefile最终结果

.PHONY : all clean   # 标志标签
# 关于=与:=的区别:https://www.cnblogs.com/baiduboy/p/7612488.html

# 命令
CC := gcc
MKDIR := mkdir
RM := rm -rf      # -r递归删除, -f强制删除文件或目录

# 已存在目录
DIR_INCLUDES := include #header     # 头文件集合
DIR_SRC := src

#需要创建的目录
DIR_BIN := bin
DIR_OBJ := obj
DIR_DEP := dep

INCLUDES := $(foreach dir,$(DIR_INCLUDES), -I$(dir))  # 头文件目录(给每个文件目录添加前缀“-I”)
DIRS := $(DIR_DEP) $(DIR_BIN) $(DIR_OBJ)          # 需要创建的目录集合

SRCS := $(wildcard $(DIR_SRC)/*.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(subst $(DIR_SRC),$(DIR_OBJ),$(OBJS))    # 将生成的.o文件均放到obj目录下
DEPS := $(SRCS:.c=.d)
DEPS := $(subst $(DIR_SRC),$(DIR_DEP),$(DEPS))    # 将生成的.d文件均放到dep目录下

#最终的可执行文件
EXE := main   #可执行文件
EXE := $(addprefix $(DIR_BIN)/,$(EXE))  # 添加路径前缀,使得生成的可执行文件都放到bin目录下

# make all 先创建目录obj和bin,再生成可执行文件
all : $(DIR_OBJ) $(DIR_BIN) $(EXE)

ifeq ("$(MAKECMDGOALS)","all")    # MAKECMDGOALS表示当前make命令生成的目标,若执行命令为"make"或者“make all”则包含“-include $(DEPS)”
include $(DEPS)
endif

ifeq ("$(MAKECMDGOALS)","")
include $(DEPS)
endif

$(EXE) : $(OBJS)
	$(CC) -o $@ $^
	@echo "Success! Target => $@"

# 模式规则,产生.o文件
$(DIR_OBJ)/%.o : $(DIR_SRC)/%.c
	@#@echo $^  # 这里打印的依赖文件包含头文件.h,有点神奇
	$(CC) $(INCLUDES) -o $@ -c $(filter $(DIR_SRC)/%.c, $^)    

$(DIRS) :
	$(MKDIR) $@

# 模式规则,产生.d文件
ifeq ("$(wildcard $(DIR_DEP))","")   # 根据是否含有dep文件夹,进行创建
$(DIR_DEP)/%.d : $(DIR_DEP) $(DIR_SRC)/%.c
else
$(DIR_DEP)/%.d : $(DIR_SRC)/%.c
endif
	@echo "Creating $@ ..."
	@# sed命令:sed是一个流编辑器,用于流文本的修改(增/删/改/查),sed的字符串替换方式为:sed 's:src:des:g',在sed中可以用正则表达式匹配替换目标。
	@set -e;\
	$(CC) -MM $(INCLUDES) $(filter %.c,$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OBJ)/\1.o $@: ,g' > $@

clean :
	$(RM) $(DIRS)
  • 对于初学者可能有点难理解,说明的是,Makefile中,不管目录还是文件,若不存在,都会自动寻找相应的规则,进行创建。
  • 具体的代码文件,在前面分享的Github链接里也有。

执行结果:

zzh@zzhdeMBP demo_06_自动生成依赖 % make      
mkdir dep
Creating dep/st_work.d ...
Creating dep/main.d ...
Creating dep/fun.d ...
mkdir obj
mkdir bin
gcc  -Iinclude   -o obj/fun.o -c src/fun.c    
gcc  -Iinclude   -o obj/main.o -c src/main.c    
gcc  -Iinclude   -o obj/st_work.o -c src/st_work.c    
gcc -o bin/main obj/fun.o obj/main.o obj/st_work.o
Success! Target => bin/main
zzh@zzhdeMBP demo_06_自动生成依赖 % 

8. 补充说明

这里写的样例也很有局限性,主要问题是.c文件的查找、.o和.d文件生成。大项目的情况下,src目录下还有多级目录,简单的src/*.c是获取不到全部c文件的。知识受限,目前也没有找到很好的解决办法。目前的一个折衷方法是:

  • 手动完善SRCS变量
  • 舍弃obj和dep目录(实际工程中也很少见),将生成的.o和.d文件直接放到对应的.c文件的同级目录下。
  • 这样.o和.d文件路径的获取,只用简单地将SRCS变量中所有c文件的后缀名进行相应的替换即可。
  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值