Loading

0 Make简介

Make是一个自动化编译工具,但是它本身并没有编译和链接的功能。其采用一种类似于批处理的方式,依据用户编写的规则文件Makefile中指定的命令来进行编译和链接。对于一个工程而言,我们使用make指令就能完全的编译整个工程的源文件,相比于使用GCC编译来说,效率就大大提高了。

1 Makefile规则语法

<target>: <prerequisites1> <prerequisites2>...
[TAB] <command>
  • target:目标,最终需要通过Make工具构建出的具体东西
  • prerequisites:依赖,构建目标的先决条件。只有在满足先决条件的情况下,才能构建目标。在出现⽬标依赖关系时,Make⼯具会按从左到右的顺序构建规则中的所有依赖。
  • command:命令,在依赖全部满足的条件下,调用命令来构建出目标

一个Makefile规则是由⽬标(targets)、先决条件(prerequisites)以及命令(commands)所组成的。
⽬标和先决条件之间表达的就是依赖关系(dependency),这种依赖关系指明在构建⽬标之前,必须保证先决条件先满⾜(或构建)。⽽先决条件可以是其它的⽬标,当先决条件是⽬标时,其必须先被构建出来。

⼀个规则中⽬标可以有多个,当存在多个⽬标,且这⼀规则是Makefile中的第⼀个规则时,如果我们运⾏ make命令不带任何⽬标,那么规则中的第⼀个⽬标将被视为是缺省⽬标。
规则的功能就是指明Make什么时候以及如何来为我们构建⽬标。

当Make在运⾏⼀个规则时,如果先决条件中相关的⽂件的时间戳⼤于⽬标的时间戳,则Make认为其发生了变化,那么就会在运⾏规则时根据命令重新构建⽬标。

1.1 命令书写

每条规则中的命令和操作系统Shell的命令行应该是一致的。Make会按照顺序一条一条的执行命令,每条命令的开头必须以TAB键开头,除非,命令是紧跟在依赖规则后面的分号后的。在命令行之间中的空格或是空行会被忽略,但是如果该空格或空行是以Tab键开头的,那么Make会认为其是一个空命令。

Make的命令默认是被 /bin/sh —— UNIX的标准Shell解释执行的(也可以特别指定一个其它的Shell)。 Makefile中,#是注释符,很像C/C++中的//,其后的本行字符都被注释。

1.2 命令显示

通常,Make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用 @ 字符在命令行前,那么,这个命令将不被Make显示出来。

如果在执行make命令时,带入参数-n--just-print,那么Make只显示命令,而不会执行命令,这个功能很有利于我们调试Makefile,观察我们书写的命令执行起来是什么样子的。

而make命令参数-s--silent--quiet则是全面禁止命令的显示,但是Make仍然会静默执行命令。

1.3 命令前缀

  • [没有前缀]:输出执行的命令以及命令执行的结果,出错的话停止执行
  • [前缀@]:只输出命令执行的结果,出错的话停止执行,但不显示命令
  • [前缀-]:命令执行有错的话,忽略错误,继续执行

1.4 命令出错

有些时候,某一条命令的出错不表示整个Makefile就是错误的。例如mkdir命令,我们需要建立一个目录,如果目录不存在,那么mkdir就成功执行,如果目录存在,那么就会报错。但是我们使用mkdir的意思就是一定要有这样的一个目录,于是我们不希望因为mkdir出错而终止规则的运行。

为了做到忽略命令的出错,我们可以在Makefile的命令行前加一个前缀“-” (在TAB键之后),指示Make即使执行命令出错,也忽略错误,继续执行下一条命令。

# 示例
clean:
    -rm -f *.o

还有一个全局的办法,就是在使用make命令时,带上-i或是--ignore-errors参数,那么,Makefile中所有命令都会忽略错误。

如果一个规则是以 .IGNORE作为目标的,那么这个规则中的所有命令将会忽略错误。这些不同级别的忽略命令出错的方法,使得我们可以根据实际情况进行设置。

2 关键字

.PHONY:

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

.PHONY: clean

override:

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

# override 用法
.PHONY: all
override foo = x
all:
	@echo "foo = $(foo)"
	
# 运行结果
$ make foo=ewqqeq
foo = x

include:

include在Makefile中的作用类似于#include在C/C++文件中的作用。

# 利用 include 指令在Makefile中包含依赖文件
include $(DEPS)

# 若在include前加上“-”, 那么即使其包含的依赖文件不存在, make也会忽略这个错误
-include $(DEPS)

# 利用 include 指令在Makefile中包含别的Makefile
include <filenames>

vpath:

vpath关键字用于设置Make的文件搜索路径。它可以指定不同的文件在不同的搜索目录中,其使用方法有三种。

# 为符合模式<pattern>的文件指定搜索目录<directories>
# vpath <pattern> <directories>
vpath %.h ../headers
# % 的意思是匹配零或若干字符,%.h 表示所有以 .h 结尾的文件
# 此命令指示make在../headers目录下搜索所有以 .h 结尾的文件

# 连续的使用vpath语句,以指定不同的搜索策略
vpath %.c OfilmSwc:GeelySwc
vpath %.c src
vpath %   Application

# 清除符合模式<pattern>的文件的搜索目录
# vpath <pattern>

# 清除所有已被设置好了的文件搜索目录
# vpath

3 变量

# 采用了变量编写的makefile
.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两个变量,⼀个用于存放可执行文件名,可另⼀个则用于放置所有的目标文件名。采用变量的话,当我们需要更改编译器时,只需更改变量赋值的地方,非常方便,如果不采用变量,那我们得更改每⼀个使用编译器的地方,很是麻烦。

注意,变量是大小写敏感的,foo、Foo和FOO是三个不同的变量名。传统的Makefile的变量名是全大写的命名方式,但笔者推荐使用大小写搭配的变量名,如:MakeFlags。这样可以避免和系统的变量冲突,而发生意外的事情。

3.1 自动变量

对于每⼀个规则,目标和先决条件的名字会在规则的命令中多次出现,每⼀次出现都是⼀种麻烦,更为麻烦的是,如果改变了目标或是依赖的名,那得在命令中全部跟着改。因此为了简化这种更改方法,我们需要用到Makefile中的自动变量。

# $@:表示⼀个规则中的⽬标。当我们的⼀个规则中有多个⽬标时,$@所指的是其中任何能使命令可以被运⾏的⽬标
# $^:表示的是规则中的所有先决条件
# $<:表示的是规则中的第⼀个先决条件
# $?: 表示的是所有比目标新的依赖

# makefile如下:
.PHONY:all
all:first second third
	@echo "\$$@ = $@"
	@echo "\$$^ = $^"
	@echo "\$$< = $<"
 
first second third:
# 需要注意的是,在 Makefile 中‘$’具有特殊的意思,因此,如果想采⽤ echo 输出‘$’,则必需⽤两个连着的‘$’。还有就是,$@对于 Shell 也有特殊的意思,我们需要在“$$@”之前再加⼀个脱字符‘\’。

# 运行结果:
$ make
$@ = all
$^ = first second third
$< = first

3.2 自带变量

3.2.1 MAKE

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

.PHONY: clean
all:
	@echo "MAKE = $(MAKE)"
	
# 运行结果:
$ make
MAKE = D:/1Software/Msys2/ucrt64/bin/make.exe

3.2.2 MAKECMDGOALS

表示的是当前用户所输入的make目标是什么,Makefile如下:

.PHONY: all clean
all clean:
	@echo "\$$@ = $@"
	@echo "MAKECMDGOALS = $(MAKECMDGOALS)"
	
# 运行结果:
$ make
$@ = all
MAKECMDGOALS = 

$ make all
$@ = all
MAKECMDGOALS = all

$ make clean
$@ = clean
MAKECMDGOALS = clean

# 从测试结果看来,MAKECMDGOALS 指的是⽤户输⼊的⽬标,当我们只运⾏ make 命令时,虽然根据 Makefile 的语法,第⼀个⽬标将成为缺省⽬标,即 all ⽬标,但 MAKECMDGOALS 仍然是空,⽽不是 all

3.2.3 MAKEFLAGS

表示当前make指令的参数信息

# 为make指令加入 -w 参数
MAKEFLAGS = -w

3.2.4 MAKELEVEL

表示当前Makefile的嵌套调用层数

3.2.5 MAKEFILE_LIST

表示当前make解析的Makefile文件,以空格隔开,存储顺序为Makefile调用顺序。
MAKEFILE_LIST会在Makefile文件前加上相对于make工作目录的路径

3.2.6 VPATH

在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件分类,并存放在不同的目录中。所以,当利用Make去找寻具有依赖关系的文件时,我们可以在文件前加上路径,但最好的方法是把一个路径告诉Make,让Make自动去寻找。

Makefile文件中的特殊变量VPATH就是完成这个功能的,如果没有指明这个变量,Make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,Make就会在当前目录找不到的情况下,到所指定的目录中去找寻文件。

# 指定两个目录,src 和 ../headers,目录由“:”分隔
VPATH = src:../headers

3.3 变量高级用法

3.3.1 变量值的替换

我们可以替换变量中的共有部分,其格式是 $(var:a=b) 或是 ${var:a=b} 。其意思是,把变量var中所有以a字串结尾的a替换成b字串。

# 示例1
foo := a.o b.o c.o
bar := $(foo:.o=.c)
# 把 $(foo) 中所有以 .o 字串结尾全部替换成 .c,所以 $(bar) 的值就是“a.c b.c c.c”

另外一种变量替换的技术是静态模式

# 示例2
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)
# 这依赖于被替换字串中有相同的模式,模式中必须包含一个 % 字符,这个例子同样让 $(bar) 变量的值为“a.c b.c c.c”

3.3.2 把变量的值再当成变量

# 示例1
x = y
y = z
a := $($(x))
# $(a)的值是“z”

# 示例2
x = y
y = z
z = u
a := $($($(x)))
# $(a)的值是“u”

# 示例3
x = $(y)
y = z
z = Hello
a := $($(x))
# $(a)的值是“Hello”

# 示例4
first_second = Hello
a = first
b = second
all = $($a_$b)
# $(all)的值是“Hello”

# 示例5
a_objects := a.o b.o c.o
1_objects := 1.o 2.o 3.o
sources := $($(a1)_objects:.o=.c)
# 如果$(a1)的值是“a”的话,$(sources)的值就是“a.c b.c c.c”;如果$(a1)的值是“1”,$(sources)的值是“1.c 2.c 3.c”

# 示例6
ifdef do_sort
    func := sort
else
    func := strip
endif

bar := a d b g q c
foo := $($(func) $(bar))
# 如果定义了do_sort,foo := $(sort a d b g q c) ,如果没有定义do_sort,foo := $(strip a d b g q c) 

3.4 目标变量

我们可以为某个目标设置局部变量,这种变量被称为Target-specific Variable。它可以和全局变量同名,因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效,而不会影响规则链以外的全局变量的值。

# 语法
<target ...> : <variable-assignment>
<target ...> : override <variable-assignment>
# <variable-assignment>可以是 = 、 := 、 += 或是 ?= 。第二个语法是针对于make命令行带入的变量,或是系统环境变量。

这个特性非常的有用,当我们设置了这样一个变量,这个变量会作用到由这个目标所引发的所有的规则中去。

# 示例:
# 定义目标变量 CFLAGS
prog : CFLAGS = -g
prog : prog.o foo.o bar.o
    $(CC) $(CFLAGS) prog.o foo.o bar.o

prog.o : prog.c
    $(CC) $(CFLAGS) prog.c

foo.o : foo.c
    $(CC) $(CFLAGS) foo.c

bar.o : bar.c
    $(CC) $(CFLAGS) bar.c
# 不管全局$(CFLAGS)的值是什么,在prog目标及其所引发的连带规则中(prog.o foo.o bar.o的规则),$(CFLAGS)的值都是-g

3.5 模式变量

既然变量可以定义在目标上,同样,我们也可以把变量定义在模式上。这样,在符合这种模式的所有目标上,我们都可以使用这个变量。

# 语法
<pattern ...> : <variable-assignment>
<pattern ...> : override <variable-assignment>
# <variable-assignment>可以是 = 、 := 、 += 或是 ?= 。第二个语法是针对于make命令行带入的变量,或是系统环境变量。

# make的模式一般至少含有一个 % ,所以我们可用如下方式给所有以 .o 结尾的目标定义目标变量
%.o : CFLAGS = -O
# 这样在符合该模式的所有目标上,其命令规则中的$(CFLAGS)的值都是-O

4 操作符

4.1 操作符 =

用”=“操作符定义的变量,称之为递归扩展变量(recursively expanded variable),其特点是可以把变量的真实值推到后面来定义。

# 使⽤“=”操作符
.PHONY: all
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
	@echo $(foo)

# 运行结果:
$ make
Huh?

4.2 操作符 :=

用”:=“操作符定义的变量,称之为简单扩展变量(simply expanded variables),其只能使用前面已定义好了的变量。

# 使⽤“:=”操作符
.PHONY: all
x = foo
y = $(x) b
x = later
xx := foo
yy := $(xx) b
xx := later
all:
	@echo "x = $(y), xx = $(yy)"

# 运行结果:
$ make
x = later b, xx = foo b

4.3 操作符 ?=

用”?=“条件赋值操作符定义变量。当变量以前没有定义时,就定义它并且将右边的值赋值给它,如果已经定义了那么就不再改变其值。条件赋值类似于提供了给变量赋缺省值的功能。

# 使用“?=”操作符
.PHONY: all
foo = x
foo ?= y
bar ?= y
all:
	@echo "foo = $(foo), bar = $(bar)"
	
# 运行结果:
$ make
foo = x, bar = y

4.4 操作符 +=

# 使用“+=”操作符
.PHONY: all
objects = main.o foo.o bar.o utils.o
objects += another.o
all:
	@echo $(objects)

# 运行结果:
$ make
main.o foo.o bar.o utils.o another.o

5 静态模式

如果对于每⼀个目标文件都得写⼀个不同的规则来描述,那会是⼀种“体力活”。对于⼀个大型项目,采用Makefile中的静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。

# 静态模式语法
<targets> : <target-pattern> : <prereq-patterns ...>
    <commands>

# targets定义了一系列的目标文件,可以有通配符,是目标的一个集合
# target-pattern是指明了targets的模式,也就是的目标集模式
# prereq-patterns是目标的依赖模式,它对target-pattern形成的模式再进行一次依赖目标的定义
# 模式示例1
objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@

# 上面的例子中,指明了我们的目标从$object中获取, %.o 表明要所有以 .o 结尾的目标,也就是 foo.o bar.o ,也就是变量 $object
# 集合的模式,而依赖模式 %.c 则取模式 %.o 的 % ,也就是 foo bar ,并为其加下 .c 的后缀,于是,我们的依赖目标就是 foo.c
# bar.c 。而命令中的 $< 和 $@ 则是自动化变量, $< 表示第一个依赖文件, $@ 表示目标集(也就是“foo.o bar.o”)。于是,上面的规# 则展开后等价于下面的规则:

foo.o : foo.c
    $(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
    $(CC) -c $(CFLAGS) bar.c -o bar.o
# 模式示例2
files = foo.elc bar.o lose.o

# $(filter %.o,$(files))表示调用Makefile的filter函数,过滤“$files”集,只要其中模式为“%.o”的内容。
$(filter %.o,$(files)): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
    emacs -f batch-byte-compile $<
# 模式示例3
.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)
	
# 运行结果:
$ make
gcc -o main.o -c main.c
gcc -o foo.o -c foo.c
gcc -o simple main.o foo.o

# 模式类似于我们在 Windows 操作系统中所使⽤的通配符,当然是⽤“%”⽽不是“*”。采⽤了模式以后,不论有多少个源⽂件要编译,我们都是应⽤同⼀个模式规则的,很显然,这⼤⼤的简化了我们的⼯作。

6 函数

6.1 字符串处理函数

subst

# Name:字符串替换函数
# brief:把字串 <text> 中的 <from> 字符串替换成 <to> 
# return:被替换过后的字符串
# 函数原型:$(subst <from>,<to>,<text>)

# 示例:
$(subst ee,EE,feet on the street)

# 运行结果
fEEt on the strEEt

patsubst

# Name:模式字符串替换函数
# brief:查找 <text> 中的单词是否符合模式 <pattern> ,如果匹配的话,则以 <replacement> 替换
# return:被替换过后的字符串
# 函数原型:$(patsubst <pattern>,<replacement>,<text>)

# 示例
.PHONY:all
mixed = foo.c bar.c main.o
objects:=$(patsubst %.c, %.o, $(mixed))
all:
	@echo $(objects)
	
# 运行结果:
$ make
foo.o bar.o main.o

strip

# Name:去空格函数
# brief:去掉 <string> 字串中多余的空格
# return:被去掉空格的字符串
# 函数原型:$(strip <string>)

# 示例:
.PHONY: all
original = foo.c     bar.c 
stripped := $(strip $(original))
all:
	@echo "original = $(original)"
	@echo "stripped = $(stripped)"
	
# 运行结果:
$ make
original = foo.c     bar.c 
stripped = foo.c bar.c

findstring

# Name:查找字符串函数
# brief:在字串 <in> 中查找 <find> 字串
# return:如果找到,那么返回 <find> ,否则返回空字符串
# 函数原型:$(findstring <find>,<in>)

# 示例:
$(findstring a,a b c)
$(findstring a,b c)

# 运行结果:
第一个函数返回 a 字符串,第二个返回空字符串

filter

# Name:过滤函数
# brief:以 <pattern> 模式过滤 <text> 字符串中的单词,保留符合模式 <pattern> 的单词。可以有多个模式
# return:返回符合模式 <pattern> 的字串
# 函数原型:$(filter <pattern...>,<text>)

# 示例
.PHONY: all
sources = foo.c bar.c baz.s ugh.h
sources := $(filter %.c %.s, $(sources))
all:
	@echo $(sources)
	
# 运行结果:
$ make
foo.c bar.c baz.s

filter-out

# Name:反过滤函数
# brief:以 <pattern> 模式过滤 <text> 字符串中的单词,去除符合模式 <pattern> 的单词。可以有多个模式
# return:返回不符合模式 <pattern> 的字串
# 函数原型:$(filter-out <pattern...>,<text>)

# 示例
.PHONY: all
objects = main1.o foo.o main2.o bar.o
result := $(filter-out main%.o, $(objects))
all:
	@echo $(result)
	
# 运行结果:
$ make
foo.o bar.o

sort

# Name:排序函数
# brief:给字符串 <list> 中的单词排序(升序)
# return:返回排序后的字符串
# note:sort 函数会去掉 <list> 中相同的单词
# 函数原型:$(sort <list>)

# 示例:
$(sort foo bar lose)

# 运行结果:
返回 bar foo lose

word

# Name:取单词函数
# brief:取字符串 <text> 中第 <n> 个单词。(从1开始)
# return:返回字符串 <text> 中第 <n> 个单词
# note:如果 <n> 溢出,那么返回空字符串
# 函数原型:$(word <n>,<text>)

# 示例:
$(word 2, foo bar baz)

# 运行结果:
返回值是 bar

wordlist

# Name:取单词串函数
# brief:从字符串 <text> 中取从 <ss> 开始到 <e> 的单词串。 <ss> 和 <e> 是一个数字。
# return:返回字符串 <text> 中从 <ss> 到 <e> 的单词字串。
# note:如果 <ss> 溢出,那么返回空字符串。如果 <e> 溢出,那么返回从 <ss> 开始,到 <text> 结束的单词串。
# 函数原型:$(wordlist <ss>,<e>,<text>)

# 示例:
$(wordlist 2, 3, foo bar baz)

# 运行结果:
返回值是 bar baz

words

# Name:单词个数统计函数
# brief:统计 <text> 中字符串中的单词个数。
# return:返回 <text> 中的单词数。
# note:如果我们要取 <text> 中最后的一个单词,我们可以这样: $(word $(words <text>),<text>) 。
# 函数原型:$(words <text>)

# 示例:
$(words, foo bar baz)

# 运行结果:
返回值是 3

firstword

# Name:首单词函数
# brief:取字符串 <text> 中的第一个单词
# return:返回字符串 <text> 的第一个单词。
# note:这个函数可以用 word 函数来实现: $(word 1,<text>)
# 函数原型:$(firstword <text>)

# 示例:
$(firstword foo bar)

# 运行结果:
返回值是 foo

lastword

# Name:尾单词函数
# brief:取字符串 <text> 中的最后一个单词
# return:返回字符串 <text> 的最后一个单词。
# note:
# 函数原型:$(lastword <text>)

6.2 文件名操作函数

dir

# Name:取目录函数
# brief:从文件名序列 <names> 中取出目录部分。
# return:返回文件名序列 <names> 的目录部分。
# note:目录部分是指最后一个反斜杠( / )之前的部分。如果没有反斜杠,那么返回 ./ 。
# 函数原型:$(dir <names...>)

# 示例:
$(dir src/foo.c hacks)

# 运行结果:
返回值是 src/ ./

notdir

# Name:取文件函数
# brief:从文件名序列 <names> 中取出非目录部分。
# return:返回文件名序列 <names> 的非目录部分。
# note:非目录部分是指最後一个反斜杠( / )之后的部分。
# 函数原型:$(notdir <names...>)

# 示例:
$(notdir src/foo.c hacks)

# 运行结果:
返回值是 foo.c hacks

suffix

# Name:取后缀函数
# brief:从文件名序列 <names> 中取出各个文件名的后缀。
# return:返回文件名序列 <names> 的后缀序列,如果文件没有后缀,则返回空字串。
# note:
# 函数原型:$(suffix <names...>)

# 示例:
$(suffix src/foo.c src-1.0/bar.c hacks)

# 运行结果:
返回值是 .c .c

basename

# Name:取前缀函数
# brief:从文件名序列 <names> 中取出各个文件名的前缀。
# return:返回文件名序列 <names> 的前缀序列,如果文件没有前缀,则返回空字串。
# note:
# 函数原型:$(basename <names...>)

# 示例:
$(basename src/foo.c src-1.0/bar.c hacks)

# 运行结果:
返回值是 src/foo src-1.0/bar hacks

addsuffix

# Name:加后缀函数
# brief:把后缀 <suffix> 加到 <names> 中的每个单词后面。
# return:返回加过后缀的文件名序列。
# note:
# 函数原型:$(addsuffix <suffix>,<names...>)

# 示例:
$(addsuffix .c,foo bar)

# 运行结果:
返回值是 foo.c bar.c 

addprefix

# Name:加前缀函数
# brief:把前缀 <prefix> 加到 <names> 中的每个单词前面。
# return:返回加过前缀的文件名序列。
# note:
# 函数原型:$(addprefix <prefix>,<names...>)

# 示例:
.PHONY:all
without_dir = foo.c bar.c main.c
with_dir := $(addprefix objs/, $(without_dir))
all:
	@echo $(with_dir)
	
# 运行结果:
$ make
objs/foo.c objs/bar.c objs/main.c

join

# Name:连接函数
# brief:把 <list2> 中的单词对应地加到 <list1> 的单词后面。
# return:返回连接过后的字符串。
# note:若 <list1> 溢出,<list1> 中溢出的单词将保持原样。若 <list2> 溢出,<list2> 中溢出的单词将被复制到 <list1> 后。
# 函数原型:$(join <list1>,<list2>)

# 示例:
$(join aaa bbb , 111 222 333)
	
# 运行结果:
返回值是 aaa111 bbb222 333

wildcard

# Name:通配符函数
# brief:在当前工作目录下,筛选出匹配模式的文件
# return:返回筛选出的文件
# note:
# 函数原型:$(wildcard pattern)

# 示例:
.PHONY: all
SRC = $(wildcard *.c)
all:
	@echo "SRC = $(SRC)"
	
# 运行结果:
$ make
SRC = bar.c foo.c main.c

6.3 其他函数

foreach

# Name:循环函数
# brief:把参数 <list> 中的单词逐一取出放到参数 <var> 所指定的变量中,然后再执行 <text> 所包含的表达式。
# return:当循环结束时,返回 <text> 所返回的每个字符串所组成的整个字符串(以空格分隔)。
# note:<var> 最好是一个变量名, <list> 可以是一个表达式,而 <text> 中一般会使用 <var> 这个参数来依次枚举 <list> 中的单词
# 函数原型:$(foreach <var>,<list>,<text>)

# 示例:
names := a b c d

files := $(foreach n,$(names),$(n).o)
	
# 运行结果:
$(files) 的值是 a.o b.o c.o d.o

if

# Name:条件函数
# brief:若 <condition> 为真(非空字符串),返回 <then-part> ,否则,返回 <else-part>。
# return:返回 <then-part> 和 <else-part> 中的一个(只会有一个被计算)。
# note:如果 <else-part> 没有被定义,那么,整个函数返回空字串。
# 函数原型:$(if <condition>,<then-part>) 或 $(if <condition>,<then-part>,<else-part>)

# 示例:
	
# 运行结果:

call

# call函数是唯一一个可以用来创建新的参数化的函数。我们可以写一个非常复杂的表达式,这个表达式中,我们可以定义许多参数,然后利用call函数来向这个表达式传递参数
# 函数原型:$(call <expression>,<parm1>,<parm2>,...,<parmn>)

# 当make执行这个函数时, <expression> 参数中的变量,如 $(1) 、 $(2) 等,会被参数 <parm1> 、 <parm2> 、 <parm3> 依次取代。# 而 <expression> 的返回值就是 call 函数的返回值。
# 示例1:
reverse =  $(1) $(2)
foo = $(call reverse,a,b)

# 运行结果:
foo 的值就是 a b 

# 示例2:
reverse =  $(2) $(1)
foo = $(call reverse,a,b)

# 运行结果:
foo 的值就是 b a

# 在向 call 函数传递参数时要注意空格的使用。call 函数在处理参数时,第2个及其之后的参数中的空格会被保留,因而可能造成一些奇怪
# 的效果。因而在向call函数提供参数时,最安全的做法是去除所有多余的空格。

origin

# origin函数不像其它的函数,他并不操作变量的值,他只是告诉你你的这个变量是哪里来的
# 函数原型:$(origin <variable>)
# 注意,<variable> 是变量的名字,不应该是引用。所以最好不要在 <variable> 中使用 $ 字符

# origin函数返回值:
# undefined:如果 <variable> 从来没有定义过,origin函数返回这个值 undefined
# default:如果 <variable> 是一个默认的定义,比如“CC”这个变量
# environment:如果 <variable> 是一个环境变量,并且当Makefile被执行时, -e 参数没有被打开
# file:如果 <variable> 这个变量被定义在Makefile中
# command line:如果 <variable> 这个变量是被命令行定义
# override:如果 <variable> 是被override指示符重新定义的
# automatic:如果 <variable> 是一个命令运行中的自动化变量

# 这些信息对于我们编写Makefile是非常有用的。例如,我们有一个Makefile包含一个定义文件 Make.def,在 Make.def中定义了一个变量
# “bletch”,而我们的环境中也有一个环境变量“bletch”,此时,我们想判断一下,如果变量来源于环境,那么我们就把之重定义,如果来源
# 于Make.def或是命令行等非环境变量,那么我们就不重新定义它。于是,在我们的Makefile中,我们可以这样写:
ifdef bletch
    ifeq "$(origin bletch)" "environment"
        bletch = barf, gag, etc.
    endif
endif
# 虽然 override 关键字也可以重新定义环境变量,但是它同时也会把从命令行定义的变量也覆盖,而我们只想重新定义环境传来的变量,而不# 想重新定义命令行传来的变量

shell

# shell函数的参数就是操作系统Shell的命令。它和反引号“`”是相同的功能。shell函数把执行操作系统命令后的输出作为函数返回。因此,
# 我们可以用操作系统命令以及字符串处理命令awk,sed等等来生成一个变量,如:
contents := $(shell cat foo)
files := $(shell echo *.c)

# 注意:shell函数会新生成一个Shell程序来执行命令,若我们的Makefile中有一些比较复杂的规则,并大量使用了shell函数,那么会影响
# 我们的系统性能。特别是Makefile的隐式规则可能会让shell函数执行的次数比想像中多得多。

error

# 函数原型:$(error <text ...>)
# brief:输出错误信息,make停止执行

# 示例1:
ifdef ERROR_001
    $(error error is $(ERROR_001))
endif

# 运行结果:
在变量ERROR_001定义了后执行时产生error调用

# 示例2:
ERR = $(error found an error!)
.PHONY: err
err: $(ERR)

# 运行结果:
在目标err被执行时才发生error调用

warning

# 函数原型:$(warning <text ...>)
# brief:输出警告信息,make继续执行

info

# 函数原型:$(warning <text ...>)
# brief:输出调试信息,make继续执行

7 创建目录

毫无疑问,我们在编译项目之前希望先准备好用于存放文件的目录。虽然我们可以在编译之前手动创建所需的目录,但是利用Makefile可以自动的创建工程目录,会方便很多。

.PHONY: all
MKDIR = mkdir
DIRS = output bin
RM = rm
RMFLAGS = -fr

all: $(DIRS)
$(DIRS):
	$(MKDIR) $@
# 注意$@的用法

clean:
	$(RM) $(RMFLAGS) $(DIRS)
	
# 运行结果:
$ make
mkdir output
mkdir bin

8 归纳文件

为了将目标文件或是可执行程序分别放入上节所创建的output和bin目录中,我们需要用到Makefile中的⼀个函数 —— addprefix。

.PHONY:all clean
MKDIR=mkdir
RM=rm
RMFLAGS=-fr
CC=gcc

# 确定要构建的目录
DIR_OBJS=output
DIR_EXE=bin
DIRS=$(DIR_OBJS) $(DIR_EXE)

EXE=test

# 为test加上前缀:bin/test,故最后的目标 test 生成在bin子目录下
EXE:=$(addprefix $(DIR_EXE)/, $(EXE))  

# 找到工作目录下的所有.c文件
SRCS=$(wildcard *.c)  

# 将.c替换为.o,此时OBJS= foo.o main.o
OBJS=$(SRCS:.c=.o) 

# 为OBJS加上前缀:output/foo.o output/main.o,故.o文件都生成在output子目录下
OBJS:=$(addprefix $(DIR_OBJS)/, $(OBJS))  
 
all:$(DIRS) $(EXE)
 
# 构建目录
$(DIRS):
	$(MKDIR) $@  

# 构建目标:bin/test, 依赖:output/foo.o,output/main.o
$(EXE):$(OBJS)
	$(CC) -o $@ $^

# 构建目标:output/foo.o,output/main.o,依赖:foo.c,main.c
# 注意 “$(DIR_OBJS)/%.o”,为目标加上子目录前缀
$(DIR_OBJS)/%.o:%.c
	$(CC) -o $@ -c $^

clean:
	$(RM) $(RMFLAGS) $(DIRS)
	
# 运行结果:
$ make
mkdir output  
mkdir bin  
gcc -o output/foo.o -c foo.c
gcc -o output/main.o -c main.c
gcc -o bin/test output/foo.o output/main.o

9 复杂的依赖关系

.PHONY: all clean
MKDIR = mkdir
RM = rm
RMFLAGS = -fr
CC = gcc

# 确定要构建的目录
DIR_OBJS = output
DIR_EXE = bin
DIRS = $(DIR_OBJS) $(DIR_EXE)

EXE = test.exe

# 为test加上前缀:bin/test,故最后的目标 test 生成在bin子目录下
EXE := $(addprefix $(DIR_EXE)/, $(EXE))

# 找到工作目录下的所有.c文件
SRCS = $(wildcard *.c)

# 将.c替换为.o,此时OBJS= foo.o main.o
OBJS = $(SRCS:.c=.o)

# 为OBJS加上前缀:output/foo.o output/main.o,故.o文件都生成在output子目录下
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
 
all: $(DIRS) $(EXE)

# 构建目录
$(DIRS):
	$(MKDIR) $@

# 构建目标:bin/test, 依赖:output/foo.o,output/main.o
$(EXE): $(OBJS)
	$(CC) -o $@ $^

# 构建目标:output/foo.o,output/main.o,依赖:foo.c,main.c
# 注意 “$(DIR_OBJS)/%.o”,为目标加上子目录前缀
# 注意 “foo.h”,此时目标的依赖项多了一个foo.h头文件,
$(DIR_OBJS)/%.o: %.c foo.h
	$(CC) -o $@ -c $<
# 此处采用$<而不是$^,因为不能用gcc编译.h文件

clean:
	$(RM) $(RMFLAGS) $(DIRS)

# 运行结果:
$ make
mkdir output
mkdir bin
gcc -o output/foo.o -c foo.c
gcc -o output/main.o -c main.c
gcc -o bin/test.exe output/foo.o output/main.o

上述Makefile在构建.o目标时添加了对头文件foo.h的依赖,因此若foo.h文件有被修改,则在下次编译时,Make工具会重新编译该.o文件。若不包含对头文件的依赖,则即使头文件被修改了,Make也不会重新编译。

当项目复杂时,如果我们要将每⼀个头文件都写入到Makefile相对应的规则中,这将会是⼀个恶梦!所以我们需要找到另⼀种更好的方法来添加头文件的依赖。此时可以利用gcc -Mgcc -MM来动态的生成文件依赖关系。

.PHONY: all clean
MKDIR = mkdir
RM = rm
RMFLAGS = -fr
CC = gcc
 
DIR_OBJS = output
DIR_EXE = bin
DIR_DEPS = deps
DIRS = $(DIR_OBJS) $(DIR_EXE) $(DIR_DEPS)

EXE = test
EXE := $(addprefix $(DIR_EXE)/, $(EXE))

SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))

DEPS = $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))
 
all: $(EXE)
 
# include关键字,用于包含依赖文件
# 当 make 看到 include 指令时,会先去查找包含的依赖⽂件是否存在。
# 若依赖文件存在,make 还会在 Makefile 中查找是否存在规则来更新它。若存在,则运⾏规则去更新需要被包含的依赖文件,更新完再将其包含进来
# 若依赖文件不存在,make 也会在 Makefile 中查找是否存在规则来构建它。若存在,则运⾏规则去构建需要被包含的依赖文件,构建完再将其包含进来
# 若依赖文件不存在,且在 Makefile 中不存在规则来构建它,则报错
include $(DEPS)
 
$(DIRS):
	$(MKDIR) $@

# 构建目标:bin/test,依赖:子目录bin,output/*.o
# 注意构建目标的依赖的第一个先决条件是子目录bin,所以当子目录不存在时,会先调用构建子目录的规则去建立该子目录
$(EXE): $(DIR_EXE) $(OBJS)
	$(CC) -o $@ $(filter %.o, $^)
# 注意在命令中利用filter函数过滤出output/*.o,否则GCC编译时会连同子目录bin一起编译,这样编译会报错

# 构建目标:output/*.o,依赖:子目录output,*.c
# 注意构建目标的依赖的第一个先决条件是子目录output,所以当子目录不存在时,会先调用构建子目录的规则去建立该子目录
$(DIR_OBJS)/%.o: $(DIR_OBJS) %.c
	$(CC) -o $@ -c $(filter %.c, $^)
# 注意在命令中利用filter函数过滤出*.c,否则GCC编译时会连同子目录output一起编译,这样编译会报错

# 此规则就是include中包含的依赖文件的构建规则
# 构建目标:deps/*.dep,依赖:子目录deps,*.c
# 注意构建目标的依赖的第一个先决条件是子目录deps,所以当子目录不存在时,会先调用构建子目录的规则去建立该子目录,然后再构建出目标deps/*.dep
$(DIR_DEPS)/%.dep: $(DIR_DEPS) %.c
	@echo "Making $@ ..."
	@set -e; \
	$(RM) $(RMFLAGS) $@.tmp ; \
	$(CC) -E -MM $(filter %.c, $^) > $@.tmp ; \
	sed 's,\(.*\)\.o[ :]*,objs/\1.o: ,g' < $@.tmp > $@ ; \
	$(RM) $(RMFLAGS) $@.tmp

clean:
	$(RM) $(RMFLAGS) $(DIRS)
	
# 运行结果:
$ make
mkdir deps
Making deps/main.dep ...
Making deps/foo.dep ...
mkdir bin
mkdir output
gcc -o output/foo.o -c foo.c
gcc -o output/main.o -c main.c
gcc -o bin/test output/foo.o output/main.o

10 条件语法

在Makefile中一共包含四种条件语法:ifdef、ifeq、ifndef 、ifneq。

特别注意:Make是在读取Makefile时就计算条件表达式的值,并根据条件表达式的值来选择语句,而自动变量($@、$^、$<)是在构建规则的命令处理阶段才被赋值,故自动变量在Makefile条件语句块中不能使用。

# Makefile中的条件语法有三种形式。其中 conditional-directive 可以是 ifdef、ifeq、ifndef 和 ifneq 中的任意⼀个
# 1
conditional-directive
	text-if-true
endif

# 2
conditional-directive
	text-if-true
else
	text-if-false
endif

# 3
conditional-directive
	text-if-one-is-true
else conditional-directive
	text-if-true
else
	text-if-false
endif
举例1:
.PHONY: all
sharp = square
desk = square
table = circle
 
ifeq ($(sharp), $(desk))
	result1 = "desk == sharp"
else
	result1 = "desk != sharp"
endif

ifneq "$(table)" "square"
	result2 = "table != square"
else
	result2 = "table == square"
endif

# 条件语法在构建规则之前被使用
all:
	@echo $(result1)
	@echo $(result2)
	
# 运行结果:
$ make
desk == sharp
table != square
举例2:
.PHONY: all
bar = defined

ifdef foo
	result1 = "foo is defined"
else
	result1 = "foo is not defined"
endif

ifndef bar
	result2 = "bar is not defined"
else
	result2 = "bar is defined"
endif

# 条件语法在构建规则之前被使用
all:
	@echo $(result1)
	@echo $(result2)
	
# 运行结果:
$ make
foo is not defined
bar is defined

11 头文件依赖与.d文件

复杂的依赖关系中,提到过头文件依赖,但是由于要用到sed命令,实现比较复杂,现在提供一种较简单的写法。

.PHONY: all clean
MKDIR = mkdir
RM = rm
RMFLAGS = -fr
CC = gcc
 
DIR_OUT = tempOut
DIR_EXE = bin
DIRS = $(DIR_OUT) $(DIR_EXE)

EXE = main.exe
EXE := $(addprefix $(DIR_EXE)/, $(EXE))

SRCS = $(wildcard *.c)

OBJS = $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OUT)/, $(OBJS))

DEPS = $(SRCS:.c=.d)
DEPS := $(addprefix $(DIR_OUT)/, $(DEPS))
 
all: $(EXE)

# 在include前加上“-”,那么即使其包含的依赖文件不存在,make也会忽略这个错误
# 在第一次编译时很有用,因为第一次编译时,往往.d文件都不存在
# 这种写法就不用先构建.d文件,才能构建可执行文件
-include $(DEPS)

$(DIRS):
	$(MKDIR) $@

$(EXE): $(DIR_EXE) $(OBJS)
	$(CC) -o $@ $(filter %.o, $^)

$(DIR_OUT)/%.o: $(DIR_OUT) %.c
	$(CC) -o $@ -c -MMD $(filter %.c, $^)

clean:
	$(RM) $(RMFLAGS) $(DIRS)	

# 运行结果:
$ make
mkdir bin
mkdir tempOut
gcc -o tempOut/foo.o -c -MMD foo.c
gcc -o tempOut/main.o -c -MMD main.c
gcc -o bin/main.exe tempOut/foo.o tempOut/main.o

12 make的嵌套执行

在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中。因此我们可以在每个目录中都书写一个该目录的Makefile,这有利于让我们的Makefile变得更加地简洁,而不至于把所有的东西全部写在一个Makefile中,这样会很难维护我们的Makefile,这个技术对于我们模块编译和分段编译有着非常大的好处。

例如,在当前工作目录下有一个子目录subdir,subdir下有一个Makefile文件,其指明了subdir目录下文件的编译规则。那么我们的总控Makefile可以这样写:

# 利用cd命令进入subdir,并调用make执行subdir下的Makefile
subsystem:
    cd subdir && $(MAKE)
    
# 利用make参数-C进入subdir,并调用make执行subdir下的Makefile
subsystem:
    $(MAKE) -C subdir

# 以上两个例子完全等价

在总控Makefile中可能显示的定义了一些变量,那么这些变量会默认被传递到下级的Makefile中,但是不会覆盖下级Makefile中定义的变量,除非为make命令指定了 -e 参数。

# 如果要传递某些变量到下级Makefile中,我们可以这样声明:
# export <variable ...>

# 如果不要传递某些变量到下级Makefile中,我们可以这样声明:
# unexport <variable ...>
# 示例1
export variable = value

# 其完全等价于
variable = value
export variable

# 示例2
export variable := value

# 其完全等价于
variable := value
export variable

# 实例3
export variable += value

# 其完全等价于
variable += value
export variable

如果需要传递所有的变量,那么我们只需要一个export就行了,后面什么都不要跟,表示传递所有变量。

需要注意MAKEFLAGS变量,其中包含了make指令的参数信息。如果我们的总控Makefile有make指令的参数或者在上层的Makefile中定义了这个变量,那么MAKEFLAGS变量将会是这些参数,并会传递到下层Makefile中。由于这是一个系统级的环境变量,所以不管我们是否export,其都会被传递到下层Makefile。但是make指令中的 -C , -f , -h, -o 和 -W 参数不会往下传递。

# 如果我们一定要让MAKEFLAGS不向下层传递,那么我们可以采取这种办法
subsystem:
    cd subdir && $(MAKE) MAKEFLAGS =

13 定义命令包

如果Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个命令包。定义这种命令包的语法以 define 开始,以 endef 结束。命令包的名字不要和Makefile中的变量重名。

# 定义命令包 run-yacc
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef

# 使用命令包 run-yacc
foo.c : foo.y
    $(run-yacc)

14 Make的运行

14.1 make的退出码

make命令执行后有三个退出码:

  • 0:指示make命令执行成功
  • 1:指示make命令执行时出现任何错误
  • 2:指示使用make命令的-q参数选项,且make使得一些目标不需要更新

14.2 make的参数

# -n, --just-print, --dry-run, --recon
不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试makefile很有用处。

# -t, --touch
这个参数的意思就是把目标文件的时间更新,但不更改目标文件。也就是说,make假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。

# -q, --question
这个参数的行为是找目标的意思,也就是说,如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。

# -W <file>, --what-if=<file>, --assume-new=<file>, --new-file=<file>
这个参数需要指定一个文件。一般是是源文件(或依赖文件),Make会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。

# -b, -m
这两个参数的作用是忽略和其它版本make的兼容性。

# -B, --always-make
认为所有的目标都需要更新(重编译)。

# -C <dir>, --directory=<dir>
指定读取makefile的目录。如果有多个“-C”参数,make的解释是后面的路径以前面的作为相对路径,并以最后的目录作为被指定目录。如:“make -C ~hchen/test -C prog”等价于“make -C ~hchen/test/prog”。

# -debug[=<options>]
输出make的调试信息。它有几种不同的级别可供选择,如果没有参数,那就是输出最简单的调试信息。下面是<options>的取值:
a: 也就是all,输出所有的调试信息。(会非常的多)
b: 也就是basic,只输出简单的调试信息。即输出不需要重编译的目标。
v: 也就是verbose,在b选项的级别之上。输出的信息包括哪个makefile被解析,不需要被重编译的依赖文件(或是依赖目标)等。
i: 也就是implicit,输出所有的隐含规则。
j: 也就是jobs,输出执行规则中命令的详细信息,如命令的PID、返回码等。
m: 也就是makefile,输出make读取makefile,更新makefile,执行makefile的信息。

# -d
相当于“–debug=a”

# -e, --environment-overrides
指明环境变量的值覆盖Makefile中定义的变量的值

# -f=<file>, --file=<file>, --makefile=<file>
指定需要执行的makefile

# -h, --help
显示帮助信息。

# -i , --ignore-errors
在执行时忽略所有的错误。

# -I <dir>, --include-dir=<dir>
指定一个被包含makefile的搜索目标。可以使用多个“-I”参数来指定多个目录。

# -j [<jobsnum>], --jobs[=<jobsnum>]
指同时运行命令的个数。如果没有这个参数,make运行命令时能运行多少就运行多少。如果有一个以上的“-j”参数,那么仅最后一个“-j”才是有效的。(注意这个参数在MS-DOS中是无用的)

# -k, --keep-going
出错也不停止运行。如果生成一个目标失败了,那么依赖于其上的目标就不会被执行了。

# -l <load>, --load-average[=<load>], -max-load[=<load>]
指定make运行命令的负载。

# -n, --just-print, --dry-run, --recon
仅输出执行过程中的命令序列,但并不执行。

# -o <file>, --old-file=<file>, --assume-old=<file>
不重新生成的指定的<file>,即使这个目标的依赖文件新于它。

# -p, --print-data-base
输出makefile中的所有数据,包括所有的规则和变量。这个参数会让一个简单的makefile都会输出一堆信息。如果你只是想输出信息而不想执行makefile,你可以使用“make -qp”命令。如果你想查看执行makefile前的预设变量和规则,你可以使用 “make –p –f /dev/null”。这个参数输出的信息会包含着你的makefile文件的文件名和行号,所以,用这个参数来调试你的 makefile会是很有用的,特别是当你的环境变量很复杂的时候。

# -q, --question
不运行命令,也不输出。仅仅是检查所指定的目标是否需要更新。如果是0则说明要更新,如果是2则说明有错误发生。

# -r, --no-builtin-rules
禁止make使用任何隐含规则。

# -R, --no-builtin-variabes
禁止make使用任何作用于变量上的隐含规则。

# -s, --silent, --quiet
在命令运行时不输出命令的输出。

# -S, --no-keep-going, --stop
取消“-k”选项的作用。因为有些时候,make的选项是从环境变量“MAKEFLAGS”中继承下来的。所以你可以在命令行中使用这个参数来让环境变量中的“-k”选项失效。

# -t, --touch
相当于UNIX的touch命令,只是把目标的修改日期变成最新的,也就是阻止生成目标的命令运行。

# -v, --version
输出make程序的版本、版权等关于make的信息。

# -w, --print-directory
输出运行makefile之前和之后的信息。这个参数对于跟踪嵌套式调用make时很有用。

# --no-print-directory
禁止“-w”选项。

# -W <file>, --what-if=<file>, --new-file=<file>, --assume-file=<file>
假定目标<file>;需要更新,如果和“-n”选项使用,那么这个参数会输出该目标更新时的运行动作。如果没有“-n”那么就像运行UNIX的“touch”命令一样,使得<file>;的修改时间为当前时间。

# --warn-undefined-variables
只要make发现有未定义的变量,那么就输出警告信息。

14.3 指定Makefile

我们可以给make命令指定一些特殊的Makefile,只需要利用make的 -f 或是 –file 或是 –makefile 参数。

例如现在有个Makefile的名字是NXL.mk,那么我们可以这样让make执行这个文件:

make –f NXL.mk

14.4 指定目标

在一个Makefile中可能有多个目标,而Make中的MAKECMDGOALS变量则存放了我们make的最终目标列表。这个变量在某些特殊情况下可以使用。

sources = foo.c bar.c
ifneq ($(MAKECMDGOALS), clean)
    include $(sources:.c=.d)
endif

# 运行结果:
只要我们输入的命令不是“make clean”,那么Makefile会自动包含“foo.d”和“bar.d”这两个依赖关系文件

由于Make可以指定所有Makefile中的目标,包括“伪目标”,因此我们可以根据这种性质来让Make根据指定的不同的目标来完成不同的事。在大型开源软件发布时,其Makefile都包含了编译、安装、打包等功能。我们可以参照这种规则来书写我们的Makefile中的目标。

  • all:这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
  • clean:这个伪目标功能是删除所有被make创建的文件。
  • install:这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
  • print:这个伪目标的功能是例出改变过的源文件。
  • tar:这个伪目标功能是把源程序打包备份。也就是一个tar文件。
  • dist:这个伪目标功能是创建一个压缩文件,一般是把tar文件压成Z文件。或是gz文件。
  • TAGS:这个伪目标功能是更新所有的目标,以备完整地重编译使用。
  • check和test:这两个伪目标一般用来测试Makefile的流程。

使用上述的规范写法一是很实用,二是可以显得我们的Makefile很专业。

15 Make隐含规则

15.1 隐含规则命令变量

  • AR : 函数库打包程序。默认命令是 ar
  • AS : 汇编语言编译程序。默认命令是 as
  • CC : C语言编译程序。默认命令是 cc
  • CXX : C++语言编译程序。默认命令是 g++
  • CO : 从 RCS文件中扩展文件程序。默认命令是 co
  • CPP : C程序的预处理器(输出是标准输出设备)。默认命令是 $(CC) –E
  • FC : Fortran 和 Ratfor 的编译器和预处理程序。默认命令是 f77
  • GET : 从SCCS文件中扩展文件的程序。默认命令是 get
  • LEX : Lex方法分析器程序(针对于C或Ratfor)。默认命令是 lex
  • PC : Pascal语言编译程序。默认命令是 pc
  • YACC : Yacc文法分析器(针对于C程序)。默认命令是 yacc
  • YACCR : Yacc文法分析器(针对于Ratfor程序)。默认命令是 yacc –r
  • MAKEINFO : 转换Texinfo源文件(.texi)到Info文件程序。默认命令是 makeinfo
  • TEX : 从TeX源文件创建TeX DVI文件的程序。默认命令是 tex
  • TEXI2DVI : 从Texinfo源文件创建军TeX DVI 文件的程序。默认命令是 texi2dvi
  • WEAVE : 转换Web到TeX的程序。默认命令是 weave
  • CWEAVE : 转换C Web 到 TeX的程序。默认命令是 cweave
  • TANGLE : 转换Web到Pascal语言的程序。默认命令是 tangle
  • CTANGLE : 转换C Web 到 C。默认命令是 ctangle
  • RM : 删除文件命令。默认命令是 rm –f

15.2 隐含规则参数变量

  • ARFLAGS : 函数库打包程序AR命令的参数。默认值是 rv
  • ASFLAGS : 汇编语言编译器参数。(当明显地调用 .s.S 文件时)
  • CFLAGS : C语言编译器参数。
  • CXXFLAGS : C++语言编译器参数。
  • COFLAGS : RCS命令参数。
  • CPPFLAGS : C预处理器参数。( C 和 Fortran 编译器也会用到)。
  • FFLAGS : Fortran语言编译器参数。
  • GFLAGS : SCCS “get”程序参数。
  • LDFLAGS : 链接器参数。(如: ld
  • LFLAGS : Lex文法分析器参数。
  • PFLAGS : Pascal语言编译器参数。
  • RFLAGS : Ratfor 程序的Fortran 编译器参数。
  • YFLAGS : Yacc文法分析器参数。
  • OS: 当前的操作系统(Operating System

16 总结

Makefile在一些简单的工程完全可以人工手写,但是当工程非常大的时候,手写Makefile也是非常麻烦的,同时Makefile是分平台的,也就是说当我们换了一个新的平台之后,又需要重新去编写Makefile文件,还是比较麻烦,因此,在这个时候就出现了CMake工具。关于CMake工具的用法,博主会在该专栏的下一篇文章讲解。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

👤本站访客数: 👁️本站访问量: