Makefile 的最佳实践

概述

make 是一个命令工具,它用来解释 Makefile 中的规则。Makefile 中可以使用系统 shell 所提供的任何命令。但注意有些像 set,setenv 等是不行的。
Makefile 最大的优点是简单,只需要一句话的解释就可以让一个之前不懂的人可以用起来并发挥作用。但只有掌握了它的内涵才能真正得心应手。

编译的知识

Makefile 开始其实是为了 C/C++的编译而诞生的,所以它里面的很多隐藏规则都是针对 C/C++的。在讲 Makefile 之前有必要对 C/C++的编译有一点了解
过程如下:

d5054ba7-7140-4fa1-9f64-96c03a0299a0

  • 预处理器:将.c 文件转化成 .i 文件,使用的 gcc 命令是:gcc –E,对应于预处理命令 cpp;
  • 编译器:将.c/.h 文件转换成.s 文件,使用的 gcc 命令是:gcc –S,对应于编译命令 cc –S;
  • 汇编器:将.s 文件转化成 .o 文件,使用的 gcc 命令是:gcc –c,对应于汇编命令是 as;
  • 链接器:将.o 文件转化成可执行程序,使用的 gcc 命令是: gcc,对应于链接命令是 ld;
  • 加载器:将可执行程序加载到内存并进行执行,loader 和 ld-linux.so。

Makefile 规则介绍

一个简单的 Makefile 规则组成如下:

Targets...: Prerequisites...
Command
Command
...

Targets: Prerequisites;Command
Command
...

下面会称 Target 为目标, Prerequisites 为目标依赖, Command 为规则的命令行
Command 必须以[Tab]开始, Command 可以写成多行,通过来继行,但行尾的后不能有空格。
规则包含了文件之间的依赖关系和更新此规则 target 所需要的 Command
targets 可以使用通配符, 如果格式是"A(M)"表示档案文件(.a)中的成员“M”
在需要用$本义的时候,使用两$$来表示

当规则的 target 是一个文件,它的任何一个依赖文件被修改后,在执行 make <target>时这个目标文件都会被重新编译或重新连接。如果有必要此 target 的一个依赖文件也会被先重新编译。

伪目标

Makefile 中把那些没胡任何依赖只有执行动作的目标称为“伪目标“(Phony targets)

.PHONY : clean
clean :
-rm edit $(objects

通过.PHONY 将 clean 声明为伪目标,避免当目录下有名为“clean”文件时,clean 无法执行
这样的目标不是为了创建或更新程序,而是执行相应动作。

自动推导规则

在使用 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 的隐含规则。

规则书写建议

书写规则建议的方式是:单目标,多依赖。就是说尽量要做到一个规则中只存在一个目标文件,可有多个依赖文件。尽量避免使用多目标,单依赖的方式。

objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

上面是不好的风格

makefile 文件搜索顺序

GNUmakefile
makefile
Makefile
当前目录下不存在以“GNUmakefile ”、“makefile ”、“Makefile ”命名的任何文件,

  1. 当前目录下存在一个源文件 foo.c 的,我们可以使用“make foo.o ”来使用 make 的隐含规则自动生成的隐含规

则自动生成 foo.o 。当执行“make foo.o ”时。我们可以看到其执行的命令为:
cc –c –o foo.o foo.c
之后,foo.o 将会被创建或者更新。

  1. 如果当前目录下没有 foo.c 文件时,就是 make 对.o 文件目标的隐含规则中依赖文件不存在。

如果使用命令“文件目标的隐含规则中依赖文件不存在。
如果使用命令“make foo.o ”时,将回到到如下提示:

make: *** No rule to make target ‘foo.o’. Stop.
  1. 如果直接使用命令“make ”时,得到的提示信息如下:
make: *** No targets specified and no makefile found. Stop.

include

include foo *.mk ${bar}会被展开为include foo a.mk b.mk c.mk bish bash
可以在 make 命令行中用-I 指定包含文件搜索目录,默认搜索

  • /usr/gnu/include
  • /usr/local/include
  • /usr/include

可以在 include 前加上-来使 make 不会因为未找到包含文件而退出

变量 MAKEFILES

如果定义了这个值,那么 make 会先读入这个变量指定的多个文件

重载另一个 makefile

有些情况下,存在两个比较类似的 makefile 文件。其中一个(makefile-A)需要使用另外一个(makefile-B)中所定义的变量和规则。通常我们会想到在“makefile-A”中使用指示符“include”包含“mkaefile-B”来达到目的。但使用这种方式,如果在两个 makefile 文件中存在相同目标,而在不同的文件中其描述规则使用不同的命令。这样,相同的目标文件就同时存在两个不同的规则命令,这是 makefile 所不允许的。遇到这种情况,使用指示符“include”显然是行不通的。GNU make 提供另外一种途径来实现此目的。具体的做法如下:
在需要包含的 makefile 文件(makefile-A)中,定义一个称之为“所有匹配模式”(参考 10.5 模式规则 一节)的规则,它用来述那些在“makefile-A”中没有给出明确创建规则的目标的重建规则。就是说,如果在当前 makefile 文件中不能找到重建一个目标的规则时,就使用“所有匹配模式”所在的规则来重建这个目标。
看一个例子,如果存在一个命名为“Makefile”的 makefile 文件,其中描述目标“foo”的规则和其他的一些规,我们也可以书写一个内容如下命名为“GNUmakefile”的文件。

foo:
frobnicate > foo
%: force
@$(MAKE) -f Makefile $@
force: ;

执行命令“make foo”,make 将使用工作目录下命名为“GNUmakefile”的文件并执行目标“foo”所在的规则,创建目标“foo”的命令是:“frobnicate > foo”。如果执行另外一个命令“make bar”,因为在“GUNmakefile”中没有此目标的更新规则。make 将使用“所有匹配模式”规则,执行命令“$(MAKE) -f Makefile bar”。如果文件“Makefile”中存在此目标更新规则的定义,那么这个规则会被执行。此过程同样适用于其它 “GNUmakefile”中没有给出的目标更新规则。此方式的灵活之处在于:如果在“Makefile”文件中存在同样一一个目标“foo”的重建规则,由于 make 执行时首先读取文件“GUNmakefile”并在其中能够找到目标“foo”的重建规则,所以 make 就不会去执行这个“所有模式匹配规则”(上例中目标“%”所在的规则)。这样就避免了使用指示符“include”包含一个 makefile 文件时所带来的目标规则的重复定义问题。
此种方式,模式规则的模式只使用了单独的“%”(我们称他为“所有模式匹配规则”),它可以匹配任何一个目标;它的依赖是“force”,保证了即使目标文件已经存在也会执行这个规则(文件已存在时,需要根据它的依赖文件的修改情况决定是否需要重建这个目标文件);
“force”规则中使用空命令是为了防止 make 程序试图寻找一个规则去创建目标“force”时,又使用了模式规则“%: force”而陷入无限循环

make 如何解析 makefile

  • 第一阶段

读取所有的 makefile 文件(包括“MAKIFILES”变量指定的、指示符“include”指定的、以及命令行选项“-f(--file)”指定的 makefile 文件),内建所有的变量、明确规则和隐含规则,并建立所有目标和依赖之间的依赖关系结构链表。

  • 第二阶段

根据第一阶段已经建立的依赖关系结构链表决定哪些目标需要更新,并使用对应的规则来重建这些目标。
在 make 执行的第一阶段中如果变量和函数被展开,那么称此展开是“立即”的,此时所有的变量和函数被展开在需要构建的结构链表的对应规则中(此规则在建立链表是需要使用)。其他的展开称之为“延后”的。这些变量和函数不会被“立即”展开,而是直到后续某些规则须要使用时或者在 make 处理的第二阶段它们才会被展开。

变量取值

IMMEDIATE = DEFERRED
IMMEDIATE ?= DEFERRED
IMMEDIATE := IMMEDIATE
IMMEDIATE += DEFERRED or IMMEDIATE
define IMMEDIATE
DEFERRED
endef

条件语句

所有使用到条件语句在产生分支的地方,make 程序会根据预设条件将正确地分支展开。就是说条件分支的展开是“立即”的。其中包括:“ifdef”、“ifeq”、“ifndef”和“ifneq”所确定的所有分支命令。

规则定义

所有的规则在 make 执行时,都按照如下的模式展开:

IMMEDIATE : IMMEDIATE ; DEFERRED
DEFERRED

其中,规则中目标和依赖如果引用其他的变量,则被立即展开。而规则的命令行中的变量引用会被延后展开。此模板适合所有的规则,包括明确规则、模式规则、后缀规则、静态模式规则。

依赖类型

TARGETS : NORMAL-PREREQUISITES | ORDER-ONLY-PREREQUISITES

两种类型:

  • 常规依赖
  • “order-only" 依赖

当"order-only"依赖更新后,不需要更新目标
比如:

LIBS = libtest.a
foo : foo.c | $(LIBS)
$(CC) $(CFLAGS) $< -o $@ $(LIBS)

文件名通配符

表示文件名时,可用的通配符有: “*", "?", "[...]"
Makefile 中通配符可以出现在以下两种场合:

  • 可以用在规则的目标、依赖中,make 在读取 Makefile 时会自动对其进行匹配处理(通配符展开);
  • 在规则的命令中,通配符的通配处理是在 shell 在执行此命令时完成的

除上面两种情况之外的上下文中,不能直接使用通配符,需要通过函数"wildcard"来实现
示例一:

print: *.c
lpr -p $?
touch print

变量定义中的通配符不会被处理,比如: "objects = _.o", 它表示 objects 的值是字符串"_.o",而不是当前文件夹下的.o 文件列表。

通配符可能带来的问题

示例如下:

objects = *.o
foo : $(objects)
cc -o foo $(CFLAGS) $(objects)

如果将工作目录下所有的.o 文件删除,重新执行 make 将会得到一个类似于“没有创建*.o 文件的规则” 的错误提示。
好的做法是:

objects = $(wildcard *.o)
foo : $(objects)
cc -o foo $(CFLAGS) $(objects)

函数 wildcard

在规则中, 通配符会被自动展开。但在变量定义和函数引用时, 通配符不会展开。这时候就需要用 wildcard
可以用$(wildcard _.c)来获取工作目录下所有的.c 文件列表,可以用$(patsubst %.c,%.o,$(wildcard _.c))来得到对应.c 的目标文件

目录搜索

在一个较大的工程中,一般会将源代码和二进制文件(.o 文件和可执行文件)安排在不同的目录来进行区分管理。这种情况下,我们可以使用 make 提供的目录搜索依赖文件功能(在指定的若干个目录下自动搜索依赖文件)。在 Makefile 中,使用依赖文件的目录搜索功能。当工程的目录结构发生变化后,就可以做到不更改 Makefile 的规则,只更改依赖文件的搜索目录。

VPATH

这是一个 makefilej 里的变量

VPATH = src:../headers

vpath

这是 make 的一个关键字。可以为不同类型文件指定不同搜索目录,有三种形式

  • vpath PATTERN DIRECTORIES : 类似上面的 VPATH
vpath %.h ../headers
  • vpath PATTERN : 清除之前为符合模式“PATTERN”的文件设置的搜索路径
  • vpath: 清除所有已被设置的文件搜索路径

当有冲突时,按顺序来查找,比如

vpath %.c foo
vpath % blish
vpath %.c bar

表示对所有的.c 文件,make 依次查找目录:“foo”、blish”、“bar”。

vpath %.c foo : bar
vpath % blish

对于所有的.c 文件 make 将依次查找目录:“foo”、“bar”、“blish”

目录搜索的机制

  • 首先,如果规则的目标文件在 Makefile 文件所在的目录(工作目录)下不存在,那么就执行目录搜寻。
  • 如果目录搜寻成功,在指定的目录下存在此规则的目标。那么搜索到的完整的路径名就被作为临时的目标文件被保存。
  • 对于规则中的所有依赖文件使用相同的方法处理
  • 后继重建规则如下: - 当规则的目标不需要被重建时,规则中的所有的文件完整的路径名有效 - 当规则的目标需要重建时,规则的目标文件会在工作目录下被重建,而不是在目录搜寻时所得到的目录

举例, 有一个目录"armgen", 它下面有一个子目录“src", 存在"sum.c"和”memcp.c"两个源文件,在"armgen"下的 Makefile 内容如下:

LIBS = libtest.a
VPATH = src
libtest.a : sum.o memcp.o
$(AR) $(ARFLAGS) $@ $^
  • 在"sum.c"和“memcp.c"都没有更新的情况下,执行 make 会搜索到 src 下的 libtest.a, 不会重建目标
  • 在.c 文件发现变量时, 它会在 armgen 里重建 prom/libtest.a

当然我们有一个变量 GPATH,可以指定目标文件的目录

命令行中的自动变量

当我们通过目录搜索得到依赖文件会在其他目录,但是如果命令行中没有路径的话,就会出错。所以必须使用自动变量

foo.o : foo.c
cc -c $(CFLAGS) $^ -o $@

规则命令行中的自动化变量“$^”代表所有通过目录搜索得到的依赖文件的完整路径名(目录 + 一般文件名)列表。“$@”代表规则的目标。

VPATH = src:../headers
foo.o : foo.c defs.h hack.h
cc -c $(CFLAGS) $< -o $@

自动化变量“$<”代表规则中通过目录搜索得到的依赖文件列表的第一个依赖文件

库文件和搜索目录

makefile 中的程序链接的静态库和共享库也可以通过搜索目录得到。这一特性需要我们在书写规则依赖时用"-I<name>"来指定一个依赖文件名

foo : foo.c -lcurses
cc $^ -o $@

上面的命令只是定义在 foo.c 和/usr/lib/libcurses.a 或.so 被更新时要重建 foo, 但不会自动重建 libcurses.a,因为 make 不知道它的依赖
如果找不到 libcurses.a 或.so 会,报出类似没有规则可以创建目标“foo”需要的目标“-lcurses 的错误
.so 或.a 可以同变量".LIBPATTERNS"来指定,它是一个多个包含模式字符%的字,多个值之间用空格分隔。它的默认值是"lib%.so lib%.a"

伪目标

有些目标并不会创建目标,只是执行命令,所以我们定义了伪目标,如常见的 clean

clean:
rm *.o temp
.PHONY: clean

如果没有定义伪目标,那么当存在文件"clean"时, "rm *.o temp"就不会被执行
另一种使用场合是在 make 的并行和递归执行中。
比如

SUBDIRS = foo bar baz
subdirs:
for dir in $(SUBDIRS); do \
$(MAKE) -C $$dir; \
done

但上面的方法有 2 个问题

  1. 当子目录执行 make 出错时,make 不会退出,还会去其他目录进行 makefile, 最终会难定位第一个 make 出错的地方, 特别是在用了-k 选项时
  2. 由于使用了 shell 的 for 循环,它没法用到 make 对目录的并行处理能力, 可以改成这样
SUBDIRS = foo bar baz
.PHONY: subdirs $(SUBDIRS)
subdirs: $(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@
foo: baz

上边的实现中有一个没有命令行的规则“foo: baz”,此规则用来限制子目录的 make 顺序。它的作用是限制同步目录“foo”和“baz”的 make 过程(在处理“foo”目录之前,需要等待“baz”目录处理完成)。在书写一个并行执行 make 的 Makefile 时,目录的处理顺序是需要特别注意的。

另外,make 存在一个内嵌隐含变量“RM”,它被定义为:“RM = rm –f”。因此在书写“clean”规则的命令行时可以使用变量“$(RM)”来代替“rm”,这样可以免出现一些不必要的麻烦!这是推荐的用法

变量展开

makefile 里变量的展开是循环进行的

t2 := t3
t1 := t2
t0 := t1
define bar
irun $($(t$(1)))
endef

all:
$(call bar,0)
$($($(t0)))

输出结果是

irun t2
t3
阅读 13.4k
::...
免责声明:
当前网页内容, 由 大妈 ZoomQuiet 使用工具: ScrapBook :: Firefox Extension 人工从互联网中收集并分享;
内容版权归原作者所有;
本人对内容的有效性/合法性不承担任何强制性责任.
若有不妥, 欢迎评注提醒:

或是邮件反馈可也:
askdama[AT]googlegroups.com


订阅 substack 体验古早写作:


点击注册~> 获得 100$ 体验券: DigitalOcean Referral Badge

关注公众号, 持续获得相关各种嗯哼:
zoomquiet


自怼圈/年度番新

DU22.4
关于 ~ DebugUself with DAMA ;-)
粤ICP备18025058号-1
公安备案号: 44049002000656 ...::