当前位置:   article > 正文

一起学习Makefile

一起学习makefile

        在介绍makefile和make的具体概念前,我们先通过一个例子来说明makefile和make到底是为解决什么问题而存在的。

假设有一个如图所示的C工程:

那么我们需要按照如下步骤依次编译

  1. # 步骤1:编译主程序模块
  2. $ gcc -o <100个主程序模块的o文件> -c <100个主程序模块的c文件>
  3. # 步骤2:编译功能模块a
  4. $ gcc -o <1000个功能模块a的o文件> <1000个功能模块a的c文件>
  5. $ ar rcs liba.a <1000个功能模块a的o文件>
  6. # 步骤3:编译功能模块b
  7. $ gcc -o <1000个功能模块b的o文件> <1000个功能模块b的c文件>
  8. $ ar rcs libb.a <1000个功能模块b的o文件>
  9. # 步骤4:生成可执行文件demo
  10. $ gcc -o demo <100个主程序模块的o文件> -L. -la -lb

上面的编译需要将所有的文件列出,并且修改其中一部分后仍然需要重新编译所有文件,这个过程如果项目很大的话,会很耗时。

make和makefile的存在正是为了解决上述两个问题的:

  1. makefile文件帮助我们记录了整个项目工程的所有需要编译的文件列表,这样我们在编译时仅需要输入简单的make命令就能编译出我们期望的结果
  2. makefile文件反映了整个项目中各个模块的依赖关系,这样我们改动了某些源文件后,仅需简单的输入make命令,make工具就会根据makefile文件里描述的依赖关系帮助我们分析哪些模块需要重新编译,并执行相应的操作。

在linux/unix开发环境中,makefile文件则是描述了一个特定编译系统所需要的策略,而make工具则是通过解析makefile文件并执行相应的命令来帮助我们构建其编译系统。

一条makefile的规则构成如下:

  1. target:prerequisites
  2. <tab> command1
  3. <tab> command2
  4. .....
  5. <tab> commandN
  • target:规则的目标,可以简单理解为这条规则存在的目的是什么。通常是程序中间或者最后需要生成的文件名,也可以不对应具体的文件,而仅仅就是个概念上的规则目标。
  • prerequisites:规则的依赖列表,可以简单的理解为要达到本条规则的目标所需要的先决条件是什么。可以是文件名,也可以是其他规则的目标;
  • command:规则的命令,可以简单的理解为当目标所需要的先决条件的满足了之后,需要执行什么动作来达成规则的目标。规则的命令其实就是shell命令。一条规则中可以有多行命令,特别注意:每行命令都必须以tab键开始!

这个简单的makefile文件,只有一条规则,规则的目标是all,没有任何依赖(规则不可以没有目标但是可以没有依赖),以及一条命令(其实规则也是可以不需要任何命令的)。

举个简单的例子,创建makefile然后填入如下代码:

  1. all:
  2. echo "Hello world"

保存后执行 make语句。

make命令的基本使用范式如下:

make [ -f makefile ] [ options ] ... [ targets ] ...

使用make命令的最简单的方式主要有以下四种形式:

  1. 简单粗暴,不带任何参数,直接执行make:

    $ make
    

  2. 指定makefile文件:

    $ make -f <makefile_name>
    

  3. 指定 makefile 目标:

    $ make <target>
    

  4. 到指定目录下执行make:

    $ make -C <subdir> <target>
    

在执行make的时候,我们可以带上-f <文件名>参数,来指定make命令从哪里读取makefile文件;而如果我们不显式指定,则make就会在当前目录下依次查找名字为GNUmakefile, makefile,和 Makefile的文件来作为其makefile文件。

在读取完makefile的内容后,make工具并不是逐条去执行makefile里的规则,而是以某条规则为突破口,多米诺骨牌效应式的去执行makefile里的规则。而这条作为突破口的规则的目标,称为终极目标 ,我们可以在执行make时以参数的形式指定终极目标,从而执行作为突破口的规则,如果我们不显式指定终极目标,make一般情况下将选择makefile的第一条规则的目标作为终极目标。

make解析makefile的流程如下:

假设有mekefile内容如下:

  1. 终极目标:依赖A 依赖B 依赖C
  2. 终极目标命令
  3. 依赖A:子依赖A1 子依赖A2
  4. 依赖A命令
  5. 依赖B:子依赖B1 子依赖B2
  6. 依赖B命令
  7. 依赖C:子依赖C1 子依赖C2
  8. 依赖C命令

 过程二,对整颗依赖树以从底到上,从左到右的顺序,解析执行每一条规则:

本节我们将通过构建一个简单的c语言项目工程(我们命名为project_simple)来理解makefile的基本概念。

在linux系统下执行如下一系列命令来探索makefile

  1. shiyanlou:Code/ $ mkdir project [14:58:09]
  2. shiyanlou:Code/ $ cd project [14:58:18]
  3. shiyanlou:project/ $ cat > main.c <<EOF [14:58:21]
  4. heredoc> extern void simple();
  5. heredoc> int main()
  6. heredoc> {
  7. heredoc> simple();
  8. heredoc> return 0;
  9. heredoc> }
  10. heredoc> EOF
  11. shiyanlou:project/ $ cat main.c [14:59:44]
  12. extern void simple();
  13. int main()
  14. {
  15. simple();
  16. return 0;
  17. }
  18. shiyanlou:project/ $ cat >simple.c <<EOF [14:59:56]
  19. heredoc> #include<stdio.h>
  20. heredoc> void simple()
  21. heredoc> {
  22. heredoc> printf("This is simple project!
  23. ");
  24. heredoc> }
  25. heredoc> EOF
  26. shiyanlou:project/ $ cat simple.c [15:01:42]
  27. #include<stdio.h>
  28. void simple()
  29. {
  30. printf("This is simple project!
  31. ");
  32. }
  33. shiyanlou:project/ $ cat >makefile <<EOF [15:01:45]
  34. heredoc> simple: main.c simple.c
  35. heredoc> gcc -o simple main.c simple.c
  36. heredoc> EOF
  37. shiyanlou:project/ $ cat ma [15:02:59]
  38. cat: ma: ?????????
  39. shiyanlou:project/ $ cat makefile [15:03:02]
  40. simple: main.c simple.c
  41. gcc -o simple main.c simple.c
  42. shiyanlou:project/ $ pwd [15:03:11]
  43. /home/shiyanlou/Code/project
  44. shiyanlou:project/ $ ls [15:03:16]
  45. main.c makefile simple.c
  46. shiyanlou:project/ $ make [15:03:17]
  47. gcc -o simple main.c simple.c
  48. shiyanlou:project/ $ ./simple [15:03:25]
  49. This is simple project!
  50. shiyanlou:project/ $

makefile 工具箱1

  1. // ---------------------------------------------
  2. // main.c
  3. #include <stdio.h>
  4. #include "complicated.h"
  5. int main()
  6. {
  7. printf("%s\n", HELLO_STRING);
  8. complicated();
  9. return 0;
  10. }
  1. // ---------------------------------------------
  2. // complicated.h
  3. #ifndef __COMPLICATED_H__
  4. #define __COMPLICATED_H__
  5. #define HELLO_STRING "Hello !"
  6. #define PROJECT_NAME "complicated"
  7. extern void complicated(void);
  8. #endif
  1. // ---------------------------------------------
  2. // complicated.c
  3. #include <stdio.h>
  4. #include "complicated.h"
  5. void complicated(void)
  6. {
  7. printf("This is a %s porject!\n", PROJECT_NAME);
  8. }

各个项目间的依赖关系

        熟悉gcc编译过程的朋友应该知道,其实我们在用gcc 编译出可执行文件的过程中是包含两个阶段的:编译阶段和链接阶段。

        我们上述的依赖关系图更加准确的反映出了整个项目的构建过程,这样我们据此写出来的makefile才能更加灵活及更具可扩展性,记住:精确的分析清楚项目的依赖关系,是编写一个好的makefile的关键。

makefile文件如下:

  1. complicated: main.o complicated.o
  2. gcc -o complicated main.o complicated.o
  3. main.o: main.c
  4. gcc -o main.o -c main.c
  5. complicated.o: complicated.c
  6. gcc -o complicated.o -c complicated.c

makefile中的变量

        makefile中的变量,与C语言中的宏类似,它为一个文本字符串(变量的值,其类型只能是字符串类型)提供了一个名字(变量名)。

变量定义的格式:

变量名 赋值符 变量值

  • 变量名指的就是该变量的名字,是不包括“:”、“#”、“=”、前置空白和尾空白的任何字符串。需要注意的是,尽管在GNU make中没有对变量的命名有其它的限制,但定义一个包含除字母、数字和下划线以外的变量的做法也是不可取的,因为除字母、数字和下划线以外的其它字符可能会在以后的make版本中被赋予特殊含义,并且这样命名的变量对于一些shell来说不能作为环境变量使用。变量名是大小写敏感的。变量“foo”、“Foo”和“FOO”指的是三个不同的变量。Makefile传统做法是变量名是全采用大写的方式。推荐的做法是在对于内部定义定义的一般变量(例如:目标文件列表objects)使用小写方式,而对于一些参数列表(例如:编译选项CFLAGS)采用大写方式。
  • 变量值,指的是变量所代表的内容,可以是一个文件名列表、编译选项列表、程序运行的选项参数列表、搜索源文件的目录列表、编译输出的目录列表和所有我们能够想到的事物。变量的值,其本质就是一个字符串。
  • 赋值符,有= 、 := 、 ?=和 +=四种格式,其中= 和 := 为基本定义类型, ?=和 +=为基于=的扩展定义类型。

一个具体的例子:

objects = program.o foo.o utils.o

变量引用 

当我们定义了一个变量之后,我们就可以在makfile中的目标依赖命令中引用我们的变量;而所谓的变量引用,就是在引用变量名的地方,用变量所代表的内容,执行一个严格的文本替换过程(该过程也称为变量被展开的过程 ),替换掉变量的名字。

变量的引用有以下几种方式:

  1. ${变量名}
  2. $(变量名)
  3. $单字符变量名 ,变量名仅包含一个字符,如$@$^
  1. # 变量定义
  2. objects = program.o foo.o utils.o
  3. program : $(objects) #在依赖中引用变量
  4. gcc -o program ${objects} #在命令中引用变量
  5. $(objects) : defs.h #在目标中引用变量

 变量分类与赋值

根据变量定义时所使用的赋值操作符的不同,可以将变量分成两种类型(或者说是两种风格):

递归展开式变量直接展开式变量

使用赋值操作符= 、 += 和 ?=定义的变量都是递归展开式变量,使用赋值操作符 :=定义的变量为直接展开式变量 。

两种变量类型的的最根本区别在于:变量值的求值时机,递归式变量的求值时机在于变量被引用时,直接展开式的求值时机在于变量被定义时

  1. foo1 = $(bar) #递归展开式变量
  2. foo2 := $(bar) #直接展开式变量
  3. bar = $(ugh)
  4. ugh = Huh?
  5. all:
  6. echo "foo1 is $(foo1), foo2 is $(foo2)"

由于变量foo1的变量值是在执行echo命令时才求的值,所以foo1的值被递归的展开为Huh?; 而变量foo2的变量值在定义时就被求值了,此时由于变量bar的值为空,因此foo2的值也为空。

注意:使用递归展开式的变量定义,可能会由于出现变量的递归定义而导致make陷入到无限的变量展开过程中,最终使make执行失败

+= 和 ?=是基于=扩展而来的两种变量赋值操作符;

+= 称为追加赋值操作符,它实现对于一个已经存在定义的变量进行追加赋值,如下例子:

  1. bar = foo1
  2. bar += foo2 #追加赋值,bar的值将为 foo1 foo2
  3. all:
  4. echo $(bar)

在makefile 中用户除了可以自定义变量外,还可以使用make工具为我们提供的一些特殊的变量及用法。

$@ -- 代表规则中的目标文件名

$< -- 代表规则的第一个依赖的文件名

$^ -- 代表规则中所有依赖文件的列表,文件名用空格分割

看例子,编写makefile 文件 /home/shiyanlou/Code/makefile_sample/auto_var.mk:

  1. all: first second third
  2. echo "\$$@ = $@"
  3. echo "$$< = $<"
  4. echo "$$^ = $^"
  5. first second third:

这里有几点需要说明一下:

  1. $字符在makefile中有特殊用途,因此如果要取消其特殊用途当成一个普通字符传递给echo命令执行,需要使用$$
  2. $@在bash shell中也有特殊用途,因此如果希望echo命令在bash中正常输出$@, 需要加上\字符
  3. 该makefile的最后一行first second third: 看起来有点奇怪,这是一条没有依赖和命令的多目标规则,读者可自行将它删除看有什么效果,并思考原因。

变量的替换引用 

  • 变量的分类与赋值

对于一个已经定义的变量,可以使用“替换引用”将其值使用指定的字符(字符串)进行替换。格式为$(VAR:A=B)或者${VAR:A=B},意思是,将变量“VAR”所表示的值中所有字符串“A”结尾的字符替换为“B”的字。“结尾”的含义是空格之前(变量值的多个字以空格分开)。而对于变量其它部分的“A”字符不进行替换。例如:

  1. sources := a.c b.c c.c d.d
  2. objects := $(sources:.c=.o)
  3. all:
  4. echo "objects = $(objects)"

  1. //结果
  2. sources := a.c b.c c.c d.d
  3. objects := $(sources:.c=.o)
  4. all:
  5. echo "objects = $(objects)"

在这个定义中,变量“objects”的值就为“a.c b.c c.c d.d”。使用变量的替换引用将变量“sources”以空格分开的值中的所有的字的尾字符“o”替换为“c”,其他部分不变,注意这里的d.d并不会被替换。

使用变量改进我们complicated项目的makefile(v1.2):

  1. # 描述:complicated 项目 makefile文件
  2. # 版本:v1.2
  3. # 修改记录:
  4. # 1. 为complicated项目makefile添加注释
  5. # 2. 使用变量改进我们complicated项目的makefile
  6. # 定义可执行文件变量
  7. executbale := complicated
  8. # 定义源文件列表变量
  9. sources := main.c complicated.c
  10. # 使用变量的引用替换,定义object文件列表
  11. objects := $(sources:.c=.o)
  12. # 定义编译命令变量
  13. CC := gcc
  14. # 终极目标规则,生成complicated可执行文件
  15. $(executbale): $(objects)
  16. # 使用自动化变量改造我们的编译命令
  17. $(CC) -o $@ $^
  18. # 子规则1, main.o的生成规则
  19. main.o: main.c
  20. $(CC) -o $@ -c $<
  21. # 子规则2,complicated.o的生成规则
  22. complicated.o: complicated.c
  23. $(CC) -o $@ -c $<

更加深入认识makefile规则 

  • 多目标规则
  1. targets...: prerequisites...
  2. commands
  3. ...
  1. all: target1 target2
  2. echo "This is a rule for $@"
  3. # 利用多目标规则合并 target1 和target2的规则
  4. target1 target2: dep
  5. echo "This is a rule for $@"
  6. dep:

  • 多规则目标

Makefile中,一个目标可以同时出现在多条规则中。

这种情况下,此目标文件的所有依赖文件将会被合并成此目标一个依赖文件列表,其中任何一个依赖文件比目标更新(比较目标文件和依赖文件的时间戳)时,make将会执行特定的命令来重建这个目标

。对于一个多规则的目标,重建此目标的命令只能出现在一个规则中(可以是多条命令)。

如果多个规则同时给出重建此目标的命令,make将使用最后一个规则的命令,同时提示错误信息。

  静态规则模式

仔细观察我们的complicated项目中的两条子规则:

  1. # 子规则1, main.o的生成规则
  2. main.o: main.c
  3. $(CC) -o $@ -c $<
  4. # 子规则2,complicated.o的生成规则
  5. complicated.o: complicated.c
  6. $(CC) -o $@ -c $<

静态模式规则:

可以理解为一种特殊的多目标规则,它仅要求多条规则具有相同的命令,而依赖可以不完全一样。

静态模式规则,其基本语法:

  1. TARGETS ...: TARGET-PATTERN: PREREQ-PATTERNS ...
  2. COMMANDS
  3. ...

其大致意思就是,用TARGET-PATTERN: PREREQ-PATTERNS ...描述的模式,从TARGETS ...取值来形成一条条规则,所有规则的命令都用COMMANDS

TARGETS ...代表具有相同模式的规则的目标列表,在我们的项目中就是main.o和complicated.o,我们可以直接引用我们先前定义的objects变量。

TARGET-PATTERN: PREREQ-PATTERNS ...部分定义了,如何为目标列表中的目标,生成依赖;TARGET-PATTERN称为目标模式,PREREQ-PATTERNS称为依赖模式;目标模式和依赖模式中,一般需要包含模式字符%

目标模式的作用就是从目标列表中的目标匹配过滤出需要的值,目标模式中的字符%表示在匹配过滤的过程中不做过滤的部分,目标模式中的其他字符表示要与目标列表中的目标精确匹配,例如,目标模式%.o, 表示从目标列表的目标中匹配所有以.o结尾的目标,然后过滤掉匹配目标的.o部分, 因此目标main.o经过目标模式%.o匹配过滤后,得到的输出就是main

依赖模式的作用就是表示要如何生成依赖文件。具体的生成过程,就是使用目标模式过滤出来的值,替换依赖模式字符%所表示的位置。因此,如果依赖模式为%.c, 则使用上述例子过滤出来的main来替换字符%, 最终得到依赖文件main.c

因此,我们可以这么用静态模式规则来简化我们的complicated项目:

  1. # 静态模式规则简化complicated makefile
  2. $(objects): %.o: %.c
  3. $(CC) -o $@ -c $<

伪目标 

我们的complicated项目编译完成后,会有可执行文件及中间目标文件,有时出于某些需求,需要将编译生成的文件都删除,让整个项目回到最初的状态。我们可以在该项目makefile定义添加一条目标为clean的规则,如下:

  1. # complicated项目添加clean 规则
  2. clean:
  3. rm -rf complicated complicated.o main.o

出现以上问题的原因是,当编译目录下存在clean文件时,由于clean规则没有依赖,所以clean文件的时间戳永远显得都是最新的,故其命令也无法被执行,这时我们就得请伪目标出手帮助了。

当我们将一个目标定义成伪目标时,意味着它不代表一个真正的文件名,在执行make时可以指定这个目标来执行其所在规则定义的命令。

定义一个伪目标的基本语法:

.PHONY: <伪目标>

因此我们以定义complicated项目的clean目标,为伪目标,如下:

  1. # complicated项目添加clean 规则
  2. .PHONY: clean
  3. clean:
  4. rm -rf complicated complicated.o main.o

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

对makefile规则更加深入认识 

  • 命令的回显

通常,make在执行命令行之前会把要执行的命令行进行输出,如以下makefile:

  1. all:
  2. echo "Hello world!"

在执行make时,其输出:

  1. $ make
  2. echo "Hello world!"
  3. Hello world!

关闭命令回显有以下几种方式:

  1. 每个需要关闭回显的命令行前加上"@"字符,上述例子关闭回显:

    1. all:
    2. @echo "Hello world!"

  2. 执行make时带上参数-s--slient禁止所有执行命令的显示

  3. 在Makefile中使用没有依赖的特殊目标.SILENT也可以禁止所有命令的回显

在Makefile中书写在同一行中的多个命令属于一个完整的shell命令行书写在独立行的一条命令是一个独立的shell命令行。所以需要注意:在一个规则的命令中,命令行cd改变目录不会对其后的命令的执行产生影响。就是说其后的命令执行的工作目录不会是之前使用cd进入的那个目录。如果要实现这个目的就不能把cd和其后的命令放在两行来书写。而应该把这两条命令写在一行上,用分号分隔。这样它们才是一个完整的shell命令行。

对比以下例子两个规则的输出,编写makefile 文件 /home/shiyanlou/Code/makefile_sample/cmd.mk:

  1. target1:
  2. @echo "target1"
  3. @cd ~
  4. @pwd
  5. target2:
  6. @echo "target2"
  7. @cd ~; pwd

  • 命令执行的错误处理

通常情况下,规则中的每一条命令在运行结束后,make都会检测命令执行的返回状态,如果返回成功,就执行下一条命令;命令出错(返回状态非0),make就会放弃对当前规则的执行,或者终止对当前makefile的解析执行。

在一些情况下,规则中的一个命令的执行失败并不代表规则执行的错误。为了忽略一些无关紧要的命令执行失败的情况,我们可以在命令之前加一个减号-,来告诉make忽略此命令的执行失败检查。 

在更加深入的认识了makefile的规则后,我们complicated项目的makefile(v1.3)就可以这样写了:

  1. # 描述:complicated 项目 makefile文件
  2. # 版本:v1.3
  3. # 修改记录:
  4. # 1. 为complicated项目makefile添加注释
  5. # 2. 使用变量改进我们complicated项目的makefile
  6. # 3. 使用静态模式规则,简化makefile
  7. # 4. 使用伪目标,加上clean规则
  8. # 定义可执行文件变量
  9. executbale := complicated
  10. # 定义源文件列表变量
  11. sources := main.c complicated.c
  12. # 使用变量的引用替换,定义object文件列表
  13. objects := $(sources:.c=.o)
  14. # 定义编译命令变量
  15. CC := gcc
  16. RM := rm -rf
  17. # 终极目标规则,生成complicated可执行文件
  18. $(executbale): $(objects)
  19. # 使用自动化变量改造我们的编译命令
  20. $(CC) -o $@ $^
  21. # 子规则, main.o和complicated.o的生成规则,使用静态模式规则
  22. $(objects):%.o:%.c
  23. $(CC) -o $@ -c $<
  24. # clean规则
  25. .PHONY: clean
  26. clean:
  27. $(RM) $(executbale) $(objects)

 内嵌函数

make的内嵌函数为我们提供了处理文件名、变量、文本和命令的方法。使我们的Makefile更为灵活和健壮。我们可以在需要的地方调用函数来处理指定的文本(参数),函数在调用它的地方被替换为它的处理结果。函数调用(引用)的展开和变量引用的展开方式类似:

函数调用方式1:

$(FUNCTION ARGUMENTS)

函数调用方式2:

${FUNCTION ARGUMENTS}

  • 内嵌函数的分类

  • 使用wildcard函数改进complicated项目makefile

wildcard函数,其使用范式:

$(wildcard PATTERN)

函数名称:wildcard

函数功能:列出当前目录下所有符合模式“PATTERN”格式的文件名。

返回值:空格分割的、存在当前目录下的所有符合模式“PATTERN”的文件名。

函数说明:“PATTERN”使用shell可识别的通配符,包括?(单字符)、*(多字符)等。

$(wildcard *.c

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

我们可以引进wildcard函数,改进complicated项目的makefile,使其能够自动扫描当前目录下的源文件:

  1. # 描述:complicated 项目 makefile文件
  2. # 版本:v1.4
  3. # 修改记录:
  4. # 1. 为complicated项目makefile添加注释
  5. # 2. 使用变量改进我们complicated项目的makefile
  6. # 3. 使用静态模式规则,简化makefile
  7. # 4. 使用伪目标,加上clean规则
  8. # 5. 引进wildcard函数,自动扫描当前目录下的源文件
  9. # 定义可执行文件变量
  10. executbale := complicated
  11. # 引进wildcard函数扫描源文件,定义列表变量
  12. sources := $(wildcard *.c)
  13. # 使用变量的引用替换,定义object文件列表
  14. objects := $(sources:.c=.o)
  15. # 定义编译命令变量
  16. CC := gcc
  17. RM := rm -rf
  18. # 终极目标规则,生成complicated可执行文件
  19. $(executbale): $(objects)
  20. # 使用自动化变量改造我们的编译命令
  21. $(CC) -o $@ $^
  22. # 子规则, main.o和complicated.o的生成规则,使用静态模式规则
  23. $(objects):%.o:%.c
  24. $(CC) -o $@ -c $<
  25. # clean规则
  26. .PHONY: clean
  27. clean:
  28. $(RM) $(executbale) $(objects)

  自动生成依赖关系

至此,我们complicated项目的makefile已经完成的比较完善了,但还存在一个问题,当我们更新头文件complicated.h的内容后,执行make时,项目并不会重新编译,看演示:

出现以上问题的根本原因是,我们的makefile还没能精确的反映整个项目工程的依赖关系!很明显,项目的依赖关系应该加入对于头文件的依赖,即如下图:

根据上述依赖关系,我们可以在makefile加上如下规则:

$(objects): complicated.h

上述新增规则,是一条多目标规则,它与之前的静态模式规则,又组成了多规则目标,读者可自行思考其工作原理。

上述规则加入虽然能解决我们的问题,但是对于大型复杂的项目,这种需要我们逐个去分析头文件被依赖的关系,几乎是不可能的。所以我们需要工具来帮我们做这个事。下面我们来介绍如何实现自动生成依赖关系。

gcc为我们提供了这样的功能,帮助我们分析一个文件对其他文件的依赖关系列表。当我们在执行gcc时带上-MM选项时,gcc工具就会为我们列出指定文件对其他文件的依赖关系列表。直接来看例子:

接下来要考虑的问题就是如何将gcc的输出导入到我们的makefile中了。

makefile支持使用sinclude关键字将指定文件导入到当前的makefile当中,它的作用与C语言的#inlucde预处理命令是一样的。使用方式sinclude <other_makefiles>。因此,我们可以将gcc对于源文件的依赖关系分析输出到某个文件(我们可以称为依赖描述文件,一般命名为与源文件同名但以.d结尾的文件)当中,然后再将依赖描述文件导入到makefile中。

# 使用变量的引用替换,定义依赖描述文件列表

deps := $(sources:.c=.d) # 导入依赖描述文件列表

sinclude $(deps)

当我们使用sinclude关键字向当前makefile导入文件时,如果所导入的文件不存在,make会试图去执行可以生产导入文件的规则去生产被导入的文件,然后再执行导入。因此我们可以使用静态模式规则,让make在执行时,去调用gcc生成依赖关系文件,我们可以这么写:

  1. $(deps):%.d:%.c
  2. gcc -MM $< > $@

 因此,我们complicated项目的最终makefile可以这么写:

  1. # 描述:complicated 项目 makefile文件
  2. # 版本:v1.5
  3. # 修改记录:
  4. # 1. 为complicated项目makefile添加注释
  5. # 2. 使用变量改进我们complicated项目的makefile
  6. # 3. 使用静态模式规则,简化makefile
  7. # 4. 使用伪目标,加上clean规则
  8. # 5. 引进wildcard函数,自动扫描当前目录下的源文件
  9. # 6. 加入自动规则依赖
  10. # 定义可执行文件变量
  11. executbale := complicated
  12. # wildcard函数扫描源文件,定义列表变量
  13. sources := $(wildcard *.c)
  14. # 使用变量的引用替换,定义object文件列表
  15. objects := $(sources:.c=.o)
  16. # 使用变量的引用替换,定义依赖描述文件列表
  17. deps := $(sources:.c=.d)
  18. # 定义编译命令变量
  19. CC := gcc
  20. RM := rm -rf
  21. # 终极目标规则,生成complicated可执行文件
  22. $(executbale): $(objects)
  23. # 使用自动化变量改造我们的编译命令
  24. $(CC) -o $@ $^
  25. # 子规则, main.o和complicated.o的生成规则,使用静态模式规则
  26. $(objects):%.o:%.c
  27. $(CC) -o $@ -c $<
  28. # clean规则
  29. .PHONY: clean
  30. clean:
  31. $(RM) $(executbale) $(objects) $(deps)
  32. # 自动规则依赖
  33. sinclude $(deps)
  34. $(deps):%.d:%.c
  35. $(CC) -MM $< > $@

总结 

我们学习了 make 工具的使用,主要包括以下的内容:

  • makefile 规则的概念
  • makefile 变量的使用
  • makefile 函数的使用
  • makefile 自动生成依赖

参考资料

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/640962
推荐阅读
相关标签
  

闽ICP备14008679号