读书笔记:《GUN Make项目管理(第三版)》

前言:

《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: ;

也可以写成工作目标之后指定一个只有跳格符的空白行,不过影响阅读。

2019/10/10 18:16
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值