MakeFile 入门及应用

MakeFile

作者:解琛
时间:2020 年 4 月 1 日

GNU makeGNU make(工程管理器 make 在不同环境有很多版本分支,比如 Qt 下的 qmake,windows 下的 nmake 等,下面提到的 make 指的是 LINUX 下的 GNU make)。

而 Makefile,是 make 的配置文件,用来配置运行 make 的时候的一些相关细节,比如指定编译选项,指定编译环境等等。一般而言,一个工程项目不管是简单还是复杂,每一个源代码子目录都会有一个 Makefile 来管理,然后一般有个所谓的顶层 Makefile 来统一管理所有的子目录 Makefile。

一、Hello World!

funny:
	echo "Hello World!"

之后,我们使用make来进行编译。

xiechen@xiechen-Ubuntu:~/文档/apc/1.serialFile/build$ mv MakeFile makefile
xiechen@xiechen-Ubuntu:~/文档/apc/1.serialFile/build$ make
echo "just for fun"
just for fun
xiechen@xiechen-Ubuntu:~/文档/apc/1.serialFile/build$ 

在这个最简单的 Makefile 只有两行,包含了其最核心的语法:第 1 行的 funny 被称之为目标,因为它后面有一个冒号,冒号后面是这个目标的依赖列表,这个例子中 funny的依赖列表为空。

紧跟着第 2 行的行首是一个制表符(即 Tab 键),这个制表符很重要,不能写成空格,更不能省略,其后紧跟着一个 SHELL 语句(事实上就因为有了那个制表符,make 才知道后面是一个 SHELL 命令)。

这个目标,以及其后的依赖列表(可以没有),以及其下的 SHELL 命令(可以没有),统称为一套规则。

二、变量

2.1 基本的变量

在Makefile中变量的特征有以下几点:

  1. 变量和函数的展开(除规则的命令行以外),是在make读取Makefile文件时进行的,这里的变量包括了使用“=”定义和使用指示符“define”定义的变量。
  2. 变量可以用来代表一个文件名列表、编译选项列表、程序运行的选项参数列表、搜索源文件的目录列表、编译输出的目录列表和所有我们能够想到的事物。
  3. 变量名不能包括“:”、“#”、“=”、前置空白和尾空白的任何字符串。需要注意的是,尽管在GNU make中没有对变量的命名有其它的限制,但定义一个包含除字母、数字和下划线以外的变量的做法也是不可取的,因为除字母、数字和下划线以外的其它字符可能会在以后的make版本中被赋予特殊含义,并且这样命名的变量对于一些SHELL来说不能作为环境变量使用。
  4. 变量名是大小写敏感的。变量“foo”、“Foo”和“FOO”指的是三个不同的变量。Makefile传统做法是变量名是全采用大写的方式。推荐的做法是在对于内部定义的一般变量(例如:目标文件列表objects)使用小写方式,而对于一些参数列表(例如:编译选项CFLAGS)采用大写方式,这并不是要求的。但需要强调一点:对于一个工程,所有Makefile中的变量命名应保持一种风格,否则会显得你是一个蹩脚的开发者(就像代码的变量命名风格一样),随时有被鄙视的危险。
  5. 另外有一些变量名只包含了一个或者很少的几个特殊的字符(符号)。称它们为自动化变量。像“<”、“@”、“?”、“*”、“@D”、“%F”、“^D”等等,后面会详述之。
  6. 变量的引用跟SHELL脚本类似,使用美元符号和圆括号,比如有个变量叫A,那么对他的引用则是 ( A ) @ (A),有个自动化变量叫@,则对他的引用是 (@),有个系统变量是CC则对其引用的格式是 ( C C ) (CC)。对于前面两个变量而言,他们都是单字符变量,因此对他们引用的括号可以省略,写成 A和$@。

来写一个简单的程序,熟悉一下这些基本变量的使用方法。

xiechen@xiechen-Ubuntu:~/文档/apc/1.serialFile/build$ cat makefile -n
     1  A = xiechen
     2  B = I love China
     3  C = handsome $(A)
     4
     5  all:
     6          @echo $(A)
     7          @echo $(B)
     8          @echo $(C)
xiechen@xiechen-Ubuntu:~/文档/apc/1.serialFile/build$ make
xiechen
I love China
handsome xiechen

2.2 系统预定义变量

CFLAGS、CC、MAKE、SHELL 等等,这些变量已经有了系统预定义好的值,当然我们可以根据需要重新给他们赋值,例如 CC 的默认值是 gcc,当我们需要使用 c 编译器的时候可以直接使用它。

这样做的好处是:在不同平台中,c 编译器的名称也许会发生变化,如果我们的 Makefile使用了 100 处 c 编译器的名字,那么换一个平台我们只需要重新给预定义变量 CC 赋值一次即可,而不需要修改 100 处不同的地方。比如我们换到 ARM 开发平台中,只需要重新给 CC 赋值为 arm-linux-gnu-gcc。

xiechen@xiechen-Ubuntu:~/文档/apc/1.serialFile/build$ cat makefile -n
     1  jerome:
     2          @echo $(CFLAGS)
     3          @echo $(CC)
     4          @echo $(MAKE)
     5          @echo $(SHELL)
xiechen@xiechen-Ubuntu:~/文档/apc/1.serialFile/build$ make

cc
make
/bin/sh

2.3 自动化变量

<、@、?、#等等,这些特殊的变量之所以称为自动化变量,是因为他们的值会“自动地”发生变化,考虑普通的变量,只要你不给他重新赋值,那么他的值是永久不变的,比如上面的 CC,只要不对他重新赋值,CC 永远都等于 arm-linux-gnu-gcc。但是自动化变量的值是不固定的,你不能说@的值等于几,但是他的含义的固定的:@代表了其所在规则的目标的完整名称。

三、变量定义方式

3.1 递归定义

A = I love $(B)
B = China

此处,在变量 B 出现之前,变量 A 的定义包含了对变量 B 的引用,由于 A 的定义方式是所谓的“递归”定义方式,因此当出现$(B)时会对全文件进行搜索,找到 B 的值并代进A 中,结果变量 A 的值是 “I love China”。

3.2 直接定义

B = China
A := I love $(B)

此处,定义 A 时用的是所谓的“直接”定义方式,说白了就是如果其定义里出现有对其他变量的引用的话,只会其前面的语句进行搜寻(不包含自己所在的那一行),而不是搜寻整个文件。

3.3 条件定义

A = apple
A ?= I love China

此处对 A 进行了两次定义,其中第二次是条件定义,其含义是:如果 A 在此之前没有定义,则定义为“I love China”,否则维持原有的值。

3.4 多行命令定义

define commands
    echo “thank you!”
    echo “you are welcome.” 
endef

此处定义了一个包含多行命令的变量commands,我们利用它的这个特点实现一个完整命令包的定义。注意其语法格式:以define开头,以endef结束,所要定义的变量名必须在指示符“define”的同一行之后,指示符define所在行的下一行开始一直到“end”所在行的上一行之间的若干行,是变量的值。这种方式定义的所谓命令包,可以理解为编程语言中的函数。

四、变量的操作方式

4.1 追加变量的值

A = apple
A += tree

这样,变量A的值就是apple tree。

4.2 修改变量的值

A = srt.c string.c tcl.c
B = $(A:%.c=%.o)

这样,变量B的值就变成了 srt.o string.o tcl.o。例子中$(A:%.c=%.o)的意思是:将变量A中所有以.c作为后缀的单词,替换为以.o作为后缀。其实这种变量的替换功能是内嵌函数patsubst的简单版本,使用patsubst也可以实现这个替换的功能:

A = srt.c string.c tcl.c
B = $(patsubst %.c, %.o, $(A)

4.3 override一个变量

override CFLAGS += -Wall

在执行make时,通常可以在命令行中携带一个变量的定义,如果这个变量跟Makefile中出现的某一变量重名,那么命令行变量的定义将会覆盖Makefile中的变量。就是说,对于一个在Makefile中使用常规方式(使用“=”、“:=”或者“define”)定义的变量,我们可以在执行make时通过命令行方式重新指定这个变量的值命令行指定的值将替代出现在Makefile中此变量的值。

4.4 导出变量

export CFLAGS = -Wall -g

在Makefile中导出一个变量的作用是:使得该变量可以传递给子Makefile。在缺省的情况下,除了两个特殊的变量”SHELL”、”MAKEFLAGS”、不为空的”MAKEFILES”以及在执行make之前就已经存在的环境变量之外,其他变量不会被传递给子Makefile。

对于默认就会被传递给子Makefile的变量,可以使用unexport来阻止他们的传递。

unexport MAKEFLAGS

五、特殊的变量

5.1 VPATH

这个特殊的变量用以指定Makefile中文件的备用搜寻路径:当Makefile中的目标文件或者依赖文件不在当前路径时,make会在此变量所指定的目录中搜寻,如果VPATH包含多个备用路径,他们使用空格或者冒号隔开。

vincent@ubuntu:~$ cat Makefile -n
    1 VPATH = src1/:src2/ 		指定文件搜寻除当前路径之外的备用路径
    2
    3 all: a b
    4 a:a.c 									若make发现当前路径下不存在a.c,则会到VPATH中去找
    5 gcc $^ -o $@
    6 b:b.c
    7 gcc $^ -o $@
vincent@ubuntu:~$ make
gcc src1/a.c -o a
gcc src2/b.c -o b

更进一步,可以使用小写的指示符vpath来更灵活地为各种不同的文件指定不同的路径。

vincent@ubuntu:~$ cat Makefile -n
    1 vpath %.c = src1/:src2/ 		指定本Makefile中.c文件的可能路径
    2 vpath %.h = include/ 				指定本Makefile中.h文件的可能路径
    3
    4 all:a b
    5
    6 a:a.c head.h
    7 $(CC) $< -o $@
    8 b:b.c head.h
    9 $(CC) $< -o $@
vincent@ubuntu:~$ make
cc src1/a.c -o a
cc src2/b.c -o b

注意,VPATH是一个变量,而vpath是一个指示符。

5.2 MAKE

当需要在一个Makefile中调用子Makefile时,用到的变量就是MAKE,实际上该变量代表了当前系统中make软件的全路径,比如:/usr/bin/make。

$(MAKE) -C subdir/

5.3 MAKEFLAGS

此变量代表了在执行make时的命令行参数,这个变量是缺省会被传递给子Makefile的特殊变量之一。

vincent@ubuntu:~$ cat Makefile -n
    1 all:
    2 echo $(MAKEFLAGS)
vincent@ubuntu:~$ make -s
s			此处,s就是make的命令行参数。

六 规则

6.1 隐式规则

vincent@ubuntu:~$ cat Makefile -n
    1 OBJ = a.o b.o x.o y.o
    2
    3 image:$(OBJ)
    4 $(CC) $(OBJ) -o image
vincent@ubuntu:~$ 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,换了个名字就找不到了,生成的编译语句也是缺省的单文件形式,像本例子中的第一个规则,隐式规则就无能为力了,因为image 的依赖文件不止一个。

清理工程项目中所有的目标文件,可以将清理工作交给 Makefile 来完成。

vincent@ubuntu:~$ cat Makefile -n
    1 OBJ = a.o b.o x.o y.o
    2
    3 image:$(OBJ)
    4 $(CC) $(OBJ) -o image
    5
    6 clean:
    7 $(RM) $(OBJ) image
    8
    9 .PHONY: clean

第 6、7 行声明了一个清理目标文件和 image 的规则,执行这条 SHELL 命令时需要指定 make 的参数 clean,clean 是一个动作的代号,而不是一个我们要生成的文件,但是根据隐式规则,假如当前目录恰巧有个文件叫做 clean.c,就可能会导致 Makefile 自动生成其对应的编译语句,从而引起混淆。在第 9 行中用指符.PHONY 来明确地告诉 Makefile不要对 clean 运用任何隐式规则,事实上,不能运用隐式规则的目标被称为伪目标

6.2 静态规则

vincent@ubuntu:~$ cat Makefile -n
    1 OBJ = a.o b.o x.o y.o
    2
    3 image:$(OBJ)
    4 $(CC) $(OBJ) -o image
    5
    6 $(OBJ):%.o:%.c
    7 $(CC) $^ -o $@ -Wall -c
    8
    9 clean:
    10 $(RM) $(OBJ) image
    11
    12 .PHONY: clean

第 6、7 行运用了所谓的静态规则,其工作原理是:$(OBJ)被称为原始列表,即(a.ob.o x.o y.o),紧跟在其后的%.o 被称为匹配模式,含义是在原始列表中按照这种指定的模式挑选出能匹配得上的单词(在本例中要找出原始列表里所有以.o 为后缀的文件)作为规则的目标。

简单地讲,就是用一个规则来生成一系列的目标文件。接着,第二个冒号后面的内容就是目标对应的依赖,%可以理解为通配符,因此本例中%.o:%.c 的意思就是:每一个匹配出来的目标所对应的依赖文件是同名的.c 文件。

可见,静态规则的目的就是用一句话来自动生成很多目标及其依赖。

静态模式规则是这样一个规则:规则存在多个目标,并且不同的目标可以根据目标文件的名字来自动构造出依赖文件。静态模式规则比多目标规则更通用,它不需要多个目标具有相同的依赖。但是静态模式规则中的依赖文件必须是相类似的而不是完全相同的。

6.3 多目标规则

vincent@ubuntu:~$ cat Makefile -n
    1 SRC = $(wildcard *.c)
    2 OBJ = $(SRC:%.c=%.o)
    3
    4 image:$(OBJ)
    5 $(CC) $(OBJ) -o image -lgcc
    6
    7 $(OBJ):$(SRC)
    8 $(CC) $(subst .o,.c,$@) -o $@ -c
    9
    10 clean:
    11 $(RM) $(OBJ) image
    12
    13 .PHONY:clean

当中的四个.o文件都是这个规则的目标,规则所定义的命令对所有的目标有效。这个具有多目标的规则相当于多个规则,规则中命令对不同的目标的执行效果不同,因为在规则的命令中可能使用自动环变量”$@”,多目标规则意味着所有的目标具有相同的依赖文件。

6.4 双冒号规则

双冒号规则就是使用“::”代替普通规则的“:”得到的规则。当同一个文件作为多个规则的目标时,双冒号规则的处理和普通规则的处理过程完全不同(双冒号规则允许在多个规则中为同一个目标指定不同的重建目标的命令)。
首先需要明确的是:Makefile中,一个目标可以出现在多个规则中。但是这些规则必须是同一种规则,要么都是普通规则,要么都是双冒号规则。而不允许一个目标同时出现在两种不同的规则中。双冒号规则有以下两个作用:

  1. 当依赖列表为空时,即使目标文件已经存在,双冒号规则能确保shell命令也会被无条件执行。
  2. 当同一个文件作为多个双冒号规则的目标时。这些不同的规则会被独立的处理,而不是像普通规则那样合并所有的依赖到一个目标文件。这就意味着对这些规则的处理就像多个不同的普通规则一样。就是说多个双冒号规则中的每一个的依赖文件被改变之后,make只执行此规则定义的命令,而其它的以这个文件作为目标的双冒号规则将不会被执行。
vincent@ubuntu:~$ ls
a.c b.c libx.so liby.so Makefile
vincent@ubuntu:~$ cat Makefile -n
    1 image::b.c
    2 $(CC) a.c -o $@ -L. -lx
    3
    4 image::b.c
    5 $(CC) b.c -o $@ -L. -ly
vincent@ubuntu:~$ make
cc a.c -o image
cc b.c -o image

七、条件判断

vincent@ubuntu:~$ cat Makefile -n
    1 OBJ = a.o b.o x.o y.o
    2
    3 ifdef TOOLCHAIN 		# ifdef 语句用来判断变量 TOOLCHAIN 是否有定义
    4 CC = $(TOOLCHAIN)
    5 else
    6 CC = gcc
    7 endif
    8
    9 image:$(OBJ)
    10 $(CC) $(OBJ) -o image
    11
    12 $(OBJ):%.o:%.c
    13 $(CC) $^ -o $@ -c
    14
    15 clean:
    16 $(RM) $(OBJ) image
    17
    18 .PHONY:clean
vincent@ubuntu:~$ 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

在 Makefile 中增加了对变量 TOOLCHAIN 的判断,用来选择用户所指定的工具链,如果用户如上述代码所示,在执行 make 时指定了参数 TOOLCHAIN=arm-linux-gcc,那第 3 行的 ifdef 语句将成立,因此编译器 CC 被调整为用户指定的 TOOLCHAIN。在这个例子中我们同时也看到了如何在命令行中给 make 传递参数。

再进一步,假如在用户使用 gcc 编译时需要链接库文件 libgcc.so,而在使用交叉工具链 arm-linux-gnu-gcc 时不需要,那么我们的 Makefile 需要再改成:

vincent@ubuntu:~$ cat Makefile -n
    1 OBJ = a.o b.o x.o y.o
    2
    3 ifdef TOOLCHAIN
    4 CC = $(TOOLCHAIN)
    5 else
    6 CC = gcc
    7 endif
    8
    9 image:$(OBJ)
    10 ifeq ($(CC), gcc)				 # ifeq ( )用来判断变量 CC 的值是否等于 gcc
    11 $(CC) $(OBJ) -o image -lgcc
    12 else
    13 $(CC) $(OBJ) -o image
    14 endif
    15
    16 $(OBJ):%.o:%.c
    17 $(CC) $^ -o $@ -c
    18
    19 clean:
    20 $(RM) $(OBJ) image
    21
    22 .PHONY:clean

在第 10 行中,使用 ifeq ( )来对 CC 进行了判断,注意:ifeq 跟后面的圆括号之间有一个空格!ifeq ( )也可以用来判断一个变量是否为空。

八、函数

怎样让 Makefile 知道我们的工程来了一个新的文件 c.c 呢?这需要一个叫 wildcard的函数帮忙。

SRC = $(wildcard *.c)

在 Makefile 中书写一个函数的格式: ( f u n c t i o n a r g 1 , a r g 2 , a r g 3 , . . . . . . ) f u n c t i o n (function arg1,arg2,arg3, ... ...) 其中 function 是函数的名字,后面跟一个空格,然后是参数列表,如果有多个参数则用逗号隔开(注意逗号后面最好不要有空格),整个函数用 ( )包裹起来(跟变量一样)。

由于 wildcard 函数的作用就是找到参数匹配的文件名,因此该语句的作用就相当于:SRC = a.c b.c c.c x.c y.c。

vincent@ubuntu:~$ cat Makefile -n
    1 SRC = $(wildcard *.c)
    2 OBJ = $(SRC:%.c=%.o)
    3
    4 ifdef TOOLCHAIN
    5 CC = $(TOOLCHAIN)
    6 else
    7 CC = gcc
    8 endif
    9
    10 image:$(OBJ)
    11 ifeq ($(CC),gcc)
    12 $(CC) $(OBJ) -o image -lgcc
    13 else
    14 $(CC) $(OBJ) -o image
    15 endif
    16
    17 $(OBJ):%.o:%.c
    18 $(CC) $^ -o $@ -c
    19
    20 clean:
    21 $(RM) $(OBJ) image
    22
    23 .PHONY:clean

8.1 文本处理函数

8.1.1 $(subst FROM,TO,TEXT)

功能:

将字符串 TEXT 中的字符 FROM 替换为 TO。

返回:

替换之后的新字符串。

范例:

A = $(subst pp,PP,apple tree)

替换之后变量 A 的值是”aPPle tree”

8.1.2 $(patsubst PATTERN,REPLACEMENT,TEXT)

功能:

按 照 PATTERN 搜 索 TEXT 中 所 有 以 空 格 隔 开 的 单 词 , 并 将 它 们 替 换 为REPLACEMENT。注意:参数 PATTERN 可以使用模式通配符%来代表一个单词中的若干字符,如果此时 REPLACEMENT 中也出现%,那么 REPLACEMENT 中的%跟PATTERN 中的%是一样的。

返回:

替换之后的新字符串。

范例:

A = $(patsubst %.c,%.o,a.c b.c)

替换之后变量 A 的值是”a.o b.o”

8.1.3 $(strip STRING)

功能:

去掉字符串中开头和结尾的多余的空白符(掐头去尾),并将其中连续的多个空白符合并为一个。注意:所谓的空白符指的是空格、制表符。

返回:

去掉多余空白符之后的新字符串。

范例:

A = $(strip apple tree )

处理之后,变量 A 的值是”apple tree”

8.1.4 $(findstring FIND, STRING)

功能:

在给定的字符串 STRING 中查找 FIND 子串。

返回:

找到则返回 FIND,否则返回空。

范例:

A = $(findstring pp, apple tree)

B = $(findstring xx, apple tree)

变量 A 的值是”pp”,变量 B 的值是空。

8.1.5 $(filter PATTERN,TEXT)

功能:

过滤掉 TEXT 中所有不符合给定模式 PATTERN 的单词。其中 PATTERN 可以是多个模式的组合。

返回:

TEXT 中所有符合模式组合 PATTERN 的单词组成的子串。

范例:

A = a.c b.o c.s d.txt

B = $(filter %.c %.o,$(A))

过滤后变量 B 的值是”a.c b.o”。

8.1.6 $(filter-out PATTERN,TEXT)

功能:

过滤掉 TEXT 中所有符合给定模式 PATTERN 的单词,与函数 filter 功能相反。

返回:

TEXT 中所有不符合模式组合 PATTERN 的单词组成的子串。

范例:

A = a.c b.o c.s d.txt

B = $(filter %.c %.o,$(A))

过滤后变量 B 的值是”c.s d.txt”。

8.1.7 $(sort LIST)

功能:

将字符串 LIST 中的单词按字母升序的顺序排序,并且去掉重复的单词。

返回:

排完序且没有重复单词的新字符串。

范例:

A = foo bar lose foo ugh

B = $(sort $(A))

处理后变量 B 的值是”bar foo lose ugh”。

8.1.8 $(word N,TEXT)

功能:

取字符串 TEXT 中的第 N 个单词。注意,N 必须为正整数。

返回:

第 N 个单词(如果 N 大于 TEXT 中单词的总数则返回空)。

范例:

A = an apple tree

B = $(word 2 $(A))

处理后变量 B 的值是”apple”。

8.1.9 $(wordlist START,END,TEXT)

功能:

取字符串 TEXT 中介于 START 和 END 之间的子串。

返回:

介于 START 和 END 之间的子串(如果 START 大于 TEXT 中单词的总数或者START 大于 END 时返回空,否则如果 END 大于 TEXT 中单词的总数则返回从 START开始到 TEXT 的最后一个单词的子串)。

范例:

A = the apple tree is over 5 meters tall

B = $(wordlist 4,100,$(A))

处理后变量 B 的值是”is over 5 meters tall”。

8.1.10 $(words TEXT)

功能:

计算字符串 TEXT 的单词数。

返回:

字符串 TEXT 的单词数。

范例:

A = the apple tree is over 5 meters tall

B = $(words $(A))

处理后变量 B 的值是”8”。

8.1.11 $(firstword TEXT)

功能:

取字符串 TEXT 中的第一个单词。相当于$(word 1 TEXT)

返回:

字符串 TEXT 的第一个单词。

范例:

A = the apple tree is over 5 meters tall

B = $(firstword $(A))

处理后变量 B 的值是”the”。

8.2 文件名处理函数

8.2.1 $(dir NAMES)

功能:

取文件列表 NAMES 中每一个路径的目录部分。

返回:

每一个路径的目录部分组成的新的字符串。

范例:

A = /etc/init.d /home/vincent/.bashrc /usr/bin/man

B = $(dir $(A))

处理后变量 B 的值是”/etc/ /home/vincent/ /usr/bin/”。

8.2.2 $(notdir NAMES)

功能:

取文件列表 NAMES 中每一个路径的文件名部分。

返回:

每一个路径的文件名部分组成的新的字符串。注意:如果NAMES中存在不包含斜线的文件名,则不改变这个文件名,而以反斜线结尾的文件名,用空串代替。

范例:

A = /etc/init.d /home/vincent/.bashrc /usr/bin/man

B = $(dir $(A))

处理后变量 B 的值是”init.d .bashrc man”。

8.2.3 $(suffix NAMES)

功能:

取文件列表 NAMES 中每一个路径的文件的后缀部分。后缀指的是最后一个.后面的子串。

返回:

每一个路径的文件名的后缀部分组成的新的字符串。

范例:

A = /etc/init.d /home/vincent/.bashrc /usr/bin/man

B = $(suffix $(A))

处理后变量 B 的值是”.d .bashrc”。

8.2.4 $(basename NAMES)

功能:

取文件列表 NAMES 中每一个路径的文件的前缀部分。前缀指的是最后一个.后面除了后缀的子串。

返回:

每一个路径的文件名的前缀部分组成的新的字符串。

范例:

A = /etc/init.d /home/vincent/.bashrc /usr/bin/man

B = $(basename $(A))

处理后变量 B 的值是”/etc/init /home/vincent/ /usr/bin/man”。

8.2.5 $(addsuffix SUFFIX,NAMES)

功能:

为文件列表 NAMES 中每一个路径的文件名添加后缀 SUFFIX。

返回:

添加了后缀 SUFFIX 的字符串。

范例:

A = /etc/init.d /home/vincent/.bashrc /usr/bin/man

B = $(addsuffix .bk,$(A))

处理后 B 为”/etc/init.d.bk /home/vincent/.bashrc.bk /usr/bin/man.bk”。

8.2.6 $(addprefix PREFIX,NAMES)

功能:

为文件列表 NAMES 中每一个路径的文件名添加前缀 PREFIX。

返回:

添加了前缀 PREFIX 的字符串。

范例:

A = /etc/init.d /home/vincent/.bashrc /usr/bin/man

B = $(addprefix host:,$(A))

处理后 B 的值为:

”host:/etc/init.d host:/home/vincent/.bashrc host:/usr/bin/man”。

8.2.7 $(wildcard PATTERN)

功能:

获取匹配模式为 PATTERN 的文件名。

返回:

匹配模式为 PATTERN 的文件名。

范例:

A = $(wildcard \*.c)

假设当前路径下有两个.c 文件 a.c 和 b.c,则处理后 A 的值为:”a.c b.c”。

8.2.8 $(foreach VAR,LIST,TEXT)

功能:

首先展开变量“VAR”和“LIST”,而表达式“TEXT”中的变量引用不被展开。执行时把“LIST”中使用空格分割的单词依次取出赋值给变量“VAR”,然后执行“TEXT”表达式,重复直到“LIST”的最后一个单词(为空时结束)。它是一个循环函数,类似于Linux的shell中的循环。注意:由于“TEXT”中的变量或者函数引用在执行时才被展开,因此如果在“TEXT”中存在对“VAR”的引用,那么“VAR”的值在每一次展开式将会到的不同的值。

返回:

以空格分隔的多次表达式“TEXT”的计算的结果。

范例:

假设当前目录下有两个子目录dir1/和dir2/,先要将他们里面的所有文件赋值给变量FILES。

vincent@ubuntu:~$ tree
.├── dir1
│ ├── file1
│ └── file2
├── dir2
│ ├── a.c
│ └── b.c
└── Makefile
vincent@ubuntu:~$ cat Makefile -n
  1 DIR = dir1 dir2
  2 FILES = $(foreach dir,$(DIR),$(wildcard $(dir)/*))
  3
  4 all:
  5 echo $(FILES)
vincent@ubuntu:~$ make -s
dir1/file1 dir1/file2 dir2/a.c dir2/b.c

8.2.9 $(if CONDITION,THEN-PART,ELSE-PART)

功能:

判断 CONDITION 是否为空,如果非空则执行 THEN-PART 且将结果作为函数的返回值,否则如果为空则执行 ELSE-PART 且将结果作为函数的返回值,如果此时没有ELSE-PART 则函数返回空。

返回:

根据 CONDITION 返回 THEN-PART 或者 ELSE-PART 的执行结果。

范例:

install-dir := $(if $(INSTALL_DIR),$(INSTALL_DIR),extra)

先判断 INSTALL_DIR 是否为空,如果为空则将 extra 赋值给 install-dir,否则如果不为空,则 install-dir 的值等于$(INSTALL_DIR)。

8.2.10 $(call VAR,ARGS,…)

功能:

执行 VAR,并将 ARGS 一一对应地替换 VAR 里面的 ( 1 ) (1)、 (2)……。因此函数$(call)被称为是 Makefile 中唯一一个创建定制参数的函数。

返回:

将 ARGS 替换 VAR 中的 ( 1 ) (1)、 (2)……之后 VAR 的执行结果。

范例 1:

A = my name is $(1) $(2)
B = $(call A Michael,Jackson)

将 Michael,Jackson 分别替换变量 A 里面的 ( 1 ) (1)和 (2),于是 B 的值就是 myname is Michael Jackson。

范例2:

使用Makefile的命令,找出指定系统SHELL指令的完整路径(类似which的功能)。

  1. 使用subst将环境变量PATH中每一个路径的分隔符冒号替换成空格:
A = $(subst :, ,$(PATH))
  1. 将指定的SHELL指令添加到每一个可能的路径后面:
B = $(addsuffix /$(1),$(A))
  1. 使用wildcard匹配所有正确的路径:
C = $(wildcard $(B))

将上述命令组合起来就能完成类似命令which的功能,暂且叫他为WHICH:

WHICH = $(wildcard $(addsuffix /$(1),$(subst :, ,$(PATH))))

注意:此处WHICH的定义只能是这样递归定义方式,而不能是直接定义方式。

最后,使用call来给这个复杂的变量传递一个定制化的参数$(1),比如要获得系统命令ps的完整路径:

$(call WHICH,ps)

8.2.11 $(origin VAR)

功能:

顾名思义,该函数用来查看参数 VAR 的出处。

返回:

参数 VAR 的出处,有如下几种情况:

  1. undefined

表示变量 VAR 尚未被定义。

  1. default

表示变量 VAR 是一个默认的内嵌变量,比如 CC、MAKEFLAGS 等。

  1. environment

表示变量 VAR 是一个系统环境变量,比如 PATH。

  1. file

表示变量 VAR 在另一个 Makefile 中被定义。

  1. command line

表示变量 VAR 是一个在命令行定义的变量。

  1. override

表示变量 VAR 在本 Makefile 定义并使用了 override 指示符。

  1. automatic

表示变量 VAR 是一个自动化变量,比如@、^等等。

范例:

ifeq (“$(origin V)”, “command line”)
KBUILD_VERBOSE = $(V)
endif

判断变量 V 的出处,如果该变量来自命令行,则将 KBUILD_VERBOSE 赋为 V的值)。

8.2.12 $(shell COMMANDS)

功能:

在 Makefile 中执行 COMMANDS,此处的 COMMANDS 是一个或几个 SHELL命令,功能与在 SHELL 脚本中使用COMMANDS的效果相同。该函数返回这些命令的最终结果。

返回:

返回 COMMANDS 的执行结果,并把其中的回车符替换成空格符。

范例:

contents := $(shell cat file.txt)

使用cat命令显示file.txt的内容,并将其中的回车符替换为空格之后赋给contents。注意到此处用的是直接定义方式而不是递归定义方式,这是为了防止后续再有对此变量的引用就不会有展开过程。这样可以防止规则命令行中的变量引用在命令行执行时展开的情况发生(因为展开“shell”函数需要另外的shell进程完成,影响命令的执行效率)。一个用来展示以上函数的示例:

vincent@ubuntu:~/ch01/1.5$ cat Makefile -n
1 V1 = /etc/init.d /home/vincent/.bashrc /usr/bin/man
2
3 A = $(dir $(V1))
4 B = $(notdir $(V1))
5 C = $(suffix $(V1))
6 D = $(basename $(V1))
7 E = $(addsuffix .bk,$(V1))
8 F = $(addprefix host:,$(V1))
9
10 V2 = my name is $(1) $(2)
11 G = $(call V2,Michael,Jackson)
12
13 V3 = $(wildcard $(addsuffix /$(1),$(subst :, ,$(PATH))))
14 H = $(call V3,nm)
15
16
17 all:
18 echo $(A)
19 echo $(B)
20 echo $(C)
21 echo $(D)
22 echo $(E)
23 echo $(F)
24 echo $(G)
25 echo $(H)

九、Make 选项

9.1 指定要执行的 Makefile 文件

make -f altmake
make --file altmake
make --makefile altm

以上三种方式都可以用来执行一个普通命令的文件作为 Makefile 文件。在缺省的情况下不指定任何 Makefile 文件,则 make 会在当前目录下依次查找命名为 GNUmakefileMakefile 以及 makefile 的文件。

9.2 指定最终目标

make TARGET

所谓的最终目标指的是 Makefile 中第一个出现的规则中的第一个目标,是缺省的整个工程或者程序编译过程的总的规则和目的。如果想要执行除该目标之外的其他普通目标位编译的最终目的,则可以在执行 make 的同时指定。

9.3 强制重建所有规则中目标

make -B
make -always-ma

9.4 指定 Makefile 的所在路径

make -C dir/
make --directory=dir/

假如要执行的 Makefile 文件不在当前目录,可以使用该选项指定。这个选项一般用在一个 Makefile 内部调用另一个子 Makefile 的场景。

十、工程应用进阶

10.1 编译第一个程序

首先需要安装gcc编译工具链,我的环境是Ubuntu18.04,安装指令如下。

sudo apt install gcc

sudo apt install build-essential

build-essential是c语言的开发包,包含了gcc make gdb和libc函数库,一般测试的话直接装个gcc就可以了。

这里通过makefile来编译一个C文件,main.c 文件内容如下。

#include <stdio.h>

int main(void)
{
    printf("Hello World!");
    return 0;    
}

在文件夹中新建一个 makefile文件,文件树如下。

xiechen@xiechen-Ubuntu:~/文档/apc/1.serialFile/test$ tree
.
├── makefile
└── main.c

0 directories, 2 files

makefile文件的内容如下。

all:
	gcc -o main main.c

结果如下。

xiechen@xiechen-Ubuntu:~/文档/apc/1.serialFile/test$ make
gcc -o main main.c
xiechen@xiechen-Ubuntu:~/文档/apc/1.serialFile/test$ ./main 
Hello World!

10.2 构建一个最小工程

这里通过一个简单的工程,打印一个“最小工程”。

新建一个 minProject.c 文件。

#include "minProject.h"
void minProject(void)
{
    printf("最小工程。");
}

接着新建它的头文件 minProject.h,在其中对该C文件所包含的头文件及其功能函数进行声明。

#include<stdio.h>

void minProject(void);

最后,编写这个工程的主函数。

#include "minProject.h"

int main(void)
{
    minProject();
    return 0;
}

这样,一个进度条就写好了,接着我们来使用makefile对这个工程进行组装并编译。

SRC = $(wildcard *.c)			//从当前目录下遍历所有的C文件;
OBJ = $(SRC:%.c=%.o)			//根据SRC变量中.c的名字生成其.o文件名;

main:$(OBJ)						//目标名为main,其依赖为OBJ变量的值;
	$(CC) $(OBJ) -o main		//CC此时为gcc,链接所有.o依赖生成的目标文件名为main;
    
$(OBJ):%.o:%.c					//指定每一个文件的目标名;
	$(CC) $^ -o $@ -c			//分别编译依赖中的每一个文件;

到这里,一个最简单的工程就编译好了,我们运行一下生成main,则实现了功能。

xiechen@xiechen-Ubuntu:~/文档/apc/1.serialFile/test$ make
cc main.c -o main.o -c
cc minProject.c -o minProject.o -c
cc main.o minProject.o -o main
xiechen@xiechen-Ubuntu:~/文档/apc/1.serialFile/test$ tree
.
├── minProject.c
├── minProject.h
├── minProject.o
├── build
├── main
├── main.c
├── main.o
└── makefile

1 directory, 7 files
xiechen@xiechen-Ubuntu:~/文档/apc/1.serialFile/test$ ./main 
最小工程。

附录:参考文献

[1] 跟我一起写Makefile-陈皓
[2] 最贴心Makefile使用文档
[3] LINUX-3.9.8源码顶层 Makefile

猜你喜欢

转载自blog.csdn.net/JeromeCoco/article/details/107763451
今日推荐