商务合作:179001057@qq.com

GNU Make手册阅读笔记(3)-- 第四章

技术2022-05-11  0


某平台价值19860元的编程课程资料免费领取【点我领取】


第四章 Makefile的规则

1.makefile的终极目标

    除了 makefile 的“终极目标”所在的规则以外,其它规则的顺序在 makefile 文件中没有意义。“终极目标”就是当没有使用 make 命令行指定具体目标时, make 默认的更新的哪一个目标。它是 makefile 文件中第一个规则的目标。如果在 makefile 中第一个规则有多个目标的话,那么多个目标中的第一个将会被作为 make 的“终极目标”。有两种情况的例外: 1. 目标名以点号“ . ”开始的并且其后不存在斜线“ / ”(“ ./ ”被认为是当前目录;“ ../ ”被认为是上一级目录); 2. 模式规则的目标。当这两种目标所在的规则是 Makefile 的第一个规则时,它们并不会被作为“终极目标”。

2."规则"的书写形式

通常规则的语法格式如下:

TARGETS : PREREQUISITES

COMMAND

...

或者:

TARGETS : PREREQUISITES ; COMMAND

COMMAND

...

1.        规则的命令部分有两种书写方式:

       a. 命令可以和目标:依赖描述放在同一行。命令在依赖文件列表后并使用分号(;)和依赖文件列表分开。

       b. 命令在目标:依赖的描述的下一行,作为独立的命令行。当作为独立的命令行时此行必须以 [Tab] 字符开始。在 Makefile 中,在第一个规则之后出现的所有以 [Tab] 字符开始的行都会被当作命令来处理。

2.        Makefile 中符号“ $ ”有特殊的含义(表示变量或者函数的引用),在规则中需要使用符号“ $ ”的地方,需要书写两个连续的(“ $$ ”)。

      3.        前边已提到过,对于 Makefile 中一个较长的行,我们可以使用反斜线“ / ”将其书写到几个独立的物理行上。

    规则的中心思想是:目标文件的内容是由依赖文件文件决定,依赖文件的任何一处改动,将导致目前已经存在的目标文件的内容过期。

3.依赖的分类

    一类是在这些依赖文件被更新后,需要更新规则的目标;另一类是更新这些依赖的,可不需要更新规则的目标。我们把第二类称为:“ order-only ”依赖。书写规则时,“ order-only ”依赖使用管道符号“ | ”开始,作为目标的一个依赖文件。规则依赖列表中管道符号“ | ”左边的是常规依赖,管道符号右边的就是“ order-only ”依赖。这样的规则书写格式如下:

TARGETS : NORMAL-PREREQUISITES | ORDER-ONLY-PREREQUISITES

order-only ”依赖的使用举例:

    LIBS = libtest.a

foo : foo.c | $(LIBS)

       $(CC) $(CFLAGS) $< -o $@ $(LIBS)

make 在执行这个规则时,如果目标文件“ foo ”已经存在。当“ foo.c ”被修改以后,目标“ foo ”将会被重建,但是当“ libtest.a ”被修改以后。将不执行规则的命令来重建目标“ foo ”。

就是说,规则中依赖文件 $(LIBS) 只有在目标文件不存在的情况下,才会参与规则的执行。当目标文件存在时此依赖不会参与规则的执行过程。

4.通配符

Makefile 中统配符可以出现在以下两种场合:

1.        可以用在规则的目标、依赖中, make 在读取 Makefile 时会自动对其进行匹配处理(通配符展开);

2.        可出现在规则的命令中,通配符的通配处理是在 shell 在执行此命令时完成的。

除这两种情况之外的其它上下文中,不能直接使用通配符。而是需要通过函数“ wildcard ”来实现。

如果规则的一个文件名包含统配字符(“ * ”、“ . ”等字符),在使用这样的文件时需要对文件名中的统配字符使用反斜线( / )进行转义处理。例如“ foo/*bar ”,在 Makefile 中它表示了文件“ foo*bar ”。

print: *.c

lpr -p $?

touch print

两点说明: 1. 上述的规则中目标“ print ”时一个空目标文件。(当前目录下存在一个文件“ print ”,但我们不关心它的实际内容,此文件的作用只是记录最后一次执行此规则的时间。 2. 自动环变量“ $? ”在这里表示依赖文件列表中被改变过的所有文件。

objects = *.o

foo : $(objects)

cc -o foo $(CFLAGS) $(objects)

这里变量“ objects ”的值是一个字符串“ *.o ”。在重建“ foo ”的规则中对变量“ objects ”进行展开,目标“ foo ”的依赖就是“ *.o ”,即所有的 .o 文件的列表。如果在工作目录下已经存在必需的 .o 文件,那么这些 .o 文件将成为目标的依赖文件,目标“ foo ”将根据规则被重建。 但是如果将工作目录下所有的 .o 文件删除,重新执行 make 将会得到一个类似于“没有创建 *.o 文件的规则” 的错误提示。

一般我们可以使用“ $(wildcard *.c) ”来获取工作目录下的所有的 .c 文件列表。复杂一些用法;可以使用“ $(patsubst %.c,%.o,$(wildcard *.c)) ”,首先使用“ wildcard ”函数获取工作目录下的 .c 文件列表;之后将列表中所有文件名的后缀 .c 替换为 .o 。这样我们就可以得到在当前目录可生成的 .o 文件列表。因此在一个目录下可以使用如下内容的 Makefile 来将工作目录下的所有的 .c 文件进行编译并最后连接成为一个可执行文件:

#sample Makefile

objects := $(patsubst %.c,%.o,$(wildcard *.c))

foo : $(objects)

cc -o foo $(objects)

这里我们使用了 make 的隐含规则来编译 .c 的源文件。对变量的赋值也用到了一个特殊的符号( := )。

5.目录搜寻

1) 一般搜索(变量 VPATH

        GNU make 可以识别一个特殊变量“ VPATH ”。通过变量“ VPATH ”可以指定依赖文件的搜索路径,当规则的依赖文件在当前目录不存在时, make 会在此变量所指定的目录下去寻找这些依赖文件。

        VPATH = src:../headers

2) 选择性搜索(关键字 vpath

1 vpath PATTERN DIRECTORIES

为所有符合模式“ PATTERN ”的文件指定搜索目录“ DIRECTORIES ”。多个目录使用空格或者冒号(:)分开。类似上一小节的“ VPATH ”变量。

2 vpath PATTERN

清除之前为符合模式“ PATTERN ”的文件设置的搜索路径。

3 vpath

            清除所有已被设置的文件搜索路径

vpath %.c foo

vpath % blish

vpath %.c bar

            表示对所有的 .c 文件, make 依次查找目录:“ foo ”、“ blish ”、“ bar ”。

3) 目录搜索的机制

1.        首先,如果规则的目标文件在 Makefile 文件所在的目录(工作目录)下不存在,那么就执行目录搜寻。

2.        如果目录搜寻成功,在指定的目录下存在此规则的目标。那么搜索到的完整的路径名就被作为临时的目标文件被保存。

3.        对于规则中的所有依赖文件使用相同的方法处理。

4.        完成第三步的依赖处理后, make 程序就可以决定规则的目标是否需要重建,两种情况时后续处理如下:

a)       规则的目标不需要重建:那么通过目录搜索得到的所有完整的依赖文件路径名有效,同样,规则的目标文件的完整的路径名同样有效。就是说,当规则的目标不需要被重建时,规则中的所有的文件完整的路径名有效。已经存在的目标文件所在的目录不会被改变。

b)       规则的目标需要重建:那么通过目录搜索所得到的目标文件的完整的路径名无效,规则中的目标文件将会被在工作目录下重建。就是说,当规则的目标需要重建时, 规则的目标文件会在工作目录下被重建,而不是在目录搜寻时所得到的目录。这里,必须明确:此种情况只有目标文件的完整路径名失效,依赖文件的完整路径名是 不会失效的。否则将无法重建目标。

实际上, GNU make 也可以实现这种功能。如果需要 make 在执行时,将目标文件在已存在的目录存下进行重建,我们可以使用“ GPATH ”变量来指定这些目标所在的目录。

为了更清楚地描述此算法,我们使用一个例子来说明。存在一个目录“ prom ”,“ prom ”的子目录“ src ”下存在“ sum.c ”和“ memcp.c ”两个源文件。在“ prom ”目录下的 Makefile 部分内容如下:

LIBS = libtest.a

VPATH = src

 

libtest.a : sum.o memcp.o

       $(AR) $(ARFLAGS) $@ $^

首先,如果在两个目录(“ prom ”和“ src ”)都不存在目标“ libtest.a ”,执行 make 时将会在当前目录下创建目标文件“ libtest.a ”。另外;如果“ src ”目录下已经存在“ libtest.a ”,以下两种不同的执行结果:

1)       当它的两个依赖文件“ sum.c ”和“ memcp.c ”没有被更新的情况下我们执行 make ,首先 make 程序会搜索到目录“ src ”下的已经存在的目标“ libtest.a ”。由于目标“ libtest.a ”的依赖文件没有发生变化,所以不会重建目标。并且目标所在的目录不会发生变化。

2)       当我们修改了文件“ sum.c ”或者“ memcp.c ”以后执行 make 。“ libtest.a ”和“ sum.o ”或者“ memcp.o ”文件将会被在当前目录下创建(目标完整路径名被废弃),而不是在“ src ”目录下更新这些已经存在的文件。此时在两个目录下(“ prom ”和“ src ”)同时存在文件“ libtest.a ”。但只有“ prom/libtest.a ”是最新的库文件。

当在上边的 Makefile 文件中使用“ GPATH ”指定目录时,情况就不一样了。首先看看怎么使用“ GPATH ”,改变后的 Makefile 内容如下:

LIBS = libtest.a

GPATH = src

VPATH = src

LDFLAGS += -L ./. –ltest

…….

……

同样;当两个目录都不存在目标文件“ libtest.a ”时,目标将会在当前目录(“ prom ”目录)下创建。如果“ src ”目录下已经存在目标文件“ libtest.a ”。当其依赖文件任何一个被改变以后执行 make ,目标“ libtest.a ”将会被在“ src ”目录下被更新(目标完整路径名不会被废弃)。

4) 命令行和搜索目录

make 在执行时,通过目录搜索得到的目标的依赖文件可能会在其它目录(此时依赖文件为文件的完整路径名),但是已经存在的规则命令却不能发生变化。因此,书写命令时我们必须保证当依赖文件在其它目录下被发现时规则的命令能够正确执行。

解决这个问题的方式是在规则的命令行中使用“自动化变量” 所以对于一个规则我们可以进行如下的描述:

foo.o : foo.c

cc -c $(CFLAGS) $^ -o $@

    自动化变量“ $< ”代表规则中通过目录搜索得到的依赖文件列表的第一个依赖文件。

    自动化变量“ $^ ”代表所有通过目录搜索得到的依赖文件的完整路径名(目录 + 一般文件名)列表。

    “ $@ ”代表规则的目标。

5) 库文件和搜索目录

        1. make 在执行规则时会在当前目录下搜索一个名字为“ libNAME.so ”的文件;

        2. 如果当前工作目录下不存在这样一个文件,则 make 会继续搜索使用“ VPATH ”或者“ vpath ”指定的搜索目录。

        3. 还是不存在, make 将搜索系统库文件存在的默认目录,顺序是:“ /lib ”、“ /usr/lib ”和“ PREFIX/lib ”(在 Linux 系统中为“ /usr/local/lib ”,其他的系统可能不同)。

假设你的系统中存在“ /usr/lib/libcurses.a ”(不存在“ /usr/lib/libcurses.so ”)这个库文件。

foo : foo.c -lcurses

cc $^ -o $@

6.伪目标

    伪目标是这样一个目标:它不代表一个真正的文件名,在执行 make 时可以指定这个目标来执行其所在规则定义的命令,有时也可以将一个伪目标称为标签。

    使用伪目标有两点原因: 1. 避免在我们的 Makefile 中定义的只执行命令的目标和工作目录下的实际文件出现名字冲突。 2. 提高执行 make 时的效率。

    目标“ clean ”的完整书写格式应该如下:

.PHONY: clean

clean:

rm *.o temp

  伪目标的另外一种使用场合是在 make 的并行和递归执行过程中。

SUBDIRS = foo bar baz

subdirs:

for dir in $(SUBDIRS); do /

$(MAKE) -C $$dir; /

          done       1. 当子目录执行 make 出现错误时, make 不会退出。 2. 另外一个问题就是使用这种 shell 的循环方式时,没有用到 make 对目录的并行处理功能,由于规则的命令是一条完整的 shell 命令,不能被并行处理。

SUBDIRS = foo bar baz

.PHONY: subdirs $(SUBDIRS)

subdirs: $(SUBDIRS)

$(SUBDIRS):

$(MAKE) -C $@

foo: baz

上边的实现中有一个没有命令行的规则“ foo: baz ”,此规则用来限制子目录的 make 顺序。它的作用是限制同步目录“ foo ”和“ baz ”的 make 过程(在处理“ foo ”目录之前,需要等待“ baz ”目录处理完成)。

Makefile 中,一个伪目标可以有自己的依赖(可以是一个或者多个文件、一个或者多个伪目标)。

当一个伪目标作为另外一个伪目标依赖时, make 将其作为另外一个伪目标的子例程来处理。

7.强制目标

    如果一个规则没有命令或者依赖,并且它的目标不是一个存在的文件名。在执行此规则时,目标总会被认为是最新的。就是说:这个规则一旦被执行, make 就认为它的目标已经被更新过。这样的目标在作为一个规则的依赖时,因为依赖总被认为被更新过,因此作为依赖所在的规则中定义的命令总会被执行。

clean: FORCE

rm $(objects)

FORCE:

8.空目标文件

    空目标文件只是用来记录上一次执行此规则命令的时间。

print: foo.c bar.c

lpr -p $?

touch print

9.Makefile的特殊目标

.PHONY:

    目标“ .PHONY ”的所有的依赖被作为伪目标。

.DEFAULT

        Makefile 中,目标“ .DEFAULT ”所在规则定义的命令,被用在重建那些没有具体规则的目标(明确规则和隐含规则)。就是说一个文件作为某个规则的依赖,但却不是另外一个规则的目标时。 Make 程序无法找到重建此文件的规则,此种情况时就执行“ .DEFAULT ”所指定的命令。

.PRECIOUS  .SECONDARY

    目标“ .PRECIOUS ”的所有依赖文件在 make 过程中会被特殊处理:当命令在执行过程中被中断时, make 不会删除它们。而且如果目标的依赖文件是中间过程文件,同样这些文件不会被删除。这一点目标“ .PRECIOUS ”和目标“ .SECONDAY ”实现的功能相同。

.INTERMEDIATE

    目标“ .INTERMEDIATE ”的依赖文件在 make 时被作为中间过程文件对待。

.DELETE_ON_ERROR

    如果在 Makefile 中存在特殊目标“ .DELETE_ON_ERROR ”, make 在执行过程中,如果规则的命令执行错误,将删除已经被修改的目标文件。

.IGNORE

    如果给目标“ .IGNORE ”指定依赖文件,则忽略创建这个文件所执行命令的错误。

.LOW_RESOLUTION_TIME

目标“ .LOW_RESOLUTION_TIME ”的依赖文件被 make 认为是低分辨率时间戳文件。给目标“ .LOW_RESOLUTION_TIME ”指定命令是没有意义的。

通常文件的时间辍都是高分辨率的, make 在处理依赖关系时、对规则目标 - 依赖文件的高分辨率的时间戳进行比较,判断目标是否过期。但是在系统中并没有提供一个修改文件高分辨率时间辍的机制(方式),因此类似“ cp -p ”这样的命令在根据源文件创建目的文件时,所产生的目的文件的高分辨率时间辍的细粒度部分被丢弃(来源于源文件)。这样可能会造成目的文件的时间戳和源文件的相等甚至不及源文件新。处理此类命令创建的文件时,需要将命令创建的文件作为目标“ .LOW_RESOLUTION_TIME ”的依赖,声明这个文件是一个低分辨率时间辍的文件。例如:

.LOW_RESOLUTION_TIME: dst

dst: src

cp -p src dst

首先规则的命令“ cp –p src dst ”,所创建的文件“ dst ”在时间戳上稍稍比“ src ”晚(因为命令不能更新文件“ dst ”的细粒度时间)。因此 make 在判断文件依赖关系时会出现误判,将文件作为目标“ .LOW_RESOLUTION_TIME ”的依赖后,只要规则中目标和依赖文件的时间戳中的初始时间相等,就认为目标已经过期。这个特殊的目标主要作用是,弥补系统在没有提供修改文件高分辨率时间戳机制的情况下,某些命令在 make 中的一些缺陷。

.SILENT

    出现在目标“ .SILENT ”的依赖列表中的文件, make 在创建这些文件时,不打印出重建此文件所执行的命令。

.EXPORT_ALL_VARIABLES

    它的功能含义是将之后所有的变量传递给子 make 进程。

.NOTPARALLEL

        Makefile 中,如果出现目标“ .NOPARALLEL ”,则所有命令按照串行方式执行,即使存在 make 的命令行参数“ -j ”。但在递归调用的字 make 进程中,命令可以并行执行。

10.多规则目标

    对于一个多规则的目标,重建此目标的命令只能出现在一个规则中(可以是多条命令)。如果多个规则同时给出重建此目标的命令, make 将使用最后一个规则中所定义的命令。

objects = foo.o bar.o

foo.o : defs.h

bar.o : defs.h test.h

$(objects) : config.h

11.静态模式

静态模式规则的基本语法:

TARGETS ...: TARGET-PATTERN: PREREQ-PATTERNS ...

COMMANDS

...

TAGETS 列出了此规则的一系列目标文件。像普通规则的目标一样可以包含通配符。

TAGET-PATTERN ”和 PREREQ-PATTERNS 说明了如何为每一个目标文件生成依赖文件。从目标模式( TAGET-PATTERN )的目标名字中抽取一部分字符串(称为“茎”)。使用“茎”替代依赖模式( PREREQ-PATTERNS )中的相应部分来产生对应目标的依赖文件。

我们来看一个例子,它根据相应的 .c 文件来编译生成“ foo.o ”和“ bar.o ”文件:

objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c

$(CC) -c $(CFLAGS) $< -o $@

例子中,规则描述了所有的 .o 文件的依赖文件为对应的 .c 文件,对于目标“ foo.o ”,取其茎“ foo ”替代对应的依赖模式“ %.c ”中的模式字符“ % ”之后可得到目标的依赖文件“ foo.c ”。这就是目标“ foo.o ”的依赖关系“ foo.o: foo.c ”,规则的命令行描述了如何完成由“ foo.c ”编译生成目标“ foo.o ”。命令行中“ $< ”和“ $@ ”是自动化变量,“ $< ”表示规则中的第一个依赖文件,“ $@ ”表示规则中的目标文件。

    如果存在一个文件列表,其中一部分符合某一种模式而另外一部分符合另外一种模式,这种情况下我们可以使用“ filter ”函数来对这个文件列表进行分类,在分类之后对确定的某一类使用模式规则。例如:

files = foo.elc bar.o lose.o

$(filter %.o,$(files)): %.o: %.c

$(CC) -c $(CFLAGS) $< -o $@

$(filter %.elc,$(files)): %.elc: %.el

emacs -f batch-byte-compile $<

12.双冒号规则

         Makefile 中,一个目标可以出现在多个规则中。但是这些规则必须是同一类型的规则,要么都是普通规则,要么都是双冒号规则。而不允许一个目标同时出现在两种不同类型的规则中

1.        双冒号规则中,当依赖文件比目标更新时。规则将会被执行。对于一个没有依赖而只有命令行的双冒号规则,当引用此目标时,规则的命令将会被无条件执行。而普通规则,当规则的目标文件存在时,此规则的命令永远不会被执行(目标文件永远是最新的)。

2.        当同一个文件作为多个双冒号规则的目标时。这些不同的规则会被独立的处理,而不是像普通规则那样合并所有的依赖到一个目标文件。这就意味着对这些规则的处理就像多个不同的普通规则一样。就是说多个双冒号规则中的每一个的依赖文件被改变之后, make 只执行此规则定义的命令,而其它的以这个文件作为目标的双冒号规则将不会被执行。

13.自动产生依赖

    现代的 c 编译器提供了通过查找源文件中的“ #include ”来自动产生这种依赖关系的功能。 Gcc 通过“ -M ”选项来实现此功能,使用“ -M ”选项 gcc 将自动找寻源文件中包含的头文件,并生成文件的依赖关系。

 


最新回复(0)