GNU Make学习笔记
对于任何编译器可以通过Shell调用的编程语言,均可以通过GNU Make(以下称make)构建。make可以用于任何这样的任务:如果某些文件所依赖的文件发生了变化,则自动更新这些文件。
需要编写makefile文件来使用make,该文件中包含了文件之间的依赖关系、提供更新每个文件的命令,在makefile所在目录中运行make命令,即可完成文件更新,make会根据文件名GNUmakefile、makefile、Makefile依次寻找,可以使用-f或--file指定特殊的Makefile文件名。
参数 | 说明 |
-b -m | 忽略和其它版本make的兼容性 |
-B --always-make | 认为所有的目标都需要更新(重编译) |
-C <dir> --directory=<dir> |
指定读取makefile的目录 |
--debug[=<options>] |
输出make的调试信息。它有几种不同的级别可供选择,如果没有参数,那就是输出最简单的调试信息。下面是<options>的取值: a 也就是all,输出所有的调试信息。(会非常的多) -d相当于--debug=a 输出的东西很多,难以阅读,不如-n |
-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运行命令时能运行多少就运行多少 |
-k --keep-going |
出错也不停止运行。如果生成一个目标失败了,那么依赖于其上的目标就不会被执行了 |
-l <load> --load-average[=<load] --max-load[=<load>] |
指定make运行命令的负载 |
-n --just-print --dry-run --recon |
仅输出执行过程中的命令序列,但并不执行 可用于调试Makefile的执行过程 |
-o <file> --old-file=<file> --assume-old=<file> |
不重新生成的指定的<file>,即使这个目标的依赖文件新于它 |
-p --print-data-base |
输出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发现有未定义的变量,那么就输出警告信息 |
1 2 |
# 列出所有目标 make -qp | awk -F':' '/^[a-zA-Z0-9][^$# \/\t=]*:([^=]|$)/ {split($1,A,/ /);for(i in A)print A[i]}' | sort |
简单的Makefile文件由一系列类似下面的“规则”组成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
target ... : prerequisites ... # 如果prerequisites中某个文件比target更新或者target不存在,则会执行相应的命令 recipe # 必须以TAB开头 ... # 或者 target ... : prerequisites ; recipe # 目标(target):可以是某个程序生成的文件的名称,例如object文件、可执行文件;亦可是某个需要执行的操作的标签(伪目标),例如clean # 前提(prerequisites)是作为target输入的文件,通常一个target依赖于多个文件。前提不是必须的 # 命令(recipe)表示需要make执行的操作,一个recipe可以包含多个命令,这些命令可以处于在同一行,或者各自一行 # 每个recipe前面需要有一个tab字符。如果任何prerequisites改变,则recipe包含的规则被执行以生成target文件 # 一个具体的例子: edit : main.o kbd.o command.o display.o insert.o search.o files.o utils.o cc -o edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o main.o : main.c defs.h cc -c main.c clean : rm edit main.o kbd.o command.o display.o \ # 反斜杠可以构成逻辑行(logical line) insert.o search.o files.o utils.o # 更好的写法: .PHONY : clean # .PHONY表示伪目标 clean : -rm edit $(objects) # 小减号表示无视命令错误 |
需要注意:
- 每个recipe前面必须是TAB
- 每个recipe在一个Subshell中执行,也就是说,你不能:
123clean:cd /tmprm -rf *这样是无法删除/tmp/*的,因为第二个recipe的当前目录并不是/tmp,你需要这样:
12clean:cd /tmp; rm -rf*如果命令太长,可以结尾 \并换行
默认的make从Makefile的第一个target开始执行(不包括.开头的target),这称为“default goal”,伪目标可以作为default goal,只要其位于第一个。
- 读入所有的Makefile
- 读入被include的其它Makefile
- 初始化文件中的变量
- 推导隐晦规则,并分析所有规则
- 为所有的目标文件创建依赖关系链
- 根据依赖关系,决定哪些目标要重新生成
- 执行生成命令
1 2 3 4 5 |
all : prog1 prog2 prog3 # 伪目标作为default goal .PHONY : all prog1 : prog1.o utils.o cc -o prog1 prog1.o utils.o ... |
变量可以定义一个文本,并在之后多次替换
1 2 3 4 5 6 |
# 定义变量 objects = main.o kbd.o command.o display.o \ insert.o search.o files.o utils.o edit : $(objects) cc -o edit $(objects) # 使用变量 |
内容 | 说明 |
明确规则(explicit rule) | 明确描述如何重新构建一个或多个文件(即目标),列出其所依赖的其他文件,可能给出如何创建或更新target的recipe |
隐含规则(implicit rule) |
隐含规则根据文件名来判断如何重新构建它,描述了一个文件如何依赖一个名称类似的文件、如何更新文件 例如,对于任何一个whatever.o,会自动判定whatever.c是其依赖 |
变量定义(variable definition) | 包含可以后续替换的预定义文本 |
指令(directive) |
指示make在读取Makefile时执行特殊的操作:
|
注释 |
# 开头的行为注释 |
include指令使make暂停当前文件的读入,并读入其它一个或多个Makefile文件:
1 2 3 |
include filenames... include foo *.mk $(bar) # 包含目录下所有的mk文件,以及bar变量展开后对应当的文件 sinclude # 或者-include,即使找不到也不会导致报告错误 |
如果包含的文件不在当前目录,则按以下顺序寻找:
- 通过‘-I’ 或者‘--include-dir’ 命令选项的目录
- /usr/local/include
- /usr/gnu/include
- /usr/local/include
- /usr/include
如果文件最终找不到,会生成一个警告信息
可以使用以下类型的Unix风格通配符和特殊符号来引用某些文件:
通配符 | 说明 |
* | 通配任意长度的字符串,例如 *.c 匹配任何C源文件 |
? | 通配单个字符 |
[...] | |
~ | 表示当前用户的HOME目录 |
注意,上述通配符不能自动在变量中展开(为文件集合),你必须使用wildcard函数:
1 |
objects := $(wildcard *.o) |
如果源文件分散在多个目录中,可以使用以下方式指定这些目录:
1 2 3 4 |
VPATH = src:../headers # 路径使用冒号分隔 # 或者使用vpath关键字,指定某种模式的文件在哪里寻找 vpath vpath %.h ../headers # %表示匹配0-N个字符 |
如果希望一个目标不做任何事情,可以:
1 2 3 |
null: # 空目标 @: |
伪目标不是文件,并不需要生成伪目标对应的文件。使用.PHONY : target的形式声明一个伪目标
Makefile中可以包含多个目标,如果多个目标依赖同一个文件,并且其recipe类似,则可以将其合并:
1 2 3 4 5 6 7 8 |
bigoutput littleoutput : text.g generate text.g -$(subst output,,$@) > $@ # $@是一个自动化变量,表示当前规则中所有target的集合 # 等价于 bigoutput : text.g generate text.g -big > bigoutput littleoutput : text.g generate text.g -little > littleoutput |
依赖(prerequisites)是指作为某个目标前提条件的目标,依赖必须先于目标构建。
依赖有两类:
- 普通依赖:依赖需要先行构建,如果任何一个依赖过期,则目标被认为是过期的
- order-only依赖:不会触发目标的重新构建
1 |
target : normal-prerequisites | order-only-prerequisites |
同一个目标的普通依赖,先于order-only依赖构建。
可以使用静态模式简化多目标规则的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<targets ...>: <target-pattern>: <prereq-patterns ...> <commands> .... # target 定义了一系列的target,可以使用通配符 # target-parrtern 可选,定义target的模式,例如%.o表示目标都是.o文件 # prereq-parrterns 定义依赖的模式,例如%.c表示依赖与目标同名,且扩展名为c # 例子: objects = foo.o bar.o all: $(objects) $(objects): %.o: %.c $(CC) -c $(CFLAGS) $< -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 # 如果%.o文件有数百个,这样的静态模式将会非常方便 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# 命令的回显 # 在命令前添加@,可以禁止回显 @echo Compiling module... # make -n | --just-print 可以只回显,而不执行命令,便于调试 # make -s | --slient可以全面禁止回显 # 命令执行 # 如果下一条命令依赖于上一条的结果,则应当放置在同一行,并分号隔之 cd /home/alex/CPP; PWD # 命令出错 # 如果命令的返回值为非0,默认make会终止执行 # 在命令前面添加-号,可以阻止该行为 clean: -rm -f *.o # make -i | --ignore-errors则可以忽视所有错误 # make -k | --keep-going可以在出错时终止当前规则,继续执行下一规则 |
大型工程中,可能把不同模块的源代码存放在不同目录中,并且在对应的目录中分别编写Makefile,这样有利于Makefile的维护。可以在父目录中编写一个总控Makefile,它负责分别调用各子Makefile:
1 2 3 4 5 6 |
MAKE = make -k subsystem: cd subdir && $(MAKE) # 或者: subsystem: $(MAKE) -C subdir |
除非指定-e参数,否则总控Makefile不会覆盖子Makefile中定义的变量。如果需要把某个变量传递到子Makefile中,可以:
1 2 3 4 5 6 7 8 9 10 11 12 |
export <variable ...> export variable = value # 等价于 variable = value export variable # 传递所有变量: export # 如果要禁止某个变量传递,则: unexport <variable ...> |
SHELL、MAKEFLAGS这两个变量,无论export与否,都会自动传递到子Makefile。MAKEFLAGS是make的参数(-C、-f、-h、-o、-W不会传递到子Makefile),可以通过下面的语句禁止传递:
1 2 |
subsystem: cd subdir && $(MAKE) MAKEFLAGS= |
在嵌套执行时,可以使用-w|--print-directory来显示正在进入的子目录,Make会打印Entering directory、Leaving directory等信息,使用-C时该标记自动打开。
如果Makefile中经常出现相同的命令序列,可以将其定义为一个变量:
1 2 3 4 5 6 7 8 |
# 以define开始,endef结束 define run-yacc # run-yacc是该命令包的名字,不能与已定义的变量重名 yacc $(firstword $^) mv y.tab.c $@ endef foo.c : foo.y $(run-yacc) # 和使用普通变量一样 |
Makefile中的变量类似于C/C++的宏,代表一个字符串,在执行时按原样展开并替换变量的引用,变量名大小写敏感,不得包含:、# 、=、空白符
- 变量声明时必须赋予初值
- 使用前,需要加前缀$,最好使用$()或者${}这样的语法
- $字符使用$$转义得到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# = 赋值,仅仅当引用变量时才延迟的扩展 # 变量可以使用后面的变量来定义,延迟扩展 foo = $(bar) bar = $(ugh) ugh = Huh? all: echo $(foo) # 打印Huh? # := 赋值,立即计算出值 # 另外一种赋值方式,只能使用前面定义的变量给新变量赋值 x := foo y := $(x) bar x := later dir := /foo/bar # 同行井号终止变量赋值,前面的空格属于变量值的一部分 # 如果FOO没有被定义,则赋值为bar,否则什么都不做 FOO ?= bar # 追加变量值 variable := value variable += more # value more |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# 变量值替换:把var中所有以a结尾的部分替换为b # 所谓结尾,是指空白或者结束符出现的地方 $(var:a=b) # 示例 foo := a.o b.o c.o bar := $(foo:.o=.c)# 所有.o换成.c # 另外一种变量值替换,在静态模式一节讲述过 # 依赖于foo中每个子串具有相同的pattern foo := a.o b.o c.o bar := $(foo:%.o=%.c) # 把变量值作为另外一个变量的名字: x = y y = z a := $($(x))# a的值为z |
如果变量是通过make命令行参数设置的,那么Makefile对其赋值会被忽略,使用override指示符可以改变此行为:
1 2 3 |
override <variable> = <value> override <variable> := <value> override <variable> += <more text> |
与命令包类似,可以通过define定义多行变量:
1 2 3 4 |
define two-lines echo foo echo $(bar) endef |
如果Makefile中的变量、make命令行传入变量,与系统环境变量重名,那么系统环境变量被覆盖
make -e使系统环境变量覆盖Makefile中的变量
可以定义局部作用域的变量:
1 2 3 4 5 6 7 8 9 10 |
<target ...> : <variable-assignment> <target ...> : overide <variable-assignment> # variable-assignment可以是各种赋值表达式,例如=、:=、+=、?= # 局部变量自动带入目标引发的所有规则中 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 # CFLAGS的值依旧是-g,只要该规则是prog目标引发的 |
可以定义一个模式,把变量定义应用到符合此模式的所有目标上。make中的模式至少包含一个%号。
1 2 3 4 5 |
<pattern ...> : <variable-assignment> <pattern ...> : override <variable-assignment> # 把变量CFLAGS应用到所有.o文件 %.o : CFLAGS = -O |
在模式规则中,目标、依赖目标都是一批文件,为了只书写一个命令就完成所有目标的生成,需要使用自动化变量。
自动化变量会把匹配模式的文件自动的、逐个取出,注意自动化变量只应当出现在规则的命令部分。
变量 | 说明 |
$@ |
目标的文件名。在模式规则中,对应匹配的单个文件 |
$% |
目标成员名。对于目标 foo.a(bar.o), $@为 foo.a, $%则为 bar.o 如果目标不是archive成员则$%为空 |
$< |
第一个依赖的名字。在模式规则中,对应匹配的单个文件 |
$? | 所有比目标新的依赖的集合,以空格分隔 |
$^ | 所有的依赖的集合,以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标 |
$+ | 同上,但是不去除重复依赖目标 |
$* | 表示目标模式%及其之前的部分,例如对于模式a.%.b、目标dir/a.foo.b,$*为dir/a.foo |
$(@D) | $@的目录部分,不以/结尾,如果$@种不包含目录,则值为.(当前目录) |
$(@F) |
$@的文件部分。类似的还有: $(*D)、$(*F) |
变量 | 说明 |
MAKE | 当前使用的make解释器的路径 |
MAKECMDGOALS | 本次构建的目标列表 |
MAKEFILE_LIST | make需要处理的makefile列表,当前makefile总是位于列表的最后 |
MAKE_VERSION | make的版本 |
CURDIR | make的工作目录 |
.VARIABLES | 所有变量的集合,包括预定义变量、自定义变量 |
使用条件判断可以让make在运行时走不同的分支:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# 语法 <conditional-directive> # 条件指令,这一行上多余的空格是允许的,只要不是TAB开头 <text-if-true> endif <conditional-directive> <text-if-true> else <text-if-false> endif # 条件指令:ifeq、ifneq、ifdef、ifndef,写法: ifeq (<arg1>, <arg2>) ifeq '<arg1>' '<arg2>' ifeq "<arg1>" "<arg2>" ifeq ($(strip $(foo)),) # 如果strip函数的返回值为空 <text-if-empty> endif ifdef var1 # 如果var1的值非空 endif # 示例 foo: $(objects) ifeq ($(CC),gcc) $(CC) -o foo $(objects) $(libs_for_gcc) else $(CC) -o foo $(objects) $(normal_libs) endif ifdef HTTP_PROXY _DOCKER_RUN_EXTRA_ARGS += --env HTTP_PROXY=${HTTP_PROXY} endif ifeq ($(ROOT_PACKAGE),) $(error the variable ROOT_PACKAGE must be set prior to including golang.mk) endif |
需要注意的是,make是在读取Makefile时就计算条件表达式的值,并根据条件表达式来选择语句,所有最好不要把自动化变量放入条件表达式中,因为自动化变量在运行时才有。
函数的返回值可以作为变量使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# 函数调用语法 $(<function> <arguments>) # 或是 ${<function> <arguments>} # 示例 comma:= , # 一个逗号 empty:= # 空 space:= $(empty) $(empty) # 利用空定义了一个空格 foo:= a b c # 值为"a b c" # 调用subst函数,参数分别为:被替换子串、替换子串、字符串 bar:= $(subst $(space),$(comma),$(foo)) # 这样相当于定义一个函数 createTmp=$(shell mktemp -d) # 每次执行$(createTmp)都会重新mktemp |
函数名 | 说明 | ||
subst |
$(subst <from>,<to>,<text>) 名称:字符串替换函数——subst。 示例:
|
||
patsubst |
$(patsubst <pattern>,<replacement>,<text>) 名称:模式字符串替换函数——patsubst。
|
||
strip |
$(strip <string>) 名称:去空格函数——strip。 |
||
findstring |
$(findstring <find>,<in>) 名称:查找字符串函数——findstring。 |
||
filter |
$(filter <pattern...>,<text>) 名称:过滤函数——filter。 |
||
filter-out |
$(filter-out <pattern...>,<text>) 名称:反过滤函数——filter-out。 |
||
sort |
$(sort <list>) 名称:排序函数——sort。 |
||
word |
$(word <n>,<text>) 名称:取单词函数——word。 |
||
wordlist |
$(wordlist <s>,<e>,<text>) 名称:取单词串函数——wordlist。 |
||
words |
$(words <text>) 名称:单词个数统计函数——words。 |
||
firstword |
$(firstword <text>) 名称:首单词函数——firstword。 |
||
lastword | 最后一个单词 |
函数名 | 说明 |
dir |
$(dir <names...>) 名称:取目录函数——dir。 |
notdir |
$(notdir <names...>) 名称:取文件函数——notdir。 |
suffix |
$(suffix <names...>) 名称:取后缀函数——suffix。 |
basename |
$(basename <names...>) 名称:取前缀函数——basename。 |
addsuffix |
$(addsuffix <suffix>,<names...>) 名称:加后缀函数——addsuffix。 |
addprefix |
$(addprefix <prefix>,<names...>) 名称:加前缀函数——addprefix。 |
join |
$(join <list1>,<list2>) 名称:连接函数——join。 |
该函数很特殊,用来产生循环结构。
1 2 3 4 5 6 7 8 9 10 11 |
# 语法 $(foreach <var>,<list>,<text>) # var 局部变量名 # list 遍历单词列表 # text针对每个单词需要执行的表达式 # 返回值:空格分隔的表达式计算结果的列表 # 举例 names := a b c d files := $(foreach n,$(names),$(n).o) # files=a.o b.o c.o d.o” |
与GNU Make的条件判断很类似
1 2 3 4 5 6 |
# 语法 $(if <condition>,<then-part>) # 或者 $(if <condition>,<then-part>,<else-part>) # 返回值:依据condition真假返回then-part、else-part,或者空字符串 |
创建一个表达式,并传入参数调用之
1 2 3 4 5 6 7 |
# 语法 $(call <expression>,<parm1>,<parm2>,<parm3>...) # 执行call函数时,实参会代替expression中类似$(1),$(2),$(3)的形参: reverse = $(2) $(1) foo = $(call reverse,a,b) # foo为 b a |
该函数说明变量的来源:
1 2 3 4 5 6 7 |
# 语法 $(origin <variable>) # 示例 ifeq ($(origin GOBIN), undefined) GOBIN := $(GOPATH)/bin endif |
返回值 | 说明 |
undefined | 如果<variable>从来没有定义过,origin函数返回这个值 |
default | 如果<variable>是一个默认的定义,比如“CC”这个变量 |
file | 如果<variable>这个变量被定义在Makefile中 |
command line | 如果<variable>这个变量是被命令行定义的 |
override | 如果<variable>是被override指示符重新定义的 |
automatic | 如果<variable>是一个命令运行中的自动化变量 |
shell函数的参数应该就是操作系统Shell的命令,类似于反引号。
1 2 3 |
# 示例 contents := $(shell cat foo) files := $(shell echo *.c) |
一些函数可以控制make的运行。可检测一些运行Makefile时的运行时信息,并且根据这些信息来决定是让make继续执行,还是停止
函数 | 说明 | ||
error |
$(error <text ...>) 产生一个致命的错误,<text ...>是错误信息 |
||
warning |
$(warning <text ...>) 这个函数很像error函数,只是它并不会让make退出,只是输出一段警告信息 |
||
eval |
动态定义一个仅仅对本次运行有效的目标
|
退出码 | 说明 |
0 | 表示成功执行 |
1 | 如果make运行时出现任何错误,其返回1 |
2 | 如果你使用了make的“-q”选项,并且make使得一些目标不需要更新,那么返回2 |
指定目标 一般来说,make的最终目标是makefile中的第一个目标,而其它目标一般是由这个目标连带出来的。这是make的默认行为。
只需要在make命令后添加目标的名字,即可执行指定的目标。
环境变量MAKECMDGOALS存放make的最终目标:
1 2 3 4 5 |
sources = foo.c bar.c # 如果最终目标不是clean,则包含foo.d、bar.d两个makefile ifneq ( $(MAKECMDGOALS),clean) include $(sources:.c=.d) endif |
伪目标 | 说明 |
all | 这个伪目标是所有目标的目标,其功能一般是编译所有的目标 |
clean | 删除所有被make创建的文件 |
install | 安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目录 |
列出改变过的源文件 | |
tar | 是把源程序打包成一个tar文件 |
dist | 创建一个压缩文件,一般是把tar文件压成zip文件或是gz文件 |
TAGS | 更新所有的目标,以备完整地重编译使用 |
check/test | 用来测试makefile的流程 |
目标 | 说明 |
.PHONY | 其所有依赖被看作是伪目标 |
.DEFAULT | 其规则用于构建那些没有声明具体规则的目标 |
.PRECIOUS | 其所有依赖在make过程中被特殊处理:当命令在执行过程中被中断时,make不会删除它们。而且如果目标的依赖文件是 中间过程文件,同样这些文件不会被删除。依赖可以是模式,例如%.o |
.INTERMEDIATE | 其依赖被作为中间过程文件看到 |
.SECONDARY |
其依赖被作为中间过程文件对待。但这些文件不会被自动删除 没有任何依赖的.SECONDARY表示:所有文件作为中间过程文件,且不自动擅长 |
.DELETE_ON_ERROR | 如果规则执行错误,则删除已经被修改的目标文件 |
本章讲述的一些在Makefile中的“隐含的”规则。所谓隐含规则,就是一种惯例,例如即使Makefile中没有书写把*.c编译为.o这一规则,make命令也会自动推倒出此规则并执行。
隐含规则会使用一些系统变量,通过改变这些变量,我们可以改变隐含规则的运行时行为,例如修改CFLAGS可以控制编译器参数。
我们可以通过“Pattern规则”、“suffix规则”来定义自己的“隐含规则”。
只要不显式编写针对某个目标的规则,那么隐含规则即可针对此目标生效,例如:
1 2 3 4 5 6 |
# 并没有编写目标foo.o、bar.o的规则,因此make会使用隐含规则推导其依赖目标、生成命令 # 自动寻找的隐含规则为: # 把*.o的依赖设置为*.c # 使用命令 cc –c $(CFLAGS) *.c来生成*.o文件 foo : foo.o bar.o cc –o foo foo.o bar.o $(CFLAGS) $(LDFLAGS) |
make会到隐含规则库中寻找可用的规则,如果找不到,则会报错。另外,在隐含规则库中,规则是有顺序的,前面的规则优先被识别和使用。
隐含规则 | 说明 |
编译C程序的隐含规则 | *.o目标的依赖目标会自动推导为*.c,并且其生成命令是 $(CC) –c $(CPPFLAGS) $(CFLAGS) |
编译C++程序的隐含规则 | *.o目标的依赖目标会自动推导为*.c或者*.C,并且其生成命令是$(CXX) –c $(CPPFLAGS) $(CFLAGS) |
编译Pascal程序的隐含规则 | *.o目标的依赖目标会自动推导为*.p,并且其生成命令是$(PC) –c $ (PFLAGS) |
汇编和汇编预处理的隐含规则 |
*.o目标的依赖目标会自动推导为*.s,默认使用编译器as,并且其生成命令是$(AS) $(ASFLAGS) *.s目标的依赖目标会自动推导为*.S,默认使用C预编译器cpp,并且其生成命令是:$(AS) $(ASFLAGS) |
链接Object文件的隐含规则 | 如果*目标的依赖目标设置为*.o,则通过运行C的编译器来运行链接程序(一般为ld)生成,ld”)命令是:$(CC) $(LDFLAGS) <n>.o $(LOADLIBES) $(LDLIBS) |
隐含规则都使用一些预先设置的变量,可以在Makefile中指定、通过命令行传入、通过环境变量设置。
变量 | 说明 |
AR | 函数库(*.a或者Windows下的*.lib)打包程序。默认命令是ar |
AS | 汇编语言编译程序。默认命令是as |
CC | C语言编译程序。默认命令是cc |
CXX | C++语言编译程序。默认命令是g++ |
CPP | C程序的预处理器(输出是标准输出设备)。默认命令是$(CC) –E |
RM | 删除文件命令。默认命令是rm -f |
ARFLAGS | 函数库打包程序AR命令的参数。默认值是rv |
ASFLAGS | 汇编语言编译器参数 |
CFLAGS | C语言编译器参数 |
CXXFLAGS | C++语言编译器参数 |
CPPFLAGS | C预处理器参数 |
LDFLAGS | 链接器参数,例如ld |
可以使用模式规则来自定义一个隐含规则。模式规则与一般规则类似,只是,规则的目标必须包含%字符,%统配一个或者多个字符(目标文件名),依赖目标中也可以使用%,其值取决于目标中%的值。
注意:变量和函数的展开发生在读取Makefile时,而模式规则中%的值取决于运行时。
模式规则示例:
1 2 3 4 5 6 |
# 把所有.c文件编译为.o文件 %.o : %.c $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@ # 自动化变量$<表示所有依赖目标的逐个值 # 自动化变量$@表示所有目标的逐个值 |
remake是对make的Fork和扩展,特性:
- 剖析,使用 --profile选项可以查看什么目标耗时严重,输出格式Callgrind Profile
- 列出目标: --tasks(打印目标之前一行的注释) --targets
- 使用 -c / --search-parent自动从上级目录查找target
- 改善执行跟踪, -x / --trace=normal,可以显示任何将要执行的命令
- 调试能力:查看目标属性、查看当前目标Stack、设置断点、设置/展开GNU变量、生成展开GNU变量后的Shell脚本以便在外面运行
到https://sourceforge.net/projects/bashdb/files/remake/下载源代码,然后安装:
1 2 3 |
./configure make sudo make install |
如果执行Makefile过程中出现问题,通常可以基于remake的Call stack检查、扩展错误信息,这两个特性,完成诊断。更进一步,可以使用--trace/-x选项。
如果这些手段都无法解决问题,考虑使用Debugger,从而:
- 检查变量状态、目标如何被定义、目标状态
- 在特定位置停止执行,例如在某个目标,或者发生错误之时
- 修改状态
使用 remake -X 或 remake --debugger发起调用,从一开始就进入调试状态。
或者,可以在目标中启用调试:
1 2 3 4 5 6 7 8 9 10 11 |
foo: bar debug: $(debugger "debug target break") bar: $(debugger "first bar command") @echo hi baz: debug @echo hello again |
最后,使用选项 --post-mortem 或 -!,可以在出错时进入调试模式。
使用-X命令以调试模式启动,你会看到类似下面的输出:
1 2 3 4 5 6 7 8 9 10 11 |
# remake -X Reading makefiles... Updating makefiles... # -> 提示尚未完成目标的前置条件检查 # 当前位置信息 -> (/tmp/libcdio-paranoia/Makefile:428) # 目标 依赖 依赖 Makefile: Makefile.in config.status # 提示符 <0>中的0表示命令历史序号,每次发生递归的remake调用,都会在内部增加一层<> remake<0> step remake<<0>> |
要进行单步跟踪,可以在提示符下输入 step命令。 Makefile的解释/执行会向前进一步,这可能导致Step in:
1 2 3 4 5 6 7 8 9 10 11 |
# 这会导致Step In,因为Makefile依赖于Makefile.in remake<1> step -> (/tmp/libcdio-paranoia/Makefile:415) # 需要注意,在Debugger中,变量已经展开 Makefile.in: Makefile.am m4/ld-version-script.m4 ... # 源码中的415行: # (srcdir)/Makefile.in: $(srcdir)/Makefile.am $(am__configure_deps) remake<2> step -> (/src/external-vcs/github/rocky/libcdio-paranoia/Makefile:443) aclocal.m4: m4/ld-version-script.m4 ... remake<3> |
要查看当前目标依赖的Trace,可以输入 backtrace命令:
1 2 3 4 5 |
remake<3> backtrace =>#0 aclocal.m4 at /tmp/libcdio-paranoia/Makefile:443 #1 Makefile.in at /tmp/libcdio-paranoia/Makefile:415 #2 Makefile at /tmp/libcdio-paranoia/Makefile:428 remake<4> |
要结束调试会话,输入命令: quit
单步跟踪非常啰嗦,我们可以通过命令 continue,直接跳转到自己感兴趣的目标:
1 2 3 4 5 6 7 |
# 在目标dist上设置断点 remake<0> continue dist Breakpoint 1 on target `dist', mask 0x0f: file Makefile, line 703. Updating goal targets... -> (/src/external-vcs/github/rocky/libcdio-paranoia/Makefile:703) dist: remake<1> |
这时,在输入step命令,可以跟踪目标dist的执行过程:
1 2 3 4 5 6 7 8 9 10 11 12 |
remake<1> step File 'dist' does not exist. Must remake target 'dist'. Makefile:704: target 'dist' does not exist # 这里是要执行的命令 ##>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> remake dist-bzip2 dist-gzip am__post_remove_distdir='@:' ##<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< # 这里的++表示正在Stepping Shell命令 ++ (/src/external-vcs/github/rocky/libcdio-paranoia/Makefile:703) dist remake<2> |
要查看Makefile中,当前正在执行的目标的源码,可以使用 list命令:
1 2 3 4 5 6 |
remake<2> list /tmp/libcdio-paranoia/Makefile:705 dist: # recipe to execute (from 'Makefile', line 706): $(MAKE) $(AM_MAKEFLAGS) $(DIST_TARGETS) am__post_remove_distdir='@:' $(am__post_remove_distdir) |
使用 print命令,可以查询任何GNU Make变量的定义,需要注意,依赖的变量不会展开:
1 2 3 4 5 6 7 |
remake<2> print MAKE # origin default表示内置变量定义 (origin default) MAKE = $(MAKE_COMMAND) remake<3> print DATA Makefile:168 (origin: makefile) DATA := libcdio_paranoia.pc libcdio_cdda.pc |
使用 expand命令,可以展开任何变量的值:
1 2 |
remake<4> expand MAKE (origin default) MAKE := remake |
除了查询,你也可以设置变量的值,使用 set、 setq或 setqx命令:
1 2 3 4 5 6 |
# set命令,在定义NAME的值之前,将$(MAKE_COMMAND)展开 remake<6> set MAKE $(MAKE_COMMAND) Variable MAKE now has value 'remake' # setq则不展开 remake<7> setq MAKE $(MAKE_COMMAND) Variable MAKE now has value '$(MAKE_COMMAND)' |
考虑下面的示例Makefile:
1 2 3 4 5 6 |
PACKAGE=make all: $(PACKAGE).txt $(PACKAGE).txt: ../doc/remake.texi makeinfo --no-headers $< > $@ |
启动调试后:
1 2 3 4 5 6 7 8 9 |
# remake -X -f test2.mk ... Reading makefiles... updating makefiles.... Updating goal targets.... /tmp/remake/src/test2.mk:3 File `all' does not exist. -> (/tmp/test2.mk:5) make.txt: ../doc/remake.texi |
暂停在目标make.txt,我们要想看该目标的信息,可以使用 target命令。但是该命令返回的信息太多了,要仅仅查看它关联的自动变量,可以:
1 2 3 4 5 6 7 8 9 10 11 |
# 这三个命令效果一样:target make.txt variables, target @ variables, info locals remake<1> target @ variables @ := all % := * := + := make.txt | := < := all ^ := make.txt ? := |
为target命令指定选项,可以仅仅列出它的Shell命令:
1 2 3 4 5 |
remake<2> target @ commands make.txt: # commands to execute (from `test2.mk', line 6): makeinfo --no-headers $< > $@ |
还可以将命令完全展开:
1 2 3 4 5 6 7 8 |
remake<5> target @ expand # commands to execute (from `test2.mk', line 6): makeinfo --no-headers $< > $@ # 完全展开 # commands to execute (from `test2.mk', line 6): -makeinfo --no-headers ../doc/remake.texi > make.txt |
使用 write命令,可以将当前target的命令写出为Shell脚本:
1 2 3 |
(/tmp/remake/src/test2.mk:6): make.txt remake<6> write File "/tmp/make.txt.sh" written. |
使用 shell命令,可以在Debugger中执行shell命令,例如查看生成的脚本的内容:
1 2 3 4 5 |
remake<7> shell cat -n /tmp/make.txt.sh #!/bin/sh # cd /tmp/remake/src/ #/tmp/remake/src/test2.mk:5 makeinfo --no-headers ../doc/remake.texi > make.txt |
如果使用 step命令,remake会一句一句的执行命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
remake<8> step Must remake target `make.txt'. Invoking recipe from test2.mk:6 to update target `make.txt'. ##>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> makeinfo --no-headers ../doc/remake.texi > make.txt ##<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< # ++ 表示我们即将运行代码 ++ (/tmp/test2.mk:5) make.txt remake<9> step Successfully remade target file `make.txt'. # <- 表示执行完毕,即将退出目标 <- (/tmp/test2.mk:5) make.txt remake<10> |
使用 finish命令,可以执行到当目标的结尾处。
图标 | 事件 |
-> | 在检查一个目标的前置条件之前,暂停了执行 |
.. | 在检查一个目标的前置条件之后,暂停了执行 |
<- | 在执行了目标的命令列表之后,暂停了执行 |
rd | 即将读取一个Makefile |
!! | 在post-mortem(出错后调试)模式下,发生了错误 |
- - | 运行一个Makefile目标的step |
++ | 运行一个POSIX命令的step |
:o | 在Makefile中,通过$(debugger)函数进入调试模式 |
|| | 完成 |
命令 | 说明 | ||
break |
设置断点: break {target | line-number} [ all | run | prereq | end ]* 查看断点: break 示例:
|
||
delete | 删除断点,需要指定断点序号: delete [ bpnumber [bpnumber…] ] | ||
打印变量定义(不展开): print [variable] | |||
expand | 展开变量: expand string | ||
write | 输出目标的命令到文件: write [target [[filename|**here*]]] | ||
target |
查看目标的信息: [target-name] [info1 [info2…]] target-name可以是目标名,或者 @表示当前目标; <表示第一个依赖 infoX可以是: commands 为了更新目标需要执行的命令列表 |
||
edit | 在当前目标的位置编辑Makefile | ||
list |
列出目标的依赖: list [ target ] 列出目标或所在行号的命令: list line-number | - |
||
load | 读入并eval Makfile: load file-glob | ||
info |
获取被调试程序的各种信息: info break 断点列表 |
||
cd | 设置当前目录 | ||
pwd | 打印当前目录 | ||
shell | 执行Shell命令 | ||
continue |
Run to xxx,继续执行,直到下一个断点或停止点 格式: continue [ target [all | run | prereq | end ]* ] 如果 target 不为空,则先在target的相关位置设置断点,然后Run to那个地方 |
||
finish |
Step out,执行直到当前目标结束的那个点 格式: finish [ count ]。count表示Step out几层,默认0 |
||
next |
Step over,单步/多步跟踪 格式: next [ count ] |
||
step | 类似于next,但是粒度更细 | ||
skip | 跳过当前目标尚未结束的命令 | ||
run | 重启执行过程: run [ args ] | ||
quit | 优雅退出 | ||
backtrace | 打印调用栈帧,每个帧都是一个目标 | ||
frame | 重新定位到栈帧中的某个帧: frame [ number ] | ||
setq | 设置变量值,不进行Makefile变量展开: setq basename value | ||
setqx | 设置变量值,进行Makefile变量展开: setqx variable value |
Leave a Reply