Makefile基础使用和实战详解

基础

Makefile 其实只是一个指示 make 程序如何为我们工作的命令文件,我们说 Makefile 其实是在说 make。而对于项目来说,Makefile 是指软件项目的编译环境。

Makefile 的好坏对于项目开发有些什么影响呢?设计得好的 Makefile,当我们重新编译时,只需编译那些上次编译成功后修改过的文件,也就是说编译的是一个 delta,而不是整个项目。反之,如果一个不好的 Makefile 环境,可能对于每一次的编译先要 clean,然后再重新编译整个项目。两种情况的差异是显然的,后者将耗费开发人员大量的时间用于编译,也就意味着低效率。

最为重要的是掌握二个概念,一个是目标(target),另一个就是依赖(dependency)。目标就是指要干什么,或说运行 make 后生成什么,而依赖是告诉 make 如何去做以实现目标。在 Makefile 中,目标和依赖是通过规则(rule)来表达的。我们最为熟悉的是采用make 来进行软件产品的代码编译,但它可以被用来做很多很多的事情。驾驭 Makefile,最为重要的是要学会采用目标和依赖关系来思考所需解决的问题。

Makefile三要素:

makefile三要素

Makefile工作原理:

makefile工作原理

 

### Makefile 的规则

2.2 Makefile规则介绍
一个简单的 Makefile 描述规则组成:
TARGET... : PREREQUISITES...
COMMAND
...
...
target规则的目标。通常是最后需要生成的文件名或者为了实现这个目的而必需
的中间过程文件名。可以是.o文件、也可以是最后的可执行程序的文件名等。另外,目
标也可以是一个make执行的动作的名称,如目标“ clean”,我们称这样的目标是“伪
目标”。参考 Makefile伪目标 一节
prerequisites规则的依赖。生成规则目标所需要的文件名列表。通常一个目标依
赖于一个或者多个文件。
command规则的命令行。是规则所要执行的动作(任意的 shell 命令或者是可在
shell 下执行的程序)。它限定了 make 执行这条规则时所需要的动作。
一个规则可以有多个命令行,每一条命令占一行。 注意: 每一个命令行必须以[Tab]
字符开始, [Tab]字符告诉 make 此行是一个命令行。 make 按照命令完成相应的动作。
这也是书写 Makefile 中容易产生,而且比较隐蔽的错误。
命令就是在任何一个目标的依赖文件发生变化后重建目标的动作描述。一个目标可
以没有依赖而只有动作(指定的命令)。比如 Makefile 中的目标“ clean”,此目标没有
依赖,只有命令。它所定义的命令用来删除 make 过程产生的中间文件(进行清理工作)。


Makefile 中“规则”就是描述在什么情况下、如何重建规则的目标文件,通常
规则中包括了目标的依赖关系(目标的依赖文件)和重建目标的命令。 make 执行重建
目标的命令,来创建或者重建规则的目标(此目标文件也可以是触发这个规则的上一个
规则中的依赖文件)。规则包含了文件之间的依赖关系和更新此规则目标所需要的命令。
一个 Makefile 文件中通常还包含了除规则以外的很多东西(后续我们会一步一步
的展开)。一个最简单的 Makefile 可能只包含规则。规则在有些 Makefile 中可能看起来
非常复杂,但是无论规则的书写是多么的复杂,它都符合规则的基本格式。
make 程序根据规则的依赖关系,决定是否执行规则所定义的命令的过程我们称之
为执行规则。

### 默认规则

在使用make编译.c源文件时,编译.c源文件规则的命令可以不用明确给出。这是因
make本身存在一个默认的规则,能够自动完成对.c文件的编译并生成对应的.o文件。
它执行命令“ cc -c”来编译.c源文件。在Makefile中我们只需要给出需要重建的目标文

件名(一个.o文件), make会自动为这个.o文件寻找合适的依赖文件(对应的.c文件。

对应是指:文件名除后缀外, 其余都相同的两个文件),而且使用正确的命令来重建这

个目标文件。对于上边的例子,此默认规则就使用命令“ cc -c main.c -o main.o”来创
建文件“ main.o”。对一个目标文件是“ N.o”,倚赖文件是“ N.c”的规则,完全可以省
略其规则的命令行,而由make自身决定使用默认命令。此默认规则称为make的隐含规
则(关于隐含规则可参考 第十章 使用隐含规则
这样,在书写 Makefile 时,我们就可以省略掉描述.c 文件和.o 依赖关系的规则,
而只需要给出那些特定的规则描述( .o 目标所需要的.h 文件)。因此上边的例子就可以
以更加简单的方式书写,我们同样使用变量“ objects”。 Makefile 内容如下:





### 简单的Makefile

(1)示例一:

all:
        @echo "hello all"
test:
        @echo "hello test"

执行结果:

$ make 
hello all 
$ make test 
hello test 
$ make all 
hello all

需要注意的是 echo 前面必须只有 TAB(即键盘TAB键),且至少有一个 TAB,而不能用空格代替。对于很多初学者,最为容易犯的就是这种“低级”错误。这种错误往往在对 Makefile 进行调试时,还不大容易发现,因为,从文本编辑器中看来,TAB 与空格有时没有太明显的区别。

Makefile 中的 all 就是目标,目标放在‘:’的前面,其名字可以是由字母和下划线‘_’组成 。echo “hello all”就是生成目标的命令,这些命令可以是任何你可以在你的环境中运行的命令以及 make 所定义的函数等等。all 目标在这里就是代表希望在终端上打印出“hello all”,有时目标会是一个比较抽象的概念。all 目标的定义,其实是定义了如何生成 all 目标,这我们也称之为规则。

(2)示例二:在示例一的基础上调换目标位置。

test:
        @echo "hello test"
all:
        @echo "hello all"

执行结果:

$ make 
hello test
$ make test 
hello test 
$ make all 
hello all

可知:

  • 一个 Makefile 中可以定义多个目标。
  • 调用 make 命令时,我们得告诉它我们的目标是什么,即要它干什么。当没有指明具体的目标是什么时,那么 make 以 Makefile 文件中定义的第一个目标作为这次运行的目标。这“第一个”目标也称之为默认目标(和是不是all没有关系)。
  • 当 make 得到目标后,先找到定义目标的规则,然后运行规则中的命令来达到构建目标的目的。现在所示例的 Makefile 中,每一个规则中都只有一条命令,而实际的 Makefile,每一个规则可以包含很多条命令。

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

(3)示例三:

all:test
        @echo "hello all"
test:
        @echo "hello test"

执行结果:

$ make 
hello test 
hello all 
$ make test 
hello test 
$ make all 
hello test 
hello all

会发现当运行 make 时,test 目标也被构建了。这里需要引入 Makefile 中依赖关系的概念,all 目标后面的 test 是告诉 make,all 目标依赖 test 目标,这一依赖目标在 Makefile 中又被称之为先决条件。出现这种目标依赖关系时,make工具会按从左到右的先后顺序先构建规则中所依赖的每一个目标。如果希望构建 all 目标,那么make 会在构建它之前得先构建 test 目标,这就是为什么我们称之为先决条件的原因。

目标和依赖的关系

### 多文件编译

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

foo.c

#include <stdio.h>
void foo()
{
    printf("This is foo() \n");
}

main.c

extern void foo();
int main()
{
    foo();
    return 0;
}

它们的依赖关系可总结为如下:

依赖关系

我们就可以根据依赖关系写Makefile了:

all: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 目标用于删除所生成的文件,包括目标文件和 simple 可执行程序,这在现实的项目中很是常见。

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

$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 会检查所有三个规则当中的目标(文件)与先决条件(文件)之间的时间先后关系,从而来决定是否要重新创建规则中的目标。

那为什么会执行一次gcc -o simple main.o foo.o呢?因为all文件在我们的编译过程中并不生成,即 make 在第二次编译时找不到,所以又重新编译了一遍。如果把all改为simple,那就是我们所期望的结果:

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
gcc -c main.c -o main.o
gcc -c foo.c -o foo.o
gcc -o simple main.o foo.o

$make
make: `simple` is up to date.

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

$ls -l foo.c
-rw-rw-r-- 1 fly fly 65 1月  30 14:15 foo.c

$touch foo.c
$ls -l foo.c
-rw-rw-r-- 1 fly fly 65 1月  30 15:42 foo.c

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

### 伪对象.PHONY

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

$ls -l clean
ls: cannot access clean: No such file or directory
 
$touch clean
$ls -l clean
-rw-rw-r-- 1 fly fly 65 1月  30 16:42 clean

$make clean
make: `clean' is up to date.

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

对于这种情况,在现实中也难免存在所定义的目标与所存在的文件是同名的,采用 Makefile如何处理这种情况呢?Makefile 中的假目标(phony target)可以解决这个问题。假目标可以采用.PHONY 关键字来定义,需要注意的是其必须是大写字母。

.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 命令的结果:

$make clean
rm simple main.o foo.o

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

### 变量

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

.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 的可维护性。既然定义了一个 CC 变量,当然也可以将-o 或是-c 命令参数也定义成为一个变量,因为如果我们更改了一个编译器,那么很有可能其使用参数也得跟着改变。

### 自动变量

有时候目标和先决条件的名字会在规则的命令中多次出,如果改变了目标或是依赖的名,那得在命令中全部跟着改。这就需要用到 Makefile 中的自动变量,它们包括:

  • $@用于表示一个规则中的目标。当我们的一个规则中有多个目标时,$@所指的是其中任何造成命令被运行的目标。
  • $^则表示的是规则中的所有先决条件。
  • $<表示的是规则中的第一个先决条件。

除了上面的两个自动变量,在 Makefile 中还有其它的动变量。如下是测试上面三个自动变量的值的 Makefile:

.PHONY:all
all:first second thrid
	@echo "\$$@=$@"
	@echo "$$^=$^"
	@echo "$$<=$<"
first second thrid:

运行结果:

$ make

$@=all
$^=first second thrid
$<=first

需要注意的是,在 Makefile 中‘$’具有特殊的意思,因此,如果想采用 echo 输出‘$’,则必需用两个连着的‘$’。还有就是,$@对于 Shell 也有特殊的意思,需要在“$$@”之前再加一个脱字符‘\’。

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

.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)

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

### 特殊变量

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

(1)第一个就是 MAKE 变量,它表示的是make 命令名是什么。当我们需要在 Makefile 中调用另一个 Makefile 时需要用到这个变量,采用这种方式,有利于写一个容易移植的 Makefile。

.PHONY: all
all:
@echo "MAKE = $(MAKE)"

执行:

$make
MAKE = make

(2)第二个特殊变量则是 MAKECMDGOALS,它表示的是当前用户所输入的 make 目标是什么。

.PHONY: all clean
all clean:
@echo "\$$@ = $@"
@echo "MAKECMDGOALS = $(MAKECMDGOALS)"

执行:

$make
$@ = all
MAKECMDGOALS =

$make all
$@ = all
MAKECMDGOALS = all

$make clean
$@ = clean
MAKECMDGOALS = clean

$make all clean
$@ = all
MAKECMDGOALS = all clean
$@ = clean
MAKECMDGOALS = all clean

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

### 变量的类别

(1)只用一个“=”符号定义的变量,称之为递归扩展变量(recursively expanded variable)。

.PHONY:all
foo=$(foo2)
foo2=$(foo3)
foo3= FLY.
all:
	@echo $(foo)

执行:

$ make
FLY.

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

foo=$(foo) -O

 

(2)除了递归扩展变量还有一种变量称之为简单扩展变量(simply expanded variables),是用“:=”操作符来定义的。对于这种变量,make 只对其进行一次扫描和替换。

.PHONY:all
x=fly
y=$(x) FLY
x=later
xx:=fly
yy:=$(xx) FLY
xx:=fly
all:
	@echo "x=$(y),xx=$(yy)"

执行:

$ make
x=later FLY,xx=fly FLY

可以明显的看出 make 是如何处理递归扩展变量和简单扩展变量的。

(3)Makefile中还存在一种条件赋值符“?=”。

.PHONY:all
foo=x
foo?=y
bar?=y
all:
	@echo "foo=$(foo),bar=$(bar)"

执行:

$ make
foo=x,bar=y

条件赋值的意思是当变量以前没有定义时,就定义它并且将左边的值赋值给它,如果已经定义了那么就不再改变其值。条件赋值类似于提供了给变量赋缺省值的功能。

对于前面所说的变量类别,是针对一个赋值操作而言的。

### 变量及其值的来源

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

(1)对于前面所说到的自动变量,其值是在每一个规则中根据规则的上下文自动获得变量值的。

(2)可以在运行 make 时,在 make 命令行上定义一个或多个变量。在 make 命令行中定义的变量及其值同样在 Makefile 中是可见的。其实,我们可以通过在 make 命令行中定义变量的方式从而覆盖 Makefile 中所定义的变量的值。

$ make bar=x

(3)变量还可以来自于 Shell 环境,例如采用 Shell 中的 export 命令定义了一个变量后再执行Makefile。

$ export bar=x
$ make
foo = x, bar = x

(4)Makefile 还可以采用“+=”操作符对变量进行赋值的方法。

.PHONY: all
objects = main.o foo.o bar.o utils.o
objects += another.o
all:
	@echo $(objects)

等价于

.PHONY:all
objects = main.o foo.o bar.o utils.o
objects:= $(objects) another.o
all:
	@echo $(objects)

### 变量引用的高级功能

在赋值的同时完成后缀替换操作。

.PHONY:all
foo= a.o b.o c.o
bar:=$(foo:.o=.c)
all:
	@echo "bar=$(bar)"

执行结果:

$ make
bar=a.c b.c c.c

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

 

 

### Makefile的运算符

1.“=”
用"="对变量进行赋值时,解析该变量的时候,该变量的值等于最后一次赋值时的值,而不是当前位置时该变量的值。所以你看变量引用的值时不能只往前面看,还要往后面看。

=是一个延迟赋值操作符,它表示变量的值在使用时确定,并且会受后续赋值的影响。

2.“:=”
用":=“来赋值的,则是就地直接解析,不会受后续赋值影响,相当于是定义了常量。 只用往前看即可,和我们在C语言里的”="逻辑上是一样的。

3.“?=”
该变量没有被赋值过则执行本条赋值语句,如果被赋值过则忽略本次赋值。

可以用来设置默认值.

4.“+=”
对变量进行接续赋值,作用就是将本次的赋值追加到原来的赋值后面。

例如

VAR = 1

VAR += 2

在这里,VAR的值是"12",因为使用 +=,新的值"2"被添加到了原始值"1"之后。

 

### override 指令

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

.PHONY:all
override foo= a.o b.o c.o
bar:=$(foo:.o=.c)
all:
	@echo "bar=$(bar)"

执行:

$ make foo="bb.o cc.o"
bar=a.c b.c c.c

### 模式

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

.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 相比,最为直观的改变就是从二条构建目标文件的规则变成了一条。模式类似于 Windows 操作系统中所使用的通配符,当然是用“%”而不是“*”。采用了模式以后,不论有多少个源文件要编译,我们都是应用同一个模式规则的,很显然,这大大的简化了我们的工作。使用了模式规则以后,同样可以用这个 Makefile 来编译或是清除 simple 项目,这与前一版本在功能上是完全一样的。

### 函数

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

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

.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 不做任何更改的情况下其是否仍能正常的工作。增加文件的方式仍然是采用 touch 命令,通过 touch 命令生成一个内容是空的 foo2.c 源文件,然后再运行 make 和 make clean。

$ touch foo2.c
$ make
gcc -o foo.o -c foo.c
gcc -o main.o -c main.c
gcc -o foo2.o -c foo2.c
gcc -o simple foo.o main.o foo2.o

$ make clean 
rm simple foo.o main.o foo2.o

从结果来看函数真的起作用了!这功能很酷!更多内容可以看一看《GUN make》以了解 Makefile 中到底有些什么函数,这样的话,当在碰到具体的问题时就会想到它们。

### addprefix 函数

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

$(addprefix prefix, names...)

示例:

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

执行:

$ make
objs/foo.c objs/foo2.c objs/main.o

### filter函数

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

$(filter pattern..., text)

示例:

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

执行:

$ make
foo.c foo2.c main.s

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

### filter-out函数

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

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

示例:

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

执行:

$ make
foo.c foo2.c main.h

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

### patsubst 函数

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

$(patsubst pattern, replacement, text)

示例:

.PHONY:all
srcs=foo.c foo2.c main1.c main2.c
objs:=$(patsubst %.c,%.o,$(srcs))
all:
	@echo $(objs)

执行:

$ make
foo.o foo2.o main1.o main2.o

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

### strip函数

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

$(strip string)

示例:

.PHONY:all
srcs=foo.c   foo2.c   main1.c main2.c
objs:=$(strip $(srcs))
all:
	@echo $(srcs)
	@echo $(objs)

执行:

$ make
foo.c foo2.c main1.c main2.c
foo.c foo2.c main1.c main2.c

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

### wildcard 函数

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

$(wildcard pattern)

示例:

.PHONY:all
srcs=$(wildcard *.c)
all:
	@echo $(srcs)

执行:

$ make
foo.c main.c foo2.c

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

 

 

###条件判断

# 使用 ifeq 进行条件判断
ifeq ("$(VAR)", "bb")
# 如果 VAR 等于 bb,执行以下命令
echo "VAR equals bb"
else
# 如果 VAR 不等于 bb,执行以下命令
echo "VAR does not equal bb"
endif

 

### 变量的引用

当我们定义了一个变量之后,就可以在 Makefile 的很多地方使用这个变量。变量
的引用方式是:“ $VARIABLE_NAME)”或者“ ${ VARIABLE_NAME }”来引用一个
变量的定义。例如:“ $(foo) ”或者“ ${foo}”就是取变量“ foo”的值。美元符号“ $
Makefile 中有特殊的含义,所有在命令或者文件名中使用“ $”时需要用两个美元符
号“ $$”来表示。对一个变量的引用可以在 Makefile 的任何上下文中,目标、依赖、
命令、绝大多数指示符和新变量的赋值中。这里有一个例子,其中变量保存了所有.o
文件的列表:

 

 

### $(shell command) 变量

$(shell command)是一个构建规则的特殊函数,它执行指定的命令,并将其输出作为变量存储起来。在这种情况下,uname是UNIX系统的一个命令,用于获取当前系统的名称和相关信息。

因此,$(shell uname)将执行uname命令,并将其输出存储为变量。然后,可以在Makefile的其他地方使用这个变量。

下面是一个示例Makefile,演示了如何使用$(shell uname)获取系统名称:

makefile复制代码
  # 获取系统名称并存储为变量
  OS := $(shell uname)
   
  # 根据系统名称执行不同的命令
  ifeq ($(OS), Linux)
  # 在Linux系统上执行的命令
  COMMAND := echo "You are running on Linux."
  else ifeq ($(OS), Darwin)
  # 在macOS系统上执行的命令
  COMMAND := echo "You are running on macOS."
  else
  # 在其他系统上执行的默认命令
  COMMAND := echo "Unknown operating system."
  endif
   
  # 执行命令并将输出显示到终端
  all:
  $(COMMAND)

在上面的示例中,$(shell uname)将执行uname命令并获取系统名称。然后,使用ifeqelse ifeq语句来检查系统名称,并根据不同的系统执行不同的命令。最后,通过执行$(COMMAND)来执行相应的命令并将输出显示到终端。

 ### $(or) 函数

makefile 中 $(or)的作用

在 Makefile 中,$(or) 是一个函数,用于在给定的参数列表中选择第一个非空的参数。它的语法如下:

$(or <arg1>,<arg2>,...)

当 Makefile 执行 $(or <arg1>,<arg2>,...) 时,它会按照参数的顺序检查参数列表中的每个参数,直到找到第一个非空的参数为止。然后它会返回该参数的值。

下面是一个示例:

foo := $(or $(bar),baz,qux)

这个示例中,$(bar) 是一个变量,如果它的值为非空,则返回它的值;否则继续检查 baz 的值,如果 baz 的值非空,则返回它的值;如果 baz 的值也为空,则返回 qux 的值。

总之,$(or) 函数在 Makefile 中允许我们从多个参数中选择一个非空值,并将其用作变量的值。

 

### "order-only" 不需要重新编译的依赖,符号'|'右边的依赖改动后不需要重新编译

order-only”依赖的使用举例:
LIBS = libtest.a
foo : foo.c | $(LIBS)
$(CC) $(CFLAGS) $< -o $@ $(LIBS)
make在执行这个规则时,如果目标文件“ foo”已经存在。当“ foo.c”被修改以后,目标“ foo
将会被重建,但是当“ libtest.a”被修改以后。将不执行规则的命令来重建目标“ foo”。
就是说,规则中依赖文件$(LIBS)只有在目标文件不存在的情况下,才会参与规则的执行。当目
标文件存在时此依赖不会参与规则的执行过程。

 

### makefile之自动化变量
常见自动化变量
应用举例
常见自动化变量
$@:目标文件。在模式规则中,$@代表符合模式的目标文件集合中的一个目标文件。

$<:第一个依赖文件。在模式规则中,$<代表符合模式的依赖文件集合中的第一个依赖文件。

$>:最后一个依赖文件。在模式规则中,$>代表符合模式的依赖文件集合中的最后一个依赖文件。

$::所有依赖文件列表。在模式规则中,$:代表符合模式的依赖文件集合中的所有依赖文件列表。

$?:所有依赖文件中最近的一个。在模式规则中,$?代表符合模式的依赖文件中最近的一个,也就是新的一组依赖文件中的第一个文件。

$~:所有依赖文件中最早的一个。在模式规则中,$~代表符合模式的依赖文件中最早的一个,也就是新的一组依赖文件中的最后一个文件。

应用举例
1)$@
这个变量代表当前目标文件。例如,如果你有一个规则来编译多个目标文件,你可以在规则的命令行中使用$@来表示当前正在处理的目标文件。例如:

test: test1 test2 test3
gcc -o $@ $<

在这个例子中,$@代表目标文件test。

2)$<
这个变量代表当前目标文件所依赖的第一个文件。例如,如果你有一个规则来编译多个目标文件,你可以在规则的命令行中使用$<来表示当前目标文件所依赖的第一个文件。例如:

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

在这个例子中,$<代表依赖文件test.c。

3)$^
这个变量代表当前目标文件所依赖的所有文件。例如,如果你有一个规则来编译多个目标文件,你可以在规则的命令行中使用$^来表示当前目标文件所依赖的所有文件。例如:

test: test1.c test2.c
gcc -o $@ $^

在这个例子中,$^代表依赖文件test1.c和test2.c。

4)$?
这个变量代表所有比当前目标文件新的依赖文件的集合。例如,如果你有一个规则来编译多个目标文件,你可以在规则的命令行中使用$?来表示所有比当前目标文件新的依赖文件的集合。例如:

test: test1.c test2.c
gcc -o $@ $^ $?

在这个例子中,$?代表所有比目标文件新的依赖文件。

$|
这个变量代表所有依赖文件的第一个非空格字符之前的部分。例如,如果你有一个规则来编译多个目标文件,你可以在规则的命令行中使用$|来表示所有依赖文件的第一个非空格字符之前的部分。例如:

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

在这个例子中,$|代表所有依赖文件的第一个非空格字符之前的部分。


总结

    1. Makefile的一个规则是由目标(targets)、先决条件(prerequisites)以及命令(commands)所组成的。需要指出的是,目标和先决条件之间表达的就是依赖关系(dependency),这种依赖关系指明在构建目标之前,必须保证先决条件先满足(或构建)。而先决条件可以是其它的目标,当先决条件是目标时,其必须先被构建出来。还有就是一个规则中目标可以有多个,当存在多个目标,且这一规则是Makefile 中的第一个规则时,如果运行make 命令不带任何目标,那么规则中的第一个目标将被视为是缺省目标。
    2. 掌握如果在头脑中勾画出我们想让 make 做的事的“依赖树”是编写 Makefile 最为重要和关键的一步。
    3. 编译时出现“undefined reference to ... ”时,有两个原因:第一种是源码没有被编译进去;第二种是对应库没有被引用。
 

posted on 2023-10-23 15:42  zxddesk  阅读(93)  评论(0编辑  收藏  举报

导航