在学习该篇文章之前,首先你需要有一定的makefile理论基础,如果不懂可以参考:Makefile教程(绝对经典,所有问题看这一篇足够了)建议看2遍以上。其中关于include可以参考Makefile 中的 include (依赖文件) 的执行过程。
在Linux C语言开发的时候我们需要自己手动编译新添加的文件。如果一个工程非常庞大这样将会是程序员的噩梦。使用
-MM
选项获取文件的依赖,并且自动推导文件依赖关系。
Demo1:以.c和.h在同一个目录来讲解
首先使用
cc -MM
分析当前目录下面所有的源文件的依赖关系,并且把结果生成到对应的.d文件中
#获取当前目录的所有.c文件
src := $(wildcard *.c)
#生成.c对应的.d目标
d_obj := $(src:.c=.d)
#最终目标,暂时什么都不做
main:$(d_obj)
#makefile 静态编译模式,$(d_obj)中所有xx.d的文件依赖对应的xx.c文件
$(d_obj):%.d:%.c
cc -MM $< -o $@
运行结果:可以看到对应的.d文件,以及.d文件的内容就是对应cpp的依赖项。

此时可以看见.d的文件内容就是我们需要的文件依赖关系,并且可以直接通过cc编译。所以如果我们需要自动编译生成主程序main,我们首先分需要析一下依赖关系:
- main 需要依赖 *.o
- *.o可以通过include *.d文件生成
- *.d通过
cc -MM
生成
所以推导的makefile文件内容如下:
#获取当前目录的所有.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
同目录,将会和makefile同目录(为什么不将.c
和.d
同目录是因为在make目录执行的时候 -I 参数为 .c
,但是在src目录 -I参数为 ./include
,为了保持参数一致,所以将.d和make保持同级)../include
%.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),$(<F)
:目标文件的文件名称
$(@D),$(<D)
:目标文件的相对目录位置
$*
:目标模式中"%"及其之前的部分,eg:test%.d 匹配的是 testmain.d那么
$*
表示testmain
sed -e 's,^.*:,$*.o:,g'
将‘,’替换成’/'就可以看出是一个s/parten1/paretn2/g的替换模式
所以上述语义 以main.d为例,则替换之后的
sed -e 's/^.*:/$*.o:/g'
正则表达
式为
sed -e 's/^.*:/main.o:/g'
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
编写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
不要随便更改目标依赖文件的位置
///
MakeFile的递归调用
#include 会在当前位置展开makefile文件内容,类似宏的展开
-include makefile
#$(MAKE) -C dir,会进入dir目录,然后执行make指令
#可以通过export 向下层传递变量
object:
$(MAKE) -C dir
假设目录结构如下:
--main.c
--add
--add.h
--add.c
--makefile
--makefile
add目录makefile内容如下:
#$(CFLAGS)由上层makefile通过export传递过来
add.o:add.c
g++ -o $@ -c $^ $(CFLAGS)
.PHONY:clean
clean:
-rm -rf *.o
main 目录下的makefile如下:
vpath %.h ./include ./add
vpath %.o ./add
CC = g++
#像下层makefile传递变量CFLAGS
export CFLAGS = -g
add_make=./add/makefile
main:main.o add.o
$(CC) -o $@ $^
main.o:main.c
$(CC) -o $@ -c $^ -I ./include -I ./add $(CFLAGS)
#-include $(add_make)
add.o:
$(MAKE) -C add
-cp add/$@ .
.PHONY:clean
clean:
-rm -rf *.o main core.*
$(MAKE) clean -C add
此时我们可以递归调用make clean,执行过程如下: