# 引入 Makefile 的原因

在使用 各种各样的 IDE 的时候往往只需要点击一个编译按键就可以实现编译

但如果使用自组的编译链,编译一个文件就需要执行一条下面这个语句

gcc -o c c.c

但是,嵌入式开发往往有大量的文件,总不能一个个文件罗列
而且大量的文件不仅容易少写,而且会占用大量的编译时间,每次每个文件都要重新编译

因此 一套用于 规定 如何编译的规则 ——Makefile 就应运而生啦

# 一个 C 语言例子

b.c

#include <stdio.h>
int main(void)
{
  call_b();
  printf("CC\r\n");
}

c.c

#include <stdio.h>
void call_b(void)
{
	printf("BBB\n");
}

对于这个程序,编译文件需要执行下面的语句,就可以实现编译
正常来说 .c 程序 ==> 得到可执行程序 它们之间要经过四个步骤:

  • 1. 预处理
  • 2. 编译
  • 3. 汇编
  • 4. 链接

事实上我们常把前三个步骤称为编译,各种.o 文件的拼接称为链接
在 gcc 中相关命令为
编译

gcc -o c.o c.c
gcc -o b.o b.c

链接

gcc -o mian c.o b.o

# Makfile 的基本规则

# 如何减少编译时间

上面说到 编译多个文件的时候,可能只修改了一个文件,这样可以大大的减少编译时间

那么问题来了,Makefile 是如何知道哪些文件被更新了呢?

其实很简单,因为每次编译都会生成 .o 文件,将.o 文件和 相对应的.c 文件进行时间比较,如果.c 更新则编译.
所有的.o 都和最终的二进制文件比较,要是有更新的.o 就产生链接

# 基本语法

# 规则

Makfile 最基本的语法是规则,规则:

目标 : 依赖1 依赖2 ...
[TAB]命令

当依赖比目标新的时候,执行目标下面对应的依赖命令,并且 所有目标都可以单独执行

例如,上面的 C 语言例子,编译 过程写成 Makefile

# 当 main.o 或 func.o 有一个或多个比 test 新的时候会执 当前目标下面的语句
test: main.o func.o
	gcc -o test main.o func.o
# 当 main.c 比 main.o 新
main.o: main.c
	gcc -c -o main.o main.c
# func.c 比 func.o 则更新 func.o
func.o: func.c
	gcc -c -o func.o func.c

执行后会在终端显示下面的内容,当然文件名却决于自己写的文件

$ make
gcc -c -o main.o main.c
gcc -c -o func.o func.c
gcc -o test main.o func.o

当我们修改 main.c 文件时,再次执行 make, 就会发现只有 第一和第二个规则被执行了

$ make
gcc -c -o main.o main.c
gcc -o test main.o func.o

如果同时修改 main.cfunc.c 就会编译两个文件,终端和一开始的界面类似

当执行 make 命令时,make 会优先查找当前目录下名为 Makefile 的文件,然后根据规则来执行判断和命令

# 通配符

在上面我们只有两个文件,所以我们的目标所需的依赖可以手动列出.
但这十分不合理,假如一个目标文件所依赖的依赖文件很多,那样岂不是要写很多规则

Makefile 提供了通配符的语法,来解决这个问题,下面时常见的的

符号作用
%.o表示所用的.o 文件
%.c表示所有的.c 文件
$@表示目标
$<表示第 1 个依赖文件
$^表示所有依赖文件
$?表示比目标还要新的依赖文件列表

运用上面的语法来改写一下上面的 Makefile

在此之前先添加一个 fun_b.c 的文件

#include <stdio.h>
void fun_b(void)
{
	printf("This is fun_b\r\n");
}

接下来对 Makefile 进行改造

# 当 main.o 或 func.o 有一个或多个比 test 新的时候会执 当前目标下面的语句
test: main.o func.o fun_b.o
	gcc -o test $^
# 当任一 .c 比 对应的 .o 新 就更新 
%.o: %.c
	gcc -c -o $@ $<

执行后终端结果为

$ make
gcc -c -o main.o main.c
gcc -c -o func.o func.c
gcc -c -o fun_b.o fun_b.c
gcc -o test main.o func.o fun_b.o

# 假想目标 .PHONY

当我们编译之后会产生大量的 .o 文件,可以使用下面这条命令进行删除

rm -rf *.o

但是每次都输入还要输入一次,而且可能还会有其它后缀的文件,有可能误删或者少删

我们试想一下,Makefile 是通过查询目标文件是否存在来判断是否要执行生成依赖的命令
并且,Makfile 本质也是将命令发到 bash 中执行,那么可以也可以发送 rm -rf *.o 指令
那么如果 定义一个目标,但是对应的生成依赖的命令行不生成依赖文件,就可以反复调用

# 当 main.o 或 func.o 有一个或多个比 test 新的时候会执 当前目标下面的语句
test: main.o func.o fun_b.o
	gcc -o test $^
# 当任一 .c 比 对应的 .o 新 就更新 
%.o: %.c
	gcc -c -o $@ $<
clean:
	rm -rf *.o *.exe

执行某个目标需要使用 make [目标] 来实现,如果添加目标,默认为第一个目标
当我们执行 make clean 时,makefile 检测不到 clean 文件,然后就会执行对应的删除语句

但是这个逻辑还存在一个问题。如果,文件夹下存在名为 clean 的文件,那么 make clean 将无法顺利执行
我们在该目录下创建一个名为 “clean” 的文件,然后重新执行: make 然后 make clean
结果 (会有下面的提示):

make: 'clean' is up to date.

其实可以理解,为什么存在 clean 文件的时候会执行失败,当文件存在并且没有依赖文件,Makfile 就没有办法判断文件的新旧
要解决这个问题就需要使用到 .PHONY 关键字,表示这是一个假象目标,不会去对比和检测文件而是直接执行

.PHONY: clean # 把 clean 定义为假象目标.
# 就不会判断名为 “clean” 的文件是否存在

# 关键字

# vpath

作用:用于定义 make 的查找路径.

make 的查找路径默认为当前路径,而在我们的工程中通常会把原文件放置于不同的目录下.
这使得如果要引用某个文件,我们就需要连同文件的目录一块给出,这就很科学.
例如,我们把某个文件移动到了其他的目录下,我们就需要去修改 makefile
关键字 vpath 就可以实现这个功能

# 语法

1. vpath <pattern> <directories> 为符合 pattern 的文件指定 directories 的搜索路径;
2. vpath <pattern> 清除符合 pattern 文件的搜索路径;
3. vpath 清除所有已设置好的搜索路径。

# 变量

Makefile 是一种脚本语言,所以必然就会有变量

Makefile 变量分为两种 即时变量和延时变量

# 即时变量

语法: A := xxx

对于即使变量使用 := 表示,它的值在定义的时候已经被确定了

# 延时变量

语法: B = xxx

对于延时变量使用 = 表示。它只有在使用到的时候才确定,在定义 / 等于时并没有确定下来

想使用变量的时候使用 $ 来引用,如果不想看到命令是,可以在命令的前面加上 @ 符号,就不会显示命令本身.

当我们执行 make 命令的时候,make 这个指令本身,会把整个 Makefile 读进去,进行全部分析,然后解析里面的变量.

# 常用变量

常用的变量的定义如下

关键字作用
:=即时变量
=延时变量
+=附加,它是即时变量还是延时变量取决于前面的定义
?=:如果这个变量在前面已经被定义了,这句话就会不会起效果,

# 例如

A := $(C)
B = $(C)
C = abc
D = D
D ?= DD
all:
	@echo A = $(A)
	@echo B = $(B)
	@echo D = $(D)
C += 123

让我们分析一下相关的逻辑:

首先 A 为即时变量,在定义期间确定,开始时 C 值为 空,所以 A 值必为空

B 为延时变量,只有当使用到时它才会被确定.
在这个文件中,当执行 make 时,会解析 Makefile 里面的所用变量,首先会解析到 C=abc , 然后再解析到 C+=123
此时 C=abc 123 , 当执行到 @echo B = $(B) 时, B 的值为 abc 123

D ?= DD , D 变量在前面定义了 D=D , 所以 D 的值为 D
如果再前面去掉 D=D 这句话,那么 D 的值最后为 DD

使用 make 指令,执行后命令行终端显示如下

$ make
A =
B = abc 123
D = D

# 函数

Makefile 内置很多函数,使用 $ 来引用函数,接下去介绍几个常用的函数

# foreach

语法:

$(foreach var,list,text)

前两个参数, varlist , 将首先扩展,注意最后一个参数 text 此时不扩展
接着,对每一个 list 扩展产生的字,将用来为 var 扩展后命名的变量赋值
然后 text 引用该变量扩展;因此它每次扩展都不相同。结果是由空格隔开的 text

# 示例
filenames = main func fun_b
objs = $(foreach f, &(filenames), $(f).o)
all:
	@echo objs = $(objs)

执行结果

objs = main.o func.o fun_b.o

# filter/filter-out

语法:

$(filter pattern...,text)     # 在 text 中取出符合 patten 格式的值
$(filter-out pattern...,text) # 在 text 中取出不符合 patten 格式的值
# 示例
file = a b c d/
D = $(filter %/, $(C)) # 选出 有 / 符号的部分输出
E = $(filter-out %/, $(C)) # 选出 没有  / 符号的部分输出
all:
        @echo D = $(D)
        @echo E = $(E)

执行结果

D = d/
E = a b c

# wildcard

语法如下

$(wildcard pattern) # pattern 定义了文件名的格式,wildcard 取出其中存在的文件
# 示例

在执行示例之前,需将在该目录下创建三个文件: a.c b.c c.c

files = $(wildcard *.c)
all:
    @echo files = $(files)

执行结果:

files = a.c b.c c.c

也可以用 wildcard 函数来判断,真实存在的文件
这样可以避免出现编译到不存在的文件然后报错

files2 = a.c b.c c.c d.c e.c  abc
files3 = $(wildcard $(files2))
all:
    @echo files3 = $(files3)

# patsubst

# 示例
files2  = a.c b.c c.c d.c e.c abc
dep_files = $(patsubst %.c,%.d,$(files2))
all:
    @echo dep_files = $(dep_files)

执行结果:

dep_files = a.d b.d c.d d.d e.d abc

# addprefix

语法: $(addprefix <prefix>, <name-1>, <name-2>...<name-n>)

作用:把前缀 prefix 加到 name 前面

例如,下面这段代码,最终 OBJECTS = build/main.o

BUILD_DIR = build
OBJECTS = $(addprefix $(BUILD_DIR)/, main.o)

# notdir

语法: $(notdir , <name-1>, <name-2>...<name-n> )

作用:字符串中的路径去掉

例如,下面这段代码,最终 OBJECTS = main.c

C_SOURCES = src/main.c
OBJECTS = $(notdir$(C_SOURCES))

# dir

语法: $(dir Name)

作用:从文件名序列 NAMES… 中取出各个文件名的目录部,包含 /

例如,下面这段代码,最终 OBJECTS=06_example/src/

OBJECTS =   $(dir  06_example/src/main.c)

# sort

语法: $(sort <List>)

作用:给字串 “LIST” 中的单词以首字母为准进行排序 (升序), 并且去除重复

例如,下面这段代码,最终 OBJECTS=06_example/src/

C_SOURCE = 06_example/src/main.c 	\
06_example/src/func_a.c 	\
06_example/src/func_b.c 	
OBJECTS =   $(sort $(dir $(C_SOURCE)))

# 一个 Makefile 例子

看了这么多,来试着写一份 makefile

咱们规定 所有的 .c 都存放在 src 文件夹中;所有的 .h 文件都存放在 inc 文件夹中;所有 编译生成的 文件都要存放在 obj

######################################
# target
######################################
# 你的项目
TARGET = test
#######################################
# paths
#######################################
# 过程文件的存放路径,以及.exe
BUILD_DIR = build
######################################
# source
######################################
#.c 源码
# C sources
C_SOURCES =  \
src/main.c	\
src/func_a.c \
src/func_b.c \
src/func_c.c 
#######################################
# 编译器
#######################################
CC = gcc
#######################################
# CFLAGS
#######################################
# 头文件 -I 不能省略
# C includes
C_INCLUDES =  \
-IInc
# 优化等级
OPT = -O0
# 统一生成配置变量
CFLAGS =  $(C_INCLUDES) $(OPT)
# 最终目标文件
all: $(BUILD_DIR)/$(TARGET).exe
#######################################
# 编译应用
#######################################
# .o 文件列表
# addprefix 负责给 每个对应的.c 生成的.o 依赖文件添加上存储路径
# C_SOURCES 里所有以.c 结尾的文件替换为.o 结尾的文件
# main.c -> build/main.o
OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o)))
# 将 .c 文件 所在的目录进行汇总和去重,添加到查找路径中
vpath %.c $(sort $(dir $(C_SOURCES)))
$(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR) 
	$(CC) $(CFLAGS) -c  $< -o $@
$(BUILD_DIR)/$(TARGET).exe: $(OBJECTS) Makefile
	$(CC) $(CFLAGS) $(OBJECTS) -o $@
	
	
$(BUILD_DIR):
	mkdir $@		
#######################################
# clean up
#######################################
.PHONY:
clean:
	-rm -fR $(BUILD_DIR)
# *** EOF ***

大道五十,天衍四十九,人遁其一!

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

黑羊 支付宝

支付宝