背景
相对于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
即可,%可根据依赖对象名自动匹配相同的源文件。本项目中编译结果与源码分离,在匹配规则上有些特殊。具体进行以下几 方面的特殊操作:
- 首先为每一个子模块创建独立的编译结果保存目录,
mk_obj_path := $(shell $(mkdir) $(obj_path) )
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脚本,为深扒其原理 ,可以通过单步执行观察每一步的输出结果
- set -e的作用是让makefile在执行指令时,出错就立即退出,不再执行后续指令。
$(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
$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