Makefile常用语法实例

Makefile常用语法实例,Makefile语法

一、Makefile基本格式:

make所看到的第一项规则会被当做默认规则使用。

一个规则可分成三个部分:

目标:依赖1 依赖2。。。

命令

注意:命令前必须使用有一个制表符(<TAB>)。

当要求make执行一条规则时,它首先找到目标和前提条件指出的文件。如果没有任何前提条件都有一条关联规则,那么make将试图先更新性前提条件。接着,更性目标文件。

如果任何前提条件都比目标要新,目标将通过执行命令得以更新。

每条命令都传给shell并在它的子shell中执行。如果任何一条命令产生错误,目标的创建将被终止,随后make退出。

如果一个文件最近被修改,那就认为它比其他的文件新。

GNU make 规则:

target — 目标文件, 可以是Object File 也可以是可执行文件,还可也是标签Label(标签内容在“伪目标”章节);
prerequisites—生成target所需的文件或目标;

command—make需要执行的命令,可以是任何shell命令。

二、一个简单的例子

创建一个名为count_word.c的文件,代码如下

另外创建一个lexer.l文件,其中所有的空白均为tab键

最后创建makefile文件,内容为:

以上内容保存在Makefile或者是makefile都可以,直接输入make命令就可以生成可执行文件count_words了;

如果要删除执行文件和中间的目标文件,那么就执行一下make clean。

注意1: 当依赖关系定好后,下面一行就是如何生成目标文件的操作系统命令了,一定要以一个Tab键开头。 另外,make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期比targets文件新,或者targets不存在,那么make就会执行这下面一行的系统命令。
注意2: clean不是一个文件,它是一个动作名,冒号后面什么都没有,make就不会自动去找它的依赖性,也不会执行它后面的系统命令。因此,要执行clean就需要显式的指出make clean。
注意3: 如果报错,可能需要先安装flex:

注意4: 运行count_works后,它会回显你的输出并统计’fee’,'fie’,'foe’,'fum’的次数。结束统计需要按Ctrl+d,然后会输出四个单词出现的次数。

三、make 如何工作

默认方式

直接输入make,则

  1. make会在当前的目录下找到名为“Makefile”或者“makefile”的文件。
  2. 如果找到,它会把文件中第一个target作为最终的目标文件(如上面例子中的count_words)。
    1. 首先,make会检查目标count_words的prerequisite文件count_words.o, lexer.o 和 -lfl。
    2. count_words.o通过编译count_words.c生成
    3. lexer.o通过编译lexer.c 生成,但是lexer.c 并不存在,因此会继续寻找lexer.c的生成方式,并找到了通过flex程序将lexer.l生成为lexer.c。
    4. 最后,make会检查-lfl,-l是gcc的一个命令选项,表示将系统库链接到程序。而”fl”对应的是libfl.a的库。(GNU make 可以识别这样的命令,当一个prerequisite是以这种-l<name>的形式表示出来的时候,make会自己搜索lib<name>.so的库文件,如果没找到则继续搜索lib<name>.a的库文件)。这里make找到的是/usr/lib/libfl.a文件,并将它与程序进行连接。
  3. 如果count_words文件不存在,或者count_words所依赖的后面的.o文件的修改时间比count_words本身更加新,那么,它会执行后面定义的命令来生成这个count_words文件。如果count_words所依赖的.o文件也不存在,那么make会继续按照前面的方式生成.o文件。
  4. 找到相应的.c和.h,用来生成.o,然后再用.o完成make的最终任务。

关于依赖关系

make会一层一层的去找文件的依赖关系,最终编译出第一个目标文件。

关于重新编译

只要任何prerequisite 比 target新,那么这个目标文件就会被下面的命令重新生成。每一个命令都会被传递到shell中,并在自己的子shell里面执行。

关于错误

如果在寻找过程中出现错误,如文件找不到,则make会直接退出并报错。对于所定义的命令错误或者编译不成功,make是不会理会的,它只负责文件的依赖性。

四、变量使用

上面的例子可以看到,文件或者目标的名字几乎都毫无例外的出现了至少两次,甚至如果算上clean的内容,有些文件名出现了三次。然而,在一个大型的工程中这种情况会更加复杂,任何不经意的错误都会导致编译失败。为了让makefile更容易维护,在makefile中我们可以使用变量,或者更确切的说是一个字符串,类似c语言中的宏。例如:

五、自动推导依赖关系

GNU make可以根据.o文件的文件名自动推导出同名的.c文件并加入依赖关系,不需要我们手动注明。并且gcc -c也会被自动推导出来,于是我们的makefile就变成了

这种方法也叫“隐式规则”。

六、关于Clean

一个好习惯是每个makefile都要写clean规则,这样不仅可以方便重编译,也有利于保持文件路径的清洁。一般的风格是:

  1. clean:
  2. rm lexer.c $(object) count_words

但是,更为稳妥的做法是:

  1. .PHONY: clean
  2. clean:
  3. -rm lexer.c $(object) count_words

.PHONY表示clean是一个“伪目标”,而rm命令前面的减号则表示,不管出现什么问题都要继续做后面的事情。

注意:clean规则不要放在makefile的开头,不然就会变成make的默认目标了。

—————————

一、显式规则(Explicit Rules)

通常在写makefile时使用的都是显式规则,这需要指明target和prerequisite文件。一条规则可以包含多个target,这意味着其中每个target的prerequisite都是相同的。当其中的一个target被修改后,整个规则中的其他target文件都会被重新编译或执行。

通配符(Wildcards)

make支持的通配符与Bourne shell基本相同,包括~, *, ?, [...], [^...]。
*.* 表示了所有文件;
? 表示任意单个字符;
[...] 表示一个字符类;
[^...] 表示相反的字符类。
~ 表示当前用户的/home路径,~加用户名可以表示该用户的/home路径。
注意: make会在读取makefile的时候就自动补充好通配符替代的内容,而shell则是在执行命令的时候才会进行通配符替代,在某些复杂情况,这两种方式会有极大的区别。

伪目标(Phony Targets)

多数情况下makefile中的target目标文件是像前面提到的那样带有指定的prerequisite文件,但也有一些target仅仅是作为一个标签,代表了一条命令,这种不代表任何文件的目标就被称为伪目标。常见的伪目标例如在makefile开头部分的第一个目标 all, 以及前面例子中见到的 clean:

  1. clean:
  2. rm -f *.o lexer.c

但是,make本身是无法区别出目标文件和伪目标的,如果碰巧在编译路径下有一个与伪目标同名的文件存在,那么make会在依赖关系图中把这个文件与伪目标名相关联。而再运行make clean 命令则会因为clean文件存在且如果没有被更新过,则makefile中的clean对应的命令将不会被执行。

为了避免这种情况,GNU make提供了一种特殊目标: “.PHONY”,用来表示目标文件不是真正的文件,即伪目标。clean命令可以被写作:

  1. .PHONY: clean
  2. clean:
  3. rm -f *.o lexer.c

这样,即使再有名为clean的文件存在,make也会执行clean后面的命令。

通常不会将一个伪目标的prerequisite设置为真是存在的文件,因为.PHONY会让他后面的文件在每次make时都进行重新编译。伪目标可以被认为是内嵌在makefile中的shell脚本。
优势: 通过使用伪目标在编译过程中进行屏幕输出,可以使make的可读性增加。例如:

这里将printf命令作为伪目标,可以使make在更新任何prerequisite之前就将指定的编译信息输出。

同时,伪目标也可也作为makefile的默认目标,放在文件的最前端,由于伪目标的特性,他指出的所有prerequisite都会被重新编译。这样可以用来同时生成多个目标。另外,与普通目标文件一样,伪目标也可也使用依赖关系,例如:

这样就可以对不同类型的文件进行单独删除。

空目标(Empty Targets)

空目标是伪目标的一种变形形式,通常情况下通过创建一个空文件来实现。例如:

这样,空文件size就被make当作时间戳,只有当count_words.o被更新时,size里面的命令才会再次被执行。另外,所有加载了size作为prerequisite的目标,都不会因为size被编译而强制编译,他们的其他prerequisite目标被更新。

二、变量

变量最简单的形式就是:

变量可以包含几乎所有的字符包括标点符号。一般情况下,变量名需要被$( )所包裹,但是当变量名只有一个字符时,括号可以省略。makefile可以定义很多变量,但同时make本身也定义了一些自动变量。
自动变量
自动变量是make自动根据规则生成的,不需要用户显式的指出相应的文件或目标名称。以下就是七个最核心的自动变量:
$@ 目标文件的文件名;
$% 仅当目标文件为归档成员文件(.lib 或者 .a)时,显示文件名,否则为空;
$< 依赖(prerequisite)列表里面的第一个文件名;
$? 所有在prerequisite列表里面比当前目标新的文件名,用空格隔开;
$^ 所有在prerequisite列表中的文件,用空格隔开; 如果有重复的文件名(包含扩展名),会自动去除重复;
$+ 与$^相似,也是prerequisite列表中的文件名用空格隔开,不同的是这里包含了所有重复的文件名;
$* 显示目标文件的主干文件名,不包含后缀部分。
此外,上面的每个变量都带有两个不同的变种,用于适应不同种类的make。分别是在后面附加一个“D”或者“F”。例如,$(^D)就是代表所有依赖文件的路径,$(<F)表示依赖文件第一个的文件部分的值。使用上述内容前面的makefile可以重写为:

 

三、 查找文件(VPATH)

 

上一篇所使用的例子中,makefile和源文件都是在同一个简单目录下,但真正的程序往往会复杂很多。让我们重新修改整个程序,添加一个叫做counter的函数,同时添加counter.c:

  1. #include <lexer.h>
  2. #include <counter.h>
  3. void counter( int counts[4]) {
  4. while ( yylex() )
  5. ;
  6. counts[0] = fee_count;
  7. counts[1] = fie_count;
  8. counts[2] = foe_count;
  9. counts[3] = fum_count;
  10. }

为了使这个库函数具有复用性,再添加一个counter.h作为头文件声明:

 

  1. #ifndef COUNTER_H_
  2. #define COUNTER_H_
  3. extern void counter( int counts[4]);
  4. #endif

同样的,也可以为lexer.l创建一个lexer.h的头文件:

 

  1. #ifndef LEXER_H_
  2. #define LEXER_H_
  3. extern int fee_count, fie_count, foe_count, fum_count;
  4. extern int yylex( void );
  5. #endif

如果将这些文件都放在根目录下,显然比较混乱。通常情况下,头文件会放到include/下,源文件被放到src/。最后,将makefile放在根目录下,整个文件系统如下所示

aHR0cHM6Ly9pbWctbXkuY3Nkbi5uZXQvdXBsb2Fkcy8yMDEzMDEvMjQvMTM1ODk3NjU0NV85MzA5LnBuZ

为了让make能够找到相应的位置,需要在makefile开头添加VPATH参数,显式的指出源文件和头文件的路径:

 

  1. VPATH = src include

此外,不仅make需要知道路径,gcc同样需要,通过添加编译选项 -I 的方式,显式的告诉gcc头文件的位置:

 

  1. CPPFLAGS = -I include

最终,makefile为:

 

  1. VPATH=src include
  2. CC = gcc
  3. CPPFLAGS = -I include
  4. count_words: count_words.o counter.o lexer.o -lfl
  5. $(CC) $^ -o $@
  6. count_words.o: count_words.c counter.h
  7. $(CC) $(CPPFLAGS) -c $<
  8. counter.o: counter.c counter.h lexer.h
  9. $(CC) $(CPPFLAGS) -c $<
  10. lexer.o: lexer.c include/lexer.h
  11. $(CC) $(CPPFLAGS) -c $<
  12. lexer.c: lexer.l
  13. flex -t $< > $@
  14. .PHONY: clean
  15. clean:
  16. rm *.o lexer.c count_words

运行make的结果为:

注意1: VPATH变量可以包含一个路径列表,当make需要一个文件时会在其中搜索。这个列表既可以作为目标文件也可作为关联文件的路径,但不能作为下面命令行程序中文件的路径。这正是为什么在命令行程序中使用自动化变量的原因,避免因为路径修改而导致的命令运行错误。

注意2: 如果是因为make的相关路径配置错误,终端会输出例如:

但如果是因为gcc的头文件路径配置错误,在终端会提示,例如:

注意3: 在UNIX系统中,路径列表可以被空格或者冒号分隔开,在Windows中则是用空格或者分号。(既然两种系统都用空格,那最好就使用空格)

注意4: make会在每次需要文件的时候搜索VPATH列表中的路径,如果有两个不同路径下文件重名,则make只会使用顺序查找到的第一个。

更加准确的方式是使用 vpath 变量,它的语法是:

因此,上面makefile中的VPATH可以写做:

这样就告诉了make去src/中寻找.c和.l文件,去include中寻找.h文件。

四、 模式匹配规则

通常情况下,编译器会将带有它可以识别后缀名的文件编译成相应的目标文件。例如,C语言的编译器会将.c后缀名的文件编译成带有.o后缀名的目标文件。再比如,前面的用到过的flex使用.l后缀名文件作为输入,输出则是.c的文件。事实上,这样一些约定可以根据文件名模式,通过内建规则来进行处理。例如,用内建规则,之前的makefile可以简写做:

 

  1. VPATH=src include
  2. CC = gcc
  3. CPPFLAGS = -I include
  4. count_words: counter.o lexer.o -lfl
  5. count_words.o: counter.h
  6. counter.o: counter.h lexer.h
  7. lexer.o: lexer.h
  8. .PHONY: clean
  9. clean:
  10. rm *.o lexer.c count_words

所有的内建规则都是模式匹配规则的实例,这个makefile之所以可以使用,是因为三个内建规则。

规则一: 从.c到.o

 

  1. %.o: %.c
  2. $(COMPILE.c) $(OUTPUT_OPTION) $<

规则二: 从.l 到.c

 

  1. %.c: %.l
  2. @$(RM) $@
  3. $(LEX.l) $< > $@

规则三: 从.c到无后缀名

当生成目标没有后缀名的时候(通常是可执行文件)

 

 

  1. %: %.c
  2. $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@

依照上述的模式匹配规则,make的生成过程如下:

STEP 1: make根据makefile中的内容,将默认目标设置为count_words(如果命令行中特别指出,则为其它,如clean)。根据依赖关系,分别是count_words.o(虽然没有在makefile显式的指出,但make会根据隐式规则自动填充), counter.o, lexer.o 和 -lfl。

STEP 2:根据依赖关系列表中的顺序,make会先找到count_words.o,由于count_words.o的依赖关系没有后续更新,因此make只需要找到count_word.c并进行编译。在当前目录下,没有count_word.c的情况下,make会根据VPATH变量继续寻找,直到在src/中找到。接下来,counter.o的编译过程也是一样的。

STEP3: 编译lexer.o的过程比前面多了一步:因为工程中并不存在lexer.c,于是make发现了从lexer.l生成lexer.c的模式匹配规则。

STEP4: make检查-lfl库的具体位置,本人用的是Ubuntu12.04 64bit, 因此对应的路径为: /usr/lib/x86_64-Linux-gnu/libfl.so,这个路径跟操作系统和make的版本有关,其实它具体在哪都不影响make的编译(只要是make可以找到的地方)。

STEP5: make已经准备好了生成count_words所需的所有依赖文件,生成。

STEP6:注意到,make创建的lexer.c是一个中间文件,makefile中并没有要生成它,因此在编译完成后将它删除。

DONE!

事实上,每一个makefile都有一个专有的内置规则库,在相应目录下可以使用下面的命令查看这个库(注意内容偏多,可以用more来分开看,或者重定向输出到文件)

 

  1. make –print-data-base

 

模式匹配

模式匹配规则中使用的百分号“%”与UNIX shell里面的通配符 “*”非常类似,它也可以代表任何长度的字符,并能被放在模式匹配中的任何位置,但在一个模式匹配中只能出现一次。

下面这些例子都是合法的模式匹配:

当然,模式匹配中只包含一个百分号也是允许的,这种方式最常用的例子就是在UNIX下创建可执行文件。

静态模式规则

静态模式规则是只对一系列特定的目标生效的规则。

  1. $(OBJECT): %.o: %.c
  2. $(CC) -c $(CFLAGS) $< -o $@

与普通的模式匹配规则的唯一区别是:初始化中的“$(OBJECT):” ,这限定了文件列表。在$(OBJECT)中的每一个目标文件,会匹配到%.o,然后再通过%.c产生依赖关系。如果,目标的匹配不存在,则make会提示一个warning。

静态模式规则可以显式的指出匹配列表,而不用仅仅指出后缀等匹配模式。

后缀规则

后缀规则是一种定义隐式规则的传统方式,因为即便其他版本的make不会识别GNU make的一些模式规则语法,但后缀规则却依然会出现在其他的makefile中。因此,尽管GNU make是一个不错的选择,但我们还是应该掌握可以适应其他编译环境的makefile编写和理解方式。

后缀规则包含一到两个连接起来的后缀作为目标文件:

注意:这里跟前面有一点不一样,首先写的.c实际上是依赖关系,.o才是目标文件。用前面的方式重写这段:

  1. %.o: %.c
  2. $(CC) $(OUTPUT_OPTION) $<

只有当目标和依赖关系的两个文件后缀都在make的已知后缀列表中存在的时候,后缀规则才会生效。上面的后缀规则又叫双后缀规则,顾名思义它包含了两种后缀。另一种后缀规则是单后缀规则,它只包含了一个属于源文件的后缀,当然这个规则是用来创建可执行文件的.

 

定义后缀规则

后缀规则的定义就像一个特别的目标文件一样:

 

  1. .SUFFIXES: .out .a .ln .o .c .cc .C .cpp .p .f .F .r .y .l

当然,你也可以自己定义其他的后缀规则,如pdf,html,xml等。要删除所有这些定义的后缀也很简单

 

  1. .SUFFIXES:

也可以使用命令行参数: –no-builtin-rules或者-r。

 

五、 隐含规则数据库

GNU make 3.80拥有90多个内建隐含规则。隐含规则即是模式匹配规则又是后缀规则。这些规则支持的语言有很多: C++, Pascal, FORTRAN, ratfor, Modula, Texinfo, TEX (包括Tangle 和 Weave), Emacs Lisp, RCS, SCCS等。但如果你想要编译JAVA或者XML,你可以自己编写规则。(别担心,事实上它们非常简单)

你可以通过–print-data-base或者-p参数来查看make的内建规则数据库(小心,输出有n多行)。

使用隐含规则

当处理一个目标文件没有发现显式规则时,make就会调用隐含规则。其实,只要不在makefile中目标文件的命令行程序部分添加任何内容,就可以调用隐含规则。

这种方式通常很好,在极特殊的情况下会导致一些问题。例如,在混编过程中使用Lisp和C两种语言,同一个路径下分别有editor.l和editor.c两个文件,使用make隐含规则编译的时候,make有可能将editor.l认做flex的文件,并将它编译成editor.c(正如前面(上)部分的例子)。于是,真正的editor.c就会被覆盖掉。要想避免这个问题,就需要将flex编译相关的两个内建规则删掉:

  1. %.o: %.l
  2. %.c: %.l

这样的模式规则不带有任何的命令,就可以将他们从make的数据库删除。尽管在实际操作中,这种规则导致的错误非常罕见,但是知道有这样一种情况总是会在不经意的时候对你有所帮助。

make的另一个强大之处在于,对于每一个符合模式匹配的目标文件,make会为它寻找相应的依附条件。如果找到了符合依附条件模式的源文件,这条规则才会生效。但当找不到时,make会再次查找所有的规则,并假设符合依附关系的源文件是另外的一个需要被生成的目标文件。这样,make会递归式的找到一个规则链用以更新目标文件(就像前面的例子一样,make可以根据规则链从lexer.l生成到lexer.o)。

例如一个名为a.o的文件的源文件可能是.c,.cpp,.cc,.p,.f,.r,.s,.mod等等。

规则结构

为了方便用户自定义,内建规则库都有标准的结构。以从C程序生成目标文件的规则为例:

  1. %.o: %.c
  2. $(COMPILE.c) $(OUTPUT_OPTION) $<

用户自定义的部分完全取决于变量的使用,事实上这两个变量也是由其他多个变量和参数决定的:

  1. COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
  2. CC = gcc
  3. OUTPUT_OPTION = -o $@

需要注意的是,在makefile中设置参数时需要避免将这些变量赋值,如果在makefile中设置:

那么,当需要在生成过程中加入命令行参数

则-I选项和它的参数就会被取消掉。因为在命令行里面的变量将重写其他所有对变量的设置。因此,这样的设置将最终导致make找不到头文件的位置,而造成编译失败。

 

帮助命令

大型的makefile会包含大量的目标文件,并且非常不容易被记住,一个简单的解决方式就是为默认的目标文件设置帮助命令,然后手工方式维护这些命令又是相当复杂和繁琐的。因此,make的规则数据库提供了命令用于直接使用,下面的例子是使用了这些命令按顺序输出了所有目标列表(每行四个):

  1. .PHONY: help
  2. help:
  3. make –print-data-base –question | \
  4. awk ‘/^[^.%][-A-Za-z0-9_]*:/ \
  5. { print substr(1)-1) }’ | \
  6. sort | \
  7. pr –omit-pagination –width=80 –columns=4

执行make help后就会在屏幕上看到所有的目标文件。

简单解释一下这个命令: 首先,使用–print-data-base查找出规则数据库的内容;然后使用awk命令从内容中抓取到目标文件信息,去掉以百分号和点号开头的文件(模式匹配规则和后缀规则文件),并删掉这一行多余的内容;最后将列表排序并按四个一行输出到屏幕。

六、 特殊目标文件

特殊目标文件是一种改变make默认方式的内建伪目标。例如,.PHONY会声明一个文件不会依赖任何其他真实的文件,并且永远都需要更新。伪文件.PHONY是最常见的特殊目标文件,但是还有些其他特殊文件。特殊文件也遵循着target: prerequisite的语法规则,但目标文件并不是一个文件,他们更像是修改make内部算法的指令。

特殊文件共有十二个,分为三类:一类是为了改变make在更新目标时的动作;还有一类是作为全局标志的形式,编译或忽略他们的目标文件;最后一类是后缀名特殊目标,当指明了旧的后缀规则时使用。

最常用的目标修饰符有:

.INTERMEDIATE

这个特殊目标文件的依赖关系被视为中间文件,当make更新其他文件时创建了列表中的文,make会在结束时删除这些文件;但如果更新前这个文件已经存在,则make不会删除它。

.SECONDARY

依赖列表中的文件会被当作中间文件,但不会被自动删除。这个特殊目标最常见的地方是针对一些库文件,为了方便调试过程,开发期间使用的库文件尽管也是中间文件,但保留着它可以减少调试中的重复编译过程。

.PRECIOUS

当make在执行过程中被中断时,它会将所有这次更新过的目标文件删除。因此,make不会将半成品文件遗留在编译路径中。但是,当某些生成的文件相当大或者运算非常费时的结果。因此,如果将这类文件定义为PRECIOUS,则它们就不会在中断时被删除掉了。尽管.PRECIOUS不太常见,但是它经常会在需要的时候起到意想不到的效果。注意:make不会在发生错误时自动删除文件,只有当它被信号中断时才会。

.DELETE_ON_ERROR

这个正好和.PRECIOUS相反,它会使依赖关系列表中的文件在发生错误时被删除。

七、 自动生成依赖关系

在通常情况下,手动添加目标文件和头文件之间的依赖关系几乎是不可能完成的。以C语言中最常见的stdio.h的头文件为例,它包含了15个其他的头文件,因此一一添加这些头文件以及它们互相之间的依赖关系必须依赖程序来实现。好在gcc提供了这样的一种方式。首先创建一个stdio.c的文件,包含了stdio.h的头文件声明:

然后,运行gcc的编译命令:

屏幕会输出相关的头文件的路径:

这样就可以将这些需要的路径复制粘贴到makefile中了。但是,这种方法有点笨,对吧。

聪明的方法是:在makefile中添加一个include指示,但目前大多数版本的make都已经有include指示了,因此小技巧是设置一个depend目标

  1. depend: count_words.c lexer.c counter.c
  2. $(CC) -M $(CPPFLAGS) $^ > $@
  3. include depend

在运行make之前,先执行make depend命令。

如果我们把每一个源文件的依赖关系都写入它自己的依赖关系文件中,例如.d为后缀的同名文件,并将.d文件作为这个依赖规则的目标文件。当源文件改变时,make就会知道.d文件需要被更新了。以下代码可以实现这个规则:

 

  1. %.d: %.c
  2. $(CC) -M $(CPPFLAGS) $< > $@.; \
  3. sed ‘s,\.o[ :]*,\1.o $@ : ,g’ < $@. > $@; \
  4. rm -f $@.

在shell中$$表示当前进程的进程号,它会确立独一无二的文件名。先将依赖关系保存到这个特殊的文件中,然后使用 “sed” 命令将.d文件作为一个目标文件添加到规则中。sed命令包含了一个查找部分 \($*\)\.o[ :]* 和一个替代部分 \1.o $@,它们都被用逗号隔开。查找部分的文件名为$*,它被包含在括号的正则表达式之中,并要求后缀名为.o。后面的[ :]*表示零到多个空格或者冒号。替代部分前面的正则表达式替换为\1.o,并把当前目标文件添加到依赖文件。

于是,我们的makefile就变成了:

  1. VPATH = src include
  2. CPPFLAGS = -I include
  3. CC = gcc
  4. SOURCES = count_words.c \
  5. lexer.c \
  6. counter.c
  7. count_words: counter.o lexer.o -lfl
  8. count_words.o: counter.h
  9. counter.o: counter.h lexer.h
  10. lexer.o: lexer.h
  11. include $(subst .c,.d, $(SOURCES))
  12. %.d: %.c
  13. $(CC) -M $(CPPFLAGS) $< > $@.; \
  14. sed ‘s,\.o[ :]*,\1.o $@ : ,g’ < $@. > $@; \
  15. rm -f $@.
  16. # help – The default goal
  17. .PHONY: help
  18. help:
  19. make –print-data-base –question | \
  20. awk ‘/^[^.%][-A-Za-z0-9_]*:/ \
  21. { print substr(1)-1) }’ | \
  22. sort | \
  23. pr –omit-pagination –width=80 –columns=4
  24. .PHONY: clean
  25. clean:
  26. rm *.o lexer.c count_words *.d

注意1: include必须放到手动设置的依赖关系之后,以防被手动设置的依赖关系覆盖。

注意2: include里面使用了subst函数,这是make的一个函数,它将$(SOURCE)文件里的所有.c替换成了.d,这两个后缀之间用逗号隔开且不能有空格。

运行make –just-print 会得到如下结果

前三行并不是报错,而是make的warning,make在所有路径和include里都找不到这几个文件。这些warning可以通过在include前面添加-(减号)消除掉。后面,make开始执行gcc -M来自动生成各种头文件依赖关系。

八、 库管理

档案库(archive library)是一类特殊类型的文件,它用来归类相关的目标文件。make提供了对库文件的专门支持,包括创建,维护和引用。继续使用上面的例子,将counter.o和lexer.o作为库文件,创建的命令为:

参数rv表示我们想用列表里面的目标文件替换掉库文件里面的相同内容,如果库中不存在则添加进去,并要求ar显示整个过程。即使库文件不存在,这个参数也可以使用。参数后面的第一个文件名就是库文件名(有些其他版本的ar需要一个参数C,来显式的创建库文件)。

使用库文件的方式十分简单,通常就是在加在命令的编译列表里面,编译器和连接器会自动根据后缀名识别:

事实上,cc会自动识别出libcounter.a 和 /lib/libfl.a 是库文件,并且也会根据定义的库文件位置搜索它们,因此,也可以使用编译器的-l 参数,直接引用库文件:

这样可以省略掉前面表示库文件的部分和后缀名。-l参数可以使编译器搜索系统的库文件路径,并且对于不同的系统都适用。此外,对于支持共享库的系统(在UNIX中扩展名为.so的库文件),连接器会自动的先查找共享库,而不需要明确指出(GNU 的编译器有这种效果)。查找路径可以通过添加-L参数进行修改,修改后的路径会在系统库之前加载,并可以被所有-l参数使用。

事实上,上面一条命令是不能执行的,因为当前工作路径并不是cc的搜索路径,它找不到counter这个库文件。因此,需要做以下修改

创建和更新库

在makefile中,库的创建与一般文件没有什么区别,简单的方式例如:

这里使用了make对ar程序的内置定义和标准参数选项 rv。但是,每次编译库的时候都会将所有的依赖文件进行编译,为了节省时间,可以将$^改成$?,这样就只会更新比ar库新的目标文件。但是,部分更新库文件所要付出的时间成本通常是远远高于整个库文件的更新,尤其是当库文件的数量比较多的时候,完整更新库会显得更加划算。

在GNU make中,引用库文件里面的成员可以用以下方式:

将上面的内容综合起来,makefile变成了下面的样子:

 

  1. VPATH = src include
  2. CPPFLAGS = -I include
  3. CC = gcc
  4. count_words: libcounter.a -lfl
  5. libcounter.a: libcounter.a(lexer.o) libcounter.a(counter.o)
  6. libcounter.a(lexer.o): lexer.o
  7. $(AR) $(ARFLAGS) $@ $<
  8. libcounter.a(counter.o): counter.o
  9. $(AR) $(ARFLAGS) $@ $<
  10. count_words.o: counter.h
  11. counter.o: counter.h lexer.h
  12. lexer.o: lexer.h
  13. # help – The default goal
  14. .PHONY: help
  15. help:
  16. make –print-data-base –question | \
  17. awk ‘/^[^.%][-A-Za-z0-9_]*:/ \
  18. { print substr(1)-1) }’ | \
  19. sort | \
  20. pr –omit-pagination –width=80 –columns=4
  21. .PHONY: clean
  22. clean:
  23. rm *.o lexer.c count_words *.d

执行make的输出为:

注意到生成库文件时使用的“$@”表示的是libcounter.a 而不是libcounter.a(lexer.o)

当然,我们也可以将内建规则应用在这里,使makefile更加精简:

 

  1. VPATH = src include
  2. CPPFLAGS = -I include
  3. CC = gcc
  4. count_words: libcounter.a -lfl
  5. libcounter.a: libcounter.a(lexer.o) libcounter.a(counter.o)
  6. count_words.o: counter.h
  7. counter.o: counter.h lexer.h
  8. lexer.o: lexer.h
  9. # help – The default goal
  10. .PHONY: help
  11. help:
  12. make –print-data-base –question | \
  13. awk ‘/^[^.%][-A-Za-z0-9_]*:/ \
  14. { print substr(1)-1) }’ | \
  15. sort | \
  16. pr –omit-pagination –width=80 –columns=4
  17. .PHONY: clean
  18. clean:
  19. rm *.o lexer.c count_words *.d

 

库文件作为依赖关系

在依赖关系中使用库文件的方式有两种,一种是直接使用绝对路径,另一种则是在库文件名前面加上一个-l参数。后者的好处在于,它会优先搜索共享路径,并且可以根据用户自定义搜索相应的路径。用户自定义的模式匹配的内容存放在.LIBPATTERNS中。

然而,并不是所有情况下-l参数都是有效的,例如:

 

  1. count_words: -lcounter -lfl
  2. libcounter.a: counter.o lexer.o
  3. $(AR) $(ARFLAGS) $@ $^

如果是第一次编译,这里面的-lcounter是无法被make找到的,因为对于在makefile中生成的库文件,他们的名字在make过程中还不能被查找到。但如果当前目录已经存在了这个库文件,或者使用库文件的全名时,就不会出现这个问题了。
注意: 通常情况下,库文件在依赖关系中的顺序很重要,如果其中一个库会引用另外一个库的成员,被引用的一定要在引用它的库之后出现,因为连接器是不会回述链接好的库文件的。例如,库文件A调用了库文件B中的一个成员,于是在依赖列表中一定要保证-lA -lB的顺序,但如果B中的另一个成员又引用了A中的成员,则形成了循环引用。这时需要将依赖关系写作:-lA -lB -lA。这种多次调用库文件的方式在大型工程中经常出现,甚至会重复多次。这提醒了我们,在用$^替代依赖关系时是有问题的,因为它会去掉所有重复的内容,因此,会使用$+作为依赖关系变量使用,它保留了重复的内容。

 

双冒号规则

这是一种模糊的规则,它允许同一个目标文件被不同的依赖列表更新,更新的依据在于那一个依赖列表比目标文件的修改时间更晚(更加新)。

 

承接各种网站开发与修改、爬虫、数据采集分析、小程序等任务

Html+Css+JS+PHP+Nodejs+Python

专治网站各种不服

一起探讨,互相学习,共同进步!有事儿您说话。

This entry was posted in C/C++ and tagged , by 织梦先生. Bookmark the permalink.