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,主要有这几点作用:

  1. 提供项目make命令执行入口,提供所有编译的目标;
  2. 定义全局变量、项目编译选项、链接选项;
  3. 通过obj-y指定要搜索的子目录;
  4. 切换目录,递归执行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,主要工作是:

  1. 包含(include)每个子目录下的Makefile文件;
  2. 取出每个子Makefile中定义的.o文件,再根据%.o:%.c 模式规则,自动寻找.c源码文件;
  3. 取出每个子Makefile中定义的子目录,再用make -C命令切换到子目录,从而实现递归目录编译;
  4. 为每个.o文件,生成依赖文件(.d),并包含进Makefile.build;
  5. 为每个子目录(含有Makefile)生成一个built-in.o文件,便于根目录下的Makefile文件编译、链接;
  6. 设置伪目标;
# 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开发板》

posted @ 2022-08-27 23:33  明明1109  阅读(1698)  评论(0编辑  收藏  举报