MakeFile
MakeFile
一、基础语法
1. 基本概念
1.1 make是什么
当一个项目中要编译的文件很多时,手工使用编译器一个个进行编译,很明显不具有可操作性,此时必须借助某些软件,协助我们有序地、正确地自动编译整个工程的所有该编译的文件。这样的软件被称为 工程管理器,make 就是一款工程管理器软件。
1.2 Makefile是什么
make 正常工作时,会读取一个称为 Makefile 的配置文件,该配置文件可以为 make 指明细致的工作规则,比如所使用的工具链、要编译的目标文件名称、要递归编译的子文件夹路径等等。
对工程管理器软件的学习,主要就是对其配置文件** Makefile 的语法的学习**。
1.3 Makefile在哪里
Makefile 是用来指导make对源代码进行编译的,因此在一个多目录结构的工程项目中,凡是有源码出现的目录,都会有一个Makefile去管理,而所有的 Makefile,都通过工程项目顶层目录下的 Makefile 去直接或简洁调用。
2. 目标与依赖
目标和依赖是 Makefile 语法中最基本的概念,假设有一个源文件 a.c,编译生成 a.o ,那么前者是依赖,后者 a.o 是目标,但进一步将a.o编译成可执行文件 a,那么 a.o 此时就变成依赖,最终的文件 a 是目标,因此目标和依赖是相对的概念。
在Makefile中,使用冒号来区隔它们:
# 目标:依赖
a.o:a.c
# 目标:依赖列表
image:a.o b.o c.o d.o
示例:
main:main.c
@echo "Hello MakeFile"
解析:
main:main.cmain为目标他依赖于main.c- 第二行中开头必须是一个制表符不能是空格
@表示执行该语句但不需要输出他- echo 实际上是shell命令的“打印函数”
- 以上示例中的两行就是一套完整的规则
3. 规则
在目标与依赖下面,使用一种特殊的语法 "
# 一套规则:
a.o:a.c
gcc a.c -o a.o -c -fPIC # 行首必须是制表符tab
请注意:在上述语句中,目标与依赖、命令共同构成了一个规则,命令的行首必须是制表符 tab 键,不能是空格,否则会报错。另外,命令可以是多行:
image:a.o b.o c.o d.o
gcc a.c -o a.o -c -fPIC
gcc b.c -o b.o -c -fPIC
gcc c.c -o c.o -c -fPIC
gcc d.c -o d.o -c -fPIC
gcc a.o b.o c.o d.o -o image
重点:规则中的各个命令什么时候被执行?
- 当目标文件不存在时。
- 当目标文件存在,但时间戳比依赖列表中的某一文件旧时。
因此,当目标文件已经被编译且其依赖文件没有修改,那么再次执行make就不会触发任何动作(make: 'main' is up to date.),这就是make和 Makefile 的最基本的逻辑:只在有需要的时候编译,尽量提高编译效率。
4. 终极目标
在一个 Makefile 中,可以有多套规则,也就说可以有多个目标,在这多个目标中,最先出现的被称为终极目标,它是执行make时默认的目标,比如:
a:a.c
gcc a.c -o a
b:b.c
gcc b.c -o b
以上Makefile中,a是终极目标,b不是,因此直接执行make时,只会针对第一套规则进行推导:
gec@ubuntu:~$ ls
a.c b.c Makefile
gec@ubuntu:~$ make
gcc a.c -o a
要执行第二套规则,则需要在执行make命令时特意指定,比如:
make 指定的目标
gec@ubuntu:~$ make b
gcc b.c -o b
或令其间接依赖于终极目标,比如:
a:a.c b
gcc a.c -o a
b:b.c
gcc b.c -o b
执行结果是:
gec@ubuntu:~$ make
gcc b.c -o b
gcc a.c -o a
5. 多目标编译
从上述第4点可见, Makefile 中可以通过目标的相互依赖来递推整条编译链,当然像上述那样将a强行依赖于b并不是一种可取的做法,因为这么做虽然可以达到目的,但在逻辑上却让人陷入困惑,毕竟在上述例子中,a和b是两个不相干的程序,他们之间并没有依赖关系。
对于这种多目标编译,更传统的做法是,虚构一个被大家共同依赖的伪目标,利用 Makefile 编译链自动编译所有的目标,比如:
all:a b
a:a.c
gcc a.c -o a
b:b.c
gcc b.c -o b
执行结果是:
gec@ubuntu:~$ make
gcc a.c -o a
gcc b.c -o b
示例:
# Makefile默认第一个规则中的目标为最终目标
main:main.o test.o
gcc main.o test.o -o main
main.o:main.c
gcc main.c -o main.o -c
test.o:test.c
gcc test.c -o test.o -c
6. 隐式规则
Makefile 会根据目标和依赖简单地自动推导出编译语句,这种情况叫隐式规则,比如:
all:a b
在上述 Makefile 中,没有任何编译语句,甚至连a和b的依赖文件都没写,但这个 Makefile 可以正常执行:
gec@ubuntu:~$ make
cc a.c -o a
cc b.c -o b
此时,Makefile 的执行逻辑是:监测到终极目标的依赖文件a和b不存在,就会自动寻找以a和b为目标的规则,在本文件中没有,然后就会尝试在本目录中寻找a.c和 b.c ,如果找到了就以它们为依赖文件,自动编译它们,这个过程就是隐式规则。
注意到,隐式规则可以帮忙处理一些比较简单地编译,它要求目标文件和依赖文件同名(除了后缀不同),不支持多文件编译,也不支持个性化编译选项。
7. 伪目标
由于有隐式规则的存在,因此伪目标在某些极端情况下可能会被误编译,比如上述例子中,all 是伪目标,不是真正要编译生成的目标,但如果源码目录中恰巧有一个文件叫 all.c ,那么根据 Makefile 的隐式规则,将会触发 all.c 的编译动作。
如何规避隐式规则这种误操作呢?很简单,明确告诉Makefile ,all是伪目标,不要编译他:
all:a b
.PHONY:all
上述语句中,.PHONY 是 Makefile 的一个关键字,用来声明伪目标,防止隐式规则滥用。
在 Makefile 中,常见的伪目标除了all之外,还有clean、distclean等,用来清除生成的中间文件,例如:
all:a b
clean: # 清除所有目标文件、可重定位文件
rm a b *.o
distclean:clean # 先执行clean,然后清除所有交换文件、核心转储文件
rm .*.sw? core
.PHONY:all clean
二、变量
1. 自定义变量
类似于shell脚本,可以在 Makefile 定义变量和引用变量:
BIN=a b
# $( ) 引用变量,把需要访问的变量写入到括号里
all:$(BIN)
clean:
rm $(BIN)
2. 内置变量
Makefile有许多跟编译相关的内置变量,比如:
CFLAGS = "-O2 -Wall" # C编译选项
LDFLAGS = "-lpthread" # 链接器参数
CC = aarch-linux-gnu-gcc # C编译器名称
CXX = aarch-linux-gnu-g++ # C编译器名称
可以通过修改上述变量的值,来个性化各种编译场景,例如:
CC = gcc
CXX = aarch64-linux-gnu-g++
CFLAGS = -O2 -Wall
LDFLAGS = -lpthread
ELF = a b
all:a b
a:a.c
$(CC) a.c -o a $(CFLAGS) $(LDFLAGS)
b:b.cpp
$(CXX) b.cpp -o b
clean:
rm a b
.PHONY:all
3. 变量的定义引用
所谓定义引用,指的是在定义一个变量的时候引用了另一个变量的值。比如下面定义变量B的时候,其值引用了变量A:
A = China
B = I love $(A)
all:
echo $(B)
执行结果:
gec@ubuntu:~$ make
echo I love China
I love China
3.1 全文搜索模式
注意到,上述变量A和B的定义,可以任意调换其顺序,比如:
B = I love $(A) # 照样可以引用出现在后面的变量A的值
all:
echo $(B)
A = China
这不会有任何影响,这是因为 Makefile 中直接用等号 “=” 定义变量时若存在对其他变量的引用,会采取全文搜索的策略去找引用值。
3.2 简单定义模式
如果不想要 Makefile 的这种全文搜索的特性,而希望只引用定义语句之前出现过的变量的值的话,就要用 “:=” 简单模式,例如:
B := I love $(A) # 只引用在此之前有定义的A的值
all:
echo $(B) # 输出"I love"
A = China
3.3 变量追加
Makefile 中的变量都是字符串,可以使用 “+=” 进行追加,例如:
CFLAGS = -O2 # 全文搜索模式
CFLAGS += -Wall
CFLAGS += -Werror
# 等价于
CFLAGS = -O2 -Wall -Werror
3.4 变量值修改
Makefile 中的变量本质是一连串单词,通常是待处理的一系列文件名称,在实际操作中经常需要对这些文件名进行模式替换,比如有一串由C语言源文件组成的字串,希望将其中的文件后缀 *.c 变成 *.o,可以这么做:
A = srt.c string.c tcl.c
B = $(A:%.c=%.o) # 此处,变量B的值是 srt.o string.o tcl.o
4. override
在执行make时,通常可以在命令行中携带一个变量的定义,如果这个变量跟Makefile中出现的某一变量重名,那么命令行变量的定义将会覆盖Makefile中的变量。就是说,对于一个在Makefile中使用常规方式(使用“=”、“:=”或者“define”)定义的变量,我们可以在执行make时通过命令行方式重新指定这个变量的值,命令行指定的值将替代出现在Makefile中此变量的值。比如:
A = an apple tree
all:
@echo $(A) # 输出变量A的值,符号@代表不输出命令本身
直接执行 make 的结果是:
gec@ubuntu:~$ make A="an elephant"
an elephant
可见,虽然Makefile定义了A的值为”an apple tree”,但被命令行定义的A的值覆盖了,变成了”an elephant”。如果不想被覆盖,则可以写成:
override A = an apple tree
all:
@echo $(A)
此时,执行结果是:
gec@ubuntu:~$ make A="an elephant"
an apple tree
但是请注意:指示符 override 并不是用来防止Makefile的内部变量被命令行参数覆盖的,其真正存在的目的是:
- 为了使用户可以改变或者追加那些使用make的命令行指定的变量的定义。
- 即:实现了在Makefile中增加或者修改命令行参数的一种机制。
通常,我们会通过命令行来指定一些附加的、个性化的编译参数,而对一些通用的参数或者必需的编译参数,我们则在Makefile中指定,为了使两个地方指定的参数和谐相处,不相互覆盖,一般就用指示符 override 来实现。
例如,无论命令行指定那些编译参数,必须打开所有的编译警告信息“-Wall”,则 Makefile 的变量 CFLAGS 应该这样写:
override CFLAGS += -Wall
test:test.c
执行结果是:
gec@ubuntu:~$ make CFLAGS="-g"
cc -g -Wall a.c -o a
5. 静态规则与自动化变量
所谓静态规则,就是可以使用模式匹配的方式,自动生成若干规则的机制。例如:
OBJ = a.o b.o c.o
image:$(OBJ)
$(CC) $(OBJ) -o image
#静态规则
$(OBJ):%.o:%.c
$(CC) $^ -o $@ -c # 运用了自动化变量自适应不同的目标和依赖
clean:
$(RM) $(OBJ) image
.PHONY: clean
所谓自动化变量,指的是它们的值会随着规则自动地发生变化,它们的含义是确定的,但是它们的值会自适应不同的规则,这个特性刚好与静态规则自动产生规则像。除了上面两个常见的自动化变量外,还有下述这些自动化变量。
| 变量 | 含义 | 备注 |
|---|---|---|
@ |
其所在规则的目标的完整名称 | - |
% |
其所在规则的静态库文件的一个成员名 | - |
< |
其所在规则的依赖列表的第一个文件的完整名称 | - |
? |
所有时间戳比目标文件新的依赖文件列表 | 用空格隔开 |
^ |
其所在规则的依赖列表 | 同一文件不可重复 |
+ |
其所在规则的依赖列表 | 同一文件可重复,主要用在程序链接时,库的交叉引用场合 |
三、Makefile函数
Makefile 中的函数可以实现一些特性的功能,其基本语法是:
VAR = $(函数 参数1[,参数2,参数3,...])
语法要点有:
- 函数及其参数用 $() 包含
- 函数与参数之间用空格隔开
- 若函数需要多个参数,则参数之间用逗号隔开
- 若函数有返回值,其值可以直接赋值给变量
$(subst FROM,TO,TEXT)
功能:将字符串TEXT中的字符FROM替换为TO。
返回:替换之后的新字符串。
范例:
A = $(subst pp,PP,apple tree)
替换之后变量A的值是"aPPle tree"
$(wildcard PATTERN)
功能:获取匹配模式为PATTERN的文件名。
返回:匹配模式为PATTERN的文件名。
范例:
A = $(wildcard *.c)
假设当前路径下有两个.c文件a.c和b.c,则处理后A的值为:“a.c b.c”
四、其他语法
嵌套Makefile
在多目录结构中,Makefile可以通过内置命令嵌套调用。例如有如下目录结构:
gec@ubuntu:~$ tree
.
├── dir/
│ └── Makefile # 子Makefile
└── Makefile # 顶层Makefile
要在顶层 Makefile 中调用子 Makefile ,只需执行如下语句:
all:
$(MAKE) -C dir/ # 调用指定目录下的子Makefile
变量导出
在嵌套调用子 Makefile 的过程中,如果需要将变量传递给子 Makefile ,可以使用如下语句:
# 顶层`Makefile`
export A = apple # 在顶层Makefile中,将变量A导出
B = banana # 在顶层Makefile中,变量B未导出
all:
@echo "rank 1: $(A)"
@echo "rank 1: $(B)"
@$(MAKE) -C dir/ # 调用位于dir/中的子Makefile
all:
@echo "rank 2: $(A)" # 从顶层Makefile获得变量的值
@echo "rank 2: $(B)" # 空值

浙公网安备 33010602011771号