这是一个参考 Linux 内核 Makefile 思路整理的通用 Makefile 模板,只要简单配置就能直接用到项目里。下面先看它解决了哪些问题。
何为通用 Makefile?
一个通用的 Makefile 应具备这些能力:
- 支持多个目录、多层目录、多个文件;
- 支持给所有文件设置编译选项;
- 支持给某个目录设置编译选项;
- 支持给某个文件单独设置编译选项;
- 简单、好用。
通用 Makefile 的设计思想
首先在 Makefile 文件中确定要编译的文件、目录。比如要编译当前目录下的 main.c 以及 a 目录下的某些文件,只需要在对应 Makefile 中添加:
obj-y += main.o
obj-y += a/
然后由 Makefile.build 设置编译规则。Makefile 文件总是被 Makefile.build 包含,所以主要解决三个问题:
- 怎么编译子目录?进入子目录编译:
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
- 怎么编译当前目录中的文件?
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
- 当前目录下的
.o 和子目录下的 built-in.o 要打包起来:
built-in.o : $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^
最后在顶层 Makefile 中把顶层目录的 built-in.o 链接成 APP:
$(TARGET) : built-in.o
$(CC) $(LDFLAGS) -o $(TARGET) built-in.o
这里的 $@、$<、$^ 是 make 自动变量,注意不要和 shell 变量混淆。
通用 Makefile 设计说明
本模板中的 Makefile 文件分为三类:
- 顶层目录的 Makefile
- 顶层目录的 Makefile.build
- 各级子目录的 Makefile
各级子目录的 Makefile
它最简单,形式如下:
EXTRA_CFLAGS :=
CFLAGS_file.o :=
obj-y += file.o
obj-y += subdir/
obj-y += file.o:把当前目录下的 file.c 编进程序;
obj-y += subdir/:进入 subdir 子目录寻找文件编进程序,具体文件由 subdir 目录下的 Makefile 决定;
EXTRA_CFLAGS:给当前目录下所有文件(不含子目录)设置额外编译选项,可以不设置;
CFLAGS_xxx.o:给当前目录下的 xxx.c 设置自己的编译选项,同样可以不设置。
注意:
subdir/ 中的斜杠 / 不可省略;
- 顶层 Makefile 中的
CFLAGS 在编译任意一个 .c 文件时都会使用;
CFLAGS、EXTRA_CFLAGS、CFLAGS_xxx.o 三者共同组成 xxx.c 的编译选项。
顶层目录的 Makefile
除了通过 obj-y 指定根目录下要编进程序的文件、子目录外,顶层 Makefile 主要定义工具链前缀 CROSS_COMPILE、编译参数 CFLAGS、链接参数 LDFLAGS,这些参数就是文件中用 export 导出的各变量。
CROSS_COMPILE =
AS = $(CROSS_COMPILE)as
LD = $(CROSS_COMPILE)ld
CC = $(CROSS_COMPILE)gcc
CPP = $(CC) -E
AR = $(CROSS_COMPILE)ar
NM = $(CROSS_COMPILE)nm
STRIP = $(CROSS_COMPILE)strip
OBJCOPY = $(CROSS_COMPILE)objcopy
OBJDUMP = $(CROSS_COMPILE)objdump
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP
CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include
LDFLAGS :=
export CFLAGS LDFLAGS
TOPDIR := $(shell pwd)
export TOPDIR
TARGET := test
obj-y += main.o
obj-y += sub.o
obj-y += a/
all : start_recursive_build $(TARGET)
@echo $(TARGET) has been built!
start_recursive_build:
make -C ./ -f $(TOPDIR)/Makefile.build
$(TARGET) : start_recursive_build
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)
clean:
rm -f $(shell find -name "*.o")
rm -f $(TARGET)
distclean:
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)
顶层目录的 Makefile.build
这是最复杂的部分,它的功能就是把某个目录及它的所有子目录中、需要编进程序去的文件都编译出来,打包为built-in.o。
PHONY := __build
__build:
obj-y :=
subdir-y :=
EXTRA_CFLAGS :=
include Makefile
# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y)) : c/ d/
# __subdir-y : c d
# subdir-y : c d
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y += $(__subdir-y)
# c/built-in.o d/built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)
# a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
dep_files := $(foreach f,$(cur_objs),.$(f).d)
dep_files := $(wildcard $(dep_files))
ifneq ($(dep_files),)
include $(dep_files)
endif
PHONY += $(subdir-y)
__build : $(subdir-y) built-in.o
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
built-in.o : $(subdir-y) $(cur_objs)
$(LD) -r -o $@ $(cur_objs) $(subdir_objs)
dep_file = .$@.d
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
.PHONY : $(PHONY)
怎么使用这套 Makefile
4.1 放入顶层文件
把顶层 Makefile 和 Makefile.build 文件放入项目工程的顶层目录,然后在各自子目录下分别创建一个空白的 Makefile 文件。
4.2 确定编译哪些源文件
修改顶层目录和各自子目录 Makefile 的 obj-y:
obj-y += xxx.o
obj-y += yyy/
obj-y += xxx.o 表示要编译当前目录下的 xxx.c;obj-y += yyy/ 表示要编译当前目录下的 yyy 子目录。
4.3 确定编译选项、链接选项
- 修改顶层目录 Makefile 的
CFLAGS 和 LDFLAGS:CFLAGS 是编译所有 .c 文件时都要用的编译选项;LDFLAGS 是链接最后应用程序时的链接选项。
- 修改各自子目录下的 Makefile:
EXTRA_CFLAGS 给当前目录下所有文件(不含子目录)设置额外编译选项,可以不设置;CFLAGS_xxx.o 给当前目录下的 xxx.c 设置它自己的编译选项,可以不设置。
4.4 使用哪个编译器
修改顶层目录 Makefile 的 CROSS_COMPILE,用来指定工具链的前缀,比如 arm-linux-。
4.5 确定应用程序的名字
修改顶层目录 Makefile 的 TARGET,用来指定编译出来的程序名字。
4.6 编译或清除
执行 make 编译,执行 make clean 清除,执行 make distclean 彻底清除。
这套模板最核心的思路是:通过 obj-y 声明目标,再由 Makefile.build 递归处理子目录,最后把各层 built-in.o 链接成最终程序。把它放到嵌入式项目里,能省去不少重复的 Makefile 工作。