Makefile(2)原理

简介: Makefile(2)原理

Makefile的原理

#include <stdio.h>
 
void foo()
{
    printf("this is foo() !\n");
}
extern void foo();
 
int main()
{
    foo();
    return 0;
}
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

编译

上面的展示了测试结果,注意到了第⼆次编译并没有构建⽬标⽂件的动作吗?但为什么有构建simple可执⾏程序的动作呢?为了明⽩为什么,我们需要了解 make 是如何决定哪些⽬标(这⾥是⽂件)是需要重新编译的。为什么 make会知道我们并没有改变 main.c 和 foo.c 呢?答案很简单,通过⽂件的时间戳!当 make 在运⾏⼀个规则时,我们前⾯已经提到 了⽬标和先决条件之间的依赖关系,make 在检查⼀个规则时,采⽤的⽅法是:如果先决条件中相关的⽂件的时间戳⼤于⽬标的时间戳,即先决条件中的⽂件⽐⽬标更新,则知道有变化,那么需要运⾏规则当中 的命令重新构建⽬标。这条规则会运⽤到所有与我们在 make时指定的⽬标的依赖树中的每⼀个规则。⽐如,对于 simple 项⽬,其依赖树中包括三个规则,make 会检查所有三个规则当中的⽬标(⽂件)与先决条件(⽂件)之间的时间先后关系,从⽽来决定是否要重新创建规则中的⽬标。

问题一:第二次构建的时候为什么simple会被重新构建?

是因为simple文件不存在,我们在这次构建的目标是all,而all在我们编译的过成中并不生成,所以第二次make的时候找不到,所以又重新编译了一遍。

修改makefile

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

运行结果

一个文件是否改变不是看这个文件的大小是否改变,而是看这个文件的时间戳是否发生了变化。可以直接使用touch指令对文件的时间戳进行修改。

这时候就会进行重新编译

假目标

如上图,如果我们的创建了一个clean文件之后,继续去运行make clean,这时候不是按照我们前面运行的make clean进行清理文件。

为什么出现上面的原因?

因为这个时候make 将clean单程是一个文件,并且在当前的目录下找到了这个文件,再加上clean目标没有任何的先决条件,这时候进行make clean时,系统会认为clean是最新的

如何解决上面的问题?使用假目标,假目标最从常用清净就是避免所定义的目标和的已经存在文件是从重名的情况,假⽬标可以采⽤.PHONY 关键字来定义,需要注意的是其必须是⼤写字⺟。使用假目标修改makefile

.PHONY: clean
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

运行结果:

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

变量

先看代码

.PHONY: clean
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
$(EXE): $(OBJS)
    $(CC) -o $(EXE) $(OBJS)
main.o: main.c
    $(CC) -o main.o -c main.c
foo.o: foo.c
    $(CC) -o foo.o -c foo.c
clean:
    $(RM) $(EXE) $(OBJS)

运行结果:

变量的使用可以提高makefile的可维护性。⼀个变量的定义很简单,就是⼀个名字(变量名)后⾯跟上⼀个等号,然后在等号的后⾯放这个变量所期望的值。对于变量的引⽤,则需要采⽤$(变量名)或者${变量名}这种模式。

在这个 Makefile 中,我们引⼊了 CC 和 RM 两个变量,⼀个⽤于保存编译器名,⽽另⼀个⽤于指示删除⽂件的命令是什么。还有就是引⼊了 EXE 和 OBJS 两个变量,⼀个⽤于存放可执⾏⽂件名,可另⼀个则⽤于放置所有的⽬标⽂件名。采⽤变量的话,当我们需要更改编译器时,只需更改变量赋值的地⽅,⾮常⽅便,如果不采⽤变量,那我们得更改每⼀个使⽤编译器的地⽅,很是麻烦。

1.自动变量(☆☆☆☆☆)

对于每⼀个规则,⽬标和先决条件的名字会在规则的命令中多次出现,每⼀次出现都是⼀种麻烦,更为麻烦的是,如果改变了⽬标或是依赖的名,那得在命令中全部跟着改。有没有简化这种更改的⽅法呢?这我们需要⽤到 Makefile 中的⾃动变量,最常用包括:

  • $@⽤于表示⼀个规则中的⽬标。当我们的⼀个规则中有多个⽬标时,$@所指的是其中任何造成命令被运⾏的⽬标。
  • $^则表示的是规则中的所有先择条件。
  • $<表示的是规则中的第⼀个先决条件。
.PHONY:all
all:first second third
    @echo "\$$@ = $@"
    @echo "\$$^ = $^"
    @echo "\$$< = $<"
 
first second third:

运行结果

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

修改simple的makefile

.PHONY: clean
CC = gcc
RM = rm
EXE = simple
OBJS = main.o foo.o
$(EXE): $(OBJS)
    $(CC) -o $@ $^
    @echo ">>>> simple:$(CC) -o $@ $^"
main.o: main.c
    $(CC) -o $@ -c $^
    @echo ">>>> main:$(CC) -o $@ -c $^"
foo.o: foo.c
    $(CC) -o $@ -c $^
    @echo ">>>> foo:$(CC) -o $@ -c $^"
clean:
    $(RM) $(EXE) $(OBJS)
    @echo ">>>> clean:$(RM) $(EXE) $(OBJS)"

运行结果:

2.特殊变量

(1)MAKE变量

它表示的是make 命令名是什么。当我们需要在 Makefile 中调⽤另⼀个 Makefile 时需要⽤到这个变量,采⽤这种⽅式,有利于写⼀个容易移植的 Makefile。

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

运行结果:

(2)MAKECMDGOALS变量

它表示的是当前⽤户所输⼊的 make ⽬标是什么。

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

运行结果:

从测试结果看来,MAKECMDGOALS 指的是⽤户输⼊的⽬标,当我们只运⾏ make 命令时,虽然根据

Makefile 的语法,第⼀个⽬标将成为缺省⽬标,即 all ⽬标,但 MAKECMDGOALS 仍然是空,⽽不是

all,这⼀点我们需要注意。

3.递归扩展变量

示例了使⽤等号进⾏变量定义和赋值,对于这种只⽤⼀个“=”符号定义的变量,我们称之为递归扩展变量(recursively expanded variable)。

.PHONY: all
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
    @echo $(foo)

运行结果:

除了递归扩展变量还有⼀种变量称之为简单扩展变量(simply expanded variables),是⽤“:=”操作符

来定义的。对于这种变量,make 只对其进⾏⼀次扫描和替换。

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

运行结果:

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

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

运行结果:

此外,还有"+="操作符,对变量进⾏赋值的⽅法

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

运行结果

4.override指令

在设计 Makefile 时,我们并不希望⽤户将我们在 Makefile 中定义的某个变量覆盖掉,那就得⽤ override 指令了。

.PHONY: all
override foo = x
all:
    @echo "foo = $(foo)"

运行结果:

5.模式

如果对于每⼀个⽬标⽂件都得写⼀个不同的规则来描述,那会是⼀种“体⼒活”,太繁了!对于⼀个⼤型项⽬,就更不⽤说了。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)

与 simple 项⽬前⼀版本的 Makefile 相⽐,最为直观的改变就是从⼆条构建⽬标⽂件的规则变成了⼀条。

模式类似于我们在 Windows 操作系统中所使⽤的通配符,当然是⽤“%”⽽不是“*”。采⽤了模式以后,

不论有多少个源⽂件要编译,我们都是应⽤同⼀个模式规则的,很显然,这⼤⼤的简化了我们的⼯作。使

⽤了模式规则以后,你同样可以⽤这个 Makefile 来编译或是清除 simple 项⽬,这与前⼀版本在功能上是

完全⼀样的。

6.函数

函数是 Makefile 中的另⼀个利器,现在我们看⼀看采⽤函数如何来简化 simple 项⽬的 Makefile。对于

simple 项⽬的 Makefile,尽管我们使⽤了模式规则,但还有⼀件⽐较恼⼈的事,我们得在这个Makefile

中指明每⼀个需要被编译的源程序。对于⼀个源程序⽂件⽐较多的项⽬,如果每增加或是删除⼀个⽂件都

得更新 Makefile,其⼯作量也不可⼩视!

采⽤了 wildcard 和 patsubst 两个函数后 simple 项⽬的 Makefile。可以先⽤它来编译⼀下 simple 项⽬代码以验证其功能性。需要注意的是函数的语法形式很是特别,对于我们来说只要记住其形式就⾏了。

.PHONY: clean
CC = gcc
RM = rm
EXE = simple
SRCS = $(wildcard *.c)
OBJS = $(patsubst %.c,%.o,$(SRCS))
$(EXE): $(OBJS)
    $(CC) -o $@ $^
%.o: %.c
    $(CC) -o $@ -c $^
clean:
    $(RM) $(EXE) $(OBJS)

现在,我们来模拟增加⼀个源⽂件的情形,看⼀看如果我们增加⼀个⽂件,在 Makefile 不做任

何更改的情况下其是否仍能正常的⼯作。增加⽂件的⽅式仍然是采⽤ touch 命令,通过 touch 命令

⽣成⼀个内容是空的 bar.c 源⽂件,然后再运⾏ make 和 make clean。

(1)addprefix函数

addprefix 函数是⽤来在给字符串中的每个⼦串前加上⼀个前缀,其形式是:$(addprefix prefix, names...)

.PHONY:all
#without_dir= foo.c bar.c main.c
without_dir = $(wildcard *.c)
with_dir:=$(addprefix objs/, $(without_dir))
all:
    @echo $(with_dir)

运行结果

(2)filter函数

filter 函数⽤于从⼀个字符串中,根据模式得到满⾜模式的字符串,其形式是:$(filter pattern..., text)

.PHONY: all
sources = foo.c bar.c baz.s ugh.h
sources := $(filter %.c %.s, $(sources))
all:
    @echo $(sources)

运行结果

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

(3)filter-out函数

filter-out 函数⽤于从⼀个字符串中根据模式滤除⼀部分字符串,其形式是:$(filter-out pattern..., text)

.PHONY: all
objects = main1.o foo.o main2.o bar.o
result := $(filter-out main%.o, $(objects))
all:
    @echo $(result)

(4)patsubst函数(☆☆☆)

patsubst 函数是⽤来进⾏字符串替换的,其形式是:$(patsubst pattern, replacement, text)

.PHONY:all
mixed=foo.c bar.c main.o
objects:=$(patsubst %.c, %.o, $(mixed))
all:
    @echo $(objects)

运行结果:

上述代码中 mixed 变量中包括了.c ⽂件也包括了.o ⽂件,采⽤patsubst 函数进⾏字符串替换时,我们希望将所有的.c ⽂件都替换成.o ⽂件。上图是最后的运⾏结果。

(5)strip

strip 函数⽤于去除变量中的多余的空格,其形式是:$(strip string)

.PHONY:all
original=foo.c     bar.c 
stripped:=$(strip $(original))
all:
    @echo "original = $(original)"
    @echo "stripped = $(stripped)"

运行结果:

(6)wildcard函数(☆☆☆)

wildcard 是通配符函数,通过它可以得到我们所需的⽂件,这个函数类似我们在 Windows 或Linux 命

令⾏中的“*”。其形式是:$(wildcard pattern)

.PHONY:all
SRC=$(wildcard *.c *.h)
all:
    @echo "SRC = $(SRC)"

运行结果:


相关文章
|
6月前
|
Shell Linux C++
Makefile编译实战
Makefile编译实战
82 0
|
C语言
Makefile教程(Makefile的结构)
Makefile教程(Makefile的结构)
100 0
|
3月前
|
编译器 C语言
针对make工具和Makefile文件的学习心得
【8月更文挑战第24天】本文分享了关于 make 工具和 Makefile 文件的学习心得。make 工具与 Makefile 在软件开发特别是大型项目中扮演着重要角色,能自动化构建流程并提升开发效率。文章首先强调了了解 make 和 Makefile 重要性的必要性,并详细解析了 Makefile 的基本结构:包括目标、依赖和命令的定义;通过变量简化命令与依赖;以及伪目标的使用等。接着,文中介绍了几个关键的 Makefile 编写技巧:如何合理组织依赖关系,有效利用变量和宏,灵活运用自动变量,以及添加清晰的注释等。
|
3月前
|
编译器 Linux C语言
Makefile实战论(一)
Makefile实战论(一)
|
11月前
|
Shell
Makefile学习2
Makefile学习2
|
11月前
|
编译器 Shell Linux
Makefile学习1
Makefile学习1
|
12月前
|
Shell
如何使用makefile
如何使用makefile
|
IDE Linux Shell
【Makefile】简单的Makefile编写
【Makefile】简单的Makefile编写
|
Java Linux 程序员
玩转Makefile | 一文入门Makefile
玩转Makefile | 一文入门Makefile
239 0
玩转Makefile | 一文入门Makefile
|
编译器 Shell C语言
Makefile文件 | 进阶指南
Makefile文件 | 进阶指南
371 0