Featured image of post C 52_C语言Makefile基础详解

C 52_C语言Makefile基础详解

一、Makefile 基础

1.1 make 与 Makefile

在学习 Linux 下的 C/C++ 编程时,一般都是直接通过 gcc 对源文件进行编译的,通过指定 gcc 的参数来指定生成什么样的文件、使用哪个库、在哪个路径搜索等等。但是,在实际的项目工程中包含数百、上千个(甚至更多)源文件,并且不同的源文件包含了不同的库(动态库、静态库、标准库等),又甚至不同的非标准库存放在不同的目录,或者有的源文件要用到多线程。这样的话,如果像初学时一样,在 shell 下使用 gcc 命令编译异的话,要使用无数的参数去指定不同路径,不同的链接库等等。这么繁琐复杂的命令显然是不太合理的,并且每次编译都要来这么一遍大大消耗了时间成本。make命令 和 Makefile 就是用来解决型项目开发过程中的编译问题的。

make 工具可以认为是一个智能批处理工具,make 工具本身并么有编译和链接的功能,而是用类似于批处理的方式通过调用 Makefile 文件中指定的命令来进行编译和链接的;它解释 Makefile 中的规则指令,Makefile 文件负责向 make 提供如何去执行的规则。在 Makefile 文件中描述了整个工程所有文件的编译顺序、编译规则等。

Makefile 有自己的书写格式、关键字、函数,就像任何一门编程语言有自己的语法一样。而且在 Makefile 中可以使用 shell 的命令来完成某些工作,也就是说 Makefile 中可以使用 shell 命令,比如说,编译完成后删除所有的中间文件,可以使用 rm -f *.o 这样的 shell 命令。

在一个工程中,源文件很多,按类型、功能、模块分别被存放在若干目录中,需要按一定顺序、规则进行编译,这时就需要使用到 Makefile。

Makefile 定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要重新编译,如何进行链接等操作。

Makefile 就是“自动化编译”,告诉 make 命令如何编译和链接。

Makefile 是 make 工具的配置脚本,默认情况下,make 命令会在当前目录下寻找该文件(按顺序找寻文件名为 “Makefile”、“makefile”、“GNUmakefile” 的文件)。也可使用 make 的 -f 参数指定 Makefile 的路径,如 make -f /home/user/Makefilemake -f make.debug

Makefile 在绝大多数的集成开发环境中也都在使用的,只不过我们看不到而已,可以说,Makefile 几乎已经成为一种工程编译的基本方法。

通过 Makefile 可以制定好相应的编译与连接规则,先编译哪个文件后编译哪个文件、哪个需要编译哪个不需要编译、如何链接、如何生成、要生成什么文件等等全部都在 Makefile 文件中提前制定好,在编译的时候只需要使用 make 工具,执行 make 命令就可以了。

另外,在项目开发中难免会对源码进行修修改改,如果每次修改都要重新编译所有的源文件,那么将浪费大量的时间,我们可以在 Makefile 中制定规则,只去编译被修改的源文件,其他文件不需要重新编译,。总之,有了 Makefile 大型项目的编译效率将大大提高。

1.2 Makefile 文件格式

Makefile 文件中记录了工程的构建规则及相关辅助信息,内容包含 显示规则隐晦规则变量定义文件知识注释 等信息。

Makefile 文件由一系列的规则构成,每条规则的格式如下:

1
2
3
4
5
<target(目标)> : <prerequisites(前置条件)>
[tab]	<commands(命令)>

# 或者 
<target(目标)> : <prerequisites(前置条件)> ; <commonds(命令)>

目标 是必须的,不可省略;前置条件命令 都是可选的,但是两者之中必须至少存在一个。

若 prerequisites 与 commonds 在同一行,需要用 ; 分隔。若 prerequisites 与 commonds 不在同一行,则 commonds 前面需要用 Tab 键开头。

Makefile 中有三要素:

  • 目标(指明要干什么,即运行 make 后生成什么)
  • 依赖(用什么去生成)
  • 命令(如何生成目标)

在 Makefile 中,目标和依赖是通过规则(rule)来表达的。目标(指明要干什么,即运行 make 后生成什么)、依赖(用什么去生成)和 命令(如何生成目标)这三个要素组成一个规则。实际上,三要素中必不可少的是目标 和 命令,依赖可以没有,这一点在后面的实战编写 Makefile 的时候会有体现。

Makefile的一个规则是由目标(targets)、先决条件(prerequisites)以及命令(commands)所组成的。需要指出的是,目标和先决条件之间表达的就是依赖关系(dependency),这种依赖关系指明在构建目标之前,必须保证先决条件先满足(或构建)。而先决条件可以是其它的目标,当先决条件是目标时,其必须先被构建出来。还有就是一个规则中目标可以有多个,当存在多个目标,且这一规则是Makefile 中的第一个规则时,如果运行make 命令不带任何目标,那么规则中的第一个目标将被视为是缺省目标。

对于 Makefile 的命名,可以是 Makefile 也可以是 makefile 或者 GNUmakefile,三种方法都可以,但也只能是这三种。这样在 shell 中执行 make 命令就会直接使用这个 makefile 文件。当然,如果你取了其它名字也是可以的,不过要在 make 命令的时候要显示指出文件,如下所示:

1
make -f make.linux

Mikefile 示例1:

1
2
3
4
all:
	@echo "hello all"
test:
	@echo "hello test"

需要注意的是echo 前面必须只有 Tab(即键盘TAB键),且至少有一个 Tab,而不能用空格代替。

Tips: makefile 中所有以 Tab 开始的行,make 都会交给 shell 去处理,所以在命令的前面一定要以 Tab 键开头。

执行结果:

1
2
3
4
5
6
[root@~ gunmakefile]# make 
hello all 
[root@~ gunmakefile]# make test 
hello test 
[root@~ gunmakefile]# make all 
hello all

Makefile 中的 all 就是目标,目标放在 : 的前面,其名字可以是由字母和下划线_组成 。echo "hello all" 就是生成目标的命令,这些命令可以是任何可以在你的环境中运行的命令以及 make 所定义的函数等等。

all 目标在这里就是代表希望在终端上打印出“hello all”,有时目标会是一个比较抽象的概念。all 目标的定义,其实是定义了如何生成 all 目标,这也称之为规则(rule)。

Mikefile 示例2,在前面示例的基础上调换目标位置:

1
2
3
4
test:
	@echo "hello test"
all:
	@echo "hello all"

执行结果:

1
2
3
4
5
6
[root@~ gunmakefile]# make 
hello test
[root@~ gunmakefile]# make test 
hello test 
[root@~ gunmakefile]# make all 
hello all

由以上示例可知,一个 Makefile 中可以定义多个目标,上述示例中对于了 all 、test 连个目标。调用 make 命令时,得告诉它我们的目标是什么,即要它干什么。当没有指明具体的目标是什么时,那么 make 以 Makefile 文件中定义的第一个目标作为这次运行的目标,这“第一个”目标也称之为默认目标(和是不是all没有关系)

当 make 确定目标后,从 Makefile 中先找到定义目标的规则,然后运行规则中的命令来达到构建目标的目的。上面所示例的 Makefile 中,每一个规则中都只有一条命令,而实际的 Makefile,每一个规则可以包含很多条命令。

Tips: 命令前加了一个 @,这一符号告诉 make,在运行时不要将这一行命令显示出来。

Mikefile 示例3:

1
2
3
4
all:makefile
	@echo "hello all"
test:
	@echo "hello test"

执行结果:

1
2
3
4
5
6
7
8
[root@~ gunmakefile]# make 
hello test 
hello all 
[root@~ gunmakefile]# make test 
hello test 
[root@~ gunmakefile]# make all 
hello test 
hello all

当运行 make 时,test 目标也被构建了。这里要引入 Makefile 中依赖关系的概念,all 目标后面的 test 是告诉 make,all 目标依赖 test 目标,这一依赖目标在 Makefile 中又被称之为先决条件

出现这种目标依赖关系时,make工具会按从左到右的先后顺序先构建规则中所依赖的每一个目标。上面示例3中表名,如果希望构建 all 目标,那么 make 会在构建它之前先构建 test 目标(因为test 是all 的依赖条件),这就是为什么称之为先决条件的原因。

1.3 Makefile 的工作原理

在执行 make 命令时,首先,make 会先去比较目标文件和依赖文件的修改日期,如果依赖文件的日期要比目标文件的日期新,或者目标文件不存在,那么 make 就会执行后面的命令。假如说在目标的后面没有依赖,比如我们经常用伪目标 clean 去清除中间文件,当 make 发现先冒号后面没有依赖的时候,它默认是不会执行后面的命令的,除非在 make 后面显示的指出这个目标的名字,这也就是我们经常使用的 make clean 命令。

总结来说,Makefile 的工作原理可以理解为它是根据依赖去递推的。

make 命令执行过程(不指定目标):

  • 首先 make 工具会在当前目录查找名为 makefile 或 Makefile 的文件,如果我们在 make 命令后面使用 -f 参数指定了文件名,make 就在当前目录查找指定的文件名。
  • 如果找到了 makefile 文件,那么会先查找文件中的第一个目标进行如下处理:
    • 如果目标不存在,检查目标是否有依赖:
      • 如果目标没有依赖,直接执行生成目标的命令
      • 如果目标存在依赖,则根据依赖的顺序,递推处理依赖,处理完依赖后执行生成目标的命令
    • 如果目标已经存在,则检查目标是否有依赖:
      • 如果目标没有依赖,提示 make: “目标”是最新的。 后退出
      • 如果目标存在依赖,则根据依赖的顺序,递推处理依赖,处理完依赖后,判断依赖文件的更新时间比目标文件的更新时间新,那么就执行后面的命令重新生成目标文件
  • 如果上一个目标文件的依赖存在,那么 make 会递推查找依赖文件的依赖,然后重复上面的操作。

所以说,makefile 是根据依赖一层一层递推的,不停的去递推寻找依赖。make 只负责在 makefile 中递推寻找依赖,并根据依赖执行命令,而不关心编译是否成功,只要最终的依赖可以找到,就能执行成功,如果最终的依赖没找到,那么 make 就会直接退出。

而对于伪目标的执行,可以直接在 make 后面指定目标,这样即使目标后面没有依赖,也会执行命令。

正是 make 的这种依赖递推查找特性,以及根据更新时间决定是否生成的特性,我们可以把依赖分解,这样就能做到某单个源文件修改可以只编译这一个源文件,而不必所有源文件都重新编译。

1.4 多文件编译

目前有两个源文件,需要编译成一个应该程序:

1
2
3
4
5
6
// foo.c
#include <stdio.h>
void foo()
{
    printf("This is foo() \n");
}
1
2
3
4
5
6
7
// main.c
extern void foo();
int main()
{
    foo();
    return 0;
}

编译时它们的依赖关系可总结为如下图所示: 根据依赖关系编写 Makefile :

1
2
3
4
5
6
7
8
all: main.o foo.o
	gcc -o simple main.o foo.o
main.o: main.c
	gcc -o main.o -c main.c
foo.o: foo.c
	gcc -o foo.o -c foo.c
clean:
	rm simple main.o foo.o

我们增加了一个 clean 目标用于删除所生成的文件,包括目标文件和 simple 可执行程序,这在现实的项目中很是常见。

值得注意的是,如果执行两次make会怎么样?执行效果如下:

1
2
3
4
5
6
7
$make
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o

$make
gcc -o simple main.o foo.o

第二次编译并没有构建目标文件的动作,但有构建simple可执行程序的动作,我们需要了解 make 是如何决定哪些目标(这里是文件)是需要重新编译的。

为什么 make 会知道我们并没有改变 main.c 和 foo.c 呢?

答案是通过文件的时间戳,当 make 在运行时,make 处理一个规则采用的方法是:判断先决条件和目标的时间,如果先决条件中相关的文件的时间戳大于目标的时间戳,即先决条件中的文件比目标更新,则说明先决条件有变化,那么需要运行该规则当中的命令重新构建目标。

这一规则会运用到所有在 make 时指定的目标及依赖树中的每一个规则。比如,对于 simple 项目,其依赖树中包括三个规则,make 会检查所有三个规则当中的目标(文件)与先决条件(文件)之间的时间先后关系,从而来决定是否要重新创建规则中的目标。

为什么会第二次make时会执行 gcc -o simple main.o foo.o 呢?

因为 all文件 在我们的编译过程中并不生成,即 make 在第二次编译时找不到 all 文件,所以又重新编译了一遍。如果把 all改为 simple,那就是我们所期望的结果:

1
2
3
4
5
6
7
8
simple: main.o foo.o
	gcc -o simple main.o foo.o
main.o: main.c
	gcc -o main.o -c main.c
foo.o: foo.c
	gcc -o foo.o -c foo.c
clean:
	rm simple main.o foo.o

执行结果:

1
2
3
4
5
6
7
[root@~ gunmakefile]# make
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o

[root@~ gunmakefile]# make
make: `simple` is up to date.

另外,对于 make 工具,一个文件是否改动不是看文件大小,而是其时间戳。比如用 touch 命令来改变文件的时间戳就行了,这相当于模拟了对文件进行了一次编辑,而不需真正对其进行编辑。make 发现了 foo.c 需要重新被编译,而这,最终也导致了 foo.o 和 simple 需要重新被编译。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[root@~ gunmakefile]# ls -l foo.c
-rw-rw-r-- 1 root root 75 4月  30 14:15 foo.c

[root@~ gunmakefile]# touch foo.c
[root@~ gunmakefile]# ls -l foo.c
-rw-rw-r-- 1 root root 75 4月  30 15:42 foo.c

[root@~ gunmakefile]# make
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o

需要特别指出的是,在规则中有些依赖虽然可以省略不写,但是这将导致依赖的更新不会被目标所识别,如下:

1
2
3
4
5
6
7
8
9
# simple 的 依赖 不可以省略,如果此处省略了依赖,将不会编译生成 .o 文件,影响目标的生成
simple: main.o foo.o
	gcc -o simple main.o foo.o
main.o: # 此处的依赖可以省略,不会影响目标的生成
	gcc -o main.o -c main.c
foo.o:  # 此处的依赖可以省略,不会影响目标的生成
	gcc -o foo.o -c foo.c
clean:  # 此处的依赖可以省略,不会影响目标的生成
	rm simple main.o foo.o

在上面的 make 规则中, simple 目标依赖 main.o 和 foo.o 如果被省略,make 将不会推导出该 simple目标 的先决条件 main.o 和 foo.o 两个子目标 并 先生成 main.o 和 foo.o 目标, 而直接执行 gcc -o simple main.o foo.o,此时并不存在 main.o 和 foo.o 两个文件,这将导致 simple 目标命令执行以失败退出。

main.o 和 foo.o 两个目标的依赖可以省略,但是当省略依赖后,对 main.c 或 foo.c 的修改,将不会 自动更新 main.o 或 foo.o,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[root@~ gunmakefile]# ls -l
总用量 12
-rw-r--r--   1 root root   75 4月   1 17:08 foo.c
-rw-r--r--   1 root root   80 4月   1 17:17 main.c
-rw-r--r--   1 root root  144 4月   1 17:29 Makefile
[root@~ gunmakefile]# make
gcc -o main.o -c main.c
gcc -o foo.o -c foo.c
gcc -o simple main.o foo.o
[root@~ gunmakefile]# touch foo.c 
[root@~ gunmakefile]# make
make: “simple”是最新的。
[root@~ gunmakefile]# 

虽然 touch foo.c 更新了 foo.c,但是由于 foo.o 目标的依赖被省略,使得 make 不知道 foo.o 的依赖已经更新,而不会重新执行命令生成更新后的目标。

1.5 伪目标 .PHONY

在前面的示例项目中,假设在程序所在的目录下面有一个 clean 文件,这个文件也可以通过 touch 命令来创建。创建以后,运行 make clean 命令:

1
2
3
4
5
6
7
8
9
[root@~ gunmakefile]# ls -l clean
ls: cannot access clean: No such file or directory
 
[root@~ gunmakefile]# touch clean
[root@~ gunmakefile]# ls -l clean
-rw-rw-r-- 1 fly fly 65 4月  30 16:42 clean

[root@~ gunmakefile]# make clean
make: 'clean' is up to date.

会发现 make 总是提示 clean 文件是最新的,而不是按我们所期望的那样进行文件删除操作。这是因为 make 将 clean 当作文件,且在当前目录找到了这个文件,加上 clean 目标没有任何先决条件,所以,当我们要求 make 为我们构建 clean 目标时,它就会认为 clean 是最新的。

对于这种情况,在现实中也难免存在所定义的目标与所存在的文件是同名的,采用 Makefile 如何处理这种情况呢?

Makefile 中的伪目标(phony target)可以解决这个问题。伪目标可以采用 .PHONY 关键字来定义,需要注意的是其必须是大写字母。伪目标 用法如下:

1
2
3
4
5
6
7
8
9
.PHONY: clean
simple:main.o foo.o
	gcc -o simple main.o foo.o
main.o:
	gcc -o main.o -c main.c
foo.o:
	gcc -o foo.o -c foo.c
clean:
	rm simple main.o foo.o

将 clean 变为假目标后的 Makefile,更改后运用 make clean 命令的结果:

1
2
[root@~ gunmakefile]# make clean
rm simple main.o foo.o

采用 .PHONY 关键字声明一个目标后,make 并不会将其当作一个文件来处理,而只是当作一个概念上的目标。对于假目标,我们可以想像的是由于并不与文件关联,所以每一次 make 这个假目标时,其所在的规则中的命令都会被执行。

二、Makefile 中的变量

2.1 Makefile 中的变量简介

在 Makefile 中通过使用变量来使得它更简洁、更具可维护性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.PHONY:clean
CC=gcc
RM=rm
EXE=simple
OBJS=main.o foo.o

$(EXE):$(OBJS)
	$(CC) -o $(EXE) $(OBJS)
main.o:
	$(CC) -o main.o -c main.c
foo.o:
	$(CC) -o foo.o -c foo.c
clean:
	$(RM) $(EXE) $(OBJS)

在 Makefile 中,一个变量的定义很简单,就是一个名字(变量名)后面跟上一个等号,然后在等号的后面放这个变量所期望的值。对于变量的引用,则需要采用 $(变量名) 或者 ${变量名} 这种模式。

如上所示,采用变量的话,当需要更改编译器时,只需更改变量赋值的地方,非常方便,如果不采用变量,那得更改每一个使用编译器的地方,很是麻烦。显然,变量的引入增加了 Makefile 的可维护性。既然定义了一个 CC 变量,当然也可以将 -o 或是 -c 命令参数也定义成为一个变量,因为如果更改了一个编译器,那么很有可能其使用参数也得跟着改变。

变量的赋值就是在变量后面写上值文本字符串,在使用时直接用后面的文本字符串去替换变量本身。Makefile中的变量的赋值方式有四种:

  • = 只用一个 = 符号定义的变量,称之为递归扩展变量(recursively expanded variable), 也称为递归赋值,定义时并不真正赋值,在实际使用时才会进行展开
1
2
3
4
5
6
A=aaa           # A 为 aaa
B=$(A)bbb       # $(A) 不使用其值 aaa 替换,B的值待定
A=AAA           # 使用 AAA 覆盖 A 的旧值 aaa,B的值任然待定
test:
	@echo "A = $(A)"    # A 为 AAA
	@echo "B = $(B)"    # B 的值为 $(A)与bbb的拼接,为 AAAbbb
1
2
3
[root@~ gunmakefile]# make
A = AAA
B = AAAbbb

Tips: 递归扩展变量的引用是递归的。这种递归性有利也有弊。利的方面是最后foo将会被展开。但也存在弊,那就是我们不能对foo变量再采用赋值操作。如下的方式会出现一个死循环:

1
foo=$(foo) -O
  • := 简单赋值,是一种最普通的赋值,立即替换,也就是说在变量赋值的时候,立即把变量展开为后面的值,或者说当前的赋值只对当前的语句有效,和后面对该变量的赋值无影响
1
2
3
4
5
6
A:= aaa         # A 为 aaa
B:=$(A)bbb      # 把 $(A) 使用其值 aaa 替换后, 给赋值B 为 aaabbb
A:=AAA          # 使用 AAA 覆盖 A 的旧值 aaa,B不受影响,仍为  aaabbb
test:
	@echo "A = $(A)"
	@echo "B = $(B)"
1
2
3
[root@~ gunmakefile]# make
A = AAA
B = aaabbb
  • ?= 条件赋值,如果变量是第一次赋值,则赋值生效,否则赋值无效。
1
2
3
4
5
6
7
8
A= aaa
B=$(A)bbb
A?=AAA      # 变量A 已经存在,此处不会再进行赋值操作
C?=CCC      # 变量A 不存在,此处进行赋值操作
test:
	@echo "A = $(A)"
	@echo "B = $(B)"
	@echo "C = $(C)"
1
2
3
4
[root@~ gunmakefile]# make
A = aaa
B = aaabbb
C = ccc
  • += 追加赋值,在变量后面追加一个值,用空格与前面的值分隔开
1
2
3
4
5
6
A= aaa      # A 为 aaa
B=$(A)bbb   # $(A) 不使用其值 aaa 替换,B的值待定
A+=AAA      # A 为 aaa AAA
test:
	@echo "A = $(A)"
	@echo "B = $(B)"    # 变量B 的值为 $(A)与bbb的拼接,为 aaa AAAbbb
1
2
3
[root@~ gunmakefile]# make
A = aaa AAA
B = aaa AAAbbb

2.2 Makefile 自动变量

自动变量 是指 makefile 根据模式规则自动推导的变量,这类变量只能在命令中使用。实际上,自动化变量属于“规则型变量”,这种变量的值依赖于规则的目标和依赖目标的定义。下面是常用的自动化变量列表:

  • $@ 用于表示当前目标,当一个规则中有多个目标时,$@ 所指的是其中任何造成命令被运行的目标。
  • $^ 则表示的是规则中的所有先决条件。
  • $< 表示的是规则中的第一个先决条件。
  • $? 指代比目标更新的所有前置条件,之间用空格分隔。比如,规则为 t:p1 p2,其中 p2 的时间戳比 t 新,那么 $? 就指代 p2。

其它自动化变量列表:

自动化变量 说明
$% 当目标文件是一个静态库文件时起作用,代表静态库的一个成员名,比如目标是 1.a 那么 $% 表示 1.o, $@ 表示 1.a
$+ 类似“$^”,但是它保留了依赖文件中重复出现的文件(主要用在程序链接时库的交叉引用场合),也就是说他也代表所有依赖文件,但是不会去除重复文件
$* 在模式规则和静态模式规则中,代表茎,茎是目标模式中 % 所代表的部分
$(@D) 表示文件的目录部分(不以斜杠结尾),如果 $@ 表示的是 dir/1.c 那么 $(@D) 表示的值就是目录 dir
$(@F) 表示的是文件除目录外的部分即文件名,如果 $@ 表示的是 dir/1.c,那么 $@F 表示的是 1.c
$(*D) $(*F) 分别代表茎中的目录部分和文件名部分
$(%D) $(%F) 当目标是静态库文件时,分别表示库文件成员中的目录部分和文件名部分
$(<D) $(<F) 分别表示第一个依赖文件的目录部分和文件名部分
$(^D) $(^F) 分别表示所有依赖文件的目录部分和文件部分(无重复文件)
$(+D) $(+F) 分别表示所有的依赖文件的目录部分和文件部分(保留了依赖文件中重复出现的文件)
$(?D) $(?F) 分别表示更新的依赖文件的目录部分和文件名部分

我们就可以将simple项目的Makefile改为如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.PHONY:clean
CC=gcc
RM=rm
EXE=simple
OBJS=main.o foo.o

$(EXE):$(OBJS)
	$(CC) -o $@ $^
main.o:main.c
	$(CC) -o $@ -c $^
foo.o:foo.c
	$(CC) -o $@ -c $^
clean:
	$(RM) $(EXE) $(OBJS)

自动变量在对它们还不熟悉时,看起来可能有那么一点吃力,但熟悉了你就会觉得其简捷(洁),那时也会觉得它们好用。

2.3 Makefile 特殊变量

在 Makefile 中有几个特殊变量,可能经常需要用到。

  • 第一个就是 MAKE 变量,它表示的是 make 命令名是什么。当我们需要在 Makefile 中调用另一个 Makefile 时需要用到这个变量,采用这种方式,有利于写一个容易移植的 Makefile。
1
2
3
4
.PHONY: all
all:
	@echo "MAKE = $(MAKE)"
	$(MAKE) -f make.linux   # 在 Makefile 中调用另一个 Makefile
  • 第二个特殊变量则是 MAKECMDGOALS,它表示的是当前用户所输入的 make 目标是什么(从命令行输入的目标,由参数传递给 make 命令的目标)。
1
2
3
4
.PHONY: all clean
all clean:
	@echo "\$$@ = $@"
	@echo "MAKECMDGOALS = $(MAKECMDGOALS)"

执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[root@~ gunmakefile]# make
$@ = all
MAKECMDGOALS =

[root@~ gunmakefile]# make all
$@ = all
MAKECMDGOALS = all

[root@~ gunmakefile]# make clean
$@ = clean
MAKECMDGOALS = clean

[root@~ gunmakefile]# make all clean
$@ = all
MAKECMDGOALS = all clean
$@ = clean
MAKECMDGOALS = all clean

Tips: MAKECMDGOALS 指的是用户输入的目标,当只运行 make 命令时,虽然根据 Makefile 的语法,第一个目标将成为缺省目标,即 all 目标,但 MAKECMDGOALS 仍然是空,而不是 all,这一点需要注意。

2.4 变量及其值的来源

在 Makefile 中我们可以对变量进行定义并赋值。此外,还有其它的地方让 Makefile 获得变量及其值。比如:

  • 对于前面所说到的自动变量,其值是在每一个规则中根据规则的上下文自动获得变量值的。
  • 可以在运行 make 时,在 make 命令行上定义一个或多个变量。在 make 命令行中定义的变量及其值同样在 Makefile 中是可见的。在 make 命令行中定义的变量将覆盖 在Makefile 中所定义的同名变量的值
1
[root@~ gunmakefile]# make var1=value1 var2="value2 val"
  • 变量还可以来自于 Shell 环境,例如采用 Shell 中的 export 命令定义了一个变量后再执行Makefile。
1
2
[root@~ gunmakefile]# export bar=x
[root@~ gunmakefile]# make

2.5 变量引用的高级功能

变量可以在赋值的同时完成后缀替换操作:

1
2
3
4
5
.PHONY:all
foo= a.o b.o c.o
bar:=$(foo:.o=.c)
all:
	@echo "bar=$(bar)"

执行结果:

1
2
[root@~ gunmakefile]# make
bar=a.c b.c c.c

bar 变量中的文件名从 .o 后缀都变成了 .c。这种功能也可以采用 patsubst 函数来实现,与函数相比,这种功能更加的简洁。当然,patsubst 功能更强,而不只是用于替换后缀。

2.6 override 指令

前面了解到,可以采用在 make 命令行上定义变量的方式,使得 Makefile 中定义的变量覆盖掉,从而不起作用。可能,在设计 Makefile 时,并不希望用户将我们在 Makefile 中定义的某个变量覆盖掉,那就得用 override 指令了。

1
2
3
4
5
.PHONY:all
override foo= a.o b.o c.o
bar:=$(foo:.o=.c)
all:
	@echo "bar=$(bar)"

执行:

1
2
[root@~ gunmakefile]# make foo="bb.o cc.o"
bar=a.c b.c c.c

三、makefile 的字符匹配和文件搜索

3.1 字符匹配

字符匹配首先想到的就是通配符,因为 Makefile 中使用的是 shell 中的命令,所以 shell 中的通配符在 Makefile 中也适用。我们在Makefile中使用的通配符主要有两个:

  • *:匹配任意个字符
  • ?:匹配一个字符

比如说,依赖是所有的 .c 文件,就可以用通配符来表示 *.c,但是如果我们在定义变量的时候要使用通配符的话,要注意一点,如果我们直接把 *.c 直接使用等号赋值给变量的话,这个变量会默认去匹配文件名为 *.c 的文件,例如:

1
Src = *.c

Src 变量表示 *.c 文件

要想使变量 Src 表示所有源文件,也就是让 * 作为通配符而不是文件名,需要借助一个函数 wildcard,该函数就是表示通配符的意思,具体使用将在后面的函数章节介绍

1
Src=$(wildcard *.c)

Src变量表示所有后缀为 .c 的文件。

还有一个通配符 [ ] 并不常用,在中括号中可以指定匹配的字符。比如说,[a-z] 表示匹配 az 中任何一个字符。

第二种用于字符匹配的是 %% 字符作用类似于通配符 * ,它和 * 的区别是,模式匹配字符可以对目标文件与依赖文件进行匹配。比如说我们在写 makefile 的时候,经常会写这样的一条规则

1
%.o : %.c

这里的 % 代表的是一个文件名,也就是一个字符串。首先,所有的 .o 文件会组成一个列表,然后挨个被拿出来,% 表示当前拿出来的 %.o 文件的文件名,然后根据文件名 % 来寻找和 .o 文件同名的 %.c 文件,并把取出的 %.o 文件和寻找到的 %.c 文件用于执行后面的命令。这是 makefile 中自动匹配的一种规则。

对于前面的 Makefile,其中存在多个规则用于构建目标文件。比如,main.o 和 foo.o 都是采用不同的规则进行描述的。如果对于每一个目标文件都得写一个不同的规则来描述,太繁了!对于一个大型项目,就更不用说了。Makefile 中的模式就是用来解决这种烦恼的。我们可以把之前的simple项目的Makefile改成这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.PHONY:clean
CC=gcc
RM=rm
EXE=simple
OBJS=main.o foo.o

$(EXE):$(OBJS)
	$(CC) -o $@ $^
%.o:%.c
	$(CC) -o $@ -c $^
clean:
	$(RM) $(EXE) $(OBJS)

与 前一版本的 Makefile 相比,最为直观的改变就是从二条构建目标文件的规则变成了一条。。采用了模式以后,不论有多少个源文件要编译,我们都是应用同一个模式规则的,很显然,这大大的简化了我们编写Makefile的工作。使用了模式规则以后,同样可以用这个 Makefile 来编译或是清除 simple 项目,这与前一版本在功能上是完全一样的。

3.2 文件搜索

默认情况下,make 会在 makefile 文件所在目录进行搜索规则中所用到的文件,如果我们把所有的文件都和 makefile 文件放在同一个目录下,那肯定是没有问题的,但是实际开发中,我们用到的源文件、头文件、库文件可能会根据用途和种类分别位于不同的目录下,所以这就需要有文件搜索的功能。makefile 中文件搜搜主要有两种方法,一个是环境变量 VPATH 一个是关键字 vpath:

  • VPATH 环境变量的用法如下
1
VPATH:=/mkdir1/:/mkdir2/

Tips: 当使用环境变量指定上面的路径后,make 会现在当前目录搜索,然后去目录 /mkdir1/ 搜索,然后再去 /mkdir2/ 搜索,搜索的顺序是先当前目录,然后按照变量赋值中的顺序去搜索。这里的 := 是变量赋值的一种方式,表示在定义时立即展开应用的变量。另外,不同的目录之间要用 : 或者空格隔开。 附:变量赋值的几种方式(后面详细介绍)

  • vpath 关键字

在上面的环境变量中,VPATH 是搜索指定路径的所有文件, vpath 关键字的搜索方式是选择性搜索,使用方法如下:

1
2
3
vpath 1.c /mkdir/	 /mkdir/ 路径下搜索 1.c
vpath 1.c			清除 1.c 的搜索路径
vpath				清除已设置好的所有搜索路径	

四、Makefile中的函数

4.1 Makefile中的函数简介

Makefile 也支持函数,可以通过函数来控制变量,函数的使用和变量类似,需要 $()${} 来标识,如果函数有参数的话直接在函数后面列出参数,参数之间用 , 隔开,比如 $(func arg1, arg2)

函数是 Makefile 中的另一个利器,现在看一看采用函数如何来简化 simple 项目的 Makefile。对于 simple 项目的 Makefile,尽管使用了模式规则,但还有一件比较恼人的事,得在这个Makefile 中指明每一个需要被编译的源程序。对于一个源程序文件比较多的项目,如果每增加或是删除一个文件都得更新 Makefile,其工作量也不可小视!

下面是采用了 wildcardpatsubst 两个函数后 simple 项目的 Makefile。需要注意的是函数的语法形式很特别,不过只要记住其形式就行了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.PHONY:clean
CC=gcc
RM=rm
EXE=simple
SRCS=$(wildcard *.c)

# 把.c替换为.o
OBJS=$(patsubst %.c,%.o,$(SRCS))

$(EXE):$(OBJS)
	$(CC) -o $@ $^
%.o:%.c
	$(CC) -o $@ -c $^
clean:
	$(RM) $(EXE) $(OBJS)

下面根据用途分类来介绍 makefile 中的函数。

4.2 字符串处理函数

1、模式字符替换函数 patsubst

  • 函数原型:
1
$(patsubst <pattern>,<replacement>,<text>)
  • 函数功能:查找 text 中的单词(单词以空格、Tab 或回车换行分隔)是否符合模式 pattern,如果匹配的话,则用 replacement 替换。pattern 可以包括通配符 % ,表示任意长度的字符串。如果 replacement 中也包含 %,那么,replacement 中的这个 % 将是 pattern 中的那个 % 所代表的字符串。
  • 函数返回:返回值为替换后的新字符串。
  • 用法示例:
1
2
3
4
5
.PHONY:all
srcs=foo.c foo2.c main1.c main2.c
objs:=$(patsubst %.c,%.o,$(srcs))
all:
	@echo $(objs)

执行:

1
2
[root@~ gunmakefile]# make
foo.o foo2.o main1.o main2.o

可以看出采用 patsubst 函数进行字符串替换时,我们希望将所有的 .c 文件都替换成 .o 文件。当然,由于 patsubst 函数可以使用模式,所以其也可以用于替换前缀等等,功能更加的强。

2、

addprefix 函数 addprefix 函数是用来在给字符串中的每个子串前加上一个前缀,其形式是:

1
$(addprefix prefix, names...)

示例:

1
2
3
4
5
.PHONY:all
no_dir=foo.c foo2.c main.o
no_dir:=$(addprefix objs/,$(no_dir))
all:
	@echo $(no_dir)

执行:

1
2
[root@~ gunmakefile]# make
objs/foo.c objs/foo2.c objs/main.o

4.3 filter 和 filter-out 函数

filter 函数 用于从一个字符串中,根据模式得到满足模式的字符串,其形式是:

1
$(filter pattern..., text)

示例:

1
2
3
4
5
.PHONY:all
srcs=foo.c foo2.c main.s main.h
srcs:=$(filter %.c %.s,$(srcs))
all:
	@echo $(srcs)

执行:

1
2
[root@~ gunmakefile]# make
foo.c foo2.c main.s

从结果来看,经过 filter 函数的调用以后,source变量中只存在 .c 文件和 .s 文件了,而 .h 文件则被过滤掉了。

filter-out 函数 用于从一个字符串中根据模式滤除一部分字符串,其形式是:

1
$(filter-out pattern..., text)

示例:

1
2
3
4
5
.PHONY:all
srcs=foo.c foo2.c main1.c main2.c main.h
srcs:=$(filter-out main%.c,$(srcs))
all:
	@echo $(srcs)

执行:

1
2
[root@~ gunmakefile]# make
foo.c foo2.c main.h

从结果来看,filter-out 函数将 main1.c 和 main2.c从 src变量中给滤除了。filter 与 filter-out 是互补的。

4.4 patsubst 函数

patsubst 函数是用来进行字符串替换的,其形式是:

1
$(patsubst pattern, replacement, text)

示例:

4.5 strip函数

strip 函数用于去除变量中的多余的空格,其形式是:

1
$(strip string)

示例:

1
2
3
4
5
6
.PHONY:all
srcs=foo.c   foo2.c   main1.c main2.c
objs:=$(strip $(srcs))
all:
	@echo $(srcs)
	@echo $(objs)

执行:

1
2
3
[root@~ gunmakefile]# make
foo.c foo2.c main1.c main2.c
foo.c foo2.c main1.c main2.c

从结果来看,strip 函数将 foo.c 和 bar.c 之间的多余的空格给去除了。

4.6 wildcard 函数

wildcard 是通配符函数,通过它可以得到我们所需的文件,这个函数如果我们在 Windows 或是Linux 命令行中的 *。其形式是:

1
$(wildcard pattern)

示例:

1
2
3
4
.PHONY:all
srcs=$(wildcard *.c)
all:
	@echo $(srcs)

执行:

1
2
[root@~ gunmakefile]# make
foo.c main.c foo2.c

从当前 Makefile 所在的目录下通过 wildcard 函数得到所有的 C 程序源文件。

五、参考博文

  1. 深度刨析makefile: https://mp.weixin.qq.com/s/8qT6xd163yQOrwUISFt16w
  2. Makefile 介绍: https://blog.csdn.net/Yang_Mao_Shan/article/details/131696714