在学习该篇文章之前,首先你需要有一定的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的依赖项。

makefile 自动推导编译和注意事项_makefile


此时可以看见.d的文件内容就是我们需要的文件依赖关系,并且可以直接通过cc编译。所以如果我们需要自动编译生成主程序main,我们首先分需要析一下依赖关系:

  1. main 需要依赖 *.o
  2. *.o可以通过include *.d文件生成
  3. *.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同级

makefile 自动推导编译和注意事项_sed_02

Demo1和Demo2的区别就是是否需要指定 -I选项,指明头文件的位置。
注意事项:​$<,$@,$^​​使用这两个变量的时候获取到的目标是带有相对路径的。

vpath %.h ./include
vpath %.c ./src
add.o:add.h
@echo $<

makefile 自动推导编译和注意事项_头文件依赖_03


所以和demo1相比我们需要处理.d的生成方式。变化主要两点:

  1. ​.d​​​的内容需要指定 ​​-I​​​参数(通过​​sed​​​命令修改 ​​-MM​​​ 生成的​​.d​​内容)
  2. ​.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),$(<F)​​​:目标文件的文件名称
​​​$(@D),$(<D)​​​:目标文件的相对目录位置
​​​$*​​​:目标模式中"%"及其之前的部分,eg:test%.d 匹配的是 testmain.d那么​​$*​​表示testmain

​sed -e 's,^.*:,$*.o:,g'​​​ 将‘,’替换成’/'就可以看出是一个s/parten1/paretn2/g的替换模式
​sed -e 's/^.*:/$*.o:/g'​​ 所以上述语义 以main.d为例,则替换之后的​​正则表达​​式为
​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 自动推导编译和注意事项_sed_04


编写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的啊,为什么会失败呢)

makefile 自动推导编译和注意事项_sed_05


为了弄清楚原因,我们把执行日志打印出来,看看两次有什么区别

main:$(o_obj)
@echo $^
gcc -o $@ $^

此时运行一下make,查看​​$(o_obj)​​​的输出(​​注意我们上文提到$<,$@,$^是相对路径的位置输出​​​),所以为什么make失败显而易见,因为​​$(o_obj)​​​是当前目录下的​​*.o​

makefile 自动推导编译和注意事项_头文件依赖_06


此时我们再次运行一次make,看看​​$(o_obj)​​的输出,此时为什么make成功,显而易见,因为​​$(o_obj)​​是​​./bin/​​目录下的​​*.o​​,这个目标文件我们在第一次make的时候已经生成了。

makefile 自动推导编译和注意事项_当前目录_07


此时我们可能会有疑问为什么第一次没有在指定的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的定位位置。

makefile 自动推导编译和注意事项_makefile_08


结果符合预期 ​​$(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,执行过程如下:

makefile 自动推导编译和注意事项_自动编译_09

此时不能使用include,因为include会展开makefile,此时会出现两个clean伪目标,会报错。include执行环境是以main目录下的makefile环境为主