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的执行机制
-
- 若目标文件不存在,以文件名为目标查找规则,若找到,执行生成目标文件,再包含到Makefile。否则,产生错误,停止运行。
-
- 若目标文件存在,将目标文件包含进当前makefile。并以文件名为目标查找规则,若找到,比较依赖文件和目标文件的最新关系,决定是否执行命令;若没找到,无操作。
-
- 若目标文件存在,且目标文件名的规则找到并执行。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文件的后缀名进行相应的替换即可。