Makefile笔记 韦东山通用Makefile解析
Makefile基础
Makefile规则与示例
简单的Makefile文件
一个简单的Makefile文件包含的一系列“规则”:
目标(target) ... : 依赖(prerequiries) ...
<tab>命令(command)
如果“依赖文件”比“目标文件”更加新,那么执行“命令”来重新生成“目标文件”。
命令被执行的2个条件:依赖文件比目标文件新,或目标文件还没生成。
2个重要的函数
A. $(foreach var,list,text)
含义是for each var in list, change it to text。
对于list中每个元素,取出来赋值给var,然后把var用text替换。
例如:
objs := a.o b.o
dep_files := $(foreach f, $(objs), .$(f).d) // 最终dep_files := .a.o.d .b.o.d
B. $(wildcard pattern)
pattern 所列出的文件是否存在,把存在的文件都列出来。
例如:
src_files := $(wildcard *.c) // src_files中列出了当前目录下的所有.c文件
一步步完善Makefile
第1个Makefile,简单,低效:
test: main.c sub.c sub.h
gcc -o test main.c sub.c
第2个Makefile,效率高,但相似规则太多太啰嗦,不支持检测头文件:
test: main.o sub.o
gcc -o test main.o sub.o
main.o: main.c
gcc -c -o main.o main.c
sub.o: sub.c
gcc -c -o sub.o sub.c
clean:
rm *.o test -f
第3个Makefile,效率高,精炼,不支持检测头文件:
test : main.o sub.o
gcc -o test main.o sub.o
%.o : %.c
gcc -c -o $@ $<
clean:
rm *.o test -f
注:Makefile变量(特殊变量)
\(* 不包含扩展名的文件名;
\)+ 所有的依赖文件,以空格分开,以出现的先后顺序,可能包含重复的依赖文件;
\(< 第一个依赖文件的名称;
\)? 所有时间戳比目标文件晚的依赖文件,并以空格分开;
\(@ 目标文件的完整名称;
\)^ 所有不重复的依赖文件,以空格分开;
$% 如果目标是归档成员,则该变量表示目标的归档成员名称;
第4个Makefile,效率高,精炼,支持检测头文件,但需要手工添加头文件规则:
test : main.o sub.o
gcc -o test main.o sub.o
%.o : %.c
gcc -c -o $@ $<
sub.o : sub.h
clean:
rm *.o test -f
第5个Makefile,效率高,精炼,支持自动检测头文件:
objs := main.o sub.o
test : $(objs)
gcc -o test $^
# 需要判断是否存在依赖文件
# .main.o.d .sub.o.d
dep_files := $(foreach f, $(objs), .$(f).d)
dep_files := $(wildcard $(dep_files))
# 把依赖文件包含进来
ifneq ($(dep_files),)
include $(dep_files)
endif
%.o : %.c
# 生成依赖文件$@.d
gcc -Wp,-MD,.$@.d -c -o $@ $<
clean:
rm *.o test -f
distclean:
rm $(dep_files) *.o test -f
注:
ifneq(ARG1,ARG2) 判断参数是否不相等;
include 类似于C语言的#include,将内容原封不动包含进来;
GCC命令加"-Wp,-MD,\(@.d"会自动生成依赖文件\)@.d;
通用Makefile
可用来编译应用程序,其特点:
1)支持多个目录、多层目录、多个文件;
2)支持给所有文件设置编译选项;
3)支持给某个目录设置编译选项;
4)支持给某个文件单独设置编译选项;
5)简单、易用;
零星知识点
A. make命令的使用
执行make命令时,会去当前目录下查找名为"Makefile"的文件,并根据它的指示执行操作,生成第一个目标。
可以使用"-f"选项指定要查找并执行的文件,而不再使用名为"Makefile"的文件,例如:
make -f Makefile.build
可以使用"-C"选项指定目录,切换到其他目录里去,例如:
make -C a/ -f Makefile.build
注:切换到目录"a/",指定查找文件Makefile.build
可以指定目标,不再默认生成第一个目标:
make -C a/ -f Makefile.build other_target
B. 立即变量、延时变量
变量定义的语法形式:
A = xxx // 延时变量
B ?= xxx // 延时变量,只有第一个定义时赋值才成功;如果曾定义过,此赋值无效
C := xxx // 立即变量
D += yyy // 如果D在前面是延时变量,那么现在还是延时变量;
// 如果D在前面是立即变量,那么现在还是立即变量
延时变量的值,在使用时才展开、确定。例如:
A = $@ # 目标文件完整名称
test: # 此时才定义目标文件名称
@echo $A
上面变量A在执行时才确定,值为test,是延时变量。
如果用"A := \(@",A是立即变量,而此时\)@为空,因此A值为空。
C. 变量的导出(export)
编译程序时,我们不断使用"make -C dir"切换到其他目录,执行其他目录的Makefile。如果想要某个变量值在所有目录都可见,需要将其export出来。
例如,"CC = $(CROSS_COMPIE)gcc",CC变量表示编译器,在整个makefile执行过程中都是一样的。定义它后,要用"export CC"将其导出。
D. Makefile中可以使用shell命令
例如:
TOPDIR := $(shell pwd)
立即变量TOPDIR等于shell命令pwd的执行结果,即当前目录(通常是执行make命令的目录)。
E. 在Makefile中怎么放置第1个目标
执行make命令时,如果不指定目标,那么它默认生成第1个目标。所以“第一个目标”位置很重要。有时不太方便将第1个目标完整地放在文件前面,此时可以做文件的前面直接放置目标,在后面再完善它的依赖和命令。
例如:
first_target: // 这句话放前面
... // 其他代码,比如include其他文件后得到后面的xxx变量
first_target: $(xxx) $(yyy) // 在文件的后面再来完善
command
F. 假想目标(伪目标)
如果Makefile中有这样的目标:
clean:
rm -f $(shell find -name "*.o")
rm -f $(TARGET)
如果当前目录下,恰好有名为"clean"的文件,那么执行"make clean"时就不会执行那些删除命令。
此时,我们需要把"clean"这个目标,设置为"假想目标",这样可以确保执行"make clean"时执行删除命令。
使用下面语句,将"clean"设置为假想目标:
.PHONY : clean
G. 常用的函数
1)foreach
用法:$(foreach var, list,text)
解释:for each var in list, change it to text
对于list中每个元素,取出来赋值给var,然后将var改为text所描述的形式
例如:
objs := a.o b.o
dep_files := $(foreach f, $(objs), .$(f).d) // 最终dep_files := .a.o.d .b.o.d
2)wildcard
用法:$(wildcard pattern)
解释:pattern所列出文件如果存在,就把存在的文件都列出来
例如:
src_files := $(wildcard *.c) // 最终src_files中列出了当前目录下所有.c文件
3) filter
用法:$(filter pattern...,text)
解释:把text中符合pattern格式的内容,filter(过滤)出来以留下来。
例如:
obj-y := a.o b.o c/ d/
DIR := $(filter %/, $(obj-y)) // 结果为:c/ d/
4)filter-out
用法:$(filter-out pattern..., text)
解释:把text中复合pattern格式的内容,filter-out(过滤)出来以丢弃。
例如:
obj-y := a.o b.o c/ d/
DIR := $(filter-out %/, $(obj-y)) // 结果为:a.o b.o
5)subst
用法:$(subst from,to,text)
解释:将text中的东西,从from替换为to
$(subst a,the,There is a big tree) // 结果为:There is the big tree
6)patsubst
用法:$(patsubst pattern, replacement, text)
解释:寻找"text"中符合格式"pattern"的字,用"replacement"替换它们。"pattern"和"replacement"中可以使用通配符。
例如:
subdir-y := c/ d/
subdir-y := $(patsubst %/, %, $(subdir-y)) // 结果为:c d
设计思想
A. 在Makefile文件中确定要编译的文件、目录,比如obj-y += main.oobj-y += a/"Makefile"文件总是被"Makefile.build"包含的。
B. 在Makefile.build中设置编译规则,有3条:
1)怎么编译子目录?进入子目录编译:
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
2)怎么编译带你过去目录中的文件?
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CLAGS) $(FLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
3)当前目录下的.o和子目录下的built-in.o要打包起来:
built-in.o: $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^
C. 顶层Makefile中把顶层目录的built-in.o链接成APP
$(TARGET) : built-in.o
$(CC) $(LDFLAGS) -o $(TARGET) built-in.o
通用Makefile源码解析
韦东山通用Makefile,是韦东山(百问网)为嵌入式Linux应用项目编写的工程构建makefile。支持多目录、单目标的项目。
目录结构
根目录下,存在Makefile和Makefile.build两个文件。这两个文件非常重要,make命令能递归查找每个子目录,就是这2个Makefile文件的功劳。
在每个需要搜索源码的子目录下,都要添加一个子Makefile文件,便于递归搜索。
项目目录结构:
$ tree
.
├── bin
│ └── led.sh
├── business
│ ├── main.c
│ └── Makefile
├── config
│ ├── config.c
│ └── Makefile
├── display
│ ├── disp_manager.c
│ ├── framebuffer.c
│ └── Makefile
├── font
│ ├── font_manager.c
│ ├── freetype.c
│ └── Makefile
├── include
│ ├── common.h
│ ├── config.h
│ ├── disp_manager.h
...
├── Makefile
├── Makefile.build
├── page
│ ├── main_page.c
│ ├── Makefile
│ └── page_manager.c
├── ui
│ ├── button.c
│ └── Makefile
└── unittest
├── client.c
├── disp_test.c
...
通用Makefile源码
根目录下的Makefile,主要有这几点作用:
- 提供项目make命令执行入口,提供所有编译的目标;
- 定义全局变量、项目编译选项、链接选项;
- 通过obj-y指定要搜索的子目录;
- 切换目录,递归执行make命令,并执行根目录下的Makefile.build文件;
# 根目录下的Makefile
# 延时变量, 只有第一次定义赋值才成功.而该变量在/etc/profile中. 已定义为arm-linux-gnueabihf-
CROSS_COMPILE ?=
# 定义延时变量
# e.g. as = arm-linux-gnueabihf-as
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全局变量, 可供其他Makefile使用
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP
# 定义编译选项
CFLAGS := -Wall -O2 -g # 立即变量
# -I 指定头文件目录
# $(shell pwd) 将shell命令pwd输出(即当前目录)作为结果
# 该句含义是为编译器指定头文件目录为当前目录(执行make命令的目录)下的include目录
CFLAGS += -I $(shell pwd)/include # 追加赋值
# 定义链接选项
LDFLAGS := -lts -lpthread -lfreetype -lm
# export 全局变量
export CFLAGS LDFLAGS
# 当前目录作为顶层目录TOPDIR
TOPDIR := $(shell pwd)
export TOPDIR
# 定义立即变量, 目标名称, 也是最终生成的二进制目标文件名称
TARGET := test
# 定义变量记录要搜索的子目录(子目录必须包含一个makefile文件)
# 注意: 由于一个目标只能包含一个main函数,因此unittest目录和business目录,只能加入一个
obj-y += display/
# obj-y += unittest/
obj-y += input/
obj-y += font/
obj-y += ui/
obj-y += page/
obj-y += config/
obj-y += business/
# 第一个目标
all : start_recursive_build $(TARGET)
@echo $(TARGET) has been built!
start_recursive_build:
# 切换到目录 $(TOPDIR), 找Makefile.build文件, 并执行make命令
# 我们查看Makefile.build文件
@echo start_recursive_build
@echo obj-y = $(obj-y)
make -C ./ -f $(TOPDIR)/Makefile.build
# 依赖built-in.o 由Makefile.build生成
$(TARGET) : built-in.o
$(CC) -o $(TARGET) built-in.o $(LDFLAGS)
clean:
rm -f $(shell find -name "*.o")
rm -f $(TARGET)
rm -f test
distclean:
rm -f $(shell find -name "*.o")
rm -f $(shell find -name "*.d")
rm -f $(TARGET)
rm -f test
根目录Makefile.build,主要工作是:
- 包含(include)每个子目录下的Makefile文件;
- 取出每个子Makefile中定义的.o文件,再根据%.o:%.c 模式规则,自动寻找.c源码文件;
- 取出每个子Makefile中定义的子目录,再用make -C命令切换到子目录,从而实现递归目录编译;
- 为每个.o文件,生成依赖文件(.d),并包含进Makefile.build;
- 为每个子目录(含有Makefile)生成一个built-in.o文件,便于根目录下的Makefile文件编译、链接;
- 设置伪目标;
# Makefile.build
# 立即变量, 用于记录伪目标
PHONY := __build
# 先声明目标, 等到文件后面完善
__build:
# 定义立即变量, 值为空
obj-y :=
subdir-y :=
EXTRA_CFLAGS :=
# include当前目录下的Makefile, 注意执行命令时, 是会通过make -C命令切换工作目录的
# 也就是说, 不同make命令执行路径下, Makefile指的是不同的文件
include Makefile
# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y)) : c/ d/
# __subdir-y : c d
# subdir-y : c d
# 定义立即变量
# $(filter %/, $(obj-y)) 从变量obj-y中过滤出以"/"结尾的目录名
# $(patsubst %/,%,$(filter %/, $(obj-y))) 去掉obj-y中以"/"结尾的目录名中的"/"
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
# 追加赋值, subdir-y表示要子目录
subdir-y += $(__subdir-y)
# c/built-in.o d/built-in.o
# foreach(var,list,text), 意为foreach var in list, change it to text
# 将子目录列表subdir-y中, 每一项(每个文件名)f, 都修改为f/built-in.o
# 也就是说, 每个子目录, 都会对应生成一个名为 "子目录名/built-in.o"的文件 (.o文件是链接文件)
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)
# a.o b.o
# 定义立即变量, 从obj-y中过滤掉目录名(名称以"/"结尾), 只剩下普通文件(.o文件)
cur_objs := $(filter-out %/, $(obj-y))
# 将cur_objs中的每个文件名f, 替换为f.d, 即加上".d"后缀, 代表该源码文件的依赖文件
dep_files := $(foreach f,$(cur_objs),.$(f).d)
# 如果依赖文件存在, 就列出来重新赋值给dep_files
dep_files := $(wildcard $(dep_files))
# 如果依赖文件列表不为空, 就直接包含(include)依赖文件列表
ifneq ($(dep_files),)
include $(dep_files)
endif
# 每个子目录名(不含"/")追加到伪目标
PHONY += $(subdir-y)
# 定义规则 (目标 : 依赖)
__build : $(subdir-y) built-in.o
# 以子目录每一项为目标, 而每一项都是一个目录名
$(subdir-y):
@echo subdir-y = $@
# 进入到每个子目录($@代表目标名称), 查找并执行顶层目录的Makefile.build
make -C $@ -f $(TOPDIR)/Makefile.build
# 定义built-in.o依赖规则
# cur_objs 从obj-y过滤出的普通文件(.o文件)
# subdir_objs 子目录下的built-in.o
built-in.o : $(cur_objs) $(subdir_objs)
# LD 代表交叉编译器的链接器; -r 选项代表可重定位的输出, 一个输出文件可再次作为ld输入
# -o 设置输出文件; $@ 目标名称; $^ 所有不重复的依赖文件
$(LD) -r -o $@ $^
# 定义延时变量, 单个目标的依赖文件: .目标名称.d
dep_file = .$@.d
# 模式规则, 所有没有显式规则的%.o目标, 会匹配该隐含规则
# 实际不一定会调用
%.o : %.c
# 生成依赖文件dep_file (即.$@.d), 后面Makefile文件会用
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
# 定义伪目标
.PHONY : $(PHONY)
子目录makefile。比如font/ 子目录,如果根目录下的Makefile添加将"font/"添加进了obj-y,那么子目录必须包含一个子makefile。
EXTRA_CFLAGS :=
CFLAGS_file.o :=
obj-y += font.o
源码参见:electronic_test_tools | gitee
参考
韦东山《嵌入式Linux应用开发完全手册V4.0-IMX6ULLa开发板》