目录
自动进行编译的 软件,GNU make(工程管理器 make 在不同环境有很多版本分支,比如 Qt 下的 qmake, Windows 下的 nmake 等,下面提到的 make 指的是 Linux 下的 GNU make)就是这样 的一款软件
而 Makefile,是 make 的配置文件,用来配置运行 make 的时候的一些相关细节,
比如指定编译选项,指定编译环境等等。一般而言,一个工程项目不管是简单还是复杂,每一个源代码子目录都会有一个 Makefile 来管理,然后一般有个所谓的顶层 Makefile 来统一管理所有的子目录 Makefile。
makefile调用方式
make
gcc编译多文件例子
假设我们有一个工程,这个工程总共有4个源文件,姑且叫做 a.c、b.c 以及 x.c 和 y.c 吧,他们最终将会链接生成可执行文件 image:
用来帮我们“自动”执 行编译的工作,此时目标是 image,而其依赖则是四个.o 文件,而 且,这四个.o 文件本身也是目标,他们依赖于其对应的.c 文件
image:a.o b.o x.o y.o
gcc a.o b.o x.o y.o -o image
a.o:a.c
gcc a.c -o a.o -c
b.o:b.c
gcc b.c -o b.o -c
x.o:x.c
gcc x.c -o x.o -c
y.o:y.c
gcc y.c -o y.o -c
这个简单的 Makefile 文件总共有 11 行,5 套规则,其中第 1 行中的 image 是第 1 个目标,冒号后面是这个目标的依赖列表(四个.o 可重定位文件)。第 2 行行首是一个制 表符,后面紧跟着一句 Shell 命令。 下面从第 4 行到第 11 行,也都是这样的目标-依赖对,及其相关的 Shell 命令。但是 这里必须注意一点:虽然这个 Makefile 总共出现了 5 个目标,但是第一个规则的目标(即 image)被称之为终极目标,终极目标指的是当你执行 make 的时候,默认生成的那个文 件。注意:如果第一个规则有多个目标,则只有第一个才是终极目标。另外,以圆点.开头 的目标不在此讨论范围内。
变量详解
跟Shell脚本非常类似,在Makefile中也会使用“弱类型”变量(相对于C语言这种强 类型语言而言),在Makefile中变量就是一个名字(像是C语言中的宏),代表一个文本字 符串(变量的值)。在Makefile的目标、依赖、命令中引用一个变量的地方,变量会被它 的值所取代(与C语言中宏引用的方式相同,因此其他版本的make也把变量称之为“宏”)。
特点
- 变量和函数的展开(除规则的命令行以外),是在make读取Makefile文件时进行 的,这里的变量包括了使用“=”定义和使用指示符“define”定义的变量。
- 变量可以用来代表一个文件名列表、编译选项列表、程序运行的选项参数列表、搜 索源文件的目录列表、编译输出的目录列表和所有我们能够想到的事物。
- 变量名不能包括“:”、“#”、“=”、前置空白和尾空白的任何字符串。需要注意的是, 尽管在GNU make中没有对变量的命名有其它的限制,但定义一个包含除字母、数字和下 划线以外的变量的做法也是不可取的,因为除字母、数字和下划线以外的其它字符可能会在 以后的make版本中被赋予特殊含义,并且这样命名的变量对于一些Shell来说不能作为环 境变量使用。
- 变量名是大小写敏感的。变量“foo”、“Foo”和“FOO”指的是三个不同的变量。 Makefile传统做法是变量名是全采用大写的方式。推荐的做法是在对于内部定义的一般变 量(例如:目标文件列表objects)使用小写方式,而对于一些参数列表(例如:编译选项 CFLAGS)采用大写方式,这并不是要求的。但需要强调一点:对于一个工程,所有Makefile 中的变量命名应保持一种风格,否则会显得你是一个蹩脚的开发者(就像代码的变量命名风 格一样),随时有被鄙视的危险。
- 另外有一些变量名只包含了一个或者很少的几个特殊的字符(符号)。称它们为自 动化变量。像“<”、“@”、“?”、“*”、“@D”、“%F”、“^D”等等,后面会详述之。
- 变量的引用跟Shell脚本类似,使用美元符号和圆括号,比如有个变量叫A,那么对 他的引用则是$(A),有个自动化变量叫@,则对他的引用是$(@),有个系统变量是CC则 对其引用的格式是$(CC)。对于前面两个变量而言,他们都是单字符变量,因此对他们引用 的括号可以省略,写成$A和$@。
例子 自定义变量
A = apple
B = I love China
C = $(A) tree
继续上方编译多个.c
OBJ = a.o b.o x.o y.o
image:$(OBJ)
gcc $(OBJ) -o image
a.o:a.c
gcc a.c -o a.o -c
b.o:b.c
gcc b.c -o b.o -c
x.o:x.c
gcc x.c -o x.o -c
y.o:y.c
gcc y.c -o y.o -c
例子 系统预定义变量
CFLAGS、CC、MAKE、Shell 等等,这些变量已经有了系统预定义好的值,当然我们 可以根据需要重新给他们赋值,例如 CC 的默认值是 gcc,当我们需要使用 c 编译器的时候 可以直接使用他
OBJ = a.o b.o x.o y.o
image:$(OBJ)
$(CC) $(OBJ) -o image
a.o:a.c
$(CC) a.c -o a.o -c
b.o:b.c
$(CC) b.c -o b.o -c
x.o:x.c
$(CC) x.c -o x.o -c
y.o:y.c
$(CC) y.c -o y.o -c
如果是要编译arm,只需修改CC
CC = arm-Linux-gnu-gcc
例子 自动化变量
<、@、?、#等等,这些特殊的变量之所以称为自动化变量,是因为他们的值会“自 动地”发生变化
上述列出的自动量变量中。其中有四个在规则中代表一个文件名(@、<、%和*)。 而其它三个的在规则中代表一个文件名的列表
通过这七个自动化变量来获取一个完整文件名中的目录部分或 者具体文件名,需要在这些变量中加入“D”或者“F”字符。这样就形成了一系列变种的自动 化变量
将上面的多文件编译修改成自动化
OBJ = a.o b.o x.o y.o
image:$(OBJ)
$(CC) $^ -o $@
a.o:a.c
$(CC) $^ -o $@ -c
b.o:b.c
$(CC) $^ -o $@ -c
x.o:x.c
$(CC) $^ -o $@ -c
y.o:y.c
$(CC) $^ -o $@ -c
例子 定义变量的不同形式
- 递归定义方式
A = I love $(B) B = China
此处,在变量 B 出现之前,变量 A 的定义包含了对变量 B 的引用,由于 A 的定义方式 是所谓的“递归”定义方式,因此当出现$(B)时会对全文件进行搜索,找到 B 的值并代进 A 中,结果变量 A 的值是“I love China“ ,缺点如下
-
使用此风格的变量定义,可能会由于出现变量的递 归定义而导致make陷入到无限的变量展开过程中,最终使make执行失败。例如A=$(A), 这将导致无限嵌套迭代。
-
这种风格变量的定义中如果使引用了某一个函数,那么函数 总会在其被引用的地方被执行。是因为这种风格变量的定义中,对函数引用的替换展开发生在展开它自身的时候,而不是在定义它的时候。这样所带来的问题是,可能可能会使make 的执行效率降低,同时对某些变量和函数的引用出现问题。特别是当变量定义中引用了 “Shell”和“wildcard”函数的情况,可能出现不可控制或者难以预料的错误,因为我们无法 确定它在何时会被展开
-
- 直接定义方式
B = China A := I love $(B
定义 A 时用的是所谓的“直接”定义方式,说白了就是如果其定义里出现有对 其他变量的引用的话,只会其前面的语句进行搜寻(不包含自己所在的那一行),而不是搜 寻整个文件
- 条件定义方式
A = apple A ?= I love China
有时我们需要先判断一个变量是否已经定义了,如果已经定义了则不作操作,如果没有 定义再来定义它的值,此处对 A 进行了两次定义,其中第二次是条件定义,其含义是:如果 A 在此之前没有 定义,则定义为“I love China”,否则维持原有的值
- 多行命令定义方式
define commands echo “thank you!” echo “you are welcome.” endef
此处定义了一个包含多行命令的变量commands,我们利用它的这个特点实现一个完 整命令包的定义。注意其语法格式:以define开头,以endef结束,所要定义的变量名必须 在指示符“define”的同一行之后,指示符define所在行的下一行开始一直到“end”所在行的 上一行之间的若干行,是变量的值。这种方式定义的所谓命令包,可以理解为编程语言中的 函数
例子 变量操作方式
- 追加变量的值
A = apple A += tree 这样,变量A的值就是apple tree
- 修改变量的值
A = srt.c string.c tcl.c B = $(A:%.c=%.o 等同于 A = srt.c string.c tcl.c B = $(patsubst %.c, %.o, $(A))
变量B的值就变成了 srt.o string.o tcl.o。例子中$(A:%.c=%.o)的意思是: 将变量A中所有以.c作为后缀的单词,替换为以.o作为后缀
- override一个变量
override CFLAGS += -Wal
在执行make时,通常可以在命令行中携带一个变量的定义,如果这个变量跟Makefile 中出现的某一变量重名,那么命令行变量的定义将会覆盖Makefile中的变量。就是说,对 于一个在Makefile中使用常规方式(使用“=”、“:=”或者“define”)定义的变量,我们可以 在执行make时通过命令行方式重新指定这个变量的值,命令行指定的值将替代出现在 Makefile中此变量的值
A = an apple tree all: @echo $(A) 运行 make A="an elephant" 会出现结果 an elephant 如果不想被覆盖就要在A前面加override override A = an apple tree
指示符“override”并不是用来防止Makefile的内部变量被命令行参数覆 盖的,其存在的目的是为了使用户可以改变或者追加那些使用make的命令行指定的变量的 定义。从另外一个角度来说,就是实现了在Makefile中增加或者修改命令行参数的一种机 制。想象一下,我们可能会有这样的需求:对一些通用的参数或者必需的编译参数我们可以 在Makefile中指定,而在命令行中可以指定一些特殊的参数。对待这种需求,我们可以使 用指示符“override”来实现
例子 导出变量
在Makefile中导出一个变量的作用是:使得该变量可以传递给子Makefile。在缺省的 情况下,除了变量”Shell”、”MAKEFLAGS”、不为空的”MAKEFILES”以及在执行make之 前就已经存在的环境变量之外,其他变量不会被传递给子Makefile
- /----dir/
- / |------Makefile
- /----Makefile
第一层Makefile
export A = apple
B = banana
all:
echo "rank 1: $(A)"
echo "rank 1: $(B)"
$(MAKE) -C dir/
第二层Makefile
all:
echo "rank 2: $(A)"
echo "rank 2: $(B)"
运行make -sw 显示目录,会出现结果如下
rank 1: apple
rank 1: banana
rank 2: apple
rank 2:
也可以防止传递
unexport MAKEFLAGS
这样,上一级Makefile的命令行参数就不会传递给子Makefile了。
例子 特殊变量
VPATH
这个特殊的变量用以指定Makefile中文件的备用搜寻路径:当Makefile中的目标文件 或者依赖文件不在当前路径时,make会在此变量所指定的目录中搜寻,如果VPATH包含多 个备用路径,他们使用空格或者冒号隔开
VPATH = dir1/:dir2/
可以使用小写的指示符vpath来更灵活地为各种不同的文件指定不同的路 径
vpath %.c = dir1/:dir2/
vpath %.h = include/
VPATH是一个变量,而vpath是一个指示符
MAKE
当需要在一个Makefile中调用子Makefile时,用到的变量就是MAKE,实际上该变量 代表了当前系统中make软件的全路径
MAKEFLAGS
在执行make时的命令行参数,这个变量是缺省会被传递给子Makefile 的特殊变量之一
如果运行 make -s ,则MAKEFLAGS就等于s
规则
隐式规则
上面用来管理四个源程序文件(a.c b.c x.c 和 y.c)的那个 Makefile 还是显得比较笨 拙,需要对每一个文件编写一个规则,但其实 Makefile 是有一定的智能的,我们可以将编 译语句省略掉,也可以将依赖文件都省略掉,甚至连目标都省略掉!
OBJ = a.o b.o x.o y.o
image:$(OBJ)
$(CC) $(OBJ) -o image
运行make就会运行
cc -c -o a.o a.c
cc -c -o b.o b.c
cc -c -o x.o x.c
cc -c -o y.o y.c
gcc a.o b.o x.o y.o -o image
虽然后四个规则的目标、依赖文件和编译语句都没写,但是执行 make 也 照样可以运行,可见 make 会自动帮我们找到.o 文件所需要的源程序文件,也能自动帮我 们生成对应的编译语句,这个情况称之为 Makefile 的隐式规则
虽然我们可以省略后四个规则的依赖文件和编译语句,但是第一个规则的 依赖文件和编译语句不能省略,因为隐式规则是有限制的,他只能自动找到跟目标同名的依 赖文件,比如目标叫 a.o,那么他会自动查找到 a.c,换了个名字就找不到了,生成的编译 语句也是缺省的单文件形式
弊端
- 第一,有时一个目标可能并不是一个文 件,而仅仅是一个动作,这时就不应该在其身上运用隐式规则。
- 第二,使用隐式规则不能让 我们更好地控制编译语句,比如我在编译的时候想要链接某个指定的库文件,或者添加某些 指定的编译选项,此时隐式规则就显得笨拙
针对第一点,有时我们需要明确地告诉 Makefile 不要对某个目标运用隐式规则,比如 我们每次想要清理工程项目中所有的目标文件,可以将清理工作交给 Makefile 来完成:
OBJ = a.o b.o x.o y.o
image:$(OBJ)
$(CC) $(OBJ) -o image
clean:
$(RM) $(OBJ) image
.PHONY: clean
第 6、7 行声明了一个清理目标文件和 image 的规则,执行这条 Shell 命令时需要指 定 make 的参数 clean,clean 是一个动作的代号,而不是一个我们要生成的文件,但是根 据隐式规则,假如当前目录恰巧有个文件叫做 clean.c,就可能会导致 Makefile 自动生成 其对应的编译语句,从而引起混淆。在第 9 行中用指示符.PHONY 来明确地告诉 Makefile 不要对 clean 运用任何隐式规则,事实上,不能运用隐式规则的目标被称为伪目标
静态规则
针对上述第二点(使用隐式规则不能让我们更好地控制编译语句),我们也许在编译.o 文件的时候需要一些特殊的编译选项,不能完全将他们弃之不管,但是又不想对每一个.o文件写一个规则,那就可以使用静态规则
OBJ = a.o b.o x.o y.o
image:$(OBJ)
$(CC) $(OBJ) -o image
$(OBJ):%.o:%.c
$(CC) $^ -o $@ -Wall -c
clean:
$(RM) $(OBJ) image
.PHONY: clea
第 6、7 行运用了所谓的静态规则,其工作原理是:$(OBJ)被称为原始列表,即(a.o b.o x.o y.o),紧跟在其后的%.o 被称为匹配模式,含义是在原始列表中按照这种指定的 模式挑选出能匹配得上的单词(在本例中要找出原始列表里所有以.o 为后缀的文件)作为 规则的目标
多目标规则
针对我们要生成的三个.o 文件,也可以使用 所谓的多目标规则,具体(其中函数$(subst)的用法和功能请参阅下面有关“函数”的小节
SRC = $(wildcard *.c)
OBJ = $(SRC:%.c=%.o)
image:$(OBJ)
$(CC) $(OBJ) -o image -lgcc
$(OBJ):$(SRC)
$(CC) $(subst .o,.c,$@) -o $@ -c
clean:
$(RM) $(OBJ) image
.PHONY:clean
着重看第 7、8 行,展开后是:
a.o b.o x.o y.o:a.c b.c x.c y.c
$(CC) $(subst .o,.c,$@) -o $@ -c
当中的四个.o文件都是这个规则的目标,规则所定义的命令对所有的目标有效。这个具 有多目标的规则相当于多个规则,规则中命令对不同的目标的执行效果不同,因为在规则的 命令中可能使用自动环变量”$@”,多目标规则意味着所有的目标具有相同的依赖文件。
比如,当目标是 a.o 时,多目标规则将自动构建如下针对 a.o 的规则:
a.o:a.c b.c x.c y.c
$(CC) $(subst .o,.c,$@) -o $@ -c
即:
a.o:a.c b.c x.c y.c
$(CC) a.c -o a.o -c
可以看到,在这个范例中使用多目标规则是比较笨拙的,因为他把所有的源文件都当成 是 a.o 的依赖文件了,因为多目标规则不能根据目标来自动改变依赖文件,要做到这一点 可以使用上面的静态规则。
多目标规则应用在以下两种场合
- 只需描述依赖关系,而不需要指定相关 Shell 命令。比如当前的所有的目标文件都 依赖于一个叫 head.h 的头文件,可以用多目标规则来表达,这样只要 head.h 有改动,四个目标文件都将会被重新编译
- 有多个具有类似构建命令的目标,就是一个.o有好多个.c编译
双冒号规则
双冒号规则就是使用“::”代替普通规则的“:”得到的规则
当同一个文件作为多个规则 的目标时,双冒号规则的处理和普通规则的处理过程完全不同(双冒号规则允许在多个规则 中为同一个目标指定不同的重建目标的命令)
首先需要明确的是:Makefile中,一个目标可以出现在多个规则中。但是这些规则必 须是同一种规则,要么都是普通规则,要么都是双冒号规则。而不允许一个目标同时出现在 两种不同的规则中。
作用
- 当依赖列表为空时,即使目标文件已经存在,双冒号规则能确保规则中的Shell命 令也会被无条件执行
- 当同一个文件作为多个双冒号规则的目标时。这些不同的规则会被独立的处理, 而不是像普通规则那样合并所有的依赖到一个目标文件。这就意味着对这些规则的处理就像 多个不同的普通规则一样。就是说多个双冒号规则中的每一个的依赖文件被改变之后, make只执行此规则定义的命令,而其它的以这个文件作为目标的双冒号规则将不会被执 行
例子
现在有a.c b.c libx.so liby.so Makefile这几个文件
image::b.c
$(CC) a.c -o $@ -L. -lx
image::b.c
$(CC) b.c -o $@ -L. -l
运行make后
cc a.c -o image
cc b.c -o image
不管a.c或者b.c都生成image文件,如果“a.c”文件被修改,执行make以后将根 据“a.c”文件重建目标“image”。而如果“b.c”被修改那么“image”将根据“b.c”被重建。
条件判断
之前提到,我们的工程文件可能在 PC 端编译,也可能在 ARM 平台运行,不同的编译 环境需要使用不同的工具链,我们可以通过手工改动 Makefile 的方式来达到更改编译器的 目的,也可以使用条件判断机制让 Makefile 自动处理
OBJ = a.o b.o x.o y.o
ifdef TOOLCHAIN # ifdef 语句用来判断变量 TOOLCHAIN 是否有定义
CC = $(TOOLCHAIN)
else
CC = gcc
endif
image:$(OBJ)
$(CC) $(OBJ) -o image
$(OBJ):%.o:%.c
$(CC) $^ -o $@ -c
clean:
$(RM) $(OBJ) image
.PHONY:clea
运行make TOOLCHAIN=arm-Linux-gnu-gcc
arm-Linux-gnu-gcc a.c -o a.o -c
arm-Linux-gnu-gcc b.c -o b.o -c
arm-Linux-gnu-gcc x.c -o x.o -c
arm-Linux-gnu-gcc y.c -o y.o -c
arm-Linux-gnu-gcc a.o b.o x.o y.o -o image
执行 make 时指定了参数 TOOLCHAIN=arm-Linux-gcc, 那第 3 行的 ifdef 语句将成立,因此编译器 CC 被调整为用户指定的 TOOLCHAIN。在这个 例子中我们同时也看到了如何在命令行中给 make 传递参数。
假如在用户使用 gcc 编译时需要链接库文件 libgcc.so,而在使用交叉工具 链 arm-Linux-gnu-gcc 时不需要,那么我们的 Makefile 需要再改成
OBJ = a.o b.o x.o y.o
ifdef TOOLCHAIN
CC = $(TOOLCHAIN)
else
CC = gcc
endif
image:$(OBJ)
ifeq ($(CC), gcc) # ifeq ( )用来判断变量 CC 的值是否等于 gcc
$(CC) $(OBJ) -o image -lgcc
else
$(CC) $(OBJ) -o image
endif
$(OBJ):%.o:%.c
$(CC) $^ -o $@ -c
clean:
$(RM) $(OBJ) image
.PHONY:clea
在第 10 行中,使用 ifeq ( )来对 CC 进行了判断,注意:ifeq 跟后面的圆括号之间有 一个空格!ifeq ( )也可以用来判断一个变量是否为空
ifeq ($(A),)
echo “$(A) is empty”
endif
函数
怎样让 Makefile 知道我们的工程来了一个新的文件 c.c 呢?这需要一个叫 wildcard 的函数帮忙
SRC = $(wildcard *.c)
注意到在 Makefile 中书写一个函数的格式:$(function arg1,arg2,arg3, ... ...) 其 中 function 是函数的名字,后面跟一个空格,然后是参数列表,如果有多个参数则用逗号 隔开(注意逗号后面最好不要有空格),整个函数用$( )包裹起来(跟变量一样)
由于 wildcard 函数的作用就是找到参数匹配的文件名,因此该语句的作用就相当于
SRC = a.c b.c c.c x.c y.c
有了源程序文件名字列表,通过变量的替换操作,很容易就可以得到.o 文件列表
OBJ = $(SRC: %.c=%.o)
于是又可以更简便了
SRC = $(wildcard *.c)
OBJ = $(SRC:%.c=%.o)
ifdef TOOLCHAIN
CC = $(TOOLCHAIN)
else
CC = gcc
endif
image:$(OBJ)
ifeq ($(CC),gcc)
$(CC) $(OBJ) -o image -lgcc
else
$(CC) $(OBJ) -o image
endif
$(OBJ):%.o:%.c
$(CC) $^ -o $@ -c
clean:
$(RM) $(OBJ) image
.PHONY:clea
Makefile 中常用到的内嵌函数的详细信息
第一类 文本处理函数。此类函数专门用于处理文本(字符串)
1. 将字符串 TEXT 中的字符 FROM 替换为 TO
$(subst FROM,TO,TEXT)
A = $(subst pp,PP,apple tree)
替换之后变量 A 的值是”aPPle tree”
返回: 替换之后的新字符串
2. 按 照 PATTERN 搜 索 TEXT 中 所 有 以 空 格 隔 开 的 单 词 , 并 将 它 们 替 换 为 REPLACEMENT
$(patsubst PATTERN,REPLACEMENT,TEXT)
范例:
A = $(patsubst %.c,%.o,a.c b.c)
替换之后变量 A 的值是”a.o b.o”
返回: 替换之后的新字符串。
注意:参数 PATTERN 可以使用模式通配符%来代表一个单词中的 若干字符,如果此时 REPLACEMENT 中也出现%,那么 REPLACEMENT 中的%跟 PATTERN 中的%是一样的。
3. 去掉字符串中开头和结尾的多余的空白符(掐头去尾),并将其中连续的多个空白 符合并为一个。
$(strip STRING)
范例:
A = $(strip apple tree )
处理之后,变量 A的值是”apple tree"
返回: 去掉多余空白符之后的新字符串。
注意:所谓的空白符指的是空格、制表符。
4. 在给定的字符串 STRING 中查找 FIND 子串
$(findstring FIND, STRING)
范例:
A = $(findstring pp, apple tree)
B = $(findstring xx, apple tree)
变量 A 的值是”pp”,变量 B 的值是空。
返回: 找到则返回 FIND,否额返回空