Makefile规则详解

Makefile 规则详解

1. 前言

GCC(即 GUN C Compiler)属于最早被开发出来的 C 编译器,至今依旧应用广泛,而且还增加了对 C++,Objective-C,Fortran,Ada,Go 等语言的编译支持。

GCC 是一个使用 CLI 交互的编译工具,不支持图形界面,所以所有的操作都需要在终端通过命令完成。然而一份完整的源代码总是被分割为多个文件,并且编译时需要使用到各种各样的控制和编译选项,你需要在终端中输入冗长的编译命令,每次只能输入一行命令,且每编译一个文件都需要重复此过程,非常费劲。

为了让编译过程轻松快速,消除使用 GCC 输入一条条命令一步一步编译的烦恼,于是 Makefile 出现了,再也不怕冗长命令了,只要把 GCC 各种各样的控制和编译选项编写到 Makefile 文件中即可,再让 Make 解析 Makefile 文件然后调用 GCC 执行编译即可,完美实现了编译(构建)自动化。

2. Make

在详细讲解 Makefile 之前先来了解一下 Make 这个工具,我们知道编写 Makefile 就是为了提供给 Make 使用的。那 Make 是什么呢?暂且可以把 Make 理解成一个解释器,类似解释 Python 程序的 Python 解释器,不过 Make 解释的程序就是 Makefile 文件。

那 Make 的作用是什么呢?我们来翻译下 Make 的中文意思,意思是“制作“或”制造”,不错 Make 的作用就是能够制作出某种格式的文件。

如果需要使用 Make 制作一个纯文本文件 hello.txt 可以执行如下命令。

make hello.txt

但是 Make 会提示 “没有制作 hello.txt 的规则” 错误,因为 Make 不知道怎么才能制作出符合你心意的 hello.txt 文件,也不知道 hello.txt 因该写入什么样的内容。

make: *** No rule to make target 'hello.txt'.  Stop.

如果 hello.txt 的文件内容与 temp.txt 的内容相同,即 hello.txt 是 temp.txt 的副本(或 hello.txt 依赖 temp.txt),那么我们可以告诉 Make 只要将 temp.txt 拷贝一份并命名为 hello.txt 即可得到符合我们心意的 hello.txt,所以我们需要告诉 Make 如下的规则。

hello.txt: temp.txt
    cp temp.txt hello.txt

执行 make hello.txt 这条命令后,Make 会确认 temp.txt 是否已经存在,如果存在使用 cp 命令将 temp.txt 文件拷贝一份输出为新文件 hello.txt。

像这样的规则,都写在前面提到的 Makefile 文件中,Make 则依赖这个文件进行构建。

实验:

准备好一个 temp.txt 文件,内容随意。新建一个文件将上面的规则复制进去然后文件名保存为 Makefile。然后我们再执行 make hello.txt 命令,得到如下的输出,查看本地文件真的得到了 hello.txt 文件,内容与我们准备的 temp.txt 相同。

make hello.txt
cp temp.txt hello.txt

震惊了吧,以为上面提供的是伪代码?实际上是完全没问题的可执行的,Make 不局限于处理 C 程序,只要给它提供规则,它都能给你处理。

总结:

Make 虽然能够制作出某种格式的文件,但是 Make 本身并不知道如何将某种格式的文件制作出来。所以 Makefile 的作用是指导 Make 如何将某种格式的文件制作出来,而我们编写 Makefile 就相当于由我们来告诉 Make 制作某种格式文件的方法。

Make 虽然能够制作出某种格式的文件,但是 Make 本身不具备制作(或编辑)文件的能力,所以 Make 需要依赖其他能够编辑,移动或生成文件的工具来制作文件,例如调用 GCC,或文本编辑器,调用方式是执行这些工具提供的命令,具体命令这里不深究,知道 Make 能够制作文件即可。

3. 规则详解

上节提到构建规则都写在 Makefile 文件中,并且将 Makefile 文件命名为 Makefile 或 makefile 不需要扩展名,在终端执行 make 命令时 Make 会在当前的目录下寻找文件名为 Makefile 或 makefile 的文件。

当然也可以命名为其他名称,例如 config.txt,不过要用 make 命令 -f 选项指定该名称。

make -f config.txt
make --file=config.txt

要学会使用 Make 构建程序,就必须学会如何编写 Makefile 文件,下面我们就深入学习一下 Makefile 的编写规则。

3.1 整体规则

打开一个 Makefile 文件,例如 Linux 源码的 Makefile 整体看起来非常复杂,各种奇怪的符号,实际上除去符号指代的意义不说,复杂的 Makefile 都由一条条简单的规则组合而成,Makefile 的规则形式如下。

 : 
[tab] 

第一行冒号前面的部分叫做 “目标”(target),冒号后面的部分叫做 “前置条件”(prerequisites),第二行必须由一个 tab 键起始,后面跟着 “命令”(commands)。

“目标” 是必需的,不可省略。“前置条件” 和 “命令” 是可省略的,但是这两者至少需要存在一个(两者不能同时省略)。

每条规则都在明确两件事:构建目标的前置条件是什么,以及如何构建出目标。

3.2 目标 target

一个目标(target)就构成一条规则(每条规则都以 “目标” 开头)。目标通常是文件名,指明 Make 命令所要构建的对象,比如前面例子的 hello.txt ,目标可以是一个或多个文件名,多个文件名之间使用空格隔开。

目标除了文件名,也可以是某个操作的名字,这样的目标称为 “伪目标”(Phony target)。

例如目标为 clean,即清除操作,那 clean 就是 “伪目标”,作用是清除一些中间文件,如果在终端执行 make clean 就可以清除中间文件。

clean:
    rm -rf *.o

发现了吧,Makefile 在 “目标” 的构建原理之上巧妙地利用 ”伪目标“ 实现了让我们能够自定义操作命令的功能。但是如果源代码正好有一个文件叫做 clean,那么伪目标 clean 对应的命令不会被执行。因为 Make 发现 clean 文件已经存在,就认为没有必要重新构建了,也就很自然不会再执行 rm -rf *.o 命令了。

为了避免这种情况,可以使用 .PHONY 明确声明 clean 是 “伪目标”,声明 clean 是 “伪目标” 之后,Make 就不再检查是否存在名为 clean 的文件,并每次运行都执行对应的命令,声明方式如下。

.PHONY: clean
clean:
    rm -rf *.o

所以 make “真目标” 就是构建出某个目标对象,make “伪目标” 就是执行某种操作。

3.3 前置条件 prerequisites

前置条件指的是 “目标” 的依赖文件,目标需要依赖前置条件才能生成,一般是一个或多个文件名,多个文件名使用空格分开。如果 “目标” 与其他文件都无关,那么前置条件则不需要列出文件名。

前置条件决定 “目标” 是否需要重新构建。只要前置条件中列出的文件中,有任何一个文件被更新(即前置条件列出的文件的最后修改时间比目标的最后修改时间新),或者 “目标” 不存在,那 “目标” 就需要重新构建。

如果前置条件中列出的文件中有任何一个文件不存在,那么 Make 就需要在 Makefile 文件中寻找并执行能够生成该文件的规则,也就是说缺失文件本身也是一个 “目标”,需要先将该 “目标” 构建出来才能利用它去构建当前 “目标”(构建 A 时发现 A 需要依赖 B,所以要先构建 B 才能继续构建 A,简称递归生成依赖)。

hello.txt: temp.txt
    cp temp.txt hello.txt

现在回过头再看前面的例子,hello.txt 是目标,构建目标的前置条件是 temp.txt。如果当前目录中,temp.txt 已经存在,那么 make hello.txt 可以正常运行,否则必须先生成 temp.txt,再构建 hello.txt 。

3.4 命令 commands

命令(commands)表示如何更新目标文件,由一行或多行的 Shell 脚本命令组成,是构建 “目标” 的具体指令,命令的运行结果通常就是生成目标文件。

注意,命令必须以一个 tab 键为起始,当然灵活的 Makefile 允许你使用别的符号替换 tab 键,替换方式是使用 Makefile 的内置变量 .RECIPEPREFIX 去声明新的符号,例如使用 >> 替换 tab 键。

.RECIPEPREFIX=>>

clean:
>>rm -rf *.o

构建一个目标可能需要使用多条命令,多条命令为了便于阅读一般需要分多行编写。但需要注意的一点是,Makefile 对于编写在不同行的命令会分配给不同的 Shell 进程去执行,这些 Shell 之间没有继承关系(即没有上下文联系,运行完的 Shell 进程则会立即退出),意味着上下相邻的两条命令之间无法实现数据交互。

所以如下 Makefile 规则中 echo 命令无法取得变量 var 的值,因为这两行命令被分配在不同的 Shell 进程中执行。

variable_test:
    export var="Hello, World!"
    echo var=$${var}

所以为了实现命令之间数据共享,解决办法是将具备关联的多条命令写在一行,且命令之间用分号隔开,如下。

variable_test:
    export var="Hello, World!"; echo var=$${var}

多条命令编写在一行终究不便于阅读,所以还是需要分行编写,但为了让多行命令达到一行的效果可以使用反斜杠 \ 转义,转义后 Make 就认为这些命令是在同一行的,需要在同一个 Shell 进程中执行。

variable_test:
    export var="Hello, World!"; \
    echo var=$${var}

如果你觉得在每条命令之后放置反斜杠 \ 很不优雅,那最后还有一种办法是使用 Makefile 的内置命令 .ONESHELL: 来声明规则,类似于反斜杠的转义效果。

.ONESHELL:
variable_test:
    export var="Hello, World!";
    echo var=$${var}

3.5 总结

一个 Makefile 文件中一般会定义多个 “目标”,例如一些 “真目标”,以及一些 “伪目标”。默认情况下执行 make 命令 Make 默认会构建 Makefile 的第一个 “目标”,然后逐步去构建第一个目标的依赖。

如果你想让 Make 构建指定 “目标”,可以在 make 命令之后跟随要构建的 “目标” 名,例如 make hello.txtmake clean,如果目标为 “真目标” 则构建出努比奥对象,如果目标为 “伪目标” 则执行某种操作。

4. 命令详解

经过上一节内容对 Makefile 的规则已经熟悉了,下面来讲解一下 Makefile 的基础语法,熟悉一下 Makefile 的一些基础组成元素。

4.1 注释

Makefile 的注释与编程语言 Pythonshell 脚本一样使用字符 # 作为注释标识,即跟随在 # 符号之后的内容会被 Make 理解为是注释故而将其忽略。

# 这是注释语句
hello.txt: temp.txt # 这是注释语句
    cp temp.txt hello.txt # 这是注释语句

4.2 回声

Make 在执行每一条命令之前,会先将要执行的命令打印到终端,然后再执行,这就是回声机制。

echoing:
    echo "Hello, World!"

在终端执行 make echoing 命令,在终端可以得到如下输出,可见 Make 将执行的命令都输出到了终端。

make echoing
----------------------------------
echo "Hello, World!"
Hello, World!

在命令的前面加上 @,就可以关闭当前命令的回声,当然一般只在注释和纯显示的 echo 命令前面加上 @,以免将 echo 本身输出到终端。

echoing:
    @echo "Hello, World!

在 echo 命令之前加上 @ 之后 echo 命令本身不再被输出到终端,而是只输出了需要被输出的内容。

make echoing
----------------------------------
Hello, World!

4.3 通配符

通配符(即 Wildcard)用来指定一组符合条件的文件名。Makefile 的通配符与 Bash 一致,主要有星号 *,问号 ?... 。比如 *.o 表示所有后缀名为.o的文件,例如通配符在之前的例子就已经使用到。

.PHONY: clean
clean:
    rm -rf *.o

4.4 模式匹配

Make 命令允许对文件名使用类似于正则表达式的方式进行匹配,主要用到的匹配符是百分号 %。比如,假定当前目录下有 a.c 和 b.c 两个源码文件,需要将它们编译为对应的对象文件 a.o 和 b.o,需要编写两条规则。

a.o: a.c
    gcc -c a.c -o a.o

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

如果工程包含大量的 C 文件显然这么做是非常繁琐的,所以为了简化可以使用 %.o: %.c 模式匹配,效果等同于如下写法。所以使用匹配符 %,可以将大量同类型的文件,只使用一条 Makefile 规则就完成构建(通过模式匹配就可以只实现一条规则将所有的 .c 文件编译为对应的 .o 文件)。

% 出现在目标中的时候,目标中 % 所代表的值决定了依赖中的 % 值,使用方
法如下:

a.o: a.c
b.o: b.c

模式规则中,至少在规则的目标定定义中要包涵 %,否则就是一般规则,目标中的 % 表示对文件名的匹配,所以 % 表示长度任意的非空字符串。

4.5 变量

Makefile 和其他编程语言一样支持定义变量,定义变量的好处是对于一个大范围使用的内容,可以将其使用变量替代,休要修改时可以修改变量的内容以达到统一修改这个大范围使用的内容。

Makefile 中定义变量的方式如下,由于 Makefile 属于脚本,所以与其他编程语言不同,Makefile 中定义变量不需要声明变量的类型(实际上 Makefile 变量保存的值都是字符串)。

同时 Makefile 的变量不是真正意义上的变量,更类似于 C/C++ 的宏定义,执行时会将变量内容原样地展开在使用的地方。

hello = Hello, World!

hello:
    @echo $(hello)

引用变量时,变量需要放在 $( )${} 之中才能够获取到变量的值。

4.6 赋值运算符

在前面定义了变量,变量需要给它赋值才能够发挥作用,Makefile 为了不同的场景定义了四种赋值运算符,分别为 =:=?=+=,这些赋值运算符具有不同的含义。

(1) = 在变量被执行时扩展,允许递归扩展。Make 会将整个 Makefile 展开后,再决定变量的值。也就是说,变量的值将会是整个 Makefile 中最后被指定的值。

x = Hi
y = $(x) World
x = Hello
---
echo $(y)
Hello World

(2) := 在变量被定义时扩展。表示变量的值决定于它在makefile中的位置,而不是整个 Makefile 展开后的最终值。

x := Hi
y := $(x) World
x := Hello
---
echo $(y)
Hi World

(3) ?= 只有在该变量为空时才设置值。

(4) += 将值追加到变量的尾端。

4.7 内置变量

Make 命令为了跨平台的兼容性提供了一系列内置变量,比如,$(CC) 指向当前使用的编译器(例如 gcc),$(MAKE) 指向当前使用的 Make 工具,还有更多的内置变量,这里不再列举,可以查阅 Make 官方手册以了解。

hello:
    $(CC) hello.c -o hello.exe

4.8 自动变量

前面讲模式匹配的内容时说到,目标和依赖都是一系列的文件,每一次对模式匹配进行解析的时候 % 都代表不同的目标和依赖文件,而命令只有一行,如何通过一行命令来从不同的依赖文件中生成依赖对应的目标?这就需要使用到 Makefile 的自动变量。

在 Makefile 中经常会见到类似 $@$^$< 这种符号,这种符号称为自动变量。自动变量是局部变量,作用域范围在当前的规则内(即自动化变量只应该出现在 Makefile 目标规则中),使用自动变量有助于简化 规则语句。

所谓自动化变量就是这种变量会把模式中所定义的一系列的文件自动的挨个取出,直至所有的符合模式的文件都取完。

(1) $@ 指代当前目标,就是 Make 命令当前构建的那个目标,例如 $@ 就代表目标 hello

hello: hello.o
    gcc hello.o -o hello

所以下方的写法效果等同于上方。

hello: hello.o
    gcc hello.o -o $@

(2) $< 指代第一个前置条件。比如规则为 hello: a b,那么 $< 就指代 a。

hello: a.o b.o
    gcc a.o b.o -o hello

所以下方的写法效果等同于上方。

hello: a.o b.o
    gcc $< b.o -o hello

(3) $? 指代比目标更新的所有前置条件,之间以空格分隔。比如规则为 hello: a b,其中 b 的时间戳比目标 hello 新,那么 $? 就指代 b。

(4) $^ 指代所有前置条件,之间以空格分隔。比如,规则为 hello: a b,那么 $^ 就指代 a 和 b。

hello: a.o b.o
    gcc a.o b.o -o hello

所以下方的写法效果等同于上方。

hello: a.o b.o
    gcc $^ -o hello

(5) $* 指代匹配符 % 匹配的部分, 比如 % 匹配 a.txt 中的 a ,$* 就表示 a。


还有更多的自动变量,这里不再列举,可以查阅 Make 官方手册加以了解。

5. 高级命令

Makefile 除了 Make 本身的命令,还可以使用 Bash 语法(即 Shell 脚本语法),例如使用 Shell 语法完成判断和循环,环境变量引用等等。

两种语法的交叉使用也有不优雅的地方,例如 Makefile 允许赋值符 = 两边包含空格,然而 Shell 不允许,否则 Shell 将空格也作为值包含进去。

5.1 判断

条件语句可以根据适当的条件控制 Make 执行或者时忽略特定的操作,与其他编程语言不同 Makefile 没有比较运算符,取而代之的是不同的比较命令,所以要实现条件的比较需要如下四种比较命令。

命令 描述
ifeq 判断参数是否不相等,相等为 true,不相等为 false
ifneq 判断参数是否不相等,不相等为 true,相等为 false
ifdef 判断是否有值,有值为 true,没有值为 false
ifndef 判断是否有值,没有值为 true,有值为 false

其中命令 ifeq 的使用方式如下,而其余的三种使用方式是相似的。

all:
ifeq ($(CC), gcc)
    @echo GCC Compiler
else
    @echo Other Compiler
endif

5.2 循环

Makefile 的 for 循环语法如下,其中 @ 只是用于关闭命令回显,$$ 实际表示 $,用来 Shell 进程下引用 Shell 变量(因为在 Shell 中 $ 会被当作转义符号),而 $a 或者 $(a) 则表示引用 Makefile 变量。

a := 123 123 123

testfor:
    @for var in $(a); do \
        echo $${var}; \
    done

除了 for 循环 Makefile 还支持 foreach 语句,这是 Makefile 的内置函数(现在不懂没关系后面小节会说到),语法如下。

a := 123 123 123

testfor:
    @$(foreach var, $(a),\
        echo $(var); \
    )

5.3 函数

Makefile 还可以使用函数,对于 Makefile 来说,函数是一种特殊的指令,函数可以接受参数,并返回一个值,格式如下。

$(function arguments) 或 ${function arguments}

Makefile 中的函数是 Make 已经定义好的,不支持我们自定义函数,我们直接使用即可,例如函数可以用于在 Makefile 中执行 Shell 命令,这里用函数执行一条能够在终端打印出工作目录的 pwd 命令。

PATH := $(shell pwd)

Make 提供的函数不太多,但是足够我们使用了,下一节就来大致了解一下 Make 提供的函数(内置函数)用途。

6. 内置函数

Makefile 官方提供了一系列的內建函数以适用于各类文件的处理,函数本身就是对操作过程的一种封装,使用这些函数可以简化一些复杂的操作。

6.1 Shell 函数

shell 函数用来执行 shell 命令。

srcfiles := $(shell echo src/{00..99}.txt)

6.2 Wildcard 函数

通配符 % 只能用在目标规则中,只有在目标规则中它才会展开,如果在变量定义和函数使用时,通配符不会自动展开,这个时候就要用到函数 wildcard。

可以用来在 Makefile 中替换 Bash 的通配符 *,以及通配符 %,返回符合 pattern 的文件名列表。

$(wildcard pattern...)

例如匹配 src 目录下的所有 .txt 文件。

srcfiles := $(wildcard src/*.txt)

6.3 Subst 函数

subst 函数用来完成文本替换(字符串替换),将文本中的 from 替换为 to,格式如下。

$(subst from,to,text)

例如将字符串 “feet on the street” 中包含的所有 “ee” 替换成 “EE”。

$(subst ee,EE,feet on the street)

看下面的例子。

comma:= ,
empty:=
# space变量用两个空变量作为标识符,当中是一个空格
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
# bar is now `a,b,c'.

6.4 Patsubst 函数

patsubst 函数用于模式匹配的替换,将文本 text 中符合 pattern 的部分替换为 replacement,格式如下。

$(patsubst pattern,replacement,text)

pattern 可以使用包括通配符 %,表示任意长度的字符串,函数返回值就是替换后
的字符串。如果 replacement 中也包涵 %,那么 replacement 中的 % 将是pattern 中的那个 % 所代表的字符串。

例如将字符串 “a.c b.c c.c” 中的所有符合 “%.c” 的字符串,替换为 “%.o”,替换完成以后的字符串为 “a.o b.o c.o”。

$(patsubst %.c,%.o,a.c b.c c.c)

6.5 Foreach 函数

foreach 函数用来完成循环。此函数的意思就是把参数 list中的单词逐一取出来放到参数 var 中,然后再执行 text 所包含的表达式。每次 都会返回一个字符串,循环的过程中,text 中所包含的每个字符串会以空格隔开,最后当整个循环结束时,text 所返回的每个字符串所组成的整个字符串将会是函数 foreach 函数的返回值。

$(foreach var, list,text)

6.6 Dir/Notdir 函数

函数 dir 用来获取目录,此函数用来从文件名序列 names 中提取出目录部分,返回值是文件名序列 names 的目录部分。

$(dir names...)

例如提取文件 “/src/hello.c” 的目录部分,也就是 “/src”。

$(dir )

函数 notdir 看名字就是知道去除文件中的目录部分,作用相反。

$(notdir names...)

例如提取文件 “/src/hello.c” 的文件名部分,也就是 “hello.c”。

$(notdir )

7. 总结

总结一下编写 Makefile 需要熟悉编译的过程阶段以及 GCC 的命令使用,如果还不了解的话可以看看我的这篇文章:

GCC 命令详解

你可能感兴趣的:(ARM嵌入式开发,Linux,Make,Makefile,GCC)