学习笔记之Makefile规则



1 GNU make介绍

make 在执行时,需要一个命名为 Makefile 的文件。这个文件告诉 make 以何种方式编译源代码和链接程序。典型地,可执行文件可由一些.o 文件按照一定的顺序生成或者更新。如果在你的工程中已经存在一个或者多个正确的 Makefile。当对工程中的若干源文件修改以后,需要根据修改来更新可执行文件或者库文件,正如前面提到的你只需要在 shell 下执行“ make”。 make 会自动根据修改情况完成源文件的对应.o 文件的更新、库文件的更新、最终的可执行程序的更新。make 通过比较对应文件(规则的目标和依赖,)的最后修改时间,来决定哪些文件需要更新、那些文件不需要更新。对需要更新的文件 make 就执行数据库中所记录的相应命令(在 make 读取 Makefile 以后会建立一个编译过程的描述数据库。此数据库中记录了所有各个文件之间的相互关系,以及它们的关系描述)来重建它,对于不需要重建的文件 make 什么也不做。

2 Makefile规则介绍

简单的Makefile描述规则组成:

TARGET... : PREREQUISITES...
        COMMAND
        ...
        ...

target:规则的目标。通常是最后需要生成的文件名或者为了实现这个目的而必需的中间过程文件名。可以是.o文件、也可以是最后的可执行程序的文件名等。另外,目标也可以是一个make执行的动作的名称,如目标“ clean”,我们称这样的目标是“伪目标”。
prerequisites:规则的依赖。生成规则目标所需要的文件名列表。通常一个目标依赖于一个或者多个文件。
command:规则的命令行。是规则所要执行的动作(任意的 shell 命令或者是可在shell 下执行的程序)。它限定了 make 执行这条规则时所需要的动作。

一个规则可以有多个命令行,每一条命令占一行。 注意: 每一个命令行必须以[Tab]字符开始,[Tab] 字符告诉 make 此行是一个命令行。 make 按照命令完成相应的动作。这也是书写 Makefile 中容易产生,而且比较隐蔽的错误。

main : main.o test1.o test2.o
        gcc -o main  main.o test1.o test2.o
main.o : main.c test1.h test2.h
        gcc -o main.o -c main.c
test1.o : test1.c test1.h
        gcc -o test1.o -c test1.c
test2.o : test2.c test2.h
        gcc -o test2.o -c test2.c

在上述Makefile中,我们的目标(target)就是可执行文件 "main"和那些.o文件(main.o,test1.o,test2.o);依赖(prerequisites)就是冒号后面的那些.c文件和.h文件。所有的.o文件既是依赖又是目标。命令包括 "gcc -o main.o -c main.c"......

3 Makefile语法

3.1 通配符

Maekfile 中表示文件名时可使用通配符。可使用的通配符有:" * ","?"和 "[...]"
Makefile 中通配符可以出现在以下两种场合:

  1. 可以用在规则的目标、依赖中, make 在读取 Makefile 时会自动对其进行匹配处理(通配符展开);
  2. 可出现在规则的命令中,通配符的通配处理是在 shell 在执行此命令时完成的。除这两种情况之外的其它上下文中,不能直接使用通配符。而是需要通过函数"wildcard"来实现。

3.2 伪目标

伪目标是这样一个目标:它不代表一个真正的文件名,在执行 make 时可以指定这个目标来执行其所在规则定义的命令,有时也可以将一个伪目标称为标签。使用伪目标有两点原因,避免在我们的 Makefile 中定义的只执行命令的目标(此目标的目的为了执行执行一些列命令,而不需要创建这个目标)和工作目录下的实际文件出现名字冲突。比如:我们书写这样一个规则,规则所定义的命令不是去创建目标文件,而是通过make命令行明确指定它来执行一些特定的命令.常见的有clean目标

clean:
        rm *.o main

规则中 "rm"不是创建文件 "clean"命令,而是删除当前目录下的所有.o文件和main文件。当工作目录下不存在 "clean"这个文件时,我们输入 "make clean"则 "rm *.o main"总会被执行。但是如果在当前工作目录下存在文件 "clean",情况就不一样了,同样输入 "make clean",由于这个规则没有任何依赖文件,所以目标被认为是最新的而不去执行规则所定义的命令,因此命令 "rm"将不会被执行。为了解决这个问题,我们需要将目标"clean"声明为伪目标,如下:

.PHONY : clean
clean:
        rm *.o main

这样目标 "clean"就被声明为一个伪目标了,无论在当前目录下是否存在 "clean"这个文件。输入 "make clean"之后,"rm"命令都会被执行.

3.3 变量

当我们定义了一个变量之后,就可以在 Makefile 的很多地方使用这个变量。变量的引用方式是:
“$(VARIABLE_NAME)”或者“ ${ VARIABLE_NAME }”来引用一个变量的定义。例如:“ $(foo) ”或者“ ${foo}”就是取变量“ foo”的值。
在 GNU make 中,变量的定义有两种方式(或者称为风格).我们把使用这两种方式定义的变量可以看作变量的两种不同风格。变量的这两种不同的风格的区别在于:1.定义方式;2. 展开时机。下边我们分别对这两种不同的风格进行详细地讨论。

3.3.1 递归展开式变量

递归展开式变量的定义是通过"="或者使用指示符 "define"定义的。这种变量的引用,在引用的地方是严格的文本替换过程,此变量的字符串原模原样的出现在引用它的地方.变量在定义时,变量值中对其它变量的引用不会被替换展开;而是变量在引用它的地方替换展开的同时,它所引用的其它变量才会被一同替换展开。

foo = $(bar)
bar = $(ugh)
ugh = hello
all:
        @echo $(foo)

执行 "make"将会打印出 "hello".整个变量的替换过程是在执行 "@echo $(foo)"时完成的.
这种递归展开式变量优点是:该变量在定义时,可以引用其它的之前没有定义的变量(可能在后续部分定义,或者是通过make的命令行选项传递的变量)。缺点是使用该变量,可能会导致make陷入到无限的变量展开过程中,最终使make执行失败。

3.3.2 直接展开式变量

直接展开式变量的定义是通过":="定义的。在使用 ":="定义变量时,变量值中对其它变量或者函数得引用在定义变量时被展开(对变量进行替换).所以变量被定义后就是一个实际需要的字符串。和递归展开式变量不同,此变量在定义时就完成了对所引用变量和函数的展开,因此不能实现对其后定义变量的引用。

3.3.3 "?="操作符

GNU make 中,还有一个被称为条件赋值的赋值操作符“ ?=”.被称为条件赋值是因为:只有此变量在之前没有赋值的情况下才会对这个变量进行赋值。例如:

FOO ?= bar

等价于:

ifeq ($(origin FOO), undefined)
FOO = bar
endif

3.3.4 追加变量值

通常,一个通用变量在定义之后的其它地方,可以对其值进行追加.在Makefile中使用 "+="(追加方式)来实现对一个变量的追加操作.

objects = main.o foo.o bar.o utils.o
objects += another.o

上边的两个操作之后变量"objects"的值就为:"main.o foo.o bar.o utils.o another.o".
注:如果被追加值的变量之前没有定义,那么,"+="会自动变成"=",此变量就被定义为一个递归展开式的变量。如果之前存在这个变量定义,那么“ +=”就继承之前定义时的变量风格.

4 为规则书写命令

4.1 命令回显

通常,make 在执行命令行之前会把要执行的命令行输出到标准输出设备。我们称之为“回显”,就好像我们在 shell 环境下输入命令执行时一样。例如:

clean:
        rm *.o main

执行make clean会显示"rm .o main" 该条打印信息,同时删除 "所有的.o文件"和main执行文件.
但是,如果规则的命令行以字符
"@"开始,则 make在执行这个命令时就不会回显这个将要被执行的命令。例如:

clean:
        @rm *.o main

执行make clean则是直接删除 "所有的.o文件"和main执行文件,不会有其它的打印信息.
如果想得到更多的输出信息,典型的用法是使用 "echo"命令,例如:

clean:
        echo rm *.o main

执行make clean会显示命令同时会显示删除了哪些文件,比如笔者的输出信息是 "echo rm .o main \n rm main.o test1.o test2.o main","\n"前面是命令,后面是删除的文件名.
如果不想显示命令,只显示删除的文件名,哪么命令配合
@使用即可,比如:

clean:
        @echo rm *.o main

执行make clean会显示删除了哪些文件,比如笔者的输出信息是 "rm main.o test1.o test2.o main",这样就只显示了删除的文件名.

4.2 命令的执行

规则中,当目标需要被重建时。此规则所定义的命令将会被执行,如果是多行命令,那么每一行命令将在一个独立的子 shell 进程中被执行(就是说,每一行命令的执行是在一个独立的 shell 进城中完成)。
因此,多行命令之间的执行是相互独立的,相互之间不存在依赖(多条命令行的执行为多个相互独立的进程)。
在 Makefile 中书写在同一行中的多个命令属于一个完整的 shell 命令行,书写在独立行的一条命令是一个独立的 shell 命令行。因此:在一个规则的命令中,命令行“ cd”改变目录不会对其后的命令的执行产生影响。就是说其后的命令执行的工作目录不会是之前使用“ cd”进入的那个目录。如果要实现这个目的, 就不能把“ cd”和其后的命令放在两行来书写。而应该把这两条命令写在一行上,用分号分隔。这样它们才是一个完整的 shell 命令行。如:

foo : bar/lose
        cd bar; gobble lose > ../foo

如果希望把一个完整的 shell 命令行书写在多行上,需要使用反斜杠( \)来对处于多行的命令进行连接,表示他们是一个完整的 shell 命令行。例如上例我们以也可以这样书写:

foo : bar/lose
        cd bar; \
        gobble lose > ../foo

5 Makefile函数

5.1 $(patsubst PATTERN,REPLACEMENT,TEXT)

函数名称:模式替换函数— patsubst。
函数功能:搜索“ TEXT”中以空格分开的单词,将否符合模式“ PTATTERN”替换为“ REPLACEMENT”。参数“PATTERN”中可以使用模式通配符“ %”来代表一个单词中的若干字符。如果参数 “REPLACEMENT”中也包含一个“ %”,那么
“ REPLACEMENT”中的“ %”将是“ TATTERN”中的那个“ %”所代表的字符串。在“ TATTERN”和“ REPLACEMENT”中,只有第一个“ %”被作为模式字符来处理,之后出现的不再作模式字符(作为一个字符)。
返回值:替换后的新字符串。
函数说明:参数“ TEXT”单词之间的多个空格在处理时被合并为一个空格,并忽略前导和结尾空格。

$(patsubst %.c,%.o,x.c.c bar.c)

把字串“ x.c.c bar.c”中以.c 结尾的单词替换成以.o 结尾的字符。函数的返回结果是“ x.c.o bar.o”

5.2 $(filter PATTERN…,TEXT)

函数名称:过滤函数-filter
函数功能:过滤掉字串“ TEXT”中所有不符合模式“ PATTERN”的单词,保留所有符合此模式的单词。可以使用多个模式。模式中一般需要包含模式字符“ %”。存在多个模式时,模式表达式之间使用空格分割。
返回值:空格分割的“ TEXT”字串中所有符合模式“ PATTERN”的字串。
函数说明:“ filter”函数可以用来去除一个变量中的某些字符串,下边的例子中就是用到了此函数。
在参数中如果需要将第一个出现的“ %”作为字符本身而不作为模式字符时,可使用反斜杠“ \”进行转义处理。

sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
        cc $(filter %.c %.s,$(sources)) -o foo

使用“ $(filter %.c %.s,$(sources))”的返回值给 cc 来编译生成目标“ foo”,函数返回值为“ foo.c bar.c baz.s”

5.3 $(filter-out PATTERN...,TEXT)

函数名称:反过滤函数— filter-out。
函数功能:和“ filter”函数实现的功能相反。过滤掉字串“ TEXT”中所有符合模式“ PATTERN”的单词,保留所有不符合此模式的单词。可以有多个模式。存在多个模式时,模式表达式之间使用空格分割。
返回值:空格分割的“ TEXT”字串中所有不符合模式“ PATTERN”的字串。
函数说明:“ filter-out”函数也可以用来去除一个变量中的某些字符串,(实现和“ filter”函数相反)。

objects=main1.o foo.o main2.o bar.o
mains=main1.o main2.o
$(filter-out $(mains),$(objects))

实现了去除变量“ objects”中“ mains”定义的字串(文件名)功能。它的返回值为“ foo.o bar.o”

5.4 $(wildcard PATTERN)

函数名称:获取匹配模式文件名函数— wildcard
函数功能:列出当前目录下所有符合模式“ PATTERN”格式的文件名。
返回值:空格分割的、存在当前目录下的所有符合模式“ PATTERN”的文件名。
函数说明:“ PATTERN”使用shell可识别的通配符,包括“ ?”(单字符)、“ *”(多字符)等。

$(wildcard *.c)

返回值为当前目录下所有.c 源文件列表。

5.5 $(foreach VAR,LIST,TEXT)

函数功能: 这个函数的工作过程是这样的:如果需要(存在变量或者函数的引用),首先展开变量“ VAR”和“ LIST”的引用;而表达式“ TEXT”中的变量引用不展开。执行时把“ LIST”中使用空格分割的单词依次取出赋值给变量“ VAR”,然后执行“ TEXT”表达式。重复直到“ LIST”的最后一个单词(为空时结束)。“ TEXT”中的变量或者函数引用在执行时才被展开,因此如果在“ TEXT”中存在对“ VAR”的引用,那么“VAR”的值在每一次展开式将会到的不同的值。
返回值:空格分割的多次表达式“ TEXT”的计算的结果。
函数说明:函数“ foreach”不同于其它函数。它是一个循环函数。类似于 Linux 的 shell 中的for 语句。

我们来看一个例子,定义变量“ files”,它的值为四个目录(变量“ dirs”代表的 a、b、 c、 d 四个目录)下的文件列表:

dirs := a b c d
files := $(foreach dir,$(dirs),$(wildcard $(dir)/*))

例子中,“ TEXT”的表达式为“ $(wildcard $(dir)/*)”。表达式第一次执行时将展开为“ $(wildcard a/)”;第二次执行时将展开为 “ $(wildcard b/)”;第三次展开为 “ $(wildcard c/)”….以此类推。所以此函数所实现的功能就和一下语句等价:

files := $(wildcard a/* b/* c/* d/*)

当函数的“ TEXT”表达式过于复杂时,我们可以通过定义一个中间变量,此变量代表表达式的一部分。并在函数的“ TEXT”中引用这个变量。上边的例子也可以这样来实现:

find_files = $(wildcard $(dir)/*)
dirs := a b c d
files := $(foreach dir,$(dirs),$(find_files))

在这里我们定义了一个变量(也可以称之为表达式),需要注意,在这里定义的是“递归展开”时的变量“ find_files”。保证了定义时变量值中的引用不展开,而是在表达式被函数处理时才展开(如果这里使用直接展开式的定义将是无效的表达式)。

6 隐含规则

6.1 模式规则

模式规则类似于普通规则。只是在模式规则中,目标名中需要包含有模式字符“ %”(一个),包含有模式字符“ %”的目标被用来匹配一个文件名,“ %”可以匹配任何非空字符串。规则的依赖文件中同样可以使用“ %”,依赖文件中模式字符“ %”的取值情况由目标中的“ %”来决定。例如:对于模式规则“ %.o : %.c”,它表示的含义是:所有的.o文件依赖于对应的.c文件。
要注意的是:模式字符“ %”的匹配和替换发生在规则中所有变量和函数引用展开之后,变量和函数的展开一般发生在make读取Makefile时,而模式规则中的“ %”的匹配和替换则发生在make执行时。
文件名中的模式字符“ %”可以匹配任何非空字符串,除模式字符以外的部分要求一致。例如:“ %.c”匹配所有以“ .c”结尾的文件(匹配的文件名长度最少为3个字母),“ s%.c”匹配所有第一个字母为“ s”,而且必须以“ .c”结尾的文件,文件名长度最小为5个字符(模式字符“ %”至少匹配一个字符)。同样一个模式规则可以存在多个目标。多目标的模式规则和普通多目标规则有些不同,普通多目标规则的处理是将每一个目标作为一个独立的规则来处理,所以多个目标就对应多个独立的规则(这些规则各自有自己的命令行,各个规则的命令行可能相同)。但对于多目标模式规则来说,所有规则的目标共同拥有依赖文件和规则的命令行,当文件符合多个目标模式中的任何一个时,规则定义的命令就有可能将会执行;因为多个目标共同拥有规则的命令行,因此一次命令执行之后,规则不会再去检查是否需要重建符合其它模式的目标。

#sample Makefile
Objects = foo.o bar.o
CFLAGS := -Wall
%x : CFLAGS += -g
%.o : CFLAGS += -O2
%.o %.x : %.c
        $(CC) $(CFLAGS) $< -o $@

当在命令行中执行“ make foo.o foo.x”时,会看到只有一个文件“ foo.o”被创建了,同时make会提示“ foo.x”文件是最新的(其实“ foo.x”并没有被创建)。

6.2 自动化变量

6.2.1 $@

表示规则的目标文件名。如果目标是一个文档文件( Linux中,一般称.a文件为文档文件,也称为静态库文件),那么它代表这个文档的文件名。在多目标模式规则中,它代表的是哪个触发规则被执行的目标文件名。

6.2.2 $%

当规则的目标文件是一个静态库文件时,代表静态库的一个成员名。例如,规则的目标是“ foo.a(bar.o)”,那么,“ $%” 的值就为“ bar.o”,“ $@”的值为“ foo.a”。如果目标不是静态库文件,其值为空。

6.2.3 $<

规则的第一个依赖文件名。如果是一个目标文件使用隐含规则来重建,则它代表由隐含规则加入的第一个依赖文件。

6.2.4 $?

所有比目标文件更新的依赖文件列表,空格分割。如果目标是静态库文件名,代表的是库成员( .o文件)。

6.2.5 $^

规则的所有依赖文件列表,使用空格分隔。如果目标是静态库文件,它所代表的只能是所有库成员( .o文件)名。一个文件可重复的出现在目标的依赖中,变量“ $^”只记录它的一次引用情况。就是说变量“ $^”会去掉重复的依赖文件。

6.2.6 $+

类似“ $^”,但是它保留了依赖文件中重复出现的文件。主要用在程序链接时库的交叉引用场合。

6.2.7 $*

在模式规则和静态模式规则中,代表“茎”。“茎”是目标模式中“ %”所代表的部分(当文件名中存在目录时,“茎”也包含目录(斜杠之前)部分,可参考 10.5.4模式的匹配 一小节)。
例如:文件“ dir/a.foo.b”,当目标的模式为“ a.%.b”时,“ $*”的值为“ dir/a.foo”。“茎”对于构造相关文件名非常有用。

注:关于Makefile更详细的信息请参考"GNU_make"文档
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容

  • 来自陈浩的一片老文,但绝对营养。 示例工程:3 个头文件*.h,和 8 个 C 文件*.c。 初 编译过程,源文件...
    周筱鲁阅读 4,696评论 0 17
  • 1.前言 在Makefile中,规则描述了用什么命令生成一个文件,该文件被称为规则的目标,生成"目标"的方式就是规...
    tianyl阅读 3,595评论 0 1
  • makefile关系到整个工程的编译规则,一个工程中的源文件不计其数,按其类型、功能、模块分别放在若干的目录当中,...
    Joe_HUST阅读 1,880评论 0 3
  • 1.Makefile规范 target 这 一 个 或 多 个 的 目 标 文 件 依 赖 于prerequisi...
    G风阅读 1,893评论 0 3
  • @(linux 编程)[开发技能, 工具使用] What is GNU Make Make 是控制工程中通过源码生...
    orientlu阅读 11,342评论 0 26