Makefile 描述的是文件编译的相关规则,它的规则主要是两个部分组成,分别是依赖的关系和执行的命令 PHONY伪目标实践

时间:2023-03-09 13:25:24
Makefile 描述的是文件编译的相关规则,它的规则主要是两个部分组成,分别是依赖的关系和执行的命令 PHONY伪目标实践

Makefile的工作流程 http://c.biancheng.net/view/7091.html

Makefile文件是什么?

我们教程主要是讲的是 Makefile 。很多 Linux(Unix) 做开发的初学者不了解 Makefile 是什么,甚至大部分 Windows 开发工程师对 Makefile 都特别陌生。这个其实很正常,如果你是在 Windows 下作开发的话不需要去考虑这个问题,因为 Windows 下的集成开发环境(IDE)已经内置了 Makefile,或者说会自动生成 Makefile,我们不用去手动编写。

Linux 中却不能这样,需要我们去手动的完成这项工作。Linux 下可以学习的开发语言有很多,常见的有 C/C++语言、python、java 等等。如果你想要在 Linux(Unix) 下做开发的话,不了解 Makefile 是一件非常失败的事情,甚至说你就成为不了一个合格的 Linux 开发工程师。不懂 Makefile,就操作不了多文件编程,就完成不了相对于大的工程项目的操作。如果你想在 Linux(Unix) 环境下做开发的话,Makefile 是必须掌握的一项技能。

那么,究竟什么是 Makefile 呢?

Makefile 可以简单的认为是一个工程文件的编译规则,描述了整个工程的编译和链接等规则。其中包含了那些文件需要编译,那些文件不需要编译,那些文件需要先编译,那些文件需要后编译,那些文件需要重建等等。编译整个工程需要涉及到的,在 Makefile 中都可以进行描述。换句话说,Makefile 可以使得我们的项目工程的编译变得自动化,不需要每次都手动输入一堆源文件和参数。

以 Linux 下的C语言开发为例来具体说明一下,多文件编译生成一个文件,编译的命令如下所示:

gcc -o outfile name1.c name2.c ...

outfile 要生成的可执行程序的名字,nameN.c 是源文件的名字。这是我们在 Linux 下使用 gcc 编译器编译 C 文件的例子。如果我们遇到的源文件的数量不是很多的话,可以选择这样的编译方式。如果源文件非常的多的话,就会遇到下面的这些问题。

1) 编译的时候需要链接库的的问题。拿C语言来说,编译的时候 gcc 只会默认链接一些基本的C语言标准库,很多源文件依赖的标准库都需要我们手动链接。

下面列举了一些需要我们手动链接的标准库:

  • name1.c 用到了数学计算库 math 中的函数,我们得手动添加参数 -Im;
  • name4.c 用到了小型数据库 SQLite 中的函数,我们得手动添加参数 -lsqlite3;
  • name5.c 使用到了线程,我们需要去手动添加参数 -lpthread。

因为有很多的文件,还要去链接很多的第三方库。所以在编译的时候命令会很长,并且在编译的时候我们可能会涉及到文件链接的顺序问题,所以手动编译会很麻烦。

如果我们学会使用 Makefile 就不一样了,它会彻底简化编译的操作。把要链接的库文件放在 Makefile 中,制定相应的规则和对应的链接顺序。这样只需要执行 make 命令,工程就会自动编译。每次想要编译工程的时候就执行 make ,省略掉手动编译中的参数选项和命令,非常的方便。

2) 编译大的工程会花费很长的时间。

如果我们去做项目开发,免不了要去修改工程项目的源文件,每次修改后都要去重新编译。一个大的工程项目可不止有几个的源文件,里面的源文件个数可能有成百上千个。例如一个内核,或者是一个软件的源码包。这些都是我们做开发经常会遇到的。要完成这样的文件的编译,我们消耗的时间可不是一点点。如果文件特别大的话我们可能要花上半天的时间。

对于这样的问题我们 Makefile 可以解决吗?当然是可以的,Makefile 支持多线程并发操作,会极大的缩短我们的编译时间,并且当我们修改了源文件之后,编译整个工程的时候,make 命令只会编译我们修改过的文件,没有修改的文件不用重新编译,也极大的解决了我们耗费时间的问题。

这其实是我们遇到的比较常见的问题,当然可能遇到的问题还会有很多,比如:工程文件中的源文件的类型很多,编译的话需要选择的编译器;文件可能会分布在不同的目录中,使用时需要调价路径。这些问题都可以通过 Makefile 解决。并且文件中的 Makefile 只需要完成一次,一般我们只要不增加或者是删除工程中的文件,Makefile 基本上不用去修改,编译时只用一个 make 命令。为我们提供了极大的便利,很大程度上提高编译的效率。

Makefile文件中包含哪些规则?

要书写一个完整的 Makefile文件,需要了解 Makefile 的相关的书写规则。我们已经知道了 Makefile 描述的是文件编译的相关规则,它的规则主要是两个部分组成,分别是依赖的关系和执行的命令,其结构如下所示:

targets : prerequisites
    command

或者是

targets : prerequisites; command
    command

相关说明如下:

  • targets:规则的目标,可以是 Object File(一般称它为中间文件),也可以是可执行文件,还可以是一个标签;
  • prerequisites:是我们的依赖文件,要生成 targets 需要的文件或者是目标。可以是多个,也可以是没有;
  • command:make 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行。

注意:我们的目标和依赖文件之间要使用冒号分隔开,命令的开始一定要使用Tab键。

通过下面的例子来具体使用一下 Makefile 的规则,Makefile文件中添代码如下:

  1. test:test.c
  2. gcc -o test test.c

上述代码实现的功能就是编译 test.c 文件,通过这个实例可以详细的说明 Makefile 的具体的使用。其中 test 是的目标文件,也是我们的最终生成的可执行文件。依赖文件就是 test.c 源文件,重建目标文件需要执行的操作是gcc -o test test.c。这就是 Makefile 的基本的语法规则的使用。

使用 Makefile 的方式:首先需要编写好 Makefile 文件,然后在 shell 中执行 make 命令,程序就会自动执行,得到最终的目标文件。

通过上面的例子我们可以了解到,Makefile 的规则很简单,但这并不是 Makefile 的全部,这个仅仅是它的冰山一角。仅仅靠一个规则满足不了我们对于大的工程项目的编译。甚至几个文件的编译都会出现问题,所以要学习的东西还有很多。

简单的概括一下Makefile 中的内容,它主要包含有五个部分,分别是:

1) 显式规则

显式规则说明了,如何生成一个或多的的目标文件。这是由 Makefile 的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。

2) 隐晦规则

由于我们的 make 命名有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写 Makefile,这是由 make 命令所支持的。

3) 变量的定义

在 Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点像C语言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。

4) 文件指示

其包括了三个部分,一个是在一个 Makefile 中引用另一个 Makefile,就像C语言中的 include 一样;另一个是指根据某些情况指定 Makefile 中的有效部分,就像C语言中的预编译 #if 一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。

5) 注释

Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用“#”字符,这个就像 C/C++ 中的“//”一样。如果你要在你的 Makefile 中使用“#”字符,可以用反斜框进行转义,如:“\#”。

Makefile的工作流程

简单了解一下 Makefile 书写规则之后,再来深入研究一下 Makefile 的是怎样工作的?当我们在执行 make 条命令的时候,make 就会去当前文件下找要执行的编译规则,也就是 Makefile 文件。我们编写 Makefile 的时可以使用的文件的名称 "GNUmakefile" 、"makefile" 、"Makefile" ,make 执行时回去寻找 Makefile 文件,找文件的顺序也是这样的。

我们推荐使用 Makefile(一般在工程中都这么写,大写的会比较的规范)。如果文件不存在,make 就会给我们报错,提示:

make:*** 没有明确目标并且找不到 makefile。停止

Makefile的工流程

Makefile 的具体工作流程可以通过例子来看一下:创建一个包含有多个源文件和 Makefile 的目录文件,源文件之间相互关联。在 Makefile 中添加下面的代码:

  1. main:main.o test1.o test2.o
  2. gcc main.o test1.o test2.o -o main
  3. main.o:main.c test.h
  4. gcc -c main.c -o main.o
  5. test1.o:test1.c test.h
  6. gcc -c test1.c -o test1.o
  7. test2.o:test2.c test.h
  8. gcc -c test2.c -o test2.o

在我们编译项目文件的时候,默认情况下,make 执行的是 Makefile 中的第一规则(Makefile 中出现的第一个依赖关系),此规则的第一目标称之为“最终目标”或者是“终极目标”。

在 shell 命令行执行的 make 命令,就可以得到可执行文件 main 和中间文件 main.o、test1.o 和 test2.o,main 就是我们要生成的最终文件。通过 Makefile 我们可以发现,目标 main"在 Makefile 中是第一个目标,因此它就是 make 的终极目标,当修改过任何 C 文件后,执行 make 将会重建终极目标 main。

它的具体工作顺序是:当在 shell 提示符下输入 make 命令以后。 make 读取当前目录下的 Makefile 文件,并将 Makefile 文件中的第一个目标作为其执行的“终极目标”,开始处理第一个规则(终极目标所在的规则)。在我们的例子中,第一个规则就是目标 "main" 所在的规则。规则描述了 "main" 的依赖关系,并定义了链接 ".o" 文件生成目标 "main" 的命令;make 在执行这个规则所定义的命令之前,首先处理目标 "main" 的所有的依赖文件(例子中的那些 ".o" 文件)的更新规则(以这些 ".o" 文件为目标的规则)。

对这些 ".o" 文件为目标的规则处理有下列三种情况:

  • 目标 ".o" 文件不存在,使用其描述规则创建它;
  • 目标 ".o" 文件存在,目标 ".o" 文件所依赖的 ".c" 源文件 ".h" 文件中的任何一个比目标 ".o" 文件“更新”(在上一次 make 之后被修改)。则根据规则重新编译生成它;
  • 目标 ".o" 文件存在,目标 ".o" 文件比它的任何一个依赖文件(".c" 源文件、".h" 文件)“更新”(它的依赖文件在上一次 make 之后没有被修改),则什么也不做。

通过上面的更新规则我们可以了解到中间文件的作用,也就是编译时生成的 ".o" 文件。作用是检查某个源文件是不是进行过修改,最终目标文件是不是需要重建。我们执行 make 命令时,只有修改过的源文件或者是不存在的目标文件会进行重建,而那些没有改变的文件不用重新编译,这样在很大程度上节省时间,提高编程效率。小的工程项目可能体会不到,项目工程文件越大,效果才越明显。

当然 make 命令能否顺利的执行,还在于我们是否制定了正确的的依赖规则,当前目录下是不是存在需要的依赖文件,只要任意一点不满足,我们在执行 make 的时候就会出错。所以完成一个正确的 Makefile 不是一件简单的事情。

清除工作目录中的过程文件

我们在使用的时候会产生中间文件会让整个文件看起来很乱,所以在编写 Makefile 文件的时候会在末尾加上这样的规则语句:

  1. .PHONY:clean
  2. clean:
  3. rm -rf *.o test

其中 "*.o" 是执行过程中产生的中间文件,"test" 是最终生成的执行文件。我们可以看到 clean 是独立的,它只是一个伪目标(在《Makefile伪目标》的章节中详细介绍),不是具体的文件。不会与第一个目标文件相关联,所以我们在执行 make 的时候也不会执行下面的命令。在shell 中执行 "make clean" 命令,编译时的中间文件和生成的最终目标文件都会被清除,方便我们下次的使用。

Makefile通配符的使用

 Makefile 是可以使用 shell 命令的,所以 shell 支持的通配符在 Makefile 中也是同样适用的。 shell 中使用的通配符有:"*","?","[...]"。具体看一下这些通配符的表示含义和具体的使用方法。
通配符 使用说明
* 匹配0个或者是任意个字符
匹配任意一个字符
[] 我们可以指定匹配的字符放在 "[]" 中

通配符可以出现在模式的规则中,也可以出现在命令中,详细的使用情况如下。

实例 1:

  1. .PHONY:clean
  2. clean:
  3. rm -rf *.o test

这是在 Makefile 中经常使用的规则语句。这个实例可以说明通配符可以使用在规则的命令当中,表示的是任意的以 .o 结尾的文件。
实例 2:

  1. test:*.c
  2. gcc -o $@ $^

这个实例可以说明我们的通配符不仅可以使用在规则的命令中,还可以使用在规则中。用来表示生所有的以 .c 结尾的文件。

但是如果我们的通配符使用在依赖的规则中的话一定要注意这个问题:不能通过引用变量的方式来使用,如下所示。

  1. OBJ=*.c
  2. test:$(OBJ)
  3. gcc -o $@ $^

我们去执行这个命令的时候会出现错误,提示我们没有 "*.c" 文件,实例中我们相要表示的是当前目录下所有的 ".c" 文件,但是我们在使用的时候并没有展开,而是直接识别成了一个文件。文件名是 "*.c"。

如果我们就是相要通过引用变量的话,我们要使用一个函数 "wildcard",这个函数在我们引用变量的时候,会帮我们展开。我们把上面的代码修改一下就可以使用了。

  1. OBJ=$(wildcard *.c)
  2. test:$(OBJ)
  3. gcc -o $@ $^

这样我们再去使用的时候就可以了。调用函数的时候,会帮我们自动展开函数。

还有一个和通配符 "*" 相类似的字符,这个字符是 "%",也是匹配任意个字符,使用在我们的的规则当中。

  1. test:test.o test1.o
  2. gcc -o $@ $^
  3. %.o:%.c
  4. gcc -o $@ $^

"%.o" 把我们需要的所有的 ".o" 文件组合成为一个列表,从列表中挨个取出的每一个文件,"%" 表示取出来文件的文件名(不包含后缀),然后找到文件中和 "%"名称相同的 ".c" 文件,然后执行下面的命令,直到列表中的文件全部被取出来为止。

这个属于 Makefile 中静态模规则:规则存在多个目标,并且不同的目标可以根据目标文件的名字来自动构造出依赖文件。跟我们的多规则目标的意思相近,但是又不相同。

Makefile变量的定义和使用

 变量对于我们来说是不陌生的,在学习各种编程语言时会经常用到。就拿C语言来说,变量的使用是十分常见的,变量可以用来保存一个值或者是使用变量进行运算操作。Makefile 中的变量也是这样,我们可以利用它来表示某些多处使用而又可能发生变化的内容,不仅可以节省重复修改的工作,还可以避免遗漏。

变量的定义

Makefile 文件中定义变量的基本语法如下:

变量的名称=值列表

Makefile 中的变量的使用其实非常的简单,因为它并没有像其它语言那样定义变量的时候需要使用数据类型。变量的名称可以由大小写字母、阿拉伯数字和下划线构成。等号左右的空白符没有明确的要求,因为在执行 make 的时候多余的空白符会被自动的删除。至于值列表,既可以是零项,又可以是一项或者是多项。如:

VALUE_LIST = one two three

调用变量的时候可以用 "$(VALUE_LIST)" 或者是 "${VALUE_LIST}" 来替换,这就是变量的引用。实例:

  1. OBJ=main.o test.o test1.o test2.o
  2. test:$(OBJ)
  3. gcc -o test $(OBJ)

这就是引用变量后的 Makefile 的编写,比我们之前的编写方式要简单的多。当要添加或者是删除某个依赖文件的时候,我们只需要改变变量 "OBJ" 的值就可以了。

变量的基本赋值

知道了如何定义,下面我们来说一下 Makefile 的变量的四种基本赋值方式:

  • 简单赋值 ( := ) 编程语言中常规理解的赋值方式,只对当前语句的变量有效。
  • 递归赋值 ( = ) 赋值语句可能影响多个变量,所有目标变量相关的其他变量都受影响。
  • 条件赋值 ( ?= ) 如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。
  • 追加赋值 ( += ) 原变量用空格隔开的方式追加一个新值。

简单赋值

  1. x:=foo
  2. y:=$(x)b
  3. x:=new
  4. test:
  5. @echo "y=>$(y)"
  6. @echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>foob
x=>new

递归赋值

  1. x=foo
  2. y=$(x)b
  3. x=new
  4. test:
  5. @echo "y=>$(y)"
  6. @echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>newb
x=>new

条件赋值

  1. x:=foo
  2. y:=$(x)b
  3. x?=new
  4. test:
  5. @echo "y=>$(y)"
  6. @echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>foob
x=>foo

追加赋值

  1. x:=foo
  2. y:=$(x)b
  3. x+=$(y)
  4. test:
  5. @echo "y=>$(y)"
  6. @echo "x=>$(x)"

在 shell 命令行执行make test我们会看到:

y=>foob
x=>foo foob

不同的赋值方式会产生不同的结果,我们使用的时候应该根据具体的情况选择相应的赋值规则。

变量使用的范围很广,它可以出现在规则的模式中,也可以出现在规则的命令中或者是作为 Makefile 函数的参数来使用。总之,变量的使用在我们的 Makefile 编写中还是非常广泛的,可以说我们的 Makefile 中必不可少的东西。

其实变量在我们的 Makefile 中还是有很多种类的,它们的意义是不相同的。比如我们的环境变量,自动变量,模式指定变量等。其他的变量我们会在其他的文章里做介绍。

Makefile自动化变量

关于自动化变量可以理解为由 Makefile 自动产生的变量。在模式规则中,规则的目标和依赖的文件名代表了一类的文件。规则的命令是对所有这一类文件的描述。我们在 Makefile 中描述规则时,依赖文件和目标文件是变动的,显然在命令中不能出现具体的文件名称,否则模式规则将失去意义。

那么模式规则命令中该如何表示文件呢?就需要使用“自动化变量”,自动化变量的取值根据执行的规则来决定,取决于执行规则的目标文件和依赖文件。下面是对所有的自动化变量进行的说明:
自动化变量 说明
$@ 表示规则的目标文件名。如果目标是一个文档文件(Linux 中,一般成 .a 文件为文档文件,也成为静态的库文件),
那么它代表这个文档的文件名。在多目标模式规则中,它代表的是触发规则被执行的文件名。
$% 当目标文件是一个静态库文件时,代表静态库的一个成员名。
$< 规则的第一个依赖的文件名。如果是一个目标文件使用隐含的规则来重建,则它代表由隐含规则加入的第一个依赖文件。
$? 所有比目标文件更新的依赖文件列表,空格分隔。如果目标文件时静态库文件,代表的是库文件(.o 文件)。
$^ 代表的是所有依赖文件列表,使用空格分隔。如果目标是静态库文件,它所代表的只能是所有的库成员(.o 文件)名。
一个文件可重复的出现在目标的依赖中,变量“$^”只记录它的第一次引用的情况。就是说变量“$^”会去掉重复的依赖文件。
$+ 类似“$^”,但是它保留了依赖文件中重复出现的文件。主要用在程序链接时库的交叉引用场合。
$* 在模式规则和静态模式规则中,代表“茎”。“茎”是目标模式中“%”所代表的部分(当文件名中存在目录时,
“茎”也包含目录部分)。

下面我们就自动化变量的使用举几个例子。

实例1:

  1. test:test.o test1.o test2.o
  2. gcc -o $@ $^
  3. test.o:test.c test.h
  4. gcc -o $@ $<
  5. test1.o:test1.c test1.h
  6. gcc -o $@ $<
  7. test2.o:test2.c test2.h
  8. gcc -o $@ $<

这个规则模式中用到了 "$@" 、"$<" 和 "$^" 这三个自动化变量,对比之前写的 Makefile 中的命令,我们可以发现 "$@" 代表的是目标文件test,“$^”代表的是依赖的文件,“$<”代表的是依赖文件中的第一个。我们在执行 make 的时候,make 会自动识别命令中的自动化变量,并自动实现自动化变量中的值的替换,这个类似于编译C语言文件的时候的预处理的作用。

实例2:

  1. lib:test.o test1.o test2.o
  2. ar r $?

假如我们要做一个库文件,库文件的制作依赖于这三个文件。当修改了其中的某个依赖文件,在命令行执行 make 命令,库文件 "lib" 就会自动更新。"$?" 表示修改的文件。

GNU make 中在这些变量中加入字符 "D" 或者 "F" 就形成了一系列变种的自动化变量,这些自动化变量可以对文件的名称进行操作。

下面是一些详细的描述:

变量名 功能
$(@D) 表示文件的目录部分(不包括斜杠)。如果 "$@" 表示的是 "dir/foo.o" 那么 "$(@D)" 表示的值就是 "dir"。如果 "$@" 不存在斜杠(文件在当前目录下),其值就是 "."。
$(@F) 表示的是文件除目录外的部分(实际的文件名)。如果 "$@" 表示的是 "dir/foo.o",那么 "$@F" 表示的值为 "dir"。
$(*D)
$(*F)
分别代表 "茎" 中的目录部分和文件名部分
$(%D)
$(%F)
当以 "archive(member)" 形式静态库为目标时,分别表示库文件成员 "member" 名中的目录部分和文件名部分。踏进对这种新型时的目标有效。
$(<D)
$(<F)
表示第一个依赖文件的目录部分和文件名部分。
$(^D)
$(^F)
分别表示所有依赖文件的目录部分和文件部分。
$(+D)
$(+F)
分别表示所有的依赖文件的目录部分和文件部分。
$(?D)
$(?F)
分别表示更新的依赖文件的目录部分和文件名部分。

Makefile目标文件搜索(VPATH和vpath)

 我们都知道一个工程文件中的源文件有很多,并且存放的位置可能不相同(工程中的文件会被放到不同的目录下),所以按照之前的方式去编写 Makefile 会有问题。

我们之前列举的例子,所有的源文件基本上都是存放在与 Makefile 相同的目录下。只要依赖的文件存在,并且依赖规则没有问题,执行 make命令整个工程就会按照对我们编写规则去编译,最终会重建目标文件。那如果需要的文件是存在于不同的路径下,在编译的时候要去怎么办呢(不改变工程的结构)?这就用到了 Makefile 中为我们提供的目录搜索文件的功能。

常见的搜索的方法的主要有两种:一般搜索VPATH和选择搜索vpath。乍一看只是大小写的区别,其实两者在本质上也是不同的。

VPATH 和 vpath 的区别:VPATH 是变量,更具体的说是环境变量,Makefile 中的一种特殊变量,使用时需要指定文件的路径;vpath 是关键字,按照模式搜索,也可以说成是选择搜索。搜索的时候不仅需要加上文件的路径,还需要加上相应限制的条件。

VPATH的使用

在 Makefile 中可以这样写:

VPATH := src

我们可以这样理解,把 src 的值赋值给变量 VPATH,所以在执行 make 的时候会从 src 目录下找我们需要的文件。

当存在多个路径的时候我们可以这样写:

VPATH := src car

或者是

VPATH := src:car

多个路径之间要使用空格或者是冒号隔开,表示在多个路径下搜索文件。搜索的顺序为我们书写时的顺序,拿上面的例子来说,我们应该先搜索 src 目录下的文件,再搜索 car 目录下的文件。

注意:无论你定义了多少路径,make 执行的时候会先搜索当前路径下的文件,当前目录下没有我们要找的文件,才去 VPATH 的路径中去寻找。如果当前目录下有我们要使用的文件,那么 make 就会使用我们当前目录下的文件。

实例:

  1. VPATH=src car
  2. test:test.o
  3. gcc -o $@ $^

假设 test.c 文件没有在当前的目录而在当前文件的子目录 "src" 或者是 "car" 下,程序执行是没有问题的,但是生成的 test 的文件没有在定义的子目录文件中而是在当前的目录下,当然生成文件路径可以指定。

vpath的使用

学习了 VPATH的使用,我们再来了解一下关键字搜索 vpath 的使用,这种搜索方式一般被称作选择性搜索。使用上的区别我们可以这样理解:VPATH 是搜索路径下所有的文件,而 vpath 更像是添加了限制条件,会过滤出一部分再去寻找。
 
具体用法:

1) vpath PATTERN DIRECTORIES 
2) vpath PATTERN
3) vpath

( PATTERN:可以理解为要寻找的条件,DIRECTORIES:寻找的路径 )

首先是用法一,命令格式如下:

vpath test.c src

可以这样理解,在 src 路径下搜索文件 test.c。多路径的书写规则如下:

vpath test.c src car         或者是         vpath test.c src : car

多路径的用法其实和 VPATH 差不多,都是使用空格或者是冒号分隔开,搜索路径的顺序是先 src 目录,然后是 car 目录。

其次是用法二,命令格式如下:

vpath test.c

用法二的意思是清除符合文件 test.c 的搜索目录。

最后是用法三,命令格式如下:

vpath

vpath 单独使的意思是清除所有已被设置的文件搜索路径。

另外在使用 vpath 的时候,搜索的条件中可以包含模式字符“%”,这个符号的作用是匹配一个或者是多个字符,例如“%.c”表示搜索路径下所有的 .c 结尾的文件。如果搜索条件中没有包含“%" ,那么搜索的文件就是具体的文件名称。
 
使用什么样的搜索方法,主要是基于编译器的执行效率。使用 VPATH 的情况是前路径下的文件较少,或者是搜索的文件不能使用通配符表示,这些情况下使用VPATH最好。如果存在某个路径的文件特别的多或者是可以使用通配符表示的时候,就不建议使用 VPATH 这种方法,为什么呢?因为 VPATH 在去搜索文件的时没有限制条件,所以它回去检索这个目录下的所有文件,每一个文件都会进行对比,搜索和我们目录名相同的文件,不仅速度会很慢,而且效率会很低。我们在这种情况下就可以使用 vpath 搜索,它包含搜索条件的限制,搜索的时候只会从我们规定的条件中搜索目标,过滤掉不符合条件的文件,当然查找的时候也会比较的快。

Makefile路径搜索使用案例

 我们了解了一下路径搜索的使用方式,我们再来看一下具体的使用方法。

为了体验实例的效果的更加明显,我们按照源代码树的布局来放置文件。我们把源代码放置在src目录下,包含的文件文件是:list1.c、list2.c、main.c 文件,我们把头文件包含在 include 的目录下,包含文件 list1.h、list2.h 文件。Makefile 放在这两个目录文件的上一级目录。

我们按照之前的方式来编写 Makefile 文件:
  1. main:main.o list1.o list2.o
  2. gcc -o $@ $<
  3. main.o:main.c
  4. gcc -o $@ $^
  5. list1.o:list1.c list1.h
  6. gcc -o $@ $<
  7. list2.o:list2.c list2.h
  8. gcc -o $@ $<

我们编译执行的 make 时候会发现命令行提示我们:

make:*** No rule to make target 'main.c',need by 'main.o'. stop.

出现错误并且编译停止了,为什么会出现错误呢?我们来看一下出现错误的原因,再去重建最终目标文件 main 的时候我们需要 main.o 文件,但是我们再去重建目标main.o 文件的时候,发现没有找到指定的 main.c 文件,这是错误的根本原因。

这个时候我们就应该添加上路径搜索,我们知道路径搜索的方法有两个:VPATH 和 vpath。我们先来使用一下 VPATH,使用方式很简单,我们只需要在上述的文件开头加上这样一句话:

VPATH=src include

再去执行 make 就不会出现错误。所以 Makefile 中的最终写法是这样的:

VPATH=src include
main:main.o list1.o list2.o
gcc -o $@ $<
main.o:main.c
gcc -o $@ $^
list1.o:list1.c list1.h
gcc -o $@ $<
list2.o:list2.c list2.h
gcc -o $@ $<

我们使用 vpath 的话同样可以解决这样的问题,只需要把上述代码中的 VPATH 所在行的代码改写成:

vpath %.c src
vpath %.h include

这样我们就可以用 vpath 实现功能,代码的最终展示为:

vpath %.c src
vpath %.h include
main:main.o list1.o list2.o
gcc -o $@ $<
main.o:main.c
gcc -o $@ $^
list1.o:list1.c list1.h
gcc -o $@ $<
list2.o:list2.c list2.h
gcc -o $@ $<

Makefile隐含规则

这个章节讲述的是 Makefile 的隐含规则,所谓的隐含规则就是需要我们做出具体的操作,系统自动完成。编写 Makefile 的时候,可以使用隐含规则来简化Makefile 文件编写。

实例:
  1. test:test.o
  2. gcc -o test test.o
  3. test.o:test.c

我们可以在 Makefile 中这样写来编译 test.c 源文件,相比较之前少写了重建 test.o 的命令。但是执行 make,发现依然重建了 test 和 test.o 文件,运行结果却没有改变。这其实就是隐含规则的作用。在某些时候其实不需要给出重建目标文件的命令,有的甚至可以不需要给出规则。实例:

  1. test:test.o
  2. gcc -o test test.o

运行的结果是相同的。

注意:隐含条件只能省略中间目标文件重建的命令和规则,但是最终目标的命令和规则不能省略。

隐含规则的具体的工作流程:make 执行过程中找到的隐含规则,提供了此目标的基本依赖关系。确定目标的依赖文件和重建目标需要使用的命令行。隐含规则所提供的依赖文件只是一个基本的(在C语言中,通常他们之间的对应关系是:test.o 对应的是 test.c 文件)。当需要增加这个文件的依赖文件的时候要在 Makefile 中使用没有命令行的规则给出。实例:

  1. test:test.o
  2. gcc -o test test.o
  3. test:test1.h

其实在有些时候隐含规则的使用会出现问题。因为有一个 make 的“隐含规则库”。库中的每一条隐含规则都有相应的优先级顺序,优先级也就会越高,使用时也就会被优先使用。

例如在 Makefile 中添加这行代码:

foo.o:foo.p

我们都知道 .p 文件是 Pascal 程序的源文件,如果书写规则时不加入命令的话,那么 make 会按照隐含的规则来重建目标文件 foo.o。如果当前目录下恰好存在 foo.c 文件的时候,隐含规则会把 foo.c 当做是 foo.o 的依赖文件进行目标文件的重建。因为编译 .c 文件的隐含规则在编译 .p 文件之前,显然优先级也会越高。当 make 找到生成 foo.o 的文件之后,就不会再去寻找下一条规则。如果我们不想使用隐含规则,在使用的时候不仅要声明规则,也要添加上执行的命令。

这里讲的是预先设置的隐含规则。如果不明确的写下规则,那么make 就会自己寻找所需要的规则和命令。当然我们也可以使用 make 选项:-r-n-builtin-rules选项来取消所有的预设值的隐含规则。当然即使是指定了“-r”的参数,某些隐含规则还是会生效。因为有很多的隐含规则都是使用了后缀名的规则来定义的,所以只要隐含规则中含有“后缀列表”那么隐含规则就会生效。默认的列表是:

.out、.a、.in、.o、.c、.cc、.C、.p、.f、.F、.r、.y、.l、.s、.S、.mod、.sym、.def、
.h、.info、.dvi、.tex、.texinfo、.texi、.txinfo、.w、.ch、.web、.sh、.elc、.el。

下面是一些常用的隐含规则:

  • 编译 C 程序
  • 编译 C++ 程序
  • 编译 Pascal 程序
  • 编译 Fortran/Ratfor 程序
  • 预处理 Fortran/Ratfor 程序
  • 编译 Modula-2 程序
  • 汇编和需要预处理的汇编程序
  • 链接单一的 object 文件
  • Yacc C 程序
  • Lex C 程序时的隐含规则

上面的编译顺序都是一些常用的编程语言执行隐含规则的顺序,我们在 Makefile 中指定规则时,可以参考这样的列表。当需要编译源文件的时候,考虑是不是需要使用隐含规则。如果不需要,就要把相应的规则和命令全部书写上去。

内嵌隐含规则的命令中,所使用的变量都是预定义的。我们将这些变量称为“隐含变量”。这些变量允许修改:可以通过命令行参数传递或者是设置系统环境变量的方式都可以对它进行重新定义。无论使用哪种方式,只要 make 在运行的,这些变量的定义有效。Makefile 的隐含规则都会使用到这些变量。

比如我们编译 .c 文件在我们的 Makefile 中就是隐含的规则,默认使用到的编译命令时cc,执行的命令时cc -c我们可以对用上面的任何一种方式将CC定义为ncc。这样我们就编译 .c 文件的时候就可以用ncc进行编译。

隐含规则中使用的变量可以分成两类:
1.代表一个程序的名字。例如:“CC”代表了编译器的这个可执行程序。
2.代表执行这个程序使用的参数.例如:变量“CFLAGS”。多个参数之间使用空格隔开。

下面我们来列举一下代表命令的变量,默认都是小写。

  • AR:函数库打包程序,科创价静态库 .a 文档。
  • AS:应用于汇编程序。
  • CC:C 编译程序。
  • CXX:C++编译程序。
  • CO:从 RCS 中提取文件的程序。
  • CPP:C程序的预处理器。
  • FC:编译器和与处理函数 Fortran 源文件的编译器。
  • GET:从CSSC 中提取文件程序。
  • LEX:将Lex语言转变为 C 或 Ratfo 的程序。
  • PC:Pascal 语言编译器。
  • YACC:Yacc 文法分析器(针对于C语言)
  • YACCR:Yacc 文法分析器。

Makefile ifeq、ifneq、ifdef和ifndef(条件判断)

 日常使用 Makefile 编译文件时,可能会遇到需要分条件执行的情况,比如在一个工程文件中,可编译的源文件很多,但是它们的类型是不相同的,所以编译文件使用的编译器也是不同的。手动编译去操作文件显然是不可行的(每个文件编译时需要注意的事项很多),所以 make 为我们提供了条件判断来解决这样的问题。

需要解决的问题:要根据判断,分条件执行语句。
条件语句的作用:条件语句可以根据一个变量的值来控制 make 执行或者时忽略 Makefile 的特定部分,条件语句可以是两个不同的变量或者是常量和变量之间的比较。

条件语句使用优点:Makefile 中使用条件控制可以做到处理的灵活性和高效性。

注意:条件语句只能用于控制 make 实际执行的 Makefile 文件部分,不能控制规则的 shell 命令执行的过程。

下面是条件判断中使用到的一些关键字:

关键字 功能
ifeq 判断参数是否不相等,相等为 true,不相等为 false。
ifneq 判断参数是否不相等,不相等为 true,相等为 false。
ifdef 判断是否有值,有值为 true,没有值为 false。
ifndef 判断是否有值,没有值为 true,有值为 false。

ifeq 和 ifneq

条件判断的使用方式如下:

ifeq (ARG1, ARG2)
ifeq 'ARG1' 'ARG2'
ifeq "ARG1" "ARG2"
ifeq "ARG1" 'ARG2'
ifeq 'ARG1' "ARG2"

实例:

  1. libs_for_gcc= -lgnu
  2. normal_libs=
  3. foo:$(objects)
  4. ifeq($(CC),gcc)
  5. $(CC) -o foo $(objects) $(libs_for_gcc)
  6. else
  7. $(CC) -o foo $(objects) $(noemal_libs)
  8. endif

条件语句中使用到三个关键字“ifeq”、“else”、“endif”。其中:“ifeq”表示条件语句的开始,并指定一个比较条件(相等)。括号和关键字之间要使用空格分隔,两个参数之间要使用逗号分隔。参数中的变量引用在进行变量值比较的时候被展开。“ifeq”,后面的是条件满足的时候执行的,条件不满足忽略;“else”表示当条件不满足的时候执行的部分,不是所有的条件语句都要执行此部分;“endif”是判断语句结束标志,Makefile 中条件判断的结束都要有。

其实 "ifneq" 和 "ifeq" 的使用方法是完全相同的,只不过是满足条件后执行的语句正好相反。

上面的例子可以换一种更加简介的方式来写:

  1. libs_for_gcc= -lgnu
  2. normal_libs=
  3. ifeq($(CC),gcc)
  4. libs=$(libs_for_gcc)
  5. else
  6. libs=$(normal_libs)
  7. endif
  8. foo:$(objects)
  9. $(CC) -o foo $(objects) $(libs)

ifdef 和 ifndef

使用方式如下:

ifdef VARIABLE-NAME

它的主要功能是判断变量的值是不是为空,实例:

实例 1:

  1. bar =
  2. foo = $(bar)
  3. all:
  4. ifdef foo
  5. @echo yes
  6. else
  7. @echo no
  8. endif

实例 2:

  1. foo=
  2. all:
  3. ifdef foo
  4. @echo yes
  5. else
  6. @echo no
  7. endif

通过两个实例对比说明:通过打印 "yes" 或 "no" 来演示执行的结果。我们执行 make 可以看到实例 1打印的结果是 "yes" ,实例 2打印的结果是 "no" 。其原因就是在实例 1 中,变量“foo”的定义是“foo = $(bar)”。虽然变量“bar”的值为空,但是“ifdef”的判断结果为真,这种方式判断显然是有不行的,因此当我们需要判断一个变量的值是否为空的时候需要使用“ifeq" 而不是“ifdef”。

注意:在 make 读取 Makefile 文件时计算表达式的值,并根据表达式的值决定判断语句中的哪一个部分作为此 Makefile 所要执行的内容。因此在条件表达式中不能使用自动化变量,自动化变量在规则命令执行时才有效,更不能将一个完整的条件判断语句分卸在两个不同的 Makefile 的文件中。在一个 Makefile 中使用指示符 "include" 包含另一个 Makefile 文件。

Makefile伪目标

 这一个章节我们主要讲的是 Makefile 中的伪目标。所谓的伪目标可以这样来理解,它并不会创建目标文件,只是想去执行这个目标下面的命令。伪目标的存在可以帮助我们找到命令并执行。

使用伪目标有两点原因:
  • 避免我们的 Makefile 中定义的只执行的命令的目标和工作目录下的实际文件出现名字冲突。
  • 提高执行 make 时的效率,特别是对于一个大型的工程来说,提高编译的效率也是我们所必需的。

我们先来看一下第一种情况的使用。如果需要书写这样一个规则,规则所定义的命令不是去创建文件,而是通过 make 命令明确指定它来执行一些特定的命令。实例:

  1. clean:
  2. rm -rf *.o test

规则中 rm 命令不是创建文件 clean 的命令,而是执行删除任务,删除当前目录下的所有的 .o 结尾和文件名为 test 的文件。当工作目录下不存在以 clean 命令的文件时,在 shell 中输入 make clean 命令,命令 rm -rf *.o test 总会被执行 ,这也是我们期望的结果。
 
如果当前目录下存在文件名为  clean 的文件时情况就会不一样了,当我们在 shell 中执行命令 make clean,由于这个规则没有依赖文件,所以目标被认为是最新的而不去执行规则所定义的命令。因此命令 rm 将不会被执行。为了解决这个问题,删除 clean 文件或者是在 Makefile 中将目标 clean 声明为伪目标。将一个目标声明称伪目标的方法是将它作为特殊的目标.PHONY的依赖,如下:

.PHONY:clean

这样 clean 就被声明成一个伪目标,无论当前目录下是否存在 clean 这个文件,当我们执行 make clean 后 rm 都会被执行。而且当一个目标被声明为伪目标之后,make 在执行此规则时不会去试图去查找隐含的关系去创建它。这样同样提高了 make 的执行效率,同时也不用担心目标和文件名重名而使我们的编译失败。

在书写伪目标的时候,需要声明目标是一个伪目标,之后才是伪目标的规则定义。目标 "clean" 的完整书写格式如下:

  1. .PHONY:clean
  2. clean:
  3. rm -rf *.o test

伪目标的另一种使用的场合是在 make 的并行和递归执行的过程中,此情况下一般会存在一个变量,定义为所有需要 make 的子目录。对多个目录进行 make 的实现,可以在一个规则的命令行中使用 shell 循环来完成。如下:

  1. SUBDIRS=foo bar baz
  2. subdirs:
  3. for dir in $(SUBDIRS);do $(MAKE) -C $$dir;done

代码表达的意思是当前目录下存在三个子文件目录,每个子目录文件都有相对应的 Makefile 文件,代码中实现的部分是用当前目录下的 Makefile 控制其它子模块中的 Makefile 的运行,但是这种实现方法存在以下几个问题:

  • 当子目录执行 make 出现错误时,make 不会退出。就是说,在对某个目录执行 make 失败以后,会继续对其他的目录进行 make。在最终执行失败的情况下,我们很难根据错误提示定位出具体实在那个目录下执行 make 发生的错误。这样给问题定位造成很大的困难。为了解决问题可以在命令部分加入错误检测,在命令执行的错误后主动退出。不幸的是如果在执行 make 时使用了 "-k" 选项,此方式将失效。
  • 另外一个问题就是使用这种 shell 循环方式时,没有用到 make 对目录的并行处理功能由于规则的命令时一条完整的 shell 命令,不能被并行处理。

有了伪目标之后,我们可以用它来克服以上方式所存在的两个问题,代码展示如下:

  1. SUBDIRS=foo bar baz
  2. .PHONY:subdirs $(SUBDIRS)
  3. subdirs:$(SUBDIRS)
  4. $(SUBDIRS):
  5. $(MAKE) -C $@
  6. foo:baz

上面的实例中有一个没有命令行的规则“foo:baz”,这个规则是用来规定三个子目录的编译顺序。因为在规则中 "baz" 的子目录被当作成了 "foo" 的依赖文件,所以 "baz" 要比 "foo" 子目录更先执行,最后执行 "bar" 子目录的编译。

一般情况下,一个伪目标不作为另外一个目标的依赖。这是因为当一个目标文件的依赖包含伪目标时,每一次在执行这个规则伪目标所定义的命令都会被执行(因为它作为规则的依赖,重建规则目标时需要首先重建规则的所有依赖文件)。当一个伪目标没有任何目标(此目标是一个可被创建或者是已存在的文件)的依赖时,我们只能通过 make 的命令来明确的指定它的终极目标,执行它所在规则所定义的命令。例如 make clean。

伪目标实现多文件编辑

如果在一个文件里想要同时生成多个可执行文件,我们可以借助伪目标来实现。使用方式如下:

  1. .PHONY:all
  2. all:test1 test2 test3
  3. test1:test1.o
  4. gcc -o $@ $^
  5. test2:test2.o
  6. gcc -o $@ $^
  7. test3:test3.o
  8. gcc -o $@ $^

我们在当前目录下创建了三个源文件,目的是把这三个源文件编译成为三个可执行文件。将重建的规则放到 Makefile 中,约定使用 "all" 的伪目标来作为最终目标,它的依赖文件就是要生成的可执行文件。这样的话只需要一个 make 命令,就会同时生成三个可执行文件。

之所以这样写,是因为伪目标的特性,它总会被执行,所以它依赖的三个文件的目标就不如 "all" 这个目标新,所以,其他的三个目标的规则总是被执行,这也就达到了我们一口气生成多个目标的目的。我们也可以实现单独的编译这三个中的任意一个源文件(我们想去重建 test1,我们可以执行命令make test1 来实现 )。 

PHONY伪目标实践

GOPATH:=$(shell go env GOPATH)

PB_PATH=./pb
GEN_PATH=MSK/console/${gen} # 协议生成,输出到指定项目位置gen=path
# make proto gen=api/genproto
# make proto gen=account/genproto
.PHONY: proto
proto:
rm -rf ${gen}/*
protoc --proto_path=${GOPATH}/src:${PB_PATH} \
--micro_out=srv/account.proto=${GEN_PATH}/srv,github.com/micro/go-micro/api/proto/api.proto=github.com/micro/go-micro/v3/api/proto:${gen} \
--go_out=srv/account.proto=${GEN_PATH}/srv,github.com/micro/go-micro/api/proto/api.proto=github.com/micro/go-micro/v3/api/proto:${gen} \
--validate_out="lang=go:${gen}" \
api/account.proto
protoc --proto_path=${GOPATH}/src:${PB_PATH} \
--micro_out=${gen} \
--go_out=:${gen} \
--validate_out="lang=go:${gen}" \
srv/account.proto # Go编译
.PHONY: build
build:
cd account && make build
cd api && make build
cd web && make vue build COMPOSE_FILE=docker-compose.yml # 启动服务,首次需要build
.PHONY: start
start:
docker-compose -f ${COMPOSE_FILE} -p $(p) build --no-cache
docker-compose --verbose -f ${COMPOSE_FILE} -p $(p) up .PHONY: stop
stop:
docker-compose --verbose -f ${COMPOSE_FILE} -p $(p) down # 重新编译并启动服务
.PHONY: restart
restart:
docker-compose -f ${COMPOSE_FILE} -p $(p) stop $(s)
cd $(s) && make build
docker-compose -f ${COMPOSE_FILE} -p $(p) build --no-cache $(s)
docker-compose -f ${COMPOSE_FILE} -p $(p) up -d --no-deps --force-recreate $(s) .PHONY: restart_service
restart_service:
docker-compose -f ${COMPOSE_FILE} -p $(p) stop $(s)
docker-compose -f ${COMPOSE_FILE} -p $(p) up -d --no-deps --force-recreate $(s) $ make proto3 gen=account/genproto
make: *** No rule to make target 'proto3'. Stop.

  

参数传递 伪目标中一行一条命令

$ make proto gen=account/genproto
rm -rf account/genproto/*
protoc --proto_path=/home/shawn/go/src:./pb \
--micro_out=Msrv/account.proto=MSK/console/account/genproto/srv,Mgithub.com/micro/go-micro/api/proto/api.proto=github.com/micro/go-micro/v3/api/proto:account/genproto \
--go_out=Msrv/account.proto=MSK/console/account/genproto/srv,Mgithub.com/micro/go-micro/api/proto/api.proto=github.com/micro/go-micro/v3/api/proto:account/genproto \
--validate_out="lang=go:account/genproto" \
api/account.proto
2020/09/23 16:52:00 WARNING: Missing 'go_package' option in "api/account.proto",
please specify it with the full Go package path as
a future release of protoc-gen-go will require this be specified.
See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information. protoc --proto_path=/home/shawn/go/src:./pb \
--micro_out=account/genproto \
--go_out=:account/genproto \
--validate_out="lang=go:account/genproto" \
srv/account.proto
2020/09/23 16:52:00 WARNING: Missing 'go_package' option in "srv/account.proto",
please specify it with the full Go package path as
a future release of protoc-gen-go will require this be specified.
See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information.

  

make proto gen=account/genprotorm -rf account/genproto/*protoc --proto_path=/home/shawn/go/src:./pb \--micro_out=Msrv/account.proto=MSK/console/account/genproto/srv,Mgithub.com/micro/go-micro/api/proto/api.proto=github.com/micro/go-micro/v3/api/proto:account/genproto \--go_out=Msrv/account.proto=MSK/console/account/genproto/srv,Mgithub.com/micro/go-micro/api/proto/api.proto=github.com/micro/go-micro/v3/api/proto:account/genproto \--validate_out="lang=go:account/genproto" \api/account.proto2020/09/23 16:52:00 WARNING: Missing 'go_package' option in "api/account.proto",please specify it with the full Go package path asa future release of protoc-gen-go will require this be specified.See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information.
protoc --proto_path=/home/shawn/go/src:./pb \--micro_out=account/genproto \--go_out=:account/genproto \--validate_out="lang=go:account/genproto" \srv/account.proto2020/09/23 16:52:00 WARNING: Missing 'go_package' option in "srv/account.proto",please specify it with the full Go package path asa future release of protoc-gen-go will require this be specified.See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information.