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

在学习该篇文章之前,首先你需要有一定的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,我们首先分需要析一下依赖关系:

  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同级
在这里插入图片描述

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

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

在这里插入图片描述
所以和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

在这里插入图片描述
以上内容参考博客: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

不要随便更改目标依赖文件的位置

///

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,执行过程如下:
在这里插入图片描述

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

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值