如何构造自己的Makefile

背景

相对于cmake、meson等高级构建工具,makefile有独特的优势,如广泛应用在linux平台,完全透明的编译过程,非常方便借助shell进行扩展。本文将针对中小型项目需求构造一套makefile模板,具备以下主要特点:

  • 支持单源码目录和 多源码目录编译、打包
  • 自动枚举子目录源码文件
  • 支持头文件依赖,头文件更新自动触发引用头文件的所有对象
  • 编译生成的中间文件定向到独立的目录

工程架构

本文以一个示例工程为对象设计makefile模组,工程目录架构如下:

.testProj
├── main
│ ├── inc
│ └── src
├── obj
├── submodule1
│ ├── inc
│ └── src
├── submodule2
│ ├── inc
│ └── src
├── inc.mk
├── Makefile
└── sub.mk

子模块编译(sub.mk)

​ 项目中通常存在多个模块,submk可将指定的模块编译、打包成独立的lib(.a),支持单个模块打包成独立的lib,也可以将多个子目录打包成一个统一的lib。

变量设置

include inc.mk  ## 
module_root = $(PWD)
src_dir = $(src_dir_in)
inc = $(addprefix -I, $(addsuffix /../inc, $(src_dir) ) )
output = $(output)
target = $(libname)
obj_root_dir = $(output)


# enumurate *.c from single/multi src dir 
all_srcs = $(foreach dir, $(subst :, , $(src_dir)), $(wildcard $(dir)/*.c ))

# generate obj in independent dir 
all_objs = $(subst $(module_root), $(module_root)/obj, $(subst .c,.o, $(all_srcs)) )

obj_path = $(dir $(all_objs))
mk_obj_path := $(shell $(mkdir) $(obj_path) )

编译规则

​ 一共三条主要规则,分别实现obj打包、源文件编译规则和头文件依赖(.d)生成。

​ 这是值得一提的是makefile灵活的模式匹配。通过情况下,如果源文件和obj文件都保存在同一目录中,规则非常简单使用%.o:%.c即可,%可根据依赖对象名自动匹配相同的源文件。本项目中编译结果与源码分离,在匹配规则上有些特殊。具体进行以下几 方面的特殊操作:

  1. 首先为每一个子模块创建独立的编译结果保存目录,mk_obj_path := $(shell $(mkdir) $(obj_path) )
  2. obj_root_dir为obj路径顶层目录.编译对象的依赖目标为 ( a l l o b j s ) , 该目标又依赖 ‘ (all_objs),该目标又依赖` (allobjs),该目标又依赖(obj_root_dir)/%.o : $(module_root)/%.c $(obj_root_dir)/%.d` 生成, 其中%自动匹配每一个子目录的相对路径名,如submodule1/src, submodule2/src.
# pack .o into lib.a
all: $(output)/$(target)
$(output)/$(target): $(all_objs)
	@echo "build sub module lib $@"
	@$(ar)  $@ $?

# generate include file dependency
$(obj_root_dir)/%.o : $(module_root)/%.c $(obj_root_dir)/%.d
	@echo "build file : $<"
	@set -e
	@$(cc) $(cflag) $(inc) -c $< -o $@


$(obj_root_dir)/%.d : $(module_root)/%.c
	@echo "Making header dependencies $@ "
	@set -e; $(rmdir) $@; 										\
	$(cc) -MM $(cflag) $(cppflag) $(inc) $< >$@.$$$$;			\
	sed 's,.*\.o:,$*.o $@: ,g' < $@.$$$$ > $@;					\
	$(rmdir) $@.$$$$		
	
-include $(all_objs:.o=.d)

clean:
	$(rmdir) $(output)/*

头文件依赖生成详解

​ 组成makefile规格的执行语句本质上shell脚本,为深扒其原理 ,可以通过单步执行观察每一步的输出结果

  1. set -e的作用是让makefile在执行指令时,出错就立即退出,不再执行后续指令。
  2. $(cc) -MM $(cflag) $(cppflag) $(inc) $< > $@.$$$$; 调用gcc生成头文件依赖,其中 < 、 < 、 <@分别为makefile规则的目标文件和依赖文件,> 是将gcc的输出结果重定义到 另外一个文件中,文件名为 依赖文件+shell进程号(使用$$$$获得)。最后生成的头文件依赖文件内容如下:
~/code/makefile/testProj$ cat  obj/submodule1/src/add.d.266165 	
add.o: /home/bonc/code/makefile/testProj/submodule1/src/add.c \
 /home/bonc/code/makefile/testProj/submodule1/src/../inc/add.h
  1. $sed 's, .*\.o: , $*.o $*.odbg $@: , g' < $@.$$$$ > $@; 作用是将add.d与加入到add.o的依赖对象集中,修改之后.d文件内容如下:
   submodule1/src/add.o /home/bonc/code/makefile/testProj/obj/submodule1/src/add.d:  /home/bonc/code/makefile/testProj/submodule1/src/add.c \
    /home/bonc/code/makefile/testProj/submodule1/src/../inc/add.h

这里有一点需要特殊说明,就是由于本项目为了将生成的.o统一放到obj目录下,规则中使用了相对路径,因此sed查找到的原字符串为“add.o”, 替换目标则为:“submodule1/src/add.o /home/bonc/code/makefile/testProj/obj/submodule1/src/add.d:“, 为add.o增加了相对路径 前缘“submodule1/src/“。

对于不带路径的情况,也可以使用如下规则:

#语句:
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@;
#展开:
sed 's,\(submodule1/src/add\)\.o[ :]*,\1.o /home/bonc/code/makefile/testProj/obj/submodule1/src/add.d : ,g' < /home/bonc/code/makefile/testProj/obj/submodule1/src/add.d.$$ > /home/bonc/code/makefile/testProj/obj/submodule1/src/add.d;
#结果
add.o /home/bonc/code/makefile/testProj/obj/submodule1/src/add.d:  /home/bonc/code/makefile/testProj/submodule1/src/add.c \
 /home/bonc/code/makefile/testProj/submodule1/src/../inc/add.h

本规则中sed语法详解参考(37条消息) Makefile中的$@, $^, $< , $?, $%, $+, $*_丹山起凤的博客-CSDN博客_makefile $@

主makefile

主makefile用于链接依赖库,并生成项目可执行程序,如果希望为不同子模块生成独立的lib, 则定义多个sub_moule变量,给sub.mk输入不同的sub_module_info即可。

include inc.mk  ## 
proj_root = $(PWD)

obj_dir = $(proj_root)/obj
mk_obj_dir := $(shell $(mkdir) $(obj_dir) )

target = hello
deplib = libsub.a
lib_dep = $(obj_dir)/$(deplib)

sub_module_info=						\
	'output=$(obj_dir)'					\
	'src_dir_in=						\
		$(proj_root)/main/src 			\
		$(proj_root)/submodule1/src 	\
		$(proj_root)/submodule2/src '	\
	'libname = $(deplib)'


#$(info "sub_module_info:" $(sub_module_info))

all: sub_moudle
	@echo "Build executable target: $(target)"
	@$(cc) -o $(target) $(lib_dep)

sub_moudle:
	@make $(sub_module_info) -f sub.mk
	
clean:
	$(rmdir) $(objs) $(target) $(obj_dir)

公共变量(inc.mk)

mkdir = mkdir -p 
rmdir = rm -rf
cc = gcc 
ar = ar rv
cflag = -g -O2
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月光技术杂谈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值