Managing Projects with GNU make 学习笔记

来源:互联网 发布:1688商品怎么上传淘宝 编辑:程序博客网 时间:2024/05/16 14:07
1. 简介
makefile定义了一种语言来描述源代码、中间文件及可执行文件之间的关系。如果命令行指定了目标,则更新指定的目标,如果没有,则取第一个目标,也即默认目标。1.1 目标与依赖makefile包含构造程序的一组规则,规则包含三个部分:目标、依赖及执行命令。target: prereq1 prereq2commandstarget是需要构造的东西,依赖则是在创建target前必须存在的东西。commands为将依赖生成为target的shell命令。1.2 依赖检查
对于-l<NAME>的依赖,make有特殊的支持,它表示依赖库文件。
make会查找libNAME.so,如果找不到,则查找libNAME.a。1.3 减少重编
文件修改时,只会影响相关的编译,整个项目一般不会从头构造
1.4 调用makemake target 更新指定的target。如果目标是最新的,则什么也不做。如果指定的target在makefile里不存在,也没有隐含规则,则提示无规则。make有很多命令行参数,最有用的是 --just-print (或 -n)它使make显示编译目标使用的命令,但不实际执行。命令行的变量可以覆盖makefile里的变量。1.5 makefile基本语法更完整的形式(不是最完整的):target1 target2 target3: prerequisite1 prerequisite2command1command2command3target可以有多个,依赖可以没有。如果没有依赖,则只有不存在的target会更新。command必须以tab开始。make另启动子shell来执行command。长的行可以用反斜杠分隔,依赖也可以使用。2. 规则规则有不同种类 * 显式规则,明确指定目标和依赖 * 模式规则,使用通配符 * 隐式规则,makefile内置的模式规则或者扩展名规则2.1 显式规则大多数规则是显式规则。规则可以有多个目标,表示每个目标的依赖是相同的。target1 target2: prereq等价于target1: prereqtarget2: prereq规则不需要一次写全,make每次搜索目标文件时,会将目标和依赖加到依赖关系图中。如果目标在依赖关系图中已存在,则将依赖添加到图中。vpath.o: vpath.c make.h config.h getopt.h gettext.h dep.hvpath.o: filedef.h hash.h job.h commands.h variable.h vpath.h2.1.1 通配make的通配符与bash一样:~, * , ?, [...], [^...]*.*表示所有包含.的文件,?表示任意一个字符,[...]表示一组字符,选择相反的字符集用[^...]~用于表示当前用户的home目录,~user表示那个用户user的home目录。目标或是依赖里的通配由make处理,而命令里的通配由shell处理。而make在读取makefile时会立即展开通配符。2.1.2 Phony目标不代表文件的目标即为phony目标,比如all和clean。一般情况下,phony总会执行,因为相关的命令不产生目标名。但是make无法区分文件目标和phony目标,假如一个phony目标恰好对应了一个文件名,make会把假的目标加到依赖图里。为了避免该问题,GNU make引入了一个特殊的目标:.PHONY来告诉make这个目标不是一个真实文件,可以用它来修饰任何一个伪目标,形式如下:    .PHONY: clean    clean:        command这样,即使存在一个叫clean的文件,make也会执行clean目标的命令。将phony目标作为真实文件的依赖没有多大意义,因为phony总是旧的,这会导致目标文件重新生成。常用的做法是将phony作为目标。按习惯,有一些标准的phony目标: * all:执行所有任务构建程序 * install:安装程序 * clean:删除从源码生成的二进制文件 * distclean:删除不在源码发布范围内的所有产生文件 * TAGS:创建tag表给编辑器使用 * info:从Texinfo源码创建GNU info文件 * check:运行与程序相关的测试TAGS不是一个phony目标,因为ctags和etags的输出就是一个TAGS文件。2.1.3 空目标空目标与phony目标类似,phony target总是过时的,导至其依赖被重新生成。有时有些命令没有输出文件,希望只在有些情况下去执行,并且不想更新依赖,此时可以创建一个空文件的规则(也称为cookie):
prog: size prog.o
$(CC) $(LDFLAGS) -o $@ $^
size: prog.o
size $^
touch size
在这例里,size规则用touch生成了一个文件名为size的空文件,这个空文件充当了一个时间戳的功能。只有在prog.o更新时,size规则才会执行。除非prog.o更新了,否则size规则不会导致prog被重编。也就是说这个规则在prog.o更新prog时执行,起到了特定条件下执行命令的目的。空文件与自动变量$?结合时特别有用。$?表示比目标新的依赖集合。    print: *.[hc]        lpr $?        touch $@这个例子打印自上次执行print以来,变化过的文件。通常,空文件是用来标记一个特定的时间。2.2 变量变量最简单的形式:$(variable-name)表示希望展开名为variable-name的变量。变量名需要用$()引用,但单字符变量可以省去括号。makefile有自定义的一些变量。2.2.1 自动变量当规则匹配时,make会设置自动变量,它们提供了目标和依赖的清单,避免显式指定文件名,代码重复,且对通用的模式规则很重要。有6个重要的自动变量:$@表示target的文件名$%the filename element of an archive member specification.可能跟库有关系。$<第一个依赖的文件名$?比目标新的所有依赖$^所有依赖的文件名(会去掉重复项)$+与$^类似,但包含重复项$*目标文件名的根。根一般是不含后缀名的文件名。不建议在pattern rule之外使用。自动变量只有在规则匹配时才会生成,因此只有在命令区才可以使用。2.3 用VPATH和vpath查找文件make只会在当前目录查找源文件VPATH = src告诉make,当前目录找不到文件时,到src里去找VPATH变量包含了一个路径列表,make需要一个文件时就从这个列表里找。这个只对目标和依赖有效,命令区里的文件名是不用的。VPATH有缺点:路径多的时候,效率会低。不同路径下有重名时,只会取第一个,vpath更好一些。vpath pattern directory-list前面的VPATH可以改写为:vpath %.c srcvpath %.h includevpath是否也存在重名的问题呢?2.4 模式规则make的内置规则都是模式规则,除了文件的根以%替代外,与普通规则无异(根是指后缀之前的部分)。可通过make --print-data-base查看make的内置规则。可以修改内置规则的命令区。2.4.1 模式模式规则里,%基本上与unix shell的* 相同,代表任意数量的任意字符。%可以放在任意位置,但只能出现一次。除%外的字符则用于匹配文件名。模式可以包含前缀、后缀和前后缀。当make查找模式规则时,首先查找可以匹配的target。如果找到,在前缀和后缀之间的部分将作为名字的根。然后make查找对应的依赖,将根替换到依赖模式里。根至少包含一个字符。模式可以只包含%,一般这种用法是生成一个可执行程序。2.4.2 静态模式规则静态模式规则是指只对特定目标生效的规则,如$(OBJECTS): %.o: %.c$(CC) -c $(CFLAGS) $< -o $@与普通的模式规则不同的只有前面的$(OBJECTS):限定,它指定该规则只对$(OBJECTS)变量列出的目标生效。2.4.3 后缀规则后缀规则是早期的(已经过时的)定义隐含规则的方式。其它版本的makefile不支持GNU make的模式规则,仍能在其它makefile里见到。后缀规则包含一个或多个串接的后缀,用作目标:.c.o:$(COMPILE.c) $(OUTPUT_OPTION) $<这里的依赖在前,目标在后,它等价于:%.o: %.c$(COMPILE.c) $(OUTPUT_OPTION) $<将目标的后缀替换为依赖的后缀得到依赖,在有后缀名在已知清单里时,make才能识别。将后缀加入已知清单,通过特殊的目标.SUFFIXES实现,如:.SUFFIXES: .out .a .ln .o .c .cc .C .cpp .p .f .F .r .y .l要删除所有已知后缀,不指定依赖即可:.SUFFIXES:2.5 隐含规则数据库GNU make 3.80有约90条内置隐含规则。隐含规则是模式规则或者后缀规则。2.5.1 使用隐含规则当一个目标没有明确规则更新时,make会使用隐含规则。当隐含规则不是所需要的时候,可以删除它们,比如:%.o: %.l%.c : %.l没有命令区的模式从make的数据库中删除该规则。由链式规则生成的文件称为中间文件,make对其处理不同。由于中间文件不出现在目标中(出现在目标中的不称为中间文件),make不会简单地更新中间文件,make创建中间文件是更新目标的一种副效应,因此make在退出前会删除中间文件。2.5.2 规则结构下面是一条内置规则:%.o: %.c$(COMPILE.c) $(OUTPUT_OPTION) $<定制这条规则可完全由它使用的一组变量来控制。COMPILE.c实质是一组变量的集合:COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -cCC = gccOUTPUT_OPTION = -o $@改变CC变量可以更换C编译器,其它变量设置编译选项。2.5.3 用于源码控制的隐含规则make支持两种源码控制系统:RCS和SCCS。2.5.4 一个简单的Help命令没什么好说的2.6 特殊目标特殊目标是内置的phony目标,用来改变make的默认行为。比如.PHONY这个特殊目标,表示它的依赖不指向实际文件,并且总是过时的。除.PHONY外,还有其它的特殊目标。一共有十二个特殊目标。.INTERMEDIATE该特殊目标的依赖是中间文件。如果make在更新其它目标时创建了这个文件,在make退出时会自动删除它。如果make在更新时文件已存在,则不会删除。在定制链式规则时比较有用。.SECONDARY与INTERMEDIATE相同,但不会自动删除。.PRECIOUS当make在执行过种中被中断时,它可能会删除正在更新的目标文件,因为make不希望保留部分构建的文件。将文件标记为precious,make中断时不会删除它。注意,当规则的命令产生错误时,make不会自动删除文件,这个只在被信号中断时出现。.DELETE_ON_ERROR与precious相反,当命令执行出错时,make会删除这些文件。其它的特殊目标在涉及到的地方再谈论。2.7 自动依赖生成gcc有一个选项可以读取源文件并生成makefile依赖gcc -M source.c这个把戏就是用gcc -M遍历所有源文件,将结果写到一个依赖文件里,然后用include把它包含进来。gcc -M 生成的结果一般需要修改一下,形成.o .d: 头文件清单这样的形式。2.8 管理库归档库,是一种特殊类型的包含其它文件的文件。库用来将相关的object文件组合到一起。库可以有几种方式链到可执行文件中,最直接的方式是在命令行中指定,比如:cc count_words.o libcounter.a /lib/libfl.a -o count_wordscc识别libcounter.a和/lib/libfl.a为库文件。另一种方式是通过-l选项:cc count_words.o -lcounter -lfl -o count_words这个选项可以省略库文件名的前缀和后缀。-l使命令行更简单,而且不需要关心库的位置。cc从系统的标准库目录查找-l列出的库。而且,支持动态链接库的系统,链接器先查找共享库,找不到了再找静态库。编译器的查找路径可以通过-L选项添加。2.8.1 创建及更新库没什么特别的2.8.2 使用库作为依赖库作为依赖时,可以用文件名,也可以用-l的方式。对-l格式的库文件名解析模式存放在.LIBPATTERNS中,可以定制支持其它库文件名格式。如果目标是一个库文件,则不能使用-l格式的依赖。因此对makefile内生成的库,必须指定文件名。库的依赖关系很重要,库是有顺序的。如果出现循环依赖,则需要重复指定。然而,自动变量一般会丢掉重复项,此时,就需要用$+而不是$^了。2.8.2 双冒号规则过时的特性。3. 变量和宏变量名区分大小写。要取用一个变量的值,用$()的形式。单符号变量可以省略()。变量也可以用${}的形式,主要是早期的makefile用。按惯例,代表常量的变量用大写,仅在makefile内使用的变量用小写,单词间以下划线分隔。用户定义的函数用小写,以破折号分隔。3.1 变量有什么用用变量来表示外部程序是个好主意。3.2 变量类型make有两类变量:简单扩展变量和递归扩展变量。简单扩展变量由:=定义:MAKE_DEPEND := $(CC) -M称为简单变量是因为make在读到这一行后,右侧会被立即展开,右侧的所有变量引用都会展开,其展开结果为变量的值。假如上面的CC没有设置,那么上面的赋值成为<space>-M变量没有定义不是一个错误。第二种变量为递归扩展变量,使用=赋值:MAKE_DEPEND = $(CC) -M称其为递归扩展是因为右侧不会被求值。只有当该变量被使用时,扩展才会发生。因为展开的滞后,赋值可以是乱序的,比如先定义MAKE_DEPEND,再定义CC。递归变量每次被使用时,右侧都会重新求值。比如右侧包含date命令时,重新计算会保证每次使用都代表着执行时的值。3.2.1 其它类型的赋值除:=和=外,还有两种赋值类型。?=是条件赋值,它只有在变量没有值的时候才会赋值。OUTPUT_DIR ?= $(PROJECT_DIR)/out这个功能与环境变量一起使用非常方便。+=: 将文本添加到一个变量3.3 宏宏只是另一种方式的变量定义,它可以包含换行。define 变量名...endef命令行前面的@表示不回显命令,@用在宏前面,则对整个宏生效。3.4 变量何时扩展make运行有两个阶段。第一个阶段,make读入mkakefile及包含的makefile,此时,变量和规则加载到make的内部数据库,并生成依赖图。第二阶段,make分析依赖图来确定需要更新的目标,然后执行相应的命令来更新。当make遇到一个递归变量或者define标志符时,它们的内容都原封不动地存下来。当展开一个宏时,make会扫描展开的文本,并展开存在的变量引用和宏。如果宏是在action区间展开,则宏内的每一行前面会插入一个TAB。总结如下: * 对于变量赋值,当make在第一阶段读取到这一行时,赋值左侧总是立即扩展(是指左侧的变量)。 * =和?=的右侧在第二阶段使用时才会扩展。 * :=的右侧立即展开。 * 如果+=的左侧是一个简单变量,则右侧立即展开。否则推迟求值。 * 对于宏定义,宏名立即展开,但宏体推迟到使用时展开。 * 对于规则,目标和依赖总是立即展开,命令则推迟展开。一个通用的规则是,在使用变量和宏之前定义他们。3.5 目标/模式特定的变量make提供了目标特定变量,也就是与一个目标绑定的变量定义,只有在处理这个目标及它的依赖时才有效。例如gui.o: CPPFLAGS += -DUSE_NEW_MALLOC=1只有在编译gui.o这个目标时,CPPFLAGS才包含-DUSE_NEW_MALLOC=1,在这个目标完成后,CPPFLAGS恢复到原始值。目标特定变量的一般格式是:target...: variable = valuetarget...: variable := valuetarget...: variable += valuetarget...: variable ?= value变量在赋值前可以是不存在的。3.6 变量从何处来到目前为止,大多数变量是直接在makefile里定义的。但变量可以有下面一些来源:文件: 变量定义在makefile或者文件,然后由makefile用include引入。命令行: 变量可以在命令行定义。命令行的变量覆盖环境变量及makefile里的赋值。可以用override关键字来覆盖命令行的赋值。环境变量: 当make启动时,环境变量的所有变量会自动定义为make变量,这些变量优先级很低,如果makefile或命令行参数指定了环境变量,会被覆盖。可以用--environment-overrides参数来产生相反的结果。当递归执行make时,父make的一些变量会传给子make,默认情况下,只有环境变量会导出到子进程的环境,但可以用export将变量导出到环境。不带变量的export会导出所有变量。可以用unexport阻止环境变量导出到子进程。自动变量: 不用说了传统上,环境变量用于区分不同的机器。3.7 条件处理和包含处理基本的条件处理形式如下:    if-condition        text if the condition is true    endif或者    if-condition        text if the condition is true    else        text if the condition is false    endifif-condition可以是下面的形式:ifdef variable-nameifndef variable-nameifeq testifneq testifdef/ifndef测试中,variable-name不能用$()的形式。test的形式可以为:"a" "b"    或者  (a,b)单引双引均可。条件处理可以用在宏定义里,命令脚本区域里。ifeq/ifneq测试入参是否相等,白字符需要注意,当使用()形式的测试时,只有逗号后面的白字符是忽略的,但其它白字符都是有效的。    ifeq (a, a)     # these are equal    endif    ifeq ( b, b )     # these are not equal  ' b' != 'b '    endif有时,变量扩展后会出现不想要的白字符,影响条件判断,可以用strip函数处理。    ifeq "$(strip $(OPTIONS)) "-d"      COMPILATION_FLAGS += -DDEBUG    endif3.7.1 include关键字一个makefile可以包含其它文件,用include关键字,入参可以是任意数量的文件,通配符,也支持make变量。3.7.2 include及依赖当make遇到include时,展开通配符及变量引用,然后读入包含的文件,如果文件存在,则正常继续,如果不存在,make会报告,并继续读取剩下的makefile。在所有文件读完后,make查找规则数据库,看是否有规则来更新包含文件(前面缺失的文件)。如果找到更新规则,则执行与普通的目标更新一样的过程。如果include的文件是有规则来更新的,make会清空内部的数据库,并重读整个makefile(使用include的这个makefile)。如果在读取,更新,再读取之后,仍有include找不到文件,make报错并结束。make把makefile也当作一个可能的目标,在整个makefile读入后,make会查找是否有规则来更新当前执行的makefile,如果有的话则处理这个规则,并检查makefile是否被更新,如果更新了,则会清空内部状态,并重新读取这个makefile。make从哪里寻找include文件呢,绝对路径和相对路径。如果找不到,则查找--include-dir命令行参数指定的路径,然后查找编译内置的一些路径。如果想让make忽略找不到的include文件,可以在include前面加减号(sinclude是-include的一个兼容方法)。3.8 标准make变量MAKE_VERSION     make版本号CURDIR           当前工作目录MAKEFILE_LIST    make已经读取的的makefile的清单MAKECMDGOALS     当前make进程命令行指定的所有目标.VARIABLES       当前已在makefile里定义的变量名的列表(除了目标特定变量),该变量只读。4. 函数函数引用就像变量引用一样,但包括一个或多个由逗号分隔的参数。4.1 用户定义的函数函数其实就是带参数的宏。宏体内使用$1, $2等来引用参数。展开参数或宏的语法为:$(call macro-name[, param1 …])call是一个内置的make函数,将参数替换成$1, $2等。macro-name是宏或者变量的名字(宏只是允许换行的变量)如果宏内引用了$n,而入参不够,则为空,如果入参多于引用,则多余部分不在宏内扩展。4.2 内置函数内置函数分为几类:字符串处理,文件名处理,流程控制,用户定义函数及其它一些函数。函数的形式如下:$(function-name arg1[,argn])第一个入参的白字符会去掉,但后续参数前的白字符会保留(所以要注意空格问题)有许多函数也支持模式作为入参。4.2.1 字符串函数$(filter pattern …,text)filter返回text中与pattern匹配的内容。text是一个以空白符分隔的单词序列。$(filter-out pattern …,text)与filter作用相反,选择text中不匹配pattern的内容。$(findstring string,text)在text中查找string,如果找到,则返回string(是string,不是包含string的字符串),否则返回空。string不接受通配符。$(subst search-string,replace-string,text)简单的非通配的查找与替换。通常是用它来将某种后缀替换成另一种。比如:sources := count_words.c counter.c lexer.cobjects := $(subst .c, .o, $(sources))subst并不知道文件名及后缀,只是简单的字符串替换,因此如果文件名里有.c的话,也会被替换掉。$(patsubst search-pattern,replace-pattern,text)通配版本的查找与替换。pattern可以包含一个%。另一种可移植的替换形式是:$(variable:search=replace)这种形式,原变量会被修改吗?会$(words text)返回text中的单词个数$(word n,text)返回text的第n个单词,从1开始编号。如果n大于单词数,则结果为空。例,取得列表的最后一项:current := $(word $(words $(MAKEFILE_LIST)), $(MAKEFILE_LIST))$(firstword text)返回text的第一个单词,等价于$(word 1,text)$(wordlist start,end,text)返回text从start到end的单词(含边界)4.2.2 重要的辅助函数$(sort list)排序,并去掉重复项。词典序。$(shell command)将命令传给子shell进程执行,将命令的标准输出读回并作为函数的返回值。不返回标准错误输出,也不返回程序退出状态。4.2.3 文件名函数($wildcard pattern …)wildcard接受一组模式并展开,如果模式找不到匹配文件,则返回空字符串。模式里可以用shell的通配符:~, * , ?, […], [^...]$(dir list …)返回list每个单词的目录部分。$(notdir name …)返回文件路径的文件名部分。$(suffix name …)返回参数中每个单词的后缀。常见用法是与findstring结合做条件处理。$(basename name …)返回文件名的文件名部分,仅去掉后缀,路径部分不变。$(addsuffix suffix,name …)给name里的每个单词附加后缀。$(addprefix prefix,name …)给name里的每个单词附加前缀。$(join prefix-list,suffix-list)是dir和notdir的补运算。join接受两个list,并将prefix-list的元素与suffix-list的元素串接,一一对应,可以用来重组被dir与notdir分开的部分。4.2.4 流程控制$(if condition,then-part,else-part)根据condition的情况,展开第一个或第二个宏。condition只要包含字符(包括空格),就为真,此时执行then-part,否则执行else-part。$(error text)打印严重错误信息,make终止。$(foreach variable,list,body)遍历处理4.2.5 次常用函数$(strip text)移除text的前后白字符,并将内部所有白字符替换为一个空格。$(origin variable)返回一个描述变量的来源的字符串。有下面一些来源:undefined  变量没有定义过default  来自make的内置数据库,如果内置变量被覆盖,则返回最近一次的定义environment  来自环境,且--environment-overrides未打开environment override 来自环境,且--environment-overrides已打开file  来自makefilecommand line  来自命令行override  来自override关键字automatic  自动变量$(warning text)类似error,但make不会退出。可以用在任意地方,不需要放在规则里,有效的调试方法。4.3 高级用户定义函数call一个函数时,其入参转化为$1, $2等,而函数名则可通过$0得到。可以利用它来打印函数调用情况。例子见书。5. 命令5.1 命令解析命令以TAB起始,下面的错误表示make在target上下文之外遇到了命令:makefile:20: *** commands commence before first target.  Stop.当make parser在合法的上下文中看到一个命令时,切换到命令解析模式,每次构造一行脚本,命令脚本里可以出现的有: * 以TAB开头的命令,开辟子shell执行。包括ifdef, 注释, include等,在命令解析模式下都当作命令。 * 忽略空行,不会传给子shell * 忽略以#开头的行(包括空格,但不是TAB) * 条件处理关键字,如ifdef, ifeq可以识别,并在脚本区执行。5.1.1 长命令接续用 \ 接续长命令。shell上需要加分号的要加分号分隔。5.1.2 命令修辞@命令不回显QUIET = @hairy_script:$(QUIET) complex script这种方式比较适合调试。- (减号)忽略出错命令+执行命令(不管--just-print是否指定)用在递归makefile中。5.1.3 错误和中断make执行的每条命令都返回一个状态码,0表示命令成功,非0表示某种错误。一般程序返回非0值时,make停止,如果想让make忽略继续,可以用--keep-going (-k) 选项。减号也可以达到这个目的,但不建议使用,因为它会使自动错误处理更复杂。5.1.3.1 删除和保留目标文件因为历史原因,make在未能更新目标时,这个目标仍会保留,因为它的时间戳已经变了,是后续命令未能形成完整的数据。如果希望不完整的目标文件,可以将其作为.DELETE_ON_ERROR的依赖。如果.DELETE_ON_ERROR没有依赖,则所有目标文件的都会在出错时删除。另一个问题就是在Ctrl+C时,make会中断,如果当前的目标文件被修改,make会将其删除。有时需要保留,用.PRECIOUS。5.2 使用哪个SHELLmake需要传递命令给子shell执行时,使用/bin/sh。可以通过make变量SHELL改变。5.3 空命令空命令什么也不做:header.h: ;5.4 命令环境make执行的命令继续了make自己的环境,包括当前工作目录,文件描述符及make传递的环境变量。创建subshell时,make还向环境添加了几个变量:MAKEFLAGS包含了传给make的命令行配置。MFLAGS与MAKEFLAGS一样,历史原因保留。MAKELEVELmake的嵌套层数。5.5 命令求值命令直到执行的时候求值,但ifdef前导符则在使用点立即求值。当要执行一个命令脚本时,make先扫描脚本查找需要展开和求值的make部件。5.6 命令行限制不同操作系统下,对命令行长度的限制不同。解决办法就是分解。

原创粉丝点击