在学习该篇文章之前,首先你需要有一定的makefile理论基础,如果不懂可以参考:Makefile教程(绝对经典,所有问题看这一篇足够了)建议看2遍以上。
在Linux C语言开发的时候我们需要自己手动编译新添加的文件。如果一个工程非常庞大这样将会是程序员的噩梦。使用 -MM
选项获取文件的依赖,并且自动推导文件依赖关系。
Demo1:以.c和.h在同一个目录来讲解
首先使用 cc -MM
分析当前目录下面所有的源文件的依赖关系,并且把结果生成到对应的.d文件中
#获取当前目录的所有.c文件
src := $(wildcard *.c)
#生成.c对应的.d目标
d_obj := $(src:.c=.d)
#最终目标,暂时什么都不做
main:$(d_obj)
$(d_obj):$(src)
cc -MM $< -o $@
运行结果:可以看到对应的.d文件,以及.d文件的内容就是对应cpp的依赖项。
此时可以看见.d的文件内容就是我们需要的文件依赖关系,并且可以直接通过cc编译。所以如果我们需要自动编译生成主程序main,我们首先分需要析一下依赖关系:
cc -MM
生成#获取当前目录的所有.c文件
src := $(wildcard *.c)
o_obj:=$(src:.c=.o)
#生成main目标
main:$(o_obj)
gcc -o $@ $^
-include $(src:.c=.d)
%.d:%.c
cc -MM $< -o $@
这样我们就可以每次执行make,自动更新依赖并且编译文件了。
所以最终的makefile如下:
ifndef GXX
GXX := g++
endif
ifndef CFLAGS
CFLAGS := -g
endif
src = $(wildcard *.c)
objs = $(src:.c=.o)
main:$(objs)
$(GXX) -o $@ $(CFLAGS) $^
-include $(src:.c=.d)
%.d:%.c
cc -MM $< -o $@
.PHONY:clean
clean:
rm -rf *.o *.d main
Demo2:.c 在src目录,.h在include目录,src、include、makefile同级
Demo1和Demo2的区别就是是否需要指定 -I选项,指明头文件的位置。
注意事项:$<,$@,$^
使用这两个变量的时候获取到的目标是带有相对路径的。
vpath %.h ./include
vpath %.c ./src
add.o:add.h
@echo $<
所以和demo1相比我们需要处理.d的生成方式。变化主要两点:
.d
的内容需要指定 -I
参数(通过sed
命令修改 -MM
生成的.d
内容).d
位置不会和.c
同目录,将会和makefile同目录(为什么不将.d
和.c
同目录是因为在make目录执行的时候 -I 参数为 ./include
,但是在src目录 -I参数为 ../include
,为了保持参数一致,所以将.d和make保持同级)%.d : %.c
rm -f $(@F); \
$(GXX) -MM $(INCLUDE) $< > $(@F).$$$$; \sed -e 's,^.*:,$*.o:,g' -e 's,$$,; $(GXX) -c $(CFLAGS) $(INCLUDE) $<,g' < $(@F).$$$$ > $(@F); \
rm -f $(@F).$$$$
$(@F),$(
$(@D),$(
eg:$@为./src/main.o
则$(@F)为main.o
,$(@D)为./src/
至于sed
的用法请百度,sed用法很灵活。所以最终的makefile内容为:
vpath %.c ./src
vpath %.h ./include
ifndef GXX
GXX := g++
endif
ifndef CFLAGS
CFLAGS := -g
endif
INCLUDE := -I ./include
src = $(wildcard ./src/*.c)
c_objs = $(notdir $(src))
objs = $(c_objs:.c=.o)
main:$(objs)
$(GXX) -o $@ $(CFLAGS) $^
-include $(objs:.o=.d)
%.d : %.c
rm -f $(@F); \
$(GXX) -MM $(INCLUDE) $< > $(@F).$$$$; \sed -e 's,^.*:,$*.o:,g' -e 's,$$,; $(GXX) -c $(CFLAGS) $(INCLUDE) $<,g' < $(@F).$$$$ > $(@F); \
rm -f $(@F).$$$$
.PHONY:clean
clean:
rm -rf *.o *.d main
以上内容参考博客:https://blog.csdn.net/shun01/article/details/22941067
编写Makefile注意事项:
对于动态依赖的文件(*.o)不要随便更改位置,否则容易导致看似依赖文件存在却找不到的错误
demo:将生成的.o文件放在bin目录,bin和src,include同级
1 vpath %.h ./include
2 vpath %.c ./src
3 vpath %.o ./bin
4
5 src = $(wildcard ./src/*.c)
6
7 src_file_name=$(notdir $(src))
8
9 o_obj = $(src_file_name:.c=.o)
10
11 main:$(o_obj)
12 gcc -o $@ $^
13
14 $(o_obj):%.o:%.c
15 gcc -c $< -o ./bin/$@ -I ./include
运行结果:第一次没有找到.o文件,导致编译失败。此时在执行一次,因为.o文件实际已经存在在bin目录,所以第二次成功了(此时大家肯定疑问,我明明指定了vpath的啊,为什么会失败呢)
为了弄清楚原因,我们把执行日志打印出来,看看两次有什么区别
main:$(o_obj)
@echo $^
gcc -o $@ $^
此时运行一下make,查看$(o_obj)
的输出(注意我们上文提到$<,$@,$^是相对路径的位置输出
),所以为什么make失败显而易见,因为$(o_obj)
是当前目录下的*.o
此时我们再次运行一次make,看看$(o_obj)
的输出,此时为什么make成功,显而易见,因为$(o_obj)
是./bin/
目录下的*.o
,这个目标文件我们在第一次make的时候已经生成了。
此时我们可能会有疑问为什么第一次没有在指定的vpath里面寻找*.o
呢?事实上是查找了的,只是没找到而已,在make的过程中我们需要知道:一旦文件位置被定位了,在整个make的执行过程中都不会改变
。下面我们来分析一下:make关于.o查找的执行过程。
第一次 make:
—>到$(o_obj)= add.o sub.o main.o
指定的位置查找即当前目录查找,没找到
—>到vpath指定的目录查找,没找到
—>最终没找到,所以$(o_obj)
,最终被定位的位置为当前目录
所以虽然后面动态生成了.o但是.o文件位置并不在当前目录,所以make失败
第二次 make:
—>到$(o_obj)= add.o sub.o main.o
指定的位置查找即当前目录查找,没找到
—>到vpath指定的目录查找,找到OK
—>所以$(o_obj)
,最终被定位的位置为./bin/
所以此时第二次make是成功的
为了验证上述make关于.o查找的执行过程是否正确,我们把./bin目录下的.o文件拷贝到make同级目录,此时.o文件将会存在两份,位置分别位于当前目录和./bin。此时我们make -n查看一下*.o的定位位置。
结果符合预期 $(o_obj)
指定位置优先,之后才是vpath位置。
所以需要解决上述看似存在却找不到的bug很简单,只需要将上述第15行代码
gcc -c $< -o ./bin/$@ -I ./include
修改为:
gcc -c $< -o $@ -I ./include
不要随便更改目标依赖文件的位置