makefile最佳实践 - 从原理到实用技巧

Makefile 的最佳实践

概述

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

编译的知识

Makefile 开始实际上是为了 C/C++的编译而诞生的,因此它里面的不少隐藏规则都是针对 C/C++的。在讲 Makefile 以前有必要对 C/C++的编译有一点了解
过程以下:shell

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 规则组成以下:bash

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

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

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

自动推导规则

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

规则书写建议

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

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
相关文章
相关标签/搜索