Makefile 入门


在 Linux( Unix )环境下使用 GNU 的 make 工具能够比较容易的构建一个属于你自己的工程,整个工程的编译只需要一个命令就可以完成编译、连接以至于最后的执行。不过这需要我们投入一些时间去完成一个或者多个称之为 Makefile 文件的编写。

所要完成的 Makefile 文件描述了整个工程的编译、连接等规则。其中包括:

  • 工程中的哪些源文件需要编译以及如何编译
  • 需要创建哪些库文件以及如何创建这些库文件
  • 如何最后产生我们想要的可执行文件。

尽管看起来可能是很复杂的事情,但是为工程编写 Makefile 的好处是能够使用一行命令来完成“自动化编译”,一旦提供一个(通常对于一个工程来说会是多个)正确的 Makefile。编译整个工程你所要做的事就是在 shell 提示符下输入 make 命令。整个工程完全自动编译,极大提高了效率。

make 是一个命令工具,它解释 Makefile 中的指令。在 Makefile 文件中描述了整个工程所有文件的编译顺序、编译规则。Makefile 有自己的书写格式、关键字、函数。像 C 语言有自己的格式、关键字和函数一样。而且在 Makefile 中可以使用系统shell所提供的任何命令来完成想要的工作。Makefile 在绝大多数的 IDE 开发环境中都在使用,已经成为一种工程的编译方法。

Makefile 基本规则

一个 Makefile 的基本语法结构如下:

Makefile
1
2
3
4
target ... : prerequisites ...
command
...
...

其中:

  • target 可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。
  • prerequisites: 生成该 target 所依赖的文件
  • command: 该target要执行的命令(任意的shell命令)
  • prerequisitescommand 都是可选的, 但是二者至少存在一个.
  • 没有 command 的规则也叫伪目标

简单地说,上面这个语句规定了一个 target 目标的生成方式. 首先需要满足 prerequisites, 也就是说需要先完成 prerequisites 的内容, 然后执行 command 指令实现 target 的生成.

另一方面, 这个语句也表明了一种依赖关系, 也就是说:

1
prerequisites 中如果有一个以上的文件比 target 文件要新的话,command 所定义的命令就会被执行。

而整个的 Makefile 就是这样的一条条规则语句组成的文件. 下面是一个简单的例子.

Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 使用 `#` 进行注释
# 使用 `\` 进行行末换行

# 将 b.txt 和 c.txt 复制到 a.txt 中
a.txt: b.txt c.txt
cat b.txt c.txt > a.txt

b.txt:
echo "this is the text B" > b.txt

c.txt:
echo "this is the text C" > c.txt

# 伪目标, 用于删除之前生成的 a.txt
clean:
rm *.txt

之后, 在命令行执行:

1
make        # 等同于 make a.txt

如果 make 命令运行时没有指定目标,默认会执行 Makefile 文件的第一个目标. 也就是 a.txt, 之后由于 a.txt 需要 b.txtc.txt, make 命令会继续查找相应的命令生成这两个文件, 最后将其一共复制到 a.txt 中.

再执行:

1
make clean

便会执行清理操作, 删除所有的 txt 文件

target

一个目标(target)就构成一条规则。目标通常是文件名,指明 Make 命令所要构建的对象,也可以是多个文件名,之间用空格分隔。除了文件名,目标还可以是某个操作的名字,这称为"伪目标"(phony target)。

比如上面示例中的 clean. 对于伪目标, 存在一种情况是, 如果当前目录下刚好存在一个名为 clean 的文件, 那么会导致 clean 这个规则不会被执行. 因为Make发现clean文件已经存在, 就认为没有必要重新构建了, 就不会执行指定的rm命令.

为了避免这种情况,可以明确声明 clean 是"伪目标",写法如下。

Makefile
1
2
3
.PHONY: clean
clean:
rm *.txt

类似于 .PHONY 这样的内置目标名可以查看make 手册

如果 Make 命令运行时没有指定目标,默认会执行 Makefile 文件的第一个目标。

prerequisites

前置条件通常也是一组文件名,之间用空格分隔。它指定了"目标"是否重新构建的判断标准:只要有一个前置文件不存在,或者有过更新(前置文件的时间戳比较新),"目标"就需要重新构建。

上面代码中,构建 a.txt 的前置条件是 b.txtc.txt 。如果当前目录中,b.txtc.txt 已经存在,那么make a.txt可以正常运行,否则必须再写一条规则,来生成 b.txtc.txt

如果一个目标后面没有前置条件,就意味着它跟其他文件都无关,只要这个文件不存在,每次调用make b.txt,它都会生成。

1
2
make a.txt
make a.txt

上面命令连续执行两次make a.txt。第一次执行会先新建 b.txtc.txt,然后再新建 a.txt。第二次执行,Make发现 b.txtc.txt 没有变动(时间戳晚于 a.txt),就不会执行任何操作,a.txt 也不会重新生成。

如果需要生成多个文件,往往采用下面的写法。

Makefile
1
source: file1 file2 file3

上面代码中,source 是一个伪目标,只有三个前置文件,没有任何对应的命令。

1
make source

执行make source命令后,就会一次性生成 file1,file2,file3 三个文件。这比下面的写法要方便很多。

1
2
3
make file1
make file2
make file3

commands

命令(commands)表示如何更新目标文件,由一行或多行的Shell命令组成。它是构建"目标"的具体指令,它的运行结果通常就是生成目标文件。需要注意的是,每行命令在一个单独的shell中执行。这些 Shell 之间没有继承关系。比如:

Makefile
1
2
3
var-lost:
export foo=bar
echo "foo=[$$foo]"

上面代码执行后(make var-lost),取不到foo的值。因为两行命令在两个不同的进程执行。解决办法有:

  • 将两行命令写在一行,中间用分号分隔
  • 在换行符前加反斜杠转义
  • 加上.ONESHELL:命令
Makefile
1
2
3
4
5
6
7
8
9
10
11
var-kept:
export foo=bar; echo "foo=[$$foo]"

var-kept:
export foo=bar; \
echo "foo=[$$foo]"

.ONESHELL:
var-kept:
export foo=bar;
echo "foo=[$$foo]"

每行命令之前必须有一个tab键。如果想用其他键,可以用内置变量.RECIPEPREFIX声明。比如使用大于号(>)替代tab键:

Makefile
1
2
3
.RECIPEPREFIX = >
all:
> echo Hello, world

Makefile 语法

回声(echoing)

正常情况下,make会打印出每条将要执行的命令,然后再执行,这就叫做回声(echoing)。在命令的前面加上@,就可以关闭回声。

1
2
3
test:
# 这是测试 1
@# 这是测试 2

执行上面的规则,会得到下面的结果, 只打印了没有关闭回声的指令。

1
2
$ make test
# 这是测试 1

由于在构建过程中,需要了解当前在执行哪条命令,所以通常只在注释纯显示的echo命令前面加上@。

通配符(wildcard)

通配符(wildcard)用来指定一组符合条件的文件名。Makefile 的通配符与 Bash 一致,主要有星号(*)、问号(?)和 [...] 。比如, *.o 表示所有后缀名为 o 的文件。

  • * : 匹配0个或者是任意个字符
  • ? : 匹配任意一个字符
  • [...] : 我们可以指定匹配的字符放在 []

通配符不仅可以使用在规则的命令中,还可以使用在规则中, 但是如果通配符使用在依赖的规则中的话一定要注意这个问题:不能通过引用变量的方式来使用, 如果想要引用变量, 需要使用一个函数 wildcard, 看下面这个例子.

Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
# 正常
test: *.c
gcc -o $@ $^

# 错误, 会提示没有 "*.c" 文件
OBJ = *.c
test: $(OBJ)
gcc -o $@ $^

# 正常, 使用一个函数 "wildcard",这个函数在引用变量的时候,会自动展开
OBJ = $(wildcard *.c)
test: $(OBJ)
gcc -o $@ $^

模式匹配

Makefile 中有一个和通配符 “*” 相类似的字符,这个字符是 “%”,也是匹配任意个字符,使用在我们的的规则当中。使用匹配符 %,可以将大量同类型的文件,只用一条规则就完成构建。

Makefile
1
2
%.o: %.c
...

等同于下面的写法。

Makefile
1
2
f1.o: f1.c
f2.o: f2.c

%.o” 把我们需要的所有的 “.o” 文件组合成为一个列表,从列表中挨个取出的每一个文件,%” 表示取出来单个文件的文件名(不包含后缀),然后找到文件中和 "%"名称相同的 “.c” 文件,然后执行下面的命令,直到列表中的文件全部被取出来为止。

变量和赋值符

Makefile 允许使用等号自定义变量。调用时,变量需要放在 $( ) 之中。

1
2
3
txt = Hello World
test:
@echo $(txt) # 变量 txt 等于 Hello World

调用 Shell 变量,需要在美元符号前,再加一个美元符号,这是因为 Make 命令会对美元符号转义。

1
2
test:
@echo $$HOME

有时,变量的值可能指向另一个变量。

1
v1 = $(v2)

上面代码中,变量 v1 的值是另一个变量 v2。这时会产生一个问题,v1 的值到底在定义时扩展(静态扩展),还是在运行时扩展(动态扩展)?如果 v2 的值是动态的,这两种扩展方式的结果可能会差异很大。

为了解决类似问题,Makefile一共提供了四个赋值运算符 (=、:=、?=、+=),它们的区别请看StackOverflow

Makefile
1
2
3
4
5
6
7
8
9
10
11
VARIABLE = value
# 在执行时扩展,允许递归扩展。

VARIABLE := value
# 在定义时扩展。

VARIABLE ?= value
# 只有在该变量为空时才设置值。

VARIABLE += value
# 将值追加到变量的尾端。

内置变量(Implicit Variables)

Make命令提供一系列内置变量,比如,$(CC) 指向当前使用的编译器,$(MAKE) 指向当前使用的Make工具。这主要是为了跨平台的兼容性,详细的内置变量清单见Make 手册

Makefie
1
2
output:
$(CC) -o output input.c

自动变量(Automatic Variables)

Make命令还提供一些自动变量,它们的值与当前规则有关。主要有以下几个。

  • $@

$@ 指代当前目标,就是Make命令当前构建的那个目标。比如,make foo$@ 就指代foo。

$(@D)$(@F) 分别指向 $@目录名文件名

比如,$@src/input.c,那么 $(@D) 的值为 src$(@F) 的值为 input.c

Makefie
1
2
3
4
5
6
7
8
a.txt b.txt: 
touch $@

# 等同于下面的写法。
a.txt:
touch a.txt
b.txt:
touch b.txt
  • $<

$< 指代第一个前置条件。比如,规则为 t: p1 p2,那么 $< 就指代 p1

$(<D)$(<F) 分别指向 $<目录名文件名

Makefie
1
2
3
4
5
6
a.txt: b.txt c.txt
cp $< $@

# 等同于下面的写法。
a.txt: b.txt c.txt
cp b.txt a.txt
  • $?

$? 指代比目标更新的所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,其中 p2 的时间戳比 t 新,$? 就指代 p2

  • $^

$^ 指代所有前置条件,之间以空格分隔。比如,规则为 t: p1 p2,那么 $^ 就指代 p1 p2

  • $*

$* 指代匹配符 % 匹配的部分, 比如 % 匹配 f1.txt 中的 f1$* 就表示 f1

所有的自动变量清单,请看Make 手册。下面是自动变量的一个例子。

Makefie
1
2
3
dest/%.txt: src/%.txt
@[ -d dest ] || mkdir dest
cp $< $@

上面代码将 src 目录下的 txt 文件,拷贝到 dest 目录下。首先判断 dest 目录是否存在,如果不存在就新建,然后,$< 指代前置文件(src/%.txt), $@ 指代目标文件(dest/%.txt)。

判断和循环

Makefile 使用 Bash 语法,完成判断和循环。

Makefie
1
2
3
4
5
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif

上面代码判断当前编译器是否 gcc ,然后指定不同的库文件。

Makefie
1
2
3
4
5
6
7
8
9
10
11
12
LIST = one two three
all:
for i in $(LIST); do \
echo $$i; \
done

# 等同于

all:
for i in one two three; do \
echo $i; \
done

上面代码的运行结果。

1
2
3
one
two
three

函数

Makefile 还可以使用函数,格式如下。

Makefie
1
2
3
$(function arguments)
# 或者
${function arguments}

Makefile提供了许多内置函数,可供调用。下面是几个常用的内置函数。

shell 函数

shell 函数用来执行 shell 命令

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

wildcard 函数

wildcard 函数用来在 Makefile 中,替换 Bash 的通配符。

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

subst 函数

subst 函数用来文本替换,格式如下。

Makefie
1
$(subst from,to,text)

下面的例子将字符串"feet on the street"替换成"fEEt on the strEEt"。

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

下面是一个稍微复杂的例子。

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

patsubst函数

patsubst 函数用于模式匹配的替换,格式如下。

Makefie
1
$(patsubst pattern,replacement,text)

下面的例子将文件名"x.c.c bar.c",替换成"x.c.o bar.o"。

Makefie
1
$(patsubst %.c,%.o,x.c.c bar.c)

替换后缀名

替换后缀名函数的写法是:变量名 + 冒号 + 后缀名替换规则。它实际上patsubst函数的一种简写形式。

Makefie
1
min: $(OUTPUT:.js=.min.js)

上面代码的意思是,将变量OUTPUT中的后缀名 .js 全部替换成 .min.js 。

Makefile 的实例

执行多个目标

Makefie
1
2
3
4
5
6
7
8
9
10
.PHONY: cleanall cleanobj cleandiff

cleanall : cleanobj cleandiff
rm program

cleanobj :
rm *.o

cleandiff :
rm *.diff

上面代码可以调用不同目标,删除不同后缀名的文件,也可以调用一个目标(cleanall),删除所有指定类型的文件。

编译C语言项目

Makefie
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
edit : main.o kbd.o command.o display.o 
cc -o edit main.o kbd.o command.o display.o

main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h
cc -c display.c

clean :
rm edit main.o kbd.o command.o display.o

.PHONY: edit clean
作者

Cheng

发布于

2020-04-09

更新于

2022-08-06

许可协议

评论