前言:
《GUN Make项目管理(第三版)》出版于2006年,中译本由东南大学出版社出版。本书详细介绍了make的规则和用法,有详细的例子。缺点就是出版太早,但基础的内容没有多少变化,建议先看这本快速上手,再读make官方文档。只看了最重要的前六章,本贴对书中的一些关键知识点作个备忘。
make命令行选项
make --just-print #显示目标命令但并不实际执行
make --print-data-base #或-p,列出make具有的默认规则和变量
make --no-builtin-rules #或-r,不使用内置规则
一、规则
makefile显式规则:
target: prereq1 prereq2
commands
工作目标: 必要条件1 必要条件2 ...
命令
自动变量
核心自动变量
$@ #工作目标文件
$% #archive member结构中的文件名元素
$< #第一个必要条件的文件名
$? #时间戳在工作目标之后的所有必要条件
$^ #所有必要条件的文件名
$+ #同$^,允许重复的文件名
$* #工作目标的主文件名
VPATN变量 vpath命令
VPATH = src include #告诉make在src和include中搜索代码文件
vpath pattern directory-list
vpath %.l %.c src #在src目录中寻找.c、.h文件
vpath %.h include #在include中寻找.h文件
模式规则
%.o: %.c
command
静态模式规则
$(OBJECTS): %.o: %.c #使该规则只作用于变量OBJECTS中的文件
command
后缀规则(过时)
.c.o:
command
#相当于
%.o: %.c
command
.p:
command
#相当于
%: %.p
command
# .SUFFIXES设定已知的扩展名
.SUFFIXES: .out .a .ln .o .c .cc .C .cpp .p .f .F .r .y .l
# 删除所有已知扩展名
.SUFFIXES:
隐含规则
当make检查一个工作目标时,如果找不到可以更新它的显式规则,就会使用隐含规则。要使用隐含规则:当你将工作目标加入makefile时,只要不指定脚本就行了。
查看内置规则库:
make --print-data-base #或-p,列出make具有的默认规则和变量
假想工作目标
任何不代表文件的工作目标就叫假想工作目标。要声明一个假想工作目标,只需要将该工作目标指定为.PHONY(一个特殊工作目标)的一个必要条件即可。如 声明假想工作目标clean:
.PHONY: clean #声明它的必要条件不代表一个实际的文件,而且应被视为尚未更新
clean:
rm -f *.o
标准的假想工作目标
all #执行所有工作
install #从已编译的二进制文件进行应用程序的安装
clean #删除产生自源代码的二进制文件
distclean #删除编译过程中产生的任何文件
TAGS #建立可供编辑器使用的标记表
info #从Texinfo源代码来创建GUN info文件
check #执行与应用程序相关的任何测试
特殊工作目标
特殊工作目标是一个内置的假想工作目标,用来变更make的默认行为。除了.PHONY以外,还有以下特殊工作目标:
.INTERMEDIATE #其必要条件会被视为中间文件,make结束时会被自动删除
.SECONDARY #其必要条件会被视为中间文件,但不会被自动删除,但加入档案库(archive)后就会被删除
.PRECIOUS #如果make在运行期被中断,它会删除除它正在更新的工作目标文件,确保不会留下未完成编译的文件。指定.PRECIOUS以阻止其删除行为
.DELETE_ON_ERROR #与.PRECIOUS作用相反,发生错误时删除工作目标文件
自动产生依存关系
一种makefile自动解决头文件依赖问题的解决方案,是通过如下的一个关键脚本:
include $(subst .c,.d,$(SOURCES)) #包含所有.c文件同名的.d文件
%.d: %.c
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
解释一下上面的命令:
gcc -M source.c
-M选项会搜索代码source.c所引用的所有头文件,并输出成makefile形式的“目标:依赖”形式。
… >
@
.
@.
@.$KaTeX parse error: Can't use function '$' in math mode at position 34: …输出重定向到文件名<code>$̲@.KaTeX parse error: Can't use function '$' in math mode at position 17: …/code>。其中<code>$̲@</code>就是工作目标的…
在shell中被扩展为当前所运行shell的进程ID,.$$$$
是为了确保产生唯一的文件名。综上,第一行命令用每一个.c文件生成了一个.d.pid文件,.d文件中描述了同名.c文件对应目标文件的头文件依赖。
读懂第二行命令需要先了解shell中的sed
命令。sed的s命令是执行字符串替换,将模式\($*\).o[ :]*
替换为\1.o $@ :
。主要作用是将.d文件名追加到.d文件中的工作目标中。
看文字有点绕,举个例子一个hello world程序main.c,gcc -M main.c > main.d生成的main.d的内容如下:
main.o: main.c /usr/include/stdc-predef.h /usr/include/stdio.h \
/usr/include/x86_64-linux-gnu/bits/libc-header-start.h \
/usr/include/features.h /usr/include/x86_64-linux-gnu/sys/cdefs.h \
/usr/include/x86_64-linux-gnu/bits/wordsize.h \
/usr/include/x86_64-linux-gnu/bits/long-double.h \
/usr/include/x86_64-linux-gnu/gnu/stubs.h \
/usr/include/x86_64-linux-gnu/gnu/stubs-64.h \
/usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h \
/usr/include/x86_64-linux-gnu/bits/types.h \
/usr/include/x86_64-linux-gnu/bits/typesizes.h \
/usr/include/x86_64-linux-gnu/bits/types/__FILE.h \
/usr/include/x86_64-linux-gnu/bits/types/FILE.h \
/usr/include/x86_64-linux-gnu/bits/libio.h \
/usr/include/x86_64-linux-gnu/bits/_G_config.h \
/usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h \
/usr/lib/gcc/x86_64-linux-gnu/7/include/stdarg.h \
/usr/include/x86_64-linux-gnu/bits/stdio_lim.h \
/usr/include/x86_64-linux-gnu/bits/sys_errlist.h
执行上述sed命令之后,通过创建并删除一个main.d.2196821968的临时文件,生成main.d内容为:
main.o main.d : main.c /usr/include/stdc-predef.h /usr/include/stdio.h \
/usr/include/x86_64-linux-gnu/bits/libc-header-start.h \
/usr/include/features.h /usr/include/x86_64-linux-gnu/sys/cdefs.h \
/usr/include/x86_64-linux-gnu/bits/wordsize.h \
/usr/include/x86_64-linux-gnu/bits/long-double.h \
/usr/include/x86_64-linux-gnu/gnu/stubs.h \
/usr/include/x86_64-linux-gnu/gnu/stubs-64.h \
/usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h \
/usr/include/x86_64-linux-gnu/bits/types.h \
/usr/include/x86_64-linux-gnu/bits/typesizes.h \
/usr/include/x86_64-linux-gnu/bits/types/__FILE.h \
/usr/include/x86_64-linux-gnu/bits/types/FILE.h \
/usr/include/x86_64-linux-gnu/bits/libio.h \
/usr/include/x86_64-linux-gnu/bits/_G_config.h \
/usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h \
/usr/lib/gcc/x86_64-linux-gnu/7/include/stdarg.h \
/usr/include/x86_64-linux-gnu/bits/stdio_lim.h \
/usr/include/x86_64-linux-gnu/bits/sys_errlist.h
第三行命令很明显,清理临时文件.d.pid。
管理程序库
ar命令选项:
r #replace,替换或创建库文件
v #verbosely,显示详细信息
t #列出程序库中的目标文件名
gcc链接程序库文件:
gcc a.o libc.a /lib/libfl.a -o main #需要指定路径,不建议使用
gcc a.o -lc -lfl -o main #方便,便于移植
gcc a.o -L. -lc -lfl -o main #指定库搜索路径,-L必须放到系统程序库之前,且作用于所有-l选项
makefile中创建和链接程序库
libcounter.a: counter.o lexer.o
$(AR) $(ARFLAGS) $@ $^ #更新.a时,其中的成员全部更新
libcounter.a: counter.o lexer.o
$(AR) $(ARFLAGS) $@ $? #只更新新的成员
libcounter.a: libcounter.a(lexer.o) libcounter.a(counter.o) #libcounter.a(lexer.o)
libgraphics.a(bitblt.o): bitblt.o
$(AR) $(ARGLAGS) $@ $< #libcounter.a(lexer.o)# 引用库文件中的成员,在隐含规则中,可以省略
以程序库为必要条件
#可以使用路径名
xpong: $(OBJECTS) /lib/X11/libX11.a /lib/X11/libXaw.a
$(LINK) $^ -o $@
#也可以使用-l语法,会优先搜索共享程序库
xpong: $(OBJECTS) -lX11 -lXaw
$(LINK) $^ -o $@
如果makefile已经将程序库文件指定为工作目标,它就不能在必要条件里对该文件使用-l选项。如果要在makefile里进行程序库的编译工作,必须使用文件名的语法。
程序库在命令行上的顺序很重要,程序库A、B循环引用的一个解决方法是:-lA -lB -lA
。但是makefile的自动变量$^
会丢弃重复的部分,此时就应该使用$+
。
双冒号规则
通常当一个工作目标多次出现时,所有必要条件都会被衔接成一个列表,只会执行一个命令脚本进行更新,前面的规则会被后面的覆盖掉。
对于双冒号规则,相同的工作目标会被视为一个独立的实体,可以以不同的命令来更新同一个工作目标,实际执行的命令取决于哪个依赖文件被更改。
file-list:: generate-list-script
chmod +x $<
generate-list-script $(files) > file-list
file-list:: $(files)
generate-list-script $(files) > file-list
二、变量与宏
变量
make包含两种语言,第一种用来描述工作目标与必要条件所组成的依存图,第二种是宏语言,用来进行文字替换。
makefile中变量名区分大小写,取得变量的值请用$()或${}。
#常数
CC := gcc
MKDIR := mkdir -p
#内部变量
sources = *.c
objects = $(subst .c,.o,$(sources))
#函数
maybe-make-dir = $(if $(wildcard $1),,$(MKDIR) $1)
assert-not-null = $(if $1,,$(error Illegal null value.))
变量的值不包含赋值号右侧的前导空格,但包含其后的所有字符。
变量有两种类型:经简单扩展的变量、经递归扩展的变量。
用:=
来定义一个经简单扩展的变量(简单变量)。一旦make从makefile中读进该变量的定义语句,赋值运算符右边的部分会立即被扩展,如果遇到变量尚未定义会被扩展为空。
用=
来定义一个经递归扩展的变量(递归变量)。make会读进赋值运算符右边的部分,不会进行任何扩展动作。扩展动作会被延迟到该变量被使用时才进行。
其他的赋值类型
?=
:附带条件的变量赋值运算符(条件赋值)。只有变量不存在时才会进行赋值动作。可以用来为环境变量提供默认值。
# 将所产生的每个文件放到$(PROJECT_DIR)/out目录中
OUTPUT_DIR ?= $(PROJECT_DIR)/out
# 只会在输出目录变驲OUTPUT_DIR不存在的情况下才对其进行赋值动作
+=
:附加运算符。会将文本附加到变量里。可以附加到递归变量中。
宏
可以用#define
创建“封装命令序列”(宏)。这种变量可以包含换行符。每一行开头必须前置TAB符。
# 定义
define create-jar
@echo Creating $@...
$(RM) $(TMP_JAR_DIR)
$(MKDIR) $(TMP_JAR_DIR)
$(CP) -r $^ $(TMP_JAR_DIR)
cd $(TMP_JAR_DIR) && $(JAR) $(JARFLAGS) $@ .
$(JAR) -ufm $@ $(MANIFEST)
$(RM) $(TMP_JAR_DIR)
endef
# 使用
$(UI_JAR): $(UI_CLASSES)
$(create-jar)
makefile中的元素扩展规则
两个阶段:
第一个阶段,make读进makefile之后,会对变量进行赋值和扩展动作并建立依存图。第二阶段,make会分析以及遍历依存图。等到make执行命令脚本时,所有变量都已经处理完毕了。
- 对于变量赋值,make会在第一阶段读进行该行时,立即扩展赋值运算符左边的部分。
- = 和 ?= 的右边部分会被延迟到被使用时扩展,并在第二阶段进行。
- := 的右边部分会被立即扩展。
- 如果 += 的左边部分原本被定义成一个简单变量,+= 的右边部分就会被立即扩展,否则,它的求值动作会被延迟。
- 对于宏定义,宏的变驲名称会被立即扩展,宏的主体会被延迟到使用时扩展。
- 对于规则,工作目标和必要条件总是被立即扩展,命令总是会延迟扩展。
工作目标与模式的专属变量
工作目标的专属变量:其定义会附加在工作目标之上,且只有在该工作目标以及相应的任何必要条件被处理的时候,它们才会发生作用。不一定要事先存在。
# 定义
target...: variable = value
target...: variable := value
target...: variable += value
target...: variable ?= value
# 原
gui.o: gui.h
$(COMPILE.C) -DUSE_NEW_MALLOC=1 $(OUTPUT_OPTION) $<
#使用专属变量,处理完工作目标之后事先存在的CPPFLAGS会恢复原有内容
gui.o: CPPFLAGS += -DUSE_NEW_MALLOC=1
gui.o: gui.h
$(COMPILE.C) #(OUTPUT_OPTION) $<
变量的来源
变量的来源可以是:
- 文件。通过 include指令引入。
- 命令行。命行行上变量的赋值结果会覆盖掉环境变量及makefile中的赋值结果。
- 环境。makefile内的赋值会覆盖环境变量,除非使用 --environment-overrides(或-e)选项。当make被递归调用时,原先来自环境的变量会被导出到下层环境之中。要导出任意变量,使用 export 指令。要避免环境变量导出到子进程,请使用 unexport指令。
- 自动创建。
条件指令
条件指令格式:
if-condition
text if the condition is true
endif
if-condition
text if the condition is true
else
text if the condition is false
endif
其中if-condition
可以是:
ifdef variable-name# 变量名不应该使用$()
ifndef variable-name
ifeq test#测试参数是否相等。 test可以表示为 "a" "b" 或 (a,b),后者逗号后的空格被忽略
ifneq test
条件处理指令可以用在宏定义和命令脚本中,也可以放在makefile顶层。
include指令
- 如果include的参数是绝对的文件引用,会直接读进该文件。
- 如果是一个相对的文件引用,make会先到当前的工作目录中查找,如果没找到,会到命令行上以–include-dir(或-I)选项所指定的目录继续查找。如果还没找到,make会到自已被编译时使用的搜索路径进行查找。仍未找到则报错,忽略错误请使用-include(sinclude)。
标准的make变量
- MAKE_VERSION
- CURDIR
- MAKEFILE_LIST
- MAKECMDGOALS
- .VARIABLES
三、函数
call函数
call函数是内置于make的函数,它会扩展第一个参数并把其余参数依次替换到出现 $1、$2、…的地方(call本质上只是做了宏扩展动作)。
call的第一个参数是一个非扩展式变量名称(并非以$开头,如果以$()方式,则会先扩展后再传递给call)。
参数扩展规则:可以为call指定任意多个参数。如果一个宏引用了参数$n但调用call的实例并不指定相应参数,该变量就会变成空值。如果call指定的参数比$n多,在宏中并不会扩展额外的参数。
自定义函数
自定义函数其实就是以$1、$2、…代替参数的宏:
# 调用:$(call program,prameter,...)
define program
commands #包含$1 $2 ...
内置函数
所有函数都有如下形式:
$(function-name arg1[, argn])
$(之后是内置函数的名称,接着是函数参数,以逗号分隔。第一个参数的前导空格会被删除,其后的参数中所有字符都会被保留。
字符串函数
-
$(filter pattern …,text)
:将text视为一系列空格隔开的单词,与pattern比较之后会返回相符者。无法在单词中匹配子字符串(只会返回整个单词都匹配的项),只接受一个通配符。如果模式包含额外的%字符,只有第一个被视为通配符。 -
$(filter-out pattern …,text)
:作用与filter相反,用来选出与模式不相符的单词。 -
$(findstring string …,text)
:在text里搜索string。找到则返回string,否则返回空。string不含通配符。 -
$(subst search-string,replace-string,text)
:不具通配符能力的搜索和替换函数。 -
$(patsubst search-pattern,replace-pattern,text)
:具备通配符能力的搜索和替换函数,模式只包含一个通配符%。 -
$(words text)
:返回text中单词的数量。 -
$(words n,text)
:返回text中第n个单词,编号从1开始。若n大于单词个数,则返回空。 -
$(firstword text)
:返回text中的第一个单词。 -
$(wordlist start,end,text)
:返回text中从start(含)到end(含)的单词。
重要的杂项函数
$(sort list)
:排序它的list参数,以空格为分隔按字典序返回。会去重,且会删除前、后的空格。$(shell command)
:参数会被扩展,并且传递给subshell来执行,make会将command的标准输出返回。输出中的换行会被缩减为空格。标准错误及程序结束状态不会返回。
文件名函数
$(wildcard pattern…)
:其参数是一份模式列表,会对列表中的每个模式进行扩展。可以在条件语句中测试文件是否存在。
sources := $(wildcard *.c *.h)
dot-emacs-exists := $(wildcard ~/.emacs)#如果不存在.emacs会返回空
-
$(dir list…)
:返回list中每个单词的目录部分。 -
$(notdir name…)
:返回文件路径的文件名部分。 -
$(suffix name…)
:返回参数中每个单词的后缀。 -
$(basename name…)
:返回文件名中不含后缀的部分。 -
$(addsuffix suffix,name…)
:将suffix附加到name中每个单词后面。 -
$(addprefix prefix,name…)
:为每个名字添加前缀。 -
$(join prefix-list,suffix-list)
:参数是两个列表,此函数将两个列表对应元素衔接在一起。
流程控制函数
-
$(if condition,then-part,else-part)
:根扰condition从then-part和else-part中选一个出来。不同于ifeq、ifne、ifdef指令。 -
$(error text)
:输出错误信息,之后终止。 -
$(foreach variable,list,body)
:反复扩展主体body,将list中的值替换分别替换到variable中进去,最后的结果累积起来并以空格分隔。
letters := $(foreach letter,a b c d,$(letter)1)
show-words:
# letters has $(words $(letters)) words: '$(letters)'
$ make
# letters has 4 words: 'a1 b1 c1 d1'
其他杂项函数
-
$(strip text)
:将从text中移除所有的前导和尾空格,并以单一空格来替换所有内部空格。 -
$(origin variable)
:返回描述变量来自何处的字符串。其返回值可能是
undefined:未定义
default:来自make的内置数据库。
environment:来自环境。
environment override:来自环境且使用了--environment-overrides选项
file:来自makefile。
command line:来自命令行。
override:来自override指令。
automatic:自动变量。
$(warning text)
:类似error函数,但不会导致make结束运行。
eval与value函数
$(eval sources := foo.c bar.c)
:将文本直接放入make解析器,就好像它是来自输入文件的一样。value
:返回它的variable参数未扩展的值。很少用到。
四、命令
-
make默认使用/bin/sh这个shell,由SHELL这个make变量控制。
-
在工作目标之后,凡是第一个字符为跳格符(TAB)的文本行一律会被视为命令(除非前一行尾是一个反斜线符号\,这表示延续上一行)。
-
以跳格符开头的文本行会被subshell执行。空行会被忽略。以#开头的文本行会被忽略(区别:以#开头且有前导TAB的是shell注释,否则它是makefile的注释)。条件处理指令,如ifdef和ifeq会在脚本中被认出并处理。
命令修饰符
-
@:不回显。要想把某个工作目标的所有命令都隐藏,可以把工作目标设为特殊工作目标.SILENT的必要条件。使用-silent(-s)选项来将其应用到所有工作目标上。
-
破折号前缀-:指示make应忽略命令中的错误。如果出错会终止该行命令继续执行下去。也可以将工作目标设为.IGNORE的必要条件。使用–ignore-errors(-i)来忽略整个makefile的错误。尽量少用。
-
加号修饰符+:要求make执行命令,就算用户是以–just-print(-n)来执行make的。当你要编写递归形式的makefile时,就会用到这个功能。
错误与中断
make每执行一个命令就会返回一个状态码。默认时,当有一个程序运行失败make就会停止执行。如果想要尽量编译完所有文件以便于一次性看到所有编译错误,可以使用–keep-going(-k)选项。
将工作目标设成.DELETE_ON_ERROR的必要条件,当有错误发生时,make会删除有问题的文件。如果使用了.DELETE_ON_ERROR但不为其指定必要条件,会使得所有工作目标文件在错误时删除。如果想抑制该行为,使用.PRECIOUS来说明某个目标很重要,这时就不会删除。
空命令
空命令注是一个什么都不做的命令:
header.h: ;
也可以写成工作目标之后指定一个只有跳格符的空白行,不过影响阅读。