Skip to content

LV050-隐含规则和模式规则

本文主要是makefile——隐含规则和模式规则相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

一、文件准备

后面的实例都是用下面的几个源码文件来做测试。

1. 目录结构

各文件所在目录的结构如下:

c
.
├── main.c
├── Makefile
├── test1.c
├── test2.c
└── test.h

2. 示例源码

2.1 main.c

c
#include <stdio.h>
#include "test.h"

int main(int argc, const char *argv[])
{
	printf("This is main file!\n");
	test1Fun();
	test2Fun();
	return 0;
}

2.2 test1.c

c
#include <stdio.h>

void test1Fun()
{
    printf("This is test1.c file\n");
}

2.3 test2.c

c
#include <stdio.h>

void test2Fun()
{
    printf("This is test2.c file\n");
}

2.4 test.h

c
void test1Fun();
void test2Fun();

二、隐含规则

1. 什么是隐含规则

在我们使用 Makefile 时,有一些我们会经常使用,而且使用频率非常高的东西,比如,我们编译 C/C++ 的源程序为中间目标文件( Unix 下是 [.o] 文件, Windows 下是 [.obj] 文件)。

隐含规则”就是一种惯例,make 会按照这种“惯例”心照不喧地来运行,即便我们的 Makefile 中没有书写这样的规则。例如,把 [.c] 文件编译成 [.o] 文件这一规则,我们根本就不用写出来, make 会自动推导出这种规则,并生成我们需要的 [.o] 文件。

“隐含规则”会使用一些系统预定义的变量,我们可以改变这些系统变量的值来定制隐含规则的运行时的参数。如系统变量“ CFLAGS ”可以控制编译时的编译器参数。我们还可以通过“模式规则”(后边会说明)的方式写下自己的隐含规则。用“后缀规则”来定义隐含规则会有许多的限制。使用“模式规则”会更回得智能和清楚,但“后缀规则”可以用来保证我们 Makefile 的兼容性。

2. 怎么用隐含规则?

2.1 自动应用

我们在测试目录中创建如下内容的 Makefile 文件:

makefile
OBJ = main.o test1.o test2.o

main: ${OBJ}
	gcc $(OBJ) -o main

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

然后在终端执行 make 命令,会看到有如下输出:

shell
cc    -c -o main.o main.c
cc    -c -o test1.o test1.c
cc    -c -o test2.o test2.c
gcc main.o test1.o test2.o -o main

由此可以看出,即便我们没有写依赖目标的规则, Makefile 依然帮我们推倒出了所需要的 main.o 、 test1.o 和 test2.o 。前边学习变量的时候笔记中有说明,系统预定义变量中有一个 CC ,它的值就是 cc ,而这里隐含规则的自动推导使用的就是系统的预定义变量了。这已经是“约定”好了的事了, make 和我们约定好了用 C 编译器 cc 生成 [.o] 文件的规则,这就是隐含规则。

如果我们为 [.o] 文件书写了自己的规则,那么 make 就不会自动推导并调用隐含规则,它会按照我们写好的规则忠实地执行 ,如下边的 Makefile :

makefile
OBJ = main.o test1.o test2.o

main: ${OBJ}
	gcc $(OBJ) -o main
main.o: main.c test.h
	gcc -c main.c -o main.o
test1.o: test1.c test.h
	gcc -c test1.c -o test1.o
test2.o: test2.c test.h
	gcc -c test2.c -o test2.o

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

我们执行 make 命令的话,就会看到如下提示信息:

shell
gcc -c main.c -o main.o
gcc -c test1.c -o test1.o
gcc -c test2.c -o test2.o
gcc main.o test1.o test2.o -o main

2.2 有哪些隐含规则?

直接参考 隐含规则一览 — 跟我一起写Makefile 1.0 文档 一节

3. 取消隐含规则

我们有时候不想使用隐含规则的话,我们就可以在执行 make 命令的时候加上 -r 参数来取消隐含规则:

shell
make -r
# 或者
make --no-builtin-rules

当然,即使是我们指定了 -r 参数,某些隐含规则还是会生效,因为有许多的隐含规则都是使用了后缀规则来定义的,所以,只要隐含规则中有“后缀列表”(也就一系统定义在目 标 .SUFFIXES 的依赖目标 ),那么隐含规则就会生效 。 默认的后缀列表是:

makefile
.out、 .a、 .ln、 .o、 .c、 .cc、 .C、 .p、 .f、 .F、 .r、 .y、 .l、 .s、 .S、 .mod、 .sym、
.def、 .h、 .info、 .dvi、 .tex、 .texinfo、 .texi、 .txinfo、 .w、 .ch .web、 .sh、 .elc、 .el

4. 常用隐含规则

4.1 编译 C 程序

[n].o 的目标的依赖目标会自动推导为 [n].c ,其生成命令是:

makefile
$(CC) –c $(CPPFLAGS) $(CFLAGS)

4.2 编译 C++ 程序

[n].o 的目标的依赖目标会自动推导为 [n].cc 或是 [n].C (建议使用 .cc 作为 C++ 源文件的后缀,而不是 .C ),其生成命令是:

makefile
$(CXX) –c $(CPPFLAGS) $(CFLAGS)

4.3 汇编和汇编预处理

[n].o 的目标的依赖目标会自动推导为 [n].s ,默认使用编译器 as ,其生成命令是:

makefile
$(AS) $(ASFLAGS)

[n].s 的目标的依赖目标会自动推导为 [n].S ,默认使用 C 预编译器 cpp ,其生成命令是:

makefile
$(AS) $(ASFLAGS)

4.4 链接 Object 文件

[n] 目标依赖于 .o ,通过运行 C 的编译器来运行链接程序生成(一般是 ld ), 其生成命令是:

makefile
$(CC) $(LDFLAGS) [n].o $(LOADLIBES) $(LDLIBS)

这个规则对于只有一个源文件的工程有效,对多个 Object 文件(由不同的源文件生成)的也有效。例如,如下规则:

makefile
main : test1.o test2.o

并且 main.c 、 test1.c 和 test2.c 都存在时,隐含规则将执行如下命令:

shell
cc -c main.c -o main.o
cc -c test1.c -o test1.o
cc -c test2.c -o test2.o
cc main.o test1.o test2.o -o main # 这一句应该是自己写的,上边三条输出是隐含规则完成的

如果没有哪个源文件(如上例中的 main.c )和我们的目标名字(如上例中的 main )相关联,那么,最好还是写出自己的生成规则,不然,隐含规则会报错的。

5. 隐含规则使用的变量

在隐含规则中的命令中,基本上都是使用了一些预先设置的变量,之前的时候其实我们已经做过笔记了,这里再写一下吧,加深印象。我们可以在 Makefile 中改变这些变量的值,或是在 make 的命令行中传入这些值,或是在环境变量中设置这些值。只要设置了这些特定的变量,那么其就会对隐含规则起作用。

例如,编译 C 程序的隐含规则的命令是

makefile
$(CC) –c $(CFLAGS)  $(CPPFLAGS)

make 默认的编译命令是 cc ,如果把变量 $(CC) 重定义成 gcc ,把 变量 ​$(CFLAGS) 重定义成 -g ,那么,隐含规则中的命令全部会以下边的形式执行:

makefile
gcc –c -g $(CPPFLAGS)

5.1 关于命令的变量

AR函数库打包程序。默认命令是“ar”
AS汇编语言编译程序。默认命令是“as”
CCC 语言编译程序。默认命令是“cc”
CXXC++语言编译程序。默认命令是“g++”
CO从 RCS 文件中扩展文件程序。默认命令是“co”
CPPC 程序的预处理器(输出是标准输出设备)。默认命令是“$(CC) –E”
FCFortran 和 Ratfor 的编译器和预处理程序。默认命令是“f77”
GET从 SCCS 文件中扩展文件的程序。默认命令是“get”
LEXLex 方法分析器程序(针对于 C 或 Ratfor)。默认命令是“lex”
PCPascal 语言编译程序。默认命令是“pc”
YACCYacc 文法分析器(针对于 C 程序)。默认命令是“yacc”
YACCRYacc 文法分析器(针对于 Ratfor 程序)。默认命令是“yacc –r”
MAKEINFO转换 Texinfo 源文件(.texi)到 Info 文件程序。默认命令是“makeinfo”
TEX从 TeX 源文件创建 TeX DVI 文件的程序。默认命令是“tex”
TEXI2DVI从 Texinfo 源文件创建军 TeX DVI 文件的程序。默认命令是“texi2dvi”
WEAVE转换 Web 到 TeX 的程序。默认命令是“weave”
CWEAVE转换 C Web 到 TeX 的程序。默认命令是“cweave”
TANGLE转换 Web 到 Pascal 语言的程序。默认命令是“tangle”
CTANGLE转换 C Web 到 C。默认命令是“ctangle”
RM删除文件命令。默认命令是“rm –f”

5.2 关于参数的变量

ARFLAGS函数库打包程序 AR 命令的参数。默认值是“rv”
ASFLAGS汇编语言编译器参数。(当明显地调用“.s”或“.S”文件时)
CFLAGSC 语言编译器参数
CXXFLAGSC++语言编译器参数
COFLAGSRCS 命令参数
CPPFLAGSC 预处理器参数。( C 和 Fortran 编译器也会用到)
FFLAGSFortran 语言编译器参数
GFLAGSSCCS “get”程序参数
LDFLAGS链接器参数。(如:“ld”)
LFLAGSLex 文法分析器参数
PFLAGSPascal 语言编译器参数
RFLAGSRatfor 程序的 Fortran 编译器参数
YFLAGSYacc 文法分析器参数

二、静态模式

1. 静态模式作用

在 Makefile 中,一个规则中可以有多个目标,规则所定义的命令对所有的目标有效,不同的目标可以根据目标文件的名字来自动构造出依赖文件。一个具有多目标的规则相当于多个规则,这样使用多目标可以使 Makefile 文件变得简洁。

2. 语法格式

makefile
<targets ...>: <target-pattern>: <prereq-patterns ...>
<Tab>[option]<commands>

参数说明

参数说明
targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。
target-pattern是指明了 targets 的模式,也就是的目标集模式。
prereq-patterns是目标的依赖模式,它对 targets-pattern 形成的模式再进行一次依赖目标的定义。
<Tab>表示命令的开始(命令前边一定要有Tab)
option@输出的信息中,不要显示此行命令(make执行过程中,默认会显示所执行的命令)。
-忽略当前此行命令执行时候所遇到的错误。如果不忽略,make在执行命令的时候,如果遇到error,会退出执行的,加上减号后,即便此行命令执行中出错,比如删除一个不存在的文件等,那么也会继续执行make。
commandmake 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行

这样描述这三个东西,可能还是没有说清楚,还是举个例子来说明一下吧。如果<target-parrtern> 定义成 %.o ,意思是我们的 <target> 集合中都是以 .o 结尾的,而如果我们的 <prereq-parrterns> 定义成 %.c , 意思是对 <target-parrtern> 所形成的目标集进行二次定义,其计算方法是,取 <target-parrtern> 模式中的 % (也就是去掉了 [.o] 这个结尾),并为其加上 [.c] 这个结尾,形成的新集合。

所以,我们的“目标模式”或是“依赖模式”中都应该有 % 这个字符,如果我们的文件名中有 % 那么可以使用反斜杠 \ 进行转义,来标明真实的 % 字符。

3. 实例说明

3.1 示例1

先看一下 书写规则 — 静态规则 这一节的示例:

makefile
objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@

上面的例子中,指明了我们的目标从$object中获取, %.o 表明要所有以 .o 结尾的目标,也就是 foo.o bar.o ,也就是变量 $object 集合的模式,而依赖模式 %.c 则取模式 %.o% ,也就是 foo bar ,并为其加下 .c 的后缀,于是,我们的依赖目标就是 foo.c bar.c 。而命令中的 $<$@ 则是自动化变量, $< 表示第一个依赖文件, $@ 表示目标集(也就是“foo.o bar.o”)。于是,上面的规则展开后等价于下面的规则:

makefile
foo.o : foo.c
    $(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
    $(CC) -c $(CFLAGS) bar.c -o bar.o

试想,如果我们的 %.o 有几百个,那么我们只要用这种很简单的“静态模式规则”就可以写完一堆规则,实在是太有效率了。“静态模式规则”的用法很灵活,如果用得好,那会是一个很强大的功能。再看一个例子:

makefile
files = foo.elc bar.o lose.o

$(filter %.o,$(files)): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
    emacs -f batch-byte-compile $<

$(filter %.o,$(files))表示调用Makefile的filter函数,过滤$files集,只要其中模式为%.o的内容。其它的内容,就不用多说了。这个例子展示了Makefile中更大的弹性。

3.2 示例2

说真的,看完 跟我一起写Makefile 1.0 文档 的上关于静态模式的语法后,它其实提供了一个示例帮助理解,我们自己再写一个。还是使用笔记开头的几个测试文件,我们修改 makefile 内容如下:

makefile
OBJ = main.o test1.o test2.o

main: ${OBJ}
	gcc $(OBJ) -o main

$(OBJ):%.o:%.c
	$(CC) -c $(CFLAGS) $< -o $@

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

然后我们在终端执行 make 命令,会看到如下信息输出:

shell
cc -c  main.c -o main.o
cc -c  test1.c -o test1.o
cc -c  test2.c -o test2.o
gcc main.o test1.o test2.o -o main

指明了我们的目标从 $(OBJ) 中获取, %.o 表明要所有以 .o 结尾的目标,也就是 main.o 、 test1.o 和 test2.o ,也就是变量 $(OBJ) 集合的模式,而依赖模式 %.c 则取模式 %.o 的 % ,也就是 main 、 test1 和 test2 ,并为其加上 .c 的后缀,于是,我们的依赖目标就是 main.c 、 test1.c 和 test2.c 。而命令中的 $< 和 $@ 则是自动化变量, $< 表示所有的依赖目标集(也就是“ main.c 、 test1.c 和 test2.c ), $@ 表示目标集(也就是 main.o 、 test1.o 和 test2.o ),总结一下就是:

OBJ目标的值列表: main.o test1.o test2.o
main: $(OBJ)最终目标,这个最终目标有依赖文件,${OBJ}表示的是就是所依赖的文件集合,下边的是最终目标的生成命令
$(OBJ): %.o: %.c$(OBJ)指明了目标为 OBJ,即 main.o test1.o test2.o
%o表明是所有以 .o 结尾的目标文件,也就是 main.o 、 test1.o 、和 test2.o ,也就是变量 $OBJ 集合的模式,
%c依赖模式 %.c 则取模式 %.o 的 % ,也就是 main 、 test1 、和 test2 ,并为其加上 .c 的后缀,于是,我们的依赖就是 main.c 、 test1.c 、和 test2.c
$<表示所有的依赖目标集(也就是main.c、test1.c、和test2.c)
$@表示目标集(也就是main.o、test1.o、和test2.o)
于是,上面的规则展开后等价于下面的规则:
makefile
OBJ = main.o test1.o test2.o

main: ${OBJ}
	gcc $(OBJ) -o main
main.o: main.c test.h
	$(CC) -c $(CFLAGS) main.c -o main.o
test1.o: test1.c test.h
	$(CC) -c $(CFLAGS) test1.c -o test1.o
test2.o: test2.c test.h
	$(CC) -c $(CFLAGS) test2.c -o test2.o

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

三、模式规则

前边学习隐含规则的时候有提到,一行规则可以使用模式规则来定义,什么是模式规则呢?又该如何使用呢?

1. 匹配符 %

在 Makefile 中 % 被称为匹配符,也可以称之为模式字符。可以使用模式规则来定义一个隐含规则。一个模式规则就好像一个一般的规则,只是在规则中,目标的定义需要有 % 字符。

make 命令允许对文件名进行类似正则运算的匹配,主要用到的匹配符是 % ,它可以匹配任何非空字符串(其实就是表示一个或多个任意字符),主要应用在模式规则中。在依赖目标中同样可以使用 % ,只是依赖目标中的 % 的取值,取决于其目标。

有一点需要注意的是, % 的展开发生在变量和函数的展开之后,变量和函数的展开发生在make载入 Makefile时,而模式规则中的 % 则发生在运行时。

2. 模式规则

2.1 语法格式

首先,还是要来了解以下什么叫模式规则。模式规则的一般格式如下:

shell
%.o : %.c
<Tab>[option]command

参数说明

%.o表示匹配所有的 .o 文件
%.c表示匹配所有的 .c 文件
<Tab>表示命令的开始(命令前边一定要有Tab)
option@输出的信息中,不要显示此行命令(make执行过程中,默认会显示所执行的命令)。
-忽略当前此行命令执行时候所遇到的错误。如果不忽略,make在执行命令的时候,如果遇到error,会退出执行的,加上减号后,即便此行命令执行中出错,比如删除一个不存在的文件等,那么也会继续执行make。
commandmake 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行

【注意】

(1)模式规则中,至少在规则的目标定义中要包含 % ,否则,就是一般的规则。

(2)模式字符 % 的匹配和替换发生在规则中所有变量和函数引用展开之后,变量和函数的展开一般发生在 make 读取 Makefile 时,而模式规则中的 % 的匹配和替换则发生在 make 执行时。

(3)目标中的 % 定义表示对文件名的匹配, % 表示长度任意的非空字符串。例如: %.c 表示以 .c 结尾的文件名(文件名的长度至少为 3 ), 而 s.%.c 则表示以 s. 开头, .c 结尾的文件名(文件名的长度至少为 5 )。

(4)如果 % 定义在目标中,那么,目标中的 % 的值决定了依赖目标中的 % 的值,也就是说,目标中的模式的 % 决定了依赖目标中 % 的样子。例如, %.o 匹配到了文件 main.o ,那么, %.c 就代表着 main.c 。

2.2 模式规则示例

2.2.1 示例1
makefile
%.o : %.c
	$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

上边的规则将把所有的 .c 文件都编译成 .o 文件。$@ 表示所有的目标的挨个值,$< 表示了所有依赖目标的挨个值。

2.2.2 示例2

下面的这个例子中有两个目标是模式的:

makefile
%.tab.c %.tab.h: %.y
	bison -d $<

这条规则告诉 make 把所有的 .y 文件都以 bison -d .y 执行,然后生成 .tab.c 和 . tab.h 文件。有如下Makefile:

makefile
CC = gcc
CFLAGS = -Wall

# 1. 最终目标:foo 依赖于两个目标文件
foo: parse.tab.o scan.o
	$(CC) $^ -o $@

# 2. 模式规则:处理通用的 .c 到 .o 的编译
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# 3. 模式规则替代特定规则:同时生成 .tab.c 和 .tab.h
# 注意:这是一个多目标规则
%.tab.c %.tab.h: %.y
	bison -d $<

# 4. 特定依赖:scan.o 除了依赖 scan.c,还依赖 parse.tab.h
scan.o: parse.tab.h

clean:
	rm -f foo *.o parse.tab.c parse.tab.h

如果我们的执行程序 foo 依赖于文件 parse.tab.o 和 scan.o ,并且文件 scan.o 依赖于文件 parse.tab.h ,如果 parse.y 文件被更新了,那么根据上述的规则,bison -d parse.y 就会被执行一次,于是,parse.tab.o 和 scan.o 的依赖文件就齐了。(假设, parse.tab.o 由 parse.tab.c 生成,和 scan.o 由 scan.c 生成,而 foo 由 parse.tab.o 和 scan.o 链 接生成,而且 foo 和其 .o 文件的依赖关系也写好,那么,所有的目标都会得到满足)

2.3 模式规则要求

模式规则与普通规则类似,但是模式规则有以下要求:

2.3.1 目标名

对于目标名来说,目标名中需要包含一个模式字符 % ,包含有模式字符 % 的目标被用来匹配一个文件名。在目标文件名中 % 匹配的部分称为,比如,目标名为 %.o ,匹配到了一个 main.o 文件,那么 % 就代表 main ,也就是茎。

若目标模式中包含斜杠(也就是目录部分),在进行目标文件匹配时,文件名中包含的目录字符串在匹配之前被移除,只进行基本文件名的匹配;匹配成功后,再将目录加入到匹配之后的字符串之前形成茎。

例如,目标模式为 m%n ,文件 src/main 和这个目标模式相匹配,那么茎就是 src/ai 。模式规则中依赖文件的产生是:首先使用茎的非目录部分( ai )替代依赖文件中的模式字符 % ,之后再将目录部分( src/ )加入到形成的依赖文件名之前构成依赖文件的全路径名。这里如果模式规则的依赖模式为 t%t ,则那么目标 src/main 对应的依赖文件就为 src/tait 。

2.3.2 依赖文件

依赖文件中同样可以使用 % ,依赖文件中模式字符 % 的取值情况由目标中的 % 来决定。例如,对于模式规则 %.o : %.c ,它表示的含义是:所有的 .o 文件依赖于对应的 .c 文件。例如,如果要生成的目标 %.o 是 a.o b.o 那么 %.c 就是 a.c b.c 。

模式规则中的依赖文件也可以不包含模式字符 % 。当依赖文件名中不包含模式字符 % 时,其含义是所有符合目标模式的目标文件都依赖于一个指定的文件(例如: %.o : test.h ,表示所有的 .o 文件都依赖于头文件 test.h )。

2.3.3 多目标

一个模式规则可以存在多个目标。多目标的模式规则和普通多目标规则有些不同:普通多目标规则的处理是将每一个目标作为一个独立的规则来处理,所以多个目 标就就对应多个独立的规则(这些规则各自有自己的命令行,各个规则的命令行可能相同)。

对于多目标模式规则来说,所有规则的目标共同拥有依赖文件和规则的命令行,当文件符合多个目标模式中的任何一个时,规则定义的命令就有可能将会执行;因为多个目标共同拥有规则的命令行,因此一次命令执行之后,规则不会再去检查是否需要重建符合其它模式的目标。

  • main.c
c
#include <stdio.h>

int main(int argc, const char *argv[]) {
	printf("This is main file!\n");
	return 0;
}
  • test.c
c
#include <stdio.h>

int main(int argc, const char *argv[]) {
	printf("This is test file!\n");
	return 0;
}
  • Makefile
makefile
Objects = main.o test.o
CFLAGS := -Wall
%x : CFLAGS += -g
%.o : CFLAGS += -O2
%.o %.x : %.c
	$(CC) $(CFLAGS) $< -o $@

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

然后那我们执行 make main.o main.x 时,会看到只有一个文件 main.o 被创建了,同时 make 会有以下提示,

shell
cc -Wall -O2 main.c -o main.o
make: 对“main.x”无需做任何事。

但是实际上, main.x 文件并未被创建。这个例子表明了多目标的模式规则在 make 处理时是作为一个整体来处理的,这是多目标模式规则和多目标的普通规则的不同之处。

注意

(1)模式规则在 Makefile 中的顺序需要注意,当一个目标文件符合多个模式规则的目标时, make 将会按照第一个找到的作为重建它的规则。

(2)在 Makefile 中指定的模式规则会覆盖隐含的模式规则。就是说在 Makefile 中明确指定的会替代隐含的模式规则。

(3)依赖文件存在或者被提及的规则,优先于那些需要使用隐含规则来创建其依赖文件的规则。

3. 实例说明

其实这个实例与静态模式的实例是一样的,静态模式的语法是建立在模式规则的基础上的,依靠模式规则完成静态模式语法。还是使用笔记开头的几个测试文件,我们修改 makefile 内容如下:

makefile
OBJ = main.o test1.o test2.o

main: ${OBJ}
	gcc $(OBJ) -o main

$(OBJ):%.o:%.c
	$(CC) -c $(CFLAGS) $< -o $@

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

然后我们在终端执行 make 命令,会看到如下信息输出:

shell
cc -c  main.c -o main.o
cc -c  test1.c -o test1.o
cc -c  test2.c -o test2.o
gcc main.o test1.o test2.o -o main

指明了我们的目标从 $(OBJ) 中获取, %.o 表明要所有以 .o 结尾的目标,也就是 main.o 、 test1.o 和 test2.o ,也就是变量 $(OBJ) 集合的模式,而依赖模式 %.c 则取模式 %.o 的 % ,也就是 main 、 test1 和 test2 ,并为其加上 .c 的后缀,于是,我们的依赖目标就是 main.c 、 test1.c 和 test2.c 。而命令中的 $< 和 $@ 则是自动化变量, $< 表示所有的依赖目标集(也就是“ main.c 、 test1.c 和 test2.c ), $@ 表示目标集(也就是 main.o 、 test1.o 和 test2.o ),总结一下就是:

OBJ目标的值列表: main.o test1.o test2.o
main: $(OBJ)最终目标,这个最终目标有依赖文件,${OBJ}表示的是就是所依赖的文件集合,下边的是最终目标的生成命令
$(OBJ): %.o: %.c$(OBJ)指明了目标为 OBJ,即 main.o test1.o test2.o
%o表明是所有以 .o 结尾的目标文件,也就是 main.o 、 test1.o 、和 test2.o ,也就是变量 $OBJ 集合的模式,
%c依赖模式 %.c 则取模式 %.o 的 % ,也就是 main 、 test1 、和 test2 ,并为其加上 .c 的后缀,于是,我们的依赖就是 main.c 、 test1.c 、和 test2.c
$<表示所有的依赖目标集(也就是main.c、test1.c、和test2.c)
$@表示目标集(也就是main.o、test1.o、和test2.o)

于是,上面的规则展开后等价于下面的规则:

makefile
OBJ = main.o test1.o test2.o

main: ${OBJ}
	gcc $(OBJ) -o main
main.o: main.c test.h
	$(CC) -c $(CFLAGS) main.c -o main.o
test1.o: test1.c test.h
	$(CC) -c $(CFLAGS) test1.c -o test1.o
test2.o: test2.c test.h
	$(CC) -c $(CFLAGS) test2.c -o test2.o

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

四、Makefile 通配符

Makefile 可以使用 Shell 命令,所以 Shell 中的通配符在 Makefile 中也同样适用。

1. 常用通配符

通配符使用说明
*匹配0个或者是任意个字符
?匹配任意一个字符
[]我们可以指定匹配的字符放在 "[]" 中

注意

(1)通配符可以用在命令中,例如,

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

(2)通配符可以用在规则中,例如,

makefile
main:*.c
    gcc -o $@ $^

但是,不可以通过引用变量的方式来使用。例如,

makefile
# 以下是不被允许的
OBJ=*.c
main:${OBJ}
    gcc -o $@ $^

如果我们就是相要通过引用变量的话,我们要使用一个函数 wildcard ,这个后边学习到函数的时候会详细说明。

2. % 与 *

上边学习模式规则的时候,我们接触到了 % ,它可以匹配任意非空字符串, * 也可以用于规则中来匹配字符串,那么它们是完全一样的嘛?当然不是一样的啦。其实我们可以尝试一下以下两种规则有什么不同,我们还是使用笔记开头的测试文件。

2.1 % 示例

Makefile 文件内容如下:

makefile
OBJ = main.o test1.o test2.o
# 可执行程序 main
main: ${OBJ}
	gcc $(OBJ) -o main

%.o: %.c
	gcc -c $< -o $@

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

运行 make 命令后,会正常生成所有文件。终端输出的信息如下:

shell
gcc -c main.c -o main.o
gcc -c test1.c -o test1.o
gcc -c test2.c -o test2.o
gcc main.o test1.o test2.o -o main

接下来我们分析一下这个文件中的 %.o: %.c , %.o 匹配所有的 .o 目标文件 main.o 、 test1.o 和 test2.o ,然后得到相应的茎,也就是 % 所匹配的内容 main 、 test1 和 test2 ,然后后边的 %.c 就分别代表 main.c 、 test1.c 和 test2.c 。

2.2 * 示例

Makefile 文件内容如下:

makefile
OBJ = main.o test1.o test2.o
# 可执行程序 main
main: ${OBJ}
	gcc $(OBJ) -o main

*.o: *.c
	gcc -c $< -o $@

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

运行 make 命令后,会正常生成所有文件。终端输出的信息如下:

shell
cc    -c -o main.o main.c
cc    -c -o test1.o test1.c
cc    -c -o test2.o test2.c
gcc main.o test1.o test2.o -o main

会发现,这个输出信息好奇怪啊,为什么是 cc 开头,我的命令不是 gcc 嘛,据推测,它默认还是使用了隐含规则来生成文件。不过我们依然可以分析一下 * 的作用, *.c 就表示只要是 .c 文件,全都是我要找的,找到后就进行编译。

五、隐含规则搜索算法

这段说真的,我没看懂,但是可能对之后理解或者编写 Makefile 有一定的帮助,笔记记在这里慢慢理解把,哈哈。

比如我们有一个目标叫 T 。下面是搜索目标 T 的规则的算法。请注意,在下面,我们没有提到后缀规则,原因是,所有的后缀规则在 Makefile 被载入内存时,会被转换成模式规则。如果目标是 archive(member) 的函数库文件模式,那么这个算法会被运行两次,第一次是找目标 T ,如果没有找到的话,那么进入第二次,第二次会把 member 当作 T (也就是目标) 来搜索。

  • 1、把 T 的目录部分分离出来。叫 D ,而剩余部分叫 N 。

例如:若 T 是 src/foo.o ,那么, D 就是 src/ , N 就是 foo.o 。

  • 2、创建所有匹配于 T 或是 N 的模式规则列表。

  • 3、如果在模式规则列表中有匹配所有文件的模式,如 % ,那么从列表中移除其它的模式。

  • 4、移除列表中没有命令的规则。

  • 5、对于第一个在列表中的模式规则:

(1)推导其"茎" S , S 应该是 T 或是 N 匹配于模式中 % 非空的部分。

(2)计算依赖文件。把依赖文件中的 % 都替换成"茎" S 。如果目标模式中没有包含斜框字符,而把 D 加在第一个依赖文件的开头。

(3)测试是否所有的依赖文件都存在或是理当存在。(如果有一个文件被定义成另外一个规则的目标文件,或者是一个显式规则的依赖文件, 那么这个文件就叫"理当存在")。

(4)如果所有的依赖文件存在或是理当存在,或是就没有依赖文件。那么这条规则将被采用,退出该算法。

  • 6、如果经过第 5 步,没有模式规则被找到,那么就做更进一步的搜索。对于存在于列表中的第一个模式规则

(1)如果规则是终止规则,那就忽略它,继续下一条模式规则。

(2)计算依赖文件。 (同第 5 步)

(3)测试所有的依赖文件是否存在或是理当存在。

(4)对于不存在的依赖文件,递归调用这个算法查找他是否可以被隐含规则找到 。

(5)如果所有的依赖文件存在或是理当存在,或是就根本没有依赖文件。那么这条规则被采用,退出该算法。

  • 7、如果没有隐含规则可以使用,查看 .DEFAULT 规则,如果有,采用,把 .DEFAULT 的命令给 T 使用。一旦规则被找到,就会执行其相当的命令,而此时,我们的自动化变量的值才会生成。