# 引入 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.c
和 func.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) |
前两个参数, var
和 list
, 将首先扩展,注意最后一个参数 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 *** |
大道五十,天衍四十九,人遁其一!