做 Linux C++,一个稳定的工程,Makefile 是很少改动的。但是如果需要修改的时候,Makefile 的语法和用法一时半会就回忆不出来(原谅我记忆力差……)。在此把自己以前的 Makefile 学习笔记记录一下吧,也作为分享。本文假设读者已经懂得了 Makefile,因此主要是作为备忘和速查用。
全文中尖括号部分表示变量。本文地址:https://segmentfault.com/a/1190000012091117
另外,速查系列还有这一篇:正则表达式速查笔记
Make 介绍
Makefile 的基本规则就是:
target ...: prerequirements ...
command ...
...
其中 target
是目标文件,可以有多个,可以是 .o
文件或者是可执行问价,甚至可以是一个标签。Prerequisites
是先决条件,可以是文件,也可以是另一个 target。
这就组成了一个依赖关系
:target
的先决条件定义在 prerequisites
中,而其生成规则又是由 command 决定的。如果包含多个规则的话,那么第一条规则就是整个 Makefile 的默认规则
。
Make 的工作流程
- 在当前目录中查找
Makefile
或者makefile
文件 - 将文件中第一条规则作为默认规则
- 如果目标不存在,则寻找对应的
.o
文件 - 如果
.o
文件不存在,则寻找.o
的依赖关系以生成它
Makefile 有很多默认的生成规则,但是本文我们不关心,因为绝大部分情况下,我们是需要自行写规则的。这便于自定义、便于移植、便于交叉编译、便于调试。
Makefile 中的变量
变量的定义和调用格式:
name = value # 注意变量的值是允许空格的
$(name)
变量值的部分可以使用换行符 "\
" 来做假换行,将两行内容连接成一行,从而缩短 Makefile 文件的宽度。
Makefile 综述
Makefile 里面都有啥?
文件指示:在一个 Makefile 里面可以制定另一个 makefile,类似于 C 的 include
Makefile 还可以做条件包含动作,类似于 #if
。Makefile 可以定一个变量为一个多行的命令。
Makefile 里面只有行注释而没有段注释。注释采用 #
开头。如果要使用 #
字符,则需要转义,写成 “\#
”。
Makefile 规则内容里所有的 shell 命令都要以制表符 Tab
开头,注意,空格符是不行的。
默认的 make 文件名为:GNUmakefile
, makefile
, Makefile
,当敲入 make
命令时,会自动搜寻这几个文件。约定俗成使用最后一个。
引用其他 Makefile
语法:
include filename ... # 不允许 include 失败
-include filename ... # 允许 include 失败
可以包含路径或者通配符,一行可以包含多个文件。
如果未指定绝对路径或者相对路径,那么 make 会按照一下的顺序去寻找:
- 当前目录
- 制定 make 时,在 -I 或者 --include-dir 的参数下寻找
-
<prefix>/include
(一般是/usr/local/bin
或/usr/include
)
建议还是手动指定吧,自动搜寻意外可能太多了。
环境变量 MAKEFILES
这里主要是要提醒:不要设置这个环境变量,否则会影响全局的 make include 动作。
Make 的几个工作方法
通配符
Make 支持三个通配符:*, ?, [...]
。可以用在规则中,也可以用在变量中。
伪目标
伪目标就是 Makefile 里面颇为常见的 .PHONY
标识,比如:".PHONY: clean
",表示这个规则名并不代表一个真实存在的、需要生成的文件名,而只是一条纯粹的规则。
- 真目标的特点是:如果目标不存在,才会被执行
- 伪目标的特点是:无视目标是否存在,必然执行
除了 make clean
之外,伪目标还有另一种使用场景,就是一个 make 动作,实际上生成了多个目标。比如:
.PHONY: all
all: exe install # 包含了生成目标文件,以及安装动作
多目标
规则的冒号前面可以有多个 target,表示多个 target 共用这条规则。
自动生成依赖关系
如果我们使用中规中矩的 makefile 写法,那么对于每个源文件都要好好写头文件依赖关系,从而在头文件更新的时候,可以自动重新编译依赖于这个头文件的源文件。
这实在是太麻烦了。好在 gcc
里有一个 -MM
(注意不是 “-M”) 的选项,可以分析出 .c
文件依赖的头文件并且打印出来。因此制作 Makefile 的时候,就可以利用这一特性自动生成依赖。
实现方法有很多,这里贴出我自己使用的例子,也可以参见我的工程代码:
EXCLUDE_C_SRCS =#
C_SRCS = $(filter-out $(EXCLUDE_C_SRCS), $(wildcard *.c))
C_OBJS = $(C_SRCS:.c=.o)
$(C_OBJS): $(C_OBJS:.o=.c)
$(CC) -c $(CFLAGS) $*.c -o $*.o
@$(CC) -MM $(CFLAGS) $*.c > $*.d
@mv -f $*.d $*.d.tmp
@sed -e 's|.*:|$*.o:|' < $*.d.tmp > $*.d
@sed -e 's/.*://' -e 's/\\$$//' < $*.d.tmp | fmt -1 | sed -e 's/^ *//' -e 's/$$/:/' >> $*.d
@rm -f $*.d.tmp
书写命令
这里的命令,指的是在 Makefile 规则里的 “command” 部分。
命令执行
将 “@
” 放在一条命令的前面,表示实际执行的时候,不打印这条命令语句,可以节省屏幕内容,减少垃圾信息(特别是我的自动生成依赖的命令,调通了之后,那就是一堆无用信息)。如果将 “-
” 放在命令前面,则表示无视这条命令的返回值是否为成功(0).
如果上一条命令的结果需要用于下一条命令时,需要将这些命令写在一行中。建议用 “\
” 分开。这最典型的是 cd
命令及其之后的一连串命令。
Make 的时候加上 -n
选项或 --just-print
选项,则表示不执行 make,而只是把过程打印出来。
嵌套执行 make
在 Makefile 里可以到另一个目录下执行 make,执行方式类似于普通的命令调用,但特别的是,make 可以识别出这是一条嵌套 make 指令,从而在 shell 中打印出 “专项哪里哪里 make” 的提示语法为:
subsystem:
$(MAKE) -C subdir
这个做法的主要好处是可以向下级 Makefile 传递变量或者语法:
export VARIABLE ... # 将相应变量变成当前 make 操作的全局变量
也恶意直接指定变量的值:
export VARIABLE = value
如果要传递所有变量(不推荐),直接写 export
就好。
注意由两个系统变量 SHELL
和 MAKEFLAGS
是永远传递的。
此外还有一个全局变量 MAKELEVEL
用来表示当前的嵌套层数。
定义命令包
命令包类似于宏、子函数等等。使用 define
来定义,以 endif
结束,比如:
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endif
注意如果是命令的话,需要以制表符
开头。调用这个命令包的方式为:$(run-yacc)
使用变量
变量赋值
变量定义时必须赋值,至少赋一个空值(只有等号,等号右边什么都没有)
使用变量的时候虽然不强制、但是为了安全起见,应该使用括号或者打括号把变量包含起来。如果要使用字符 "$
",则使用 "$$
" 来转义
赋值时,等号右侧可以有未定义的变量,并且在其实际使用时才展开变量的内容。但这可能会导致循环引用。为了避免这一点,可以使用 ":=
" 符号来避免使用未定义的变量
"+=
" 的作用是 “追加” 值。如果右侧有变量未定义,则等价于 “:=
”
"?=
" 的作用是:如果等号左侧的变量未定义,则使用等号右边内容定义,即:
ifeq ($(some_var), undefined)
some_var = some_val
endif
另外:$@
表示当前规则的编译目标$^
表示当前规则的所有依赖文件$$<
表示当前规则的第一个依赖。
定义一个空格变量
NULL_STR :=#
SPACE_STR := $(NULL_STR) # end of line
注意第二行的注释与 “)” 之间是包含一个空格的。注释的 “#” 必须有,否则不会定义一个空格出来。
变量替换
第一个方式为:$(var: .o = .c)
,意思是将等号左边的字符换成右边的字符
第二个方式为所谓的 “静态模式”:$(var: %.o = %.c)
把变量值作为变量
这很类似于指针,只是地址值变成了变量值。可以用变量值生成变量名,比如:a := $($(var))
或者是 $($(var)_$(idx))
之类的写法。
override
在命令行调用 make 时,可以直接指定某个变量的全局值,使得它在整个 make 的过程中一直不变。为了防止这个特性,可以使用这个关键字来处理:override <variable> = <value>
等号也可以用 :=
和 ?=
目标变量(局部变量)
如果某条约束里面不想使用已经定义了的全局变量,可以这样写:
prog: CFLAGS = -g
prog: a.o b.o
$(CC) $(CFLAGS) a.o b.o
条件判断
语法
<条件语句>
<true 执行语句>
else
<false 执行语句>
endif
其中条件语句有四种情形:
1、表示是否相等
ifeq (<arg1>, <arg2>) # 推荐
ifeq '<arg1>' '<arg2>'
ifeq "<arg1>" "<arg2>"
2、表示是否不等,上面的 ifeq 换成 ifneq
3、ifdef
4、ifndef
使用函数
Make 的所有函数都是内置函数,不能自己定义(命令包除外)。下面列出常用的函数,如果看不懂再详细查阅。
常规函数
字符串替换
$(subst <from>, <to>, <text>)
模式字符串替换
$(patsubst <pattern>, <replacement>, <text>)
去开头和结尾的空格
$(strip <string>)
查找字符串
$(findstring <find>, <in>)
反过滤
$(filter-out <pattern_or_string>, <text>)
排序(单词升序)
$(sort <list>)
取单词
$(word <n>, <text>)
取单词串
$(wordlist <n_start>, <n_end>, <text>)
单词个数统计
$(words <text>)
去掉每个单词的最后文件名部分,只剩下目录部分
$(dir <names ...>)
去掉每个单词的目录部分,只剩下文件名部分
$(notdir <names ...>)
读取各文件名的后缀
$(suffix <names ...>)
加后缀
$(addsuffix <suffix>, <names ...>)
加前缀
加前后缀在动态创建局部变量很有用$(addprefix <prefix>, <names ...>)
连接字符串
$(join <list1>, <list2>)
for 循环
$(foreach <var>, <list>, <text>)
这其实是一个函数,作用是:将 list
的单词逐一取出,放到 var
指定的变量中,然后执行 text
的表达式。返回值则是 text
的最终执行值。
Shell 函数
执行 shell 命令,并且将 stdout 作为返回值返回,如:contents := $(shell ls -la)
控制 make 输出
$(error <text ...>)
$(warning <text ...>)
这也同时是调试和定位 make 的好方法。
判断文件是否存在
ifeq ($(FILE), $(wildcard $(FILE)))
...
endif
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。