Makefile 备忘录
网上收集的关于 Makefile 资料的整理。
文件格式
# 注释前面不能有制表符,但可以有空格
目标:依赖
[制表符]命令
目标可以是文件名,也可以是伪目标,前面不能有制表符,但可以有空格。多个相同规则的目标可以写在一起。
命令是 Shell 命令,前面必须有制表符,制表符后面可以有空格。命令可以使用反斜线 \
折行。多条命令分别在不同的 Shell 环境中执行,所以不共享环境变量。要想共享环境变量,可以将多条命令合并成一条命令,中间用分号分隔。命令前面可以添加 @
符号,表示不回显(即不显示要执行的命令本身,只显示执行结果)。命令前面可以添加 -
字符,表示即使命令执行出错,也继续执行。
make 会比较目标文件和依赖文件的修改日期,如果依赖文件比目标文件新,或目标文件不存在,则 make 会执行该目标所定义的命令。
执行过程
执行 make 命令后,make 程序会在当前目录中查找 Makefile 文件,找到后,会执行第一条规则,也可以在 make 命令后面指定目标名称,从而执行指定的规则。如果要指定特定的 Makefile 文件,可以使用 make -f filename
的格式。
# 由于 hello 这个目标文件不存在,所以执行了该目标所定义的命令。
# 这个 hello 就是一个伪目标,不是一个文件名。
hello:
@echo "Hello World!" # 输出 Hello World!
# 通过 make hi 命令执行该目标的规则
hi:
@echo "Hi Makefile!" # 输出 Hi Makefile!
如果确实有 hello
这个文件存在,也可以用下面的方法将 hello
声明为伪目标:
# 声明 hello 为伪目标,解析时不進行文件查找。
.PHONY: hello
hello:
@echo "Hello World!"
可以多次指定依赖项
# hello 依赖 a b 和 c,所以会先执行 a b c 中的命令
hello: a b
hello: c
@echo "Hello World!"
a:
@echo "Hello A!"
b:
@echo "Hello B!"
c:
@echo "Hello C!"
更新规则
-
如果目标没有被编译过,那么目标及其依赖项都要被编译。
-
如果目标的某些依赖项被修改,则只编译被修改的依赖项和目标。
-
如果某个头文件被修改,则只编译引用了该头文件的目标。
变量
定义变量的方式如下(变量名区分大小写):
# 如果 Value 是一个变量,比如 $(Value),则其值是在 Var 被引用时展开。
# 所以这里相当于 C 中的宏替换(此时 Var 的值不固定,用到 Var 时才去解析其值)
Var = Value
# 如果 Value 是一个变量,比如 $(Value),则其值是在 Var 被定义时展开。
# 所以这里相当于 C 中的常量(此时 Var 的值已固定,定义时就已经解析完毕)
Var := Value
# Var 的值为空时才赋值
Var ?= Value
# Var 的值后面追加 Value 的内容
Var += Value
注意,如果 Value 后面有注释,则注释符号 #
之前的空格也会被算入 Value 的值中,所以尽量不要在变量定义后面添加注释。
可以使用 $()
或 ${}
来引用变量,比如 $(Var)
或 ${Var}
。
如果要引用 Shell 中的环境变量,可以使用 $$
或 $${}
来引用,比如 $$HOME
或 $${HOME}
。也就是说,在执行命令前,会将命令中的 $$
替换为 $
使用,比如 $$?
就表示上个程序的执行结果。
Var = Hello World!
Var += $$HOME
hello:
@echo $(Var) # 输出 Hello World! /home/xx
Makefile 有内置变量 $(CC)
表示当前编译器,$(MAKE)
表示当前 make 程序,$(VPATH)
表示依赖项的搜索路径(多个路径之间用冒号分隔)。
hello:
@echo $(CC) # 输出 cc
@echo $(MAKE) # 输出 make
@echo $(VPATH) # 输出空
也可以使用 vpath
命令指定不同类型的依赖文件的搜索路径(百分号匹配零个或多个任意字符):
# 在 ../lib 目录中搜索 *.o 文件
vpath %.o ../lib
# 清除 *.o 文件的搜索路径
vpath %.o
# 清除所有文件的搜索路径
vpath
在命令中可以使用如下几个变量来引用目标和依赖项:
$@ 变量表示当前目标
$^ 变量表示当前目标的所有依赖项
$< 变量表示当前目标的首个依赖项
$? 变量表示比当前目标更新的依赖项
$(?D) 当前目标的目录名
$(?F) 当前目标的文件名
$(<D) 首个依赖项的目录名
$(<F) 首个依赖项的文件名
示例:
Var = Hello World!
hello: a b c
@echo $@ # 输出 hello
@echo $^ # 输出 a b c
@echo $< # 输出 a
# 为了上面的示例而定义的空目标
a:
b:
c:
如果要覆盖命令行参数中定义的变量,可以使用 override
:
override Var = Value
include
可以使用 include
关键字引入外部文件:
include Makefile.inc *.mk
hello:
@echo $(Var)
make 命令中可以添加 -I
参数,指定 include 文件的搜索路径。
通配符
规则中可以使用通配符 *
、?
、[a-z]
、[^a-z]
。如果要用到真实的 *
、?
字符,可以使用 \*
和 \?
转义。注意,变量定义中的通配符是不会展开的,如果要展开,可以使用 $(wildcard *.c)
。
Var := $(wildcard *.c)
hello: *.c
@echo ${Var}
@echo $?
多目标
all: a b c
a b c :
@echo gcc $@.c -o $@
相当于:
all: a b c
a:
@echo gcc $@.c -o $@
b:
@echo gcc $@.c -o $@
c:
@echo gcc $@.c -o $@
静态模式
多个目标 : 目标模式 : 依赖模式
[Tab]命令
示例:
objects = a.o b.o c.o
all: $(objects)
# 相当于多条规则,每条规则从 objects 中取出一个条目,
# 然后生成 "%.o : %.c" 目标,命令内容不变。
$(objects) : %.o : %.c
@echo $@, $^
# 为了上面的示例而定义的空目标
a.c:
b.c:
c.c:
相当于:
objects = a.o b.o c.o
all: $(objects)
a.o : a.c
@echo $@, $^
b.o : b.c
@echo $@, $^
c.o : c.c
@echo $@, $^
a.c:
b.c:
c.c:
大多数的 C/C++ 编译器都支持一个 -M
的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。例如下面的命令(假设 main.c
中有 include "xxx.h"
):
cc -M main.c
其输出是:
main.o : main.c xxx.h
如果使用 GNU 的 C/C++ 编译器,则应该使用 -MM
参数,因为 -M
参数会把一些标准库的头文件也包含进来。
子目录
执行所有子目录中的 Makefile 文件:
# 获取当前目录下的所有子目录列表
SUBDIR = $(shell find . -maxdepth 1 -type d)
# 在所有子目录中执行 make
all: $(SUBDIR)
@for i in $(SUBDIR); do $(MAKE) -C $$i; \
if test $$? -ne 0; then break; fi; done
# 在所有子目录中执行 make clean
clean: $(SUBDIR)
@for i in $(SUBDIR); do $(MAKE) clean -C $$i; done
如果要传递变量到下级 Makefile 中,可以使用如下声明:
export Var1 Var2 Var3
# 或
export Var1 = Value
如果要传递所有变量,可以直接使用 export
,后面不跟任何内容。
多行变量
define hello
@echo Hello World!
@echo Hi world!
endef
all:
$(hello)
目标特定变量
hello: Var = Hello World!
hello:
@echo $(Var)
模式变量
Var = Hello World!
%.o: Var = Hello O!
all: a.c a.o
a.c:
@echo $(Var)
a.o:
@echo $(Var)
条件判断
Var = aaa
hello:
# ifeq、ifneq、ifdef、ifndef
ifeq ($(Var), aaa)
@echo Hello A
else
@echo Hello B
endif
函数
格式:$(函数名 逗号分隔的参数列表不含空格)
objects = a.o b.o c.o
print=@echo $(1)
hello:
@echo $(objects)
# 字符串替换
@echo $(objects:.o=.c) # a.c b.c c.c
@echo $(subst .o,.c,$(objects)) # a.c b.c c.c
# 模式替换
@echo $(patsubst %.o,%.c,$(objects)) # a.c b.c c.c
# 去除多余空格
@echo " Hello World! "
@echo $(strip " Hello World! ") # " Hello World! "
# 判断字符串是否存在
@echo $(findstring aa,aaa bbb ccc) # aa
# 过滤
@echo $(filter a%,a1 a2 b1 b2) # a1 a2
# 反过滤
@echo $(filter-out a%,a1 a2 b1 b2) # b1 b2
# 排序
@echo $(sort c b d a) # a b c d
# 取单词
@echo $(word 3,a b c d) # c
# 取连续单词
@echo $(wordlist 3,5,a b c d e f) # c d e
# 统计单词个数
@echo $(words a b c d e f) # 6
# 取目录
@echo $(dir ./a.c) # ./
# 取文件名
@echo $(notdir ./a.c) # a.c
# 取后缀
@echo $(suffix ./a.c) # .c
# 去后缀
@echo $(basename ./a.c) # ./a
# 添加后缀
@echo $(addsuffix .c,./a.c) # .././a
# 添加前缀
@echo $(addprefix ../,./a.c) # .././a.c
# 链接名称
@echo $(join aaa bbb,111 222 333) # aaa111 bbb222 333
# 遍历
@echo $(foreach i,a b c,$(i).c) # a.c b.c c.c
# 条件
@echo $(if 1,true,false) # true
@echo $(if 1,true) # true
# 获取 Shell 命令的执行结果
@echo $(shell ls)
@echo `ls`
# 调用前面定义的 print,传入"Hello World!"
$(call print,Hello World!) # Hello World!
# 查询变量源头
# undefined 变量不存在
# default 变量存在,使用内部默认值
# environment 变量在环境变量中定义
# file 变量在 Makefile 文件中定义
# command line 变量在命令行中定义
# automatic 自动化变量
@echo $(origin Var) # undefined
@echo $(origin objects) # file
# 生成警告,继续执行
$(warning warning from user) # warning from user
# 生成错误
# $(error abort from user) # abort by user
隐含规则
假设有 main.c
、foo.c
、foo.h
三个文件,内容如下:
// main.c
#include "foo.h"
int main() {
hello();
}
// foo.c
#include <stdio.h>
void hello() {
printf("Hello World!");
}
// foo.h
void hello();
要将这三个文件编译成 main
程序,只需要下面这个 Makefile 文件即可:
main: foo.o
这里的 main
程序依赖 foo.o
,但是并没有指明 foo.o
如何生成。同时也没有指明 main
程序如何生成。这就要用到 make 的隐含规则,它会自动推断 main
和 foo.o
的生成规则。
如果存在 foo.c
,则会自动生成如下规则:
foo.o: foo.c
$(CC) -c foo.c $(CPPFLAGS) $(CFLAGS)
如果存在 foo.cc
(或 foo.C
,注意后缀是大写),则会自动生成如下规则:
foo.o: foo.cc
$(CXX) -c foo.cc $(CPPFLAGS) $(CFLAGS)
对于 main
程序,则会自动生成如下规则:
main: main.c
$(CC) -c main.c foo.o $(CPPFLAGS) $(CFLAGS)
所以,如果把上面的 Makefile 文件写完整,就是下面这个样子:
main : main.c foo.o
$(CC) main.c foo.o -o main
foo.o: foo.c
$(CC) -c foo.c $(CPPFLAGS) $(CFLAGS)
该 Makefile 文件还可以写成下面的样子:
main: foo.c
这样就不会生成临时文件 foo.o
了,该 Makefile 文件如果写完整,就是下面的样子:
main: main.c foo.c
$(CC) main.c foo.c -o main $(CPPFLAGS) $(CFLAGS)
常用伪目标
all 编译所有的目标。
clean 删除所有由 make 创建的文件。
install 安装已编译好的程序,其实就是把目标文件拷贝到指定目录中去。
print 列出改变过的源文件。
tar 把源程序打包备份。
dist 把源程序压缩备份。
TAGS 更新所有的目标,以备完整地重编译使用。
check 和 test 一般用来测试 makefile 的流程。
命令行参数
用法:make [选项] [目标] ...
-b, -m 忽略和其它版本 make 的兼容性。
-f 文件, --file=文件, --makefile=文件
从 <文件> 中读入 makefile。
-I 目录, --include-dir=目录 在 <目录> 中搜索被包含的 makefile。
-E 字串, --eval=字串 将 <字串> 作为 makefile 语句解析。
-e, --environment-overrides 用环境变量覆盖 makefile 中的变量。
-C 目录, --directory=目录 在执行前先切换到 <目录>。
-r, --no-builtin-rules 禁用内置隐含规则。
-R, --no-builtin-variables 禁用内置变量设置。
-s, --silent, --quiet 不输出配方命令。
--no-silent 对配方进行回显(禁用 --silent 模式)。
-w, --print-directory 打印当前目录。
--no-print-directory 关闭 -w,即使 -w 默认开启。
--warn-undefined-variables 当引用未定义变量的时候发出警告。
-i, --ignore-errors 忽略来自命令配方的错误。
-k, --keep-going 当某些目标无法制作时仍然继续。
-S, --no-keep-going, --stop 关闭 -k。
-n, --just-print, --dry-run, --recon
只打印命令配方,不实际执行。
-q, --question 不运行命令,也不输出。仅仅是检查所指定的目标是否需要更新。如果是 0 则说明要更新,如果是 2 则说明有错误发生。
--trace 打印跟踪信息。
-d 相当于 --debug=a。
--debug[=旗标] 打印各种调试信息,旗标如下:
a 输出所有的调试信息。
b 只输出简单的调试信息。即输出不需要重编译的目标。
v 在 b 选项的级别之上。输出的信息包括哪个 makefile 被解析,不需要被重编译的文件等。
i 输出所有的隐含规则。
j 输出执行规则中命令的详细信息,如命令的 PID、返回码等。
m 输出 make 读取 makefile,更新 makefile,执行 makefile 的信息。
-B, --always-make 无条件 make 所有目标。
-t, --touch 更新目标文件的修改时间,而不更改它们的内容。
-o 文件, --old-file=文件, --assume-old=文件
假定 <文件> 不需要更新(old 表示此文件未发生改动)。
-W 文件, --what-if=文件, --new-file=文件, --assume-new=文件
假定 <文件> 需要更新(new 表示此文件发生了改动)。
-L, --check-symlink-times 使用软链接及软链接目标中修改时间较晚的一个。
-j [N], --jobs[=N] 同时允许 N 个任务;无参数表明允许无限个任务。
-l [N], --load-average[=N], --max-load[=N]
在系统负载高于 N 时不启动多任务。
-O[类型], --output-sync[=类型]
使用 <类型> 方式同步并行任务输出。
-h, --help 打印该帮助信息并退出。
-v, --version 打印 make 的版本号并退出。
-p, --print-data-base 打印 make 的内部数据库。