原址

简易计算器的编译

课程简介

实验简介

Makefile 是一种描述工程编译、链接的文件。在一个庞大的项目或工程中,往往存在非常复杂的编译和链接流程,而 Makefile 文件可以描述哪些源文件在何时需要编译,如何编译这些源文件,甚至可以调用 shell 和其它的工具来执行更加复杂的项目构建流程。一旦 Makefile 文件构建完毕,用户只需要使用 GNU make 工具读入 Makefile 即可完成整个工程的编译和链接流程,极大提高了项目开发和测试的效率。

本系列的实验主要是让大家学习 Makefile 的基本规则。在正式讲述 make 工具的使用方式和 Makefile 书写规则之前,本次实验先介绍一些简单的前导知识,这也是 GNU make 官方手册中采用的教学模式。本次实验用于演示 GNU GCC 编译和链接的基本方法,通过编译、链接、静态链接、动态链接让用户学习和理解 GCC 的使用方式。另一方面,用户也将在实验过程中体验手动编译链接的效率,从而理解自动编译的在项目工程管理中的重要性。

  • 编写基本代码
  • 对代码进行编译,链接,并执行查看效果
  • 添加代码扩展功能,并进行静态链接
  • 添加代码扩展功能,并进行动态链接
  • 使用静态+动态的混合链接

    实验知识点

    本课程项目完成过程中将学习:

  • GCC 编译的使用方式

  • GCC 链接的使用方式
  • GCC 静态链接的使用方式
  • GCC 动态链接的使用方式
  • GCC 静态链接 + 动态链接混用的方式

    实验环境

  • Xfce 终端

  • GCC
  • Gedit

    适合人群

    本课程难度为一般,属于初级级别课程,适合有代码编写能力的用户,熟悉和掌握GCC的一般用法。

    代码获取

    你可以通过下面命令将本课程里的所有源代码下载到实验楼环境中,作为参照对比进行学习。
    1. $ wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip
    请尽量按照实验步骤自己写出 C 语言程序。

实验步骤

本章节的源代码位于 /home/shiyanlou/Code/make_example/chapter0/目录下。

  • 项目文件结构:

    main.c : 主要文件 add_minus.c add_minus.h: 加减法API及实现 multi_div.c multi_div.h : 乘除法API及实现

这一章节我们将正式开始讲解简易计算器的编译的实验,分步骤进行。

编译,链接和执行Hello Cacu

查看main.c文件,内容如下:

  1. #include <stdio.h>
  2. int main(void)
  3. {
  4. printf("Hello Cacu!\n");
  5. return 0;
  6. }
  • 只编译不链接 main.o

执行命令:

  1. gcc -c main.c

可以发现当前文件夹下多了一个 main.o 文件

  • 使用 file 查看 main.o 的格式,并尝试执行

执行:

  1. file main.o

会打印出 log:“main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped”
表明 main.o 实际上是一个 relocatable 文件。

修改 main.o 的文件属性为可执行:

  1. chmod 777 main.o

再尝试执行main.o文件:

  1. ./main.o

会出现错误:“zsh: exec format error: ./main.o” 实际上relocatable文件是不可执行的

  • 对main.o进行链接,并尝试执行

那么怎样才能生成可执行文件呢? 可执行文件需要通过链接来生成.
使用 gcc 将 main.o 链接为 main 文件:

  1. gcc -o main main.o

可以发现文件夹下多了一个 main 文件。
用 file 查看 main 文件格式:

  1. file main

会打印出 log:“main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=3753fcc57530a2eb08e63879f8363013bef5d161, not stripped” 此时文件类型已经变更为 "executable",执行此文件:

  1. ./main

可以看到有log印出“Hello Cacu!”。 这正是我们main.c里希望打印的语句,说明文件被正常执行。 感兴趣的同学也可以使用readelf工具查看main文件的更多细节。 实验截图如下: Makefile 基础入门 - 图1

为 Cacu 增加加减法并链接执行

  • 添加 add_minus.h文件,声明 add()minus()

源代码已有 add_minus.h 文件,文件内容为:

  1. #ifndef __ADD_MINUS_H__
  2. #define __ADD_MINUS_H__
  3. int add(int a, int b);
  4. int minus(int a, int b);
  5. #endif /*__ADD_MINUS_H__*/
  • 添加 add_minus.c文件,实现add()minus()

源代码已有 add_minus.c 文件,文件内容为:

  1. #include "add_minus.h"
  2. int add(int a, int b)
  3. {
  4. return a+b;
  5. }
  6. int minus(int a, int b)
  7. {
  8. return a-b;
  9. }
  • 编译生成 add_minus.o

执行:

  1. gcc -c add_minus.c

会生成文件add_minus.o

  • 修改 main.c,增加加减法运算并编译

给 main.c 打上 patch v1.0:

  1. patch -p2 < v1.0.patch

打完 patch 后 main.c 内容如下:

  1. #include <stdio.h>
  2. #include "add_minus.h"
  3. int main(void)
  4. {
  5. int rst;
  6. printf("Hello Cacu!\n");
  7. rst = add(3,2);
  8. printf("3 + 2 = %d\n",rst);
  9. rst = minus(3,2);
  10. printf("3 - 2 = %d\n",rst);
  11. return 0;
  12. }

编译 main.c:

  1. gcc -c main.c

链接main.o:

  1. gcc -o main main.o

链接过程会出现错误:

  1. main.o: In function `main':
  2. main.c:(.text+0x1f): undefined reference to `add'
  3. main.c:(.text+0x47): undefined reference to `minus'
  4. collect2: error: ld returned 1 exit status

这是因为链接时,找不到 addminus这两个symbol导致的。

  • main.oadd_minus.o链接成可执行文件并执行测试

现在将 add_minus.o 也一起链接进来:

  1. gcc -o main main.o add_minus.o

目录下会重新生成main文件,执行:

  1. ./main

会有如下打印:

  1. Hello Cacu!
  2. 3 + 2 = 5
  3. 3 - 2 = 1

说明程序正常执行,实验截图如下: Makefile 基础入门 - 图2

将 Cacu 的加减法做成静态库,并静态链接执行

  • 重新编译add_minus.c生成静态库文件

重新编译 add_minus.c文件:

  1. gcc -c add_minus.c

add_minus.o 打包到静态库中:

  1. ar rc libadd_minus.a add_minus.o

将会生成libadd_minus.a 静态库文件 使用 file 查看libadd_minus.a

  1. file libadd_minus.a

可以看到说明:“libadd_minus.a: current ar archive” 实际上libxxx.a只是将指定的.o文件打包汇集在一起,它的本质上还是 relocatable文件集合。

  • 链接main.o和静态库文件并执行

执行:

  1. gcc -o main2 main.o -L./ -ladd_minus

说明1:-L./表明库文件位置在当前文件夹
说明2: -ladd_minus 表示链接 libadd_minus.a 文件,使用“-l”参数时,前缀“lib”和后缀“.a”是需要省略的。

执行:

  1. ./main2

会有如下log打印:

  1. Hello Cacu!
  2. 3 + 2 = 5
  3. 3 - 2 = 1
  4. copy

说明程序得到正确执行,实验截图如下: Makefile 基础入门 - 图3

库,并动态执行


  • 添加multi_div.h文件,声明multi()div()

源代码已有multi_div.h 文件,文件内容为:

  1. #ifndef __MULTI_DIV_H__
  2. #define __MULTI_DIV_H__
  3. int multi(int a, int b);
  4. int div(int a, int b);
  5. #endif /*__MULTI_DIV_H__*/
  • 添加multi_div.c文件,实现 multi()div()

源代码已有multi_div.c 文件,文件内容为:

  1. #include "multi_div.h"
  2. int multi(int a, int b)
  3. {
  4. return a*b;
  5. }
  6. int div(int a, int b)
  7. {
  8. return a/b;
  9. }
  • multi_div.c编译成动态链接库

执行:

  1. gcc multi_div.c -fPIC -shared -o libmulti_div.so

生成 libmulti_div.so文件
使用file查看 libmulti_div.so

  1. file libmulti_div.so

可得到文件格式:“libmulti_div.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=2334680eed2923cb153d687fd0605d320f7fb8a2, not stripped” 即表明 libmulti_div.so是一个 shared object 文件。

  • 修改 main.c,注释加减运算,新增乘除运算并编译

先还原 main.c 文件:

  1. git checkout main.c

main.c 打上 patch v2.0

  1. patch -p2 < v2.0.patch

打完patchmain.c内容如下:

  1. #include <stdio.h>
  2. /*
  3. #include "add_minus.h"
  4. */
  5. #include "multi_div.h"
  6. int main(void)
  7. {
  8. int rst;
  9. printf("Hello Cacu!\n");
  10. /*
  11. rst = add(3,2);
  12. printf("3 + 2 = %d\n",rst);
  13. rst = minus(3,2);
  14. printf("3 - 2 = %d\n",rst);
  15. */
  16. rst = multi(3,2);
  17. printf("3 * 2 = %d\n",rst);
  18. rst = div(6,2);
  19. printf("6 / 2 = %d\n",rst);
  20. return 0;
  21. }

编译 main.c 生成 main.o:

  1. gcc -c main.c
  • 将 main.o 与动态链接库进行链接并执行

我们已经知道链接时需要指定库文件,否则会找不到 symbol

因此需要执行如下命令:

  1. gcc -o main3 main.o -L./ -lmulti_div

现在执行 main3 文件:

  1. ./main3

会打印错误:“./main3: error while loading shared libraries: libmulti_div.so: cannot open shared object file: No such file or directory”

这是因为我们生成的动态库 libmulti_div.so 并不在库文件搜索路径中,解决方法可以二选一: 方法一:将 libmulti_div.so copy/lib//usr/lib/ 下。 方法二:在 LD_LIBRARY_PATH 变量中指定库文件路径,如我的库文件存放在“/home/shiyanlou/Code/make_example/chapter0/”下,则执行:

  1. export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/shiyanlou/Code/make_example/chapter0/

此例使用方法二,修改LD_LIBRARY_PATH环境变量后,再次执行main3:./main3
会打印如下log:

  1. Hello Cacu!
  2. 3 * 2 = 6
  3. 6 / 2 = 3

说明程序得到正确执行,实验截图如下: Makefile 基础入门 - 图4

合链接并测试

  • 修改 main.c 加回加减运算,并编译

现在测试完成的加减乘除运算,先还原 main.c 文件:

  1. git checkout main.c

为 main.c 打上 patch v3.0:

  1. patch -p2 < v3.0.patch

编译 main.c 得到 main.o:

  1. gcc -c main.c
  • 测试混用静态链接和动态链接的方式并执行

同时链接两个动态库文件:

  1. gcc -o main4 main.o -L./ -ladd_minus -lmulti_div

由于我们之前已经修改过 LD_LIBRARY_PATH变量,此次无需再次修改

执行main4:

  1. ./main4

打印如下log:

  1. Hello Cacu!
  2. 3 + 2 = 5
  3. 3 - 2 = 1
  4. 3 * 2 = 6
  5. 6 / 2 = 3

说明程序得到正确执行,实验截图如下: Makefile 基础入门 - 图5

实验总结

本实验说明了 GCC 基本编译,链接的方法。 学员在修改和测试代码的过程中需要反复执行编译和链接动作,由此产生基本的自动化编译需求。

课后习题

  • 请思考和验证若静态库和动态库名称关键字相同,如: 静态库名称:libxxx.a 动态库名称:libxxx.so 二者的链接优先级如何?如何指定链接其中之一?
  • 请按照本课程的实验步骤自行编写 script 进行自动编译.
  • 并思考用 script 的优点和缺陷.

Makefile 基础规则(1)

课程简介

实验简介

上次实验介绍了对不同源代码文件进行编译、链接生成可执行文件的基本过程,有了这些前导知识作为基础之后, 就可以开始学习makefile的基础规则了。 首先,我们已经知道makefile作为工程管理文件可以提供工程下各个源代码的编译、链接规则。 GNU make 工具可以读入makefile并解析其中的规则,并自动对工程进行编译链接,提高项目开发的效率。

那么,makefile到底如何实现对工程编译、链接的管理呢?本实验将通过介绍makefile的基础规则来回答这个问题。

  • makefile基本规则。
  • makefile时间戳检验测试。
  • 验证makefile依赖文件的执行顺序。
  • 变量,PHONY和“-”功能测试。
  • makefile文件命名规则。
  • 编写一段程序的makefile文件。

    实验知识点

    本课程项目完成过程中将学习:

  • makefile 的基本编译规则

  • make 更新目标的依据
  • makefile 目标依赖的执行顺序
  • makefile 变量的赋值与使用
  • .PHONY 的作用
  • “-” 的作用
  • make 搜寻 makefile的命名规则

    实验环境

  • Xfce 终端

  • GCC
  • Gedit

    适合人群

    本课程难度为简单,属于入门级别课程,适合有代码编写能力的用户,熟悉和掌握make的一般用法。

    代码获取

    你可以通过下面命令将本课程里的所有源代码下载到实验楼环境中,作为参照对比进行学习。

    1. $ wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip

    请尽量按照实验步骤自己写出 C 语言程序。

    实验步骤

    本章节的源代码位于 /home/shiyanlou/Code/make_example/chapter1/目录下。

  • 项目文件结构:

    main.c:主要的 C 语言源代码。 makefile:make工程文件。

makefile 基本规则

  • 编写 main.c 源文件

实验中将用“hello world!”程序来验证makefile的基本规则,因此先编写一段小程序main.c。 源代码中已有main.c文件,代码如下:

  1. #include <stdio.h>
  2. int main(void)
  3. {
  4. printf("hello world!\n");
  5. return 0;
  6. }
  • 熟悉makefile的基础规则

makefile 是为了自动管理编译、链接流程而存在的。
makefile 的基本书写规则如下:

  1. TARGET... : PREREQUISITES...
  2. COMMAND

TARGET:规则目标,可以为文件名或动作名 PREREQUISITES:规则依赖 COMMAND:命令行,必须以[TAB]开始,由shell执行

  • 编写简单的makefile文件管理main.c的编译

源代码中已有makefile文件,内容如下:

  1. # this is a makefile example
  2. main:main.o
  3. gcc -o main main.o
  4. main.o:main.c
  5. gcc -c main.c

line1:“#”为注释符号,后面接注释文本。 line3 - line4:声明目标 main 的依赖文件 main.o 及链接 command。 line6 - line7:声明目标 main.o 的编译command。

  • 测试make命令

make工具的基本使用方法为:make TARGET。 在终端输入命令:

  1. make main.o

可以看见shell会执行:

  1. gcc -c main.c

接下来输入:

  1. make main

可以看见shell执行:

  1. gcc -o main main.o

执行main文件:

  1. ./main

终端会打印:

  1. hello world!

说明程序正常执行。

  • 自动化编译目标

清除掉main.omain文件:

  1. rm main.o main

由于我们的最终的目标是main文件,实际上我们并不关心中间目标“main.o”。 现在尝试只运行一次make编译出我们需要的最终目标。

  1. make main

终端会打印出 make 实际执行的命令:

  1. gcc -c main.c
  2. gcc -o main main.o

可见make还是先生成makefilemain的依赖文件main.o,再链接生成main文件。

  • make自动寻找目标

再次清除掉main.omain文件:

  1. rm main.o main

并执行 make,但不输入目标:

  1. make

终端打印出 make 的执行命令还是一样:

  1. gcc -c main.c
  2. gcc -o main main.o

这是因为默认情况下,make 会以第一条规则作为其“终极目标”。 现在我们尝试修改makefile,在目标“main”之前再增加一条规则:

  1. dft_test:middle_file
  2. mv middle_file dft_test
  3. middle_file:
  4. touch middle_file

执行:

  1. make

可以看见终端印出:

  1. touch middle_file
  2. mv middle_file dft_test

当前文件夹下会多出一个dft_test文件。

实验过程如下图所示: Makefile 基础入门 - 图6 Makefile 基础入门 - 图7 Makefile 基础入门 - 图8

makefile 时间戳检验测试

  • 文件的时间戳检测规则

make在执行命令时会检测依赖文件的时间戳:

  1. 若依赖文件不存在或者依赖文件的时间戳比目标文件新,则执行依赖文件对应的命令。
  2. 若依赖文件的时间戳比目标文件老,则忽略依赖文件对应的命令。
  • 文件时间戳测试

还原makefile文件,并打上v1.0补丁:

  1. git checkout makefile
  2. patch -p2 < v1.0.patch

此时makefile文件内容如下:

  1. #this is a makefile example
  2. main:main.o testa testb
  3. gcc -o main main.o
  4. main.o:main.c
  5. gcc -c main.c
  6. testa:
  7. touch testa
  8. testb:
  9. touch testb

清除可能存在的中间文件:

  1. rm main.o testa testb

执行 make:

  1. make

终端会输出:

  1. gcc -c main.c
  2. touch testa
  3. touch testb
  4. gcc -o main main.o

make 会分别生成main.otestatestb这三个中间文件。这验证了2.1中说明的第一条特性。 现在删除testb文件,再看看make会如何执行:

  1. rm testb
  2. make

终端打印:

  1. touch testb
  2. gcc -o main main.o

可见make分别执行了testbmain两条规则,main.otesta规则对应的命令没有被执行到。 这验证了时间戳检测规则的第二条特性。

实验过程如下图所示: Makefile 基础入门 - 图9

实验 makefile 依赖文件的执行顺序

从上述实验可以看出make目标文件的依赖文件是按照从左到右的顺序生成的。 对应规则“main”:

  1. main:main.o testa testb
  2. gcc -o main main.o

make 按照顺序分别执行main.o testa testb所对应的规则。 现在我们调换main.o testa testb的顺序。

修改makefile文件的main规则的依赖顺序:

  1. main:testb testa main.o

清除上次编译过程中产生的中间文件:

  1. rm main.o testa testb

执行 make:

  1. make

终端有如下打印:

  1. touch testb
  2. touch testa
  3. gcc -c main.c
  4. gcc -o main main.o

可见make的确是按照从左到右的规则分别执行依赖文件对应的命令。

变量,.PHONY 和“-”功能测试

  • makefile的变量定义

makefile 也可以使用变量,它类似于 C 语言中的宏定义。 变量可以直接使用“vari=string”的写法来定义,并以“$(vari)”格式来使用。 我们用变量来定义目标的依赖项,使makefile保持良好的扩展性。

  • makefile中添加变量并使用

先还原makefile文件到v1.0补丁,并清除上一次编译的中间文件。

  1. git checkout makefile
  2. patch -p2 < v1.0.patch
  3. rm main.o testa testb

在目标“main”之前定义一个变量“depen”

  1. depen=main.o testa testb

修改 main 目标的依赖项声明:

  1. main:$(depen)

执行 make :

  1. make

终端打印:

  1. gcc -c main.c
  2. touch testa
  3. touch testb
  4. gcc -o main main.o

可见makefile还是能够正常执行。 之后main目标的依赖项有变化时,只需修改“depen”变量即可。

  • makefile添加clean规则

每次测试makefile的时候我们都要清除中间文件,为了使得编译工程更加自动化,我们在makefile中添加规则让其自动清除。 在makefile中修改depen变量,增加clean依赖:

  1. depen=clean main.o testa testb

增加clean规则及其命令:

  1. rm main.o testa testb

当前目录下是存在main.o testa testb三个中间文件的,执行make看看效果:

  1. make

可以看见终端打印:

  1. rm main.o testa testb
  2. gcc -c main.c
  3. touch testa
  4. touch testb
  5. gcc -o main main.o

说明现在make会先清除掉上次编译的中间文件并重建。

  • clean规则也使用变量

makefile 中定义了depen变量来声明各个依赖项。 但新增的clean规则没有使用这个变量,这会让makefile的维护产生麻烦:当依赖项变更的时候需要同时修改depen变量和clean规则。 因此,我们让clean规则的rm命令也使用depen变量。 修改clean规则下的rm命令行:

  1. rm $(depen)

再次执行make猜猜会发生什么?

  1. make

终端打印:

  1. rm clean main.o testa testb
  2. rm: cannot remove 'clean': No such file or directory
  3. make: *** [clean] Error 1

原来是因为depen变量指明clean为依赖项,因此rm命令也会试图删除clean文件时出现错误。 而 make 在执行命令行过程中出现错误后会退出执行。

  • clean命令出错后make也能继续执行

rm 某个不存在的文件是很常见的错误,在大部分情况下我们也不将其真正作为错误来看待。 如何让make忽略这个错误呢? 我们需要用到“-”符号。 “-”:让make忽略该指令的错误。 修改makefile中的clean规则:

  1. clean:
  2. -rm $(depen)

再次执行make

  1. make

终端打印:

  1. rm clean main.o testa testb
  2. rm: cannot remove 'clean': No such file or directory
  3. rm: cannot remove 'main.o': No such file or directory
  4. rm: cannot remove 'testa': No such file or directory
  5. rm: cannot remove 'testb': No such file or directory
  6. makefile:18: recipe for target 'clean' failed
  7. make: [clean] Error 1 (ignored)
  8. gcc -c main.c
  9. touch testa
  10. touch testb
  11. gcc -o main main.o

看起来效果不错,虽然 rm 指令报出错误,make 却依然可以生成我们的最终目标:main 文件。

  • 使用伪目标

前面提到makefile依赖文件的时间戳若比目标文件旧,则对应规则的命令不会执行。 我们现在定义了一个clean规则,但如果文件夹下正好有一个clean文件会发生什么样的冲突呢? 先在当前目录下新建一个clean文件:

  1. touch clean

再执行make命令:

  1. make

终端打印:

  1. gcc -o main main.o

看来由于clean文件已经存在,make不会再执行clean目标对应的规则了。 但实际上clean是一个伪目标,我们不期望它会与真正clean文件有任何关联。 此时需要使用“.PHONY”来声明伪目标。 修改makefile在变量depen之前加入一条伪目标声明:

  1. .PHONY: clean

执行 make:

  1. make

终端打印:

  1. rm clean main.o testa testb
  2. gcc -c main.c
  3. touch testa
  4. touch testb
  5. gcc -o main main.o

makefile 又能得到正常执行了,所有流程都符合我们的预期。 现在减除掉依赖项testa testb,因为实际上main文件并不需要用到这两个文件。

修改makefiledepen变量:

  1. depen=clean main.o

执行 make:

  1. make

终端打印:

  1. rm clean main.o
  2. rm: cannot remove 'clean': No such file or directory
  3. makefile:20: recipe for target 'clean' failed
  4. make: [clean] Error 1 (ignored)
  5. gcc -c main.c
  6. gcc -o main main.o

我们已经可以随心所欲的定制main文件的依赖规则了。

实验过程如下图所示: Makefile 基础入门 - 图10 Makefile 基础入门 - 图11 Makefile 基础入门 - 图12 Makefile 基础入门 - 图13 Makefile 基础入门 - 图14 Makefile 基础入门 - 图15 Makefile 基础入门 - 图16

makefile 文件命名规则

  • make默认调用的文件名

迄今为止,我们写的自动编译规则都放在makefile中,通过实验也可以明确了解到make工具会自动调用makefile文件。 是否文件名必须命名为“makefile”呢? 不是的,GNU make 会按默认的优先级查找当前文件夹的文件,查找的优先级为: “GNUmakefile”> “makefile”> “Makefile”

  • 测试make调用的文件优先级

新建GNUmakefile文件,添加以下内容:

  1. #this is a GNUmakefile
  2. .PHONY: all
  3. all:
  4. @echo "this is GNUmakefile"

新建 Makefile 文件,添加以下内容:

  1. #this is a Makefile
  2. .PHONY: all
  3. all:
  4. @echo "this is Makefile"

查看以下当前目录文件,现在应该有三个 makefile 能够识别到的文件。

  1. ls *file* -hl

终端打印:

  1. -rw-r--r-- 1 root root 71 Jun 25 12:22 GNUmakefile
  2. -rw-r--r-- 1 root root 192 Jun 25 09:18 makefile
  3. -rw-r--r-- 1 root root 65 Jun 25 12:23 Makefile

执行一次make看看哪个文件被调用:

  1. make

终端打印:

  1. this is GNUmakefile

说明make调用的是GNUmakefile。 删除GNUmakefile再执行一次make

  1. rm GNUmakefile
  2. make

终端打印:

  1. rm clean main.o
  2. rm: cannot remove 'clean': No such file or directory
  3. makefile:20: recipe for target 'clean' failed
  4. make: [clean] Error 1 (ignored)
  5. gcc -c main.c
  6. gcc -o main main.o

说明make调用的是makefile。 删除makefile,执行 make

  1. rm makefile
  2. make

终端打印:

  1. this is Makefile

说明Makefile属于三者中优先级最低的文件。 建议:推荐以makefile或者Makefile进行命名,而不使用GNUmakefile,因为GNUmakefile只能被GNUmake工具识别到。

实验过程如下图所示: Makefile 基础入门 - 图17

编写一段程序的 makefile 文件

  • 小型计算程序说明

现在我们已经掌握了makefile的基本规则,可以尝试自己写一个makefile进行工程管理。 在make_example/chapter0目录下有一段简单的计算器示例程序,现在要为它建立一个makefile文件。

切换到chapter0目录,查看目录下的文件:

  1. cd ../chapter0
  2. ls

终端打印:

  1. add_minus.c add_minus.h main.c multi_div.c multi_div.h readme.md v1.0.patch v2.0.patch v3.0.patch

简单介绍一下程序的需求:

  1. add_minus.c要求被编译成静态链接库libadd_minus.a
  2. multi_div.c要求被编译成动态链接库libmulti_div.so
  3. main.c是主要的源文件,会调用上述两个代码文件中的API,main.c要求被编译为main.o
  4. 将main.o libadd_minus.a libmulti_div.so链接成可执行文件 main。
  5. 每次编译前要清除上次编译时产生的文件。

打上补丁 v3.0 并增加库文件路径,export环境变量LD_LIBRARY_PATH为当前路径:

  1. patch -p2 < v3.0.patch
  2. export LD_LIBRARY_PATH=$PWD
  • makefile文件示例

请参照 2.6 的要求完成makefile文件,完成后可参考文件makefile_for_chapter0的内容:

  1. # this is a chapter0 makefile
  2. .PHONY:all clean depen
  3. depen=clean main.o add_minus.o libadd_minus.a libmulti_div.so
  4. all:$(depen)
  5. -gcc -o main main.o -L./ -ladd_minus -lmulti_div
  6. main.o:main.c
  7. gcc -c main.c
  8. add_minus.o:
  9. gcc -c add_minus.c
  10. libadd_minus.a:add_minus.o
  11. ar rc libadd_minus.a add_minus.o
  12. libmulti_div.so:
  13. gcc multi_div.c -fPIC -shared -o libmulti_div.so
  14. clean:
  15. -rm $(depen)

实验过程如下图所示: Makefile 基础入门 - 图18

实验总结

本实验测试了makefile的基础规则和一些简单的特性。

课后习题

  • 请自行设计一段包含多个源文件的小型工程,并使用makefile进行管理。

    Makefile 基础规则(2)

    课程简介

    实验简介

    本实验在上一个实验的基础上,继续深入介绍makefile的基础规则。

  • 验证makefile的自动推导规则。

  • 验证makefile include文件规则。
  • 验证makefile环境变量MAKEFILES,MAKEFILE_LIST.VARIABLES的作用。
  • 测试makefile的重载。

    实验知识点

    本课程项目完成过程中将学习:

  • make 的自动推导规则

  • include 指示符
  • MAKEFILES 变量
  • makefile 重载另一个 makefile
  • makefile 的“所有匹配模式”的使用

    实验环境

  • Xfce 终端

  • GCC
  • Gedit

    适合人群

    本课程难度为简单,属于入门级别课程,适合有代码编写能力的用户,熟悉和掌握make的一般用法。

    代码获取

    你可以通过下面命令将本课程里的所有源代码下载到实验楼环境中,作为参照对比进行学习。
    1. $ wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip
    请尽量按照实验步骤自己写出 C 语言程序。

    实验步骤

    本章节的源代码位于 /home/shiyanlou/Code/make_example/chapter2/目录下。
    项目文件结构

    makefile:make工程文件。

makefile 的自动推导规则

  • makefile自动推导规则说明

makefile 有一套隐含的自动推导规则:

  1. 对于xxx.o目标会默认使用命令“cc -c xxx.c -o xxx.o”来进行编译。

对于xxx目标会默认使用命令“cc xxx.o -o xxx”

下面用两个小实验来验证makefile的自动推导规则。

  • 编写main.c源文件

代码中已有main.c文件,内容如下:

  1. #include <stdio.h>
  2. int main(void)
  3. {
  4. printf("Hello world!\n");
  5. return 0;
  6. }
  • 使用make main.o验证规则

确认当前目录下没有makefile类型的文件。 输入如下命令:

  1. make main.o

终端打印:

  1. cc -c -o main.o main.c

说明make自动使用cc -c命令生成了main.o文件。

  • 使用make main验证规则

接下来验证另一条规则,输入如下命令:

  1. make main

终端打印:

  1. cc main.o -o main

说明make自动使用cc命令生成了main文件。 由于main.o文件是上一个小实验生成的,现在我们删掉它和

main文件:

  1. rm main.o main

再次输入:

  1. make main

终端打印:

  1. cc main.c -o main

这说明当main.o不存在时,make 会尝试直接使用源文件编译来生成目标文件。

实验过程如下图所示: Makefile 基础入门 - 图19

makefile include 使用规则

makefile 中可以使用include指令来包含另一个文件。 当make识别到include指令时,会暂停读入当前的makefile文件,并转而读入include指定的文件,之后再继续读取本文件的剩余内容。

  • 编写makefile需要包含的文件

makefile_dir目录下有一份需要被包含的文件inc_a,文件内容如下:

  1. #this is a include file for makefile
  2. vari_c="vari_c from inc_a"
  • 编写基本的makefile文件

拷贝makefile_dir目录下的makefile文件到当前目录:

  1. cp makefile_dir/makefile ./

makefile内容如下:

  1. # this is a basic makefile
  2. .PHONY:all clean
  3. vari_a="original vari a"
  4. vari_b="original vari b"
  5. include ./makefile_dir/inc_a
  6. all:
  7. @echo $(vari_a)
  8. @echo $(vari_b)
  9. @echo $(vari_c)
  10. clean:
  • 测试 make 能否正常工作

执行指令:

  1. make

终端打印:

  1. original vari a
  2. original vari b
  3. vari_c from inc_a

从打印信息可以看出来makefile已经成功包含了inc_a文件,并且正确获取到了vari_c变量。 值得一提的是include指示符所指示的文件名可以是任何shell能够识别的文件名,这表明include还可以支持包含通配符的文件名。我们将在下面的实验中进行验证。

  • 新建另一个被包含文件

makefile_dir 目录下有一份需要被包含的文件inc_b`,文件内容如下:

  1. #this is a include file for makefile
  2. vari_d="vari_d from inc_b"
  • 使用通配符让makefile包含匹配的文件

修改makefile,使用通配符同时包含inc_ainc_b文件。 修改后的makefile内容如下:

  1. # this is a basic makefile
  2. .PHONY:all clean
  3. vari_a="original vari a"
  4. vari_b="original vari b"
  5. include ./makefile_dir/inc_*
  6. all:
  7. @echo $(vari_a)
  8. @echo $(vari_b)
  9. @echo $(vari_c)
  10. @echo $(vari_d)
  11. clean:

执行:

  1. make

终端打印出:

  1. original vari a
  2. original vari b
  3. vari_c from inc_a
  4. vari_d from inc_b

说明文件inc_ainc_b被同时包含到makefile中。

  • makefile include 文件的查找路径

include指示符包含的文件不包含绝对路径,且在当前路径下也无法寻找到时,make 会按以下优先级寻找文件:

  1. -I指定的目录
  2. /usr/gnu/include
  3. /usr/local/include
  4. /usr/include
  5. 指定makefileinclude路径

修改makefile,不再指定inc_ainc_b的相对路径:
再执行一次make

  1. make

终端打印:

  1. makefile:8: inc_*: No such file or directory
  2. make: *** No rule to make target 'inc_*'. Stop.

可以看到makefile无法找到inc_ainc_b文件。 使用“-I”命令来指定搜寻路径:

  1. make -I ./makefile_dir/

终端依然打印:

  1. makefile:8: inc_*: No such file or directory
  2. make: *** No rule to make target 'inc_*'. Stop.

看起来make在搜寻“inc_*”档案。 修改makefile,将“inc_*”改为"inc_a"``"inc_b"

  1. include inc_a inc_b

执行:

  1. make -I ./makefile_dir/

终端打印:

  1. original vari a
  2. original vari b
  3. vari_c from inc_a
  4. vari_d from inc_b

可见不使用通配符的情况下include配合-I选项才能得到预期效果。

  • makefile include的处理细节

下面再研究一下makeinclude指示符的处理细节。 前面提到make读入makefile时遇见include指示符会暂停读入当前文件,转而读入include指定的文件,之后才继续读入当前文件。 拷贝文件

makefile_dir/makefile_b到当前目录并命名为makefile

  1. cp makefile_dir/makefile_b ./makefile

查看makefile的内容:

  1. #this makefile is test for include process
  2. .PHONY:all clean
  3. vari_a="vari_a @ 1st"
  4. include ./makefile_dir/c_inc
  5. vari_a += " @2nd ..."
  6. all:
  7. @echo $(vari_a)
  8. clean:

可以看出makefile是先设定vari_a变量,再包含c_inc文件,之后再修改vari_a变量。 查看c_inc文件内容:

  1. #this is a include file for include process
  2. vari_a="vari_a from c_inc"

可以看出c_inc文件中也设定了vari_a变量。 执行make看最终vari_a变量定义为什么:

  1. make

终端打印:

  1. vari_a from c_inc @2nd ...

这说明vari_ainclude过程中被修改掉,并在其后添加了字串" @2nd ...",结果与预期中make处理include指示符的行为一致。

实验过程如下图所示: Makefile 基础入门 - 图20 Makefile 基础入门 - 图21 Makefile 基础入门 - 图22 Makefile 基础入门 - 图23 Makefile 基础入门 - 图24

makefile的几个通用变量测试

  • 测试MAKEFILES变量指定的文件是否能正确被包含

MAKEFILES 环境变量有定义时,它起到类似于include的作用。 该变量在被展开时以空格作为文件名的分隔符。

删掉当前makefile文件:

  1. rm makefile

新建makefile内容如下:

#this makefile is test for include process
.PHONY:all clean
vari_a += " 2nd vari..."
all:
    @echo $(vari_a)
clean:

执行 make

make

终端打印:

2nd vari...

增加环境变量MAKEFILES

export MAKEFILES=./makefile_dir/c_inc

再次执行make

make

终端打印:

vari_a from c_inc  2nd vari...

可见make按照MAKEFILES的文件列表载入了makefile_dir/c_inc文件。

  • 测试MAKEFILES变量的使用限制

需要注意:

  1. MAKEFILES指定文件的目标不能作为make的终极目标。
  2. MAKEFILES是环境变量,它对所有的makefile都会产生影响,因此尽量不要使用该变量。

新建一个文件aim_b_file,内容如下:

#this is aim_b file
.PHONY:aim_b
aim_b:
    @echo "now we exe aim_b"

此文件定一个aim_b规则,执行此规则时打印“now we exe aim_b”。 修改MAKEFILES变量:

export MAKEFILES=./aim_b_file

执行make

make

终端打印:

2nd vari...

可见make虽然先包含aim_b_file文件,但依然以makefile中的all作为最终目标。 我们再来验证aim_b规则是否已经被正常解析到,修改makefile,为all增加一条依赖:

all: aim_b

这样,执行all规则之前必须先执行aim_b规则。 执行make

make

终端打印:

now we exe aim_b
2nd vari...

再执行:

make aim_b

终端打印:

now we exe aim_b

“make”“makeaim_b”的打印都说明aim_b已经能够被正确执行,但它的确不会作为默认的目标规则,只有明确指定此规则时才会执行其对应的命令。

  • 打印MAKEFILE_LIST

所有MAKEFILES指定的文件名,命令行指定的文件名,默认makefile文件以及include指定的文件名都记录下来。 当前路径下总共有./aim_b_file./makefile./makefile_dir/inc_a./makefile_dir/inc_b./makefile_dir/c_inc这5个文件。 现在我们使用不同的方式将它们包含进来。 ./aim_b_file已经被包含在MAKEFILES变量中。 ./makefile会在执行make时被自动调用。 修改makefileinclude指示符包含文件./makefile_dir/inc_a和./makefile_dir/inc_b。 并在all目标中打印MAKEFILE_LIST变量,修改后的makefile`内容如下:

#this makefile is test for include process
.PHONY:all clean
include ./makefile_dir/inc_a ./makefile_dir/inc_b
vari_a += " 2nd vari..."
all:
    @echo $(vari_a)
    @echo $(MAKEFILE_LIST)
clean:

执行 make

make

终端打印:

now we exe aim_b
2nd vari...
./aim_b_file makefile makefile_dir/inc_a makefile_dir/inc_b

第二行打印内容说明MAKEFILE_LIST已经包含了./aim_b_file makefile makefile_dir/inc_a makefile_dir/inc_b

实验过程如下图所示: Makefile 基础入门 - 图25 Makefile 基础入门 - 图26 Makefile 基础入门 - 图27 Makefile 基础入门 - 图28

重载另一个makefile

  • 使用make -f 重载另一个makefile

现在拷贝makefile文件为inc_test

cp makefile inc_test

再使用make -f命令指定需要读取的makefile文件为inc_test

make -f inc_test

终端打印:

now we exe aim_b
2nd vari...
./aim_b_file inc_test makefile_dir/inc_a makefile_dir/inc_b

可见原来默认执行的makefile文件被替换成了inc_test文件,且被MAKEFILE_LIST正确记录。

  • 测试重载makefile的限制条件

makefile重载另一个makefile的时,不允许有规则名重名。 若是有规则发生重名会发生什么状况呢? 修改aim_b_file增加all规则:

all:
    @echo "all in aim_b"

执行:

make

终端打印:

makefile:10: warning: overriding commands for target `all'
./aim_b_file:9: warning: ignoring old commands for target `all'
now we exe aim_b
2nd vari...
./aim_b_file makefile makefile_dir/inc_a makefile_dir/inc_b

从打印日志中可以看出makefile重写了aim_b_file文件中的all规则。

  • 用“所有匹配模式”重载另一个makefile

从上面的实验中可以看出,对于两个文件中同名的规则,make后读入的规则会重写先读入的规则。 现在假如有两个makefile文件,AMakeBMake,它们都定义了一条intro规则,但行为不同。 用户希望执行在生成目标AAimBAim的时候分别调用AMakeBMakeintro规则,要怎样来做呢?
我们无法用include指示符来包含这两个makefile,否则会产生重写规则的行为。 此时需要用到重载另一个makefile的技巧。 具体方法就是在对应的规则中重新调用make并传入需要重载的makefile文件名及目标名。
chapter2/makefile_dir/目录底下的makefile_c`` AMake`` BMake 这三个文件可以演示我们所需的功能。

先拷贝三个文件到当前目录下:

cp makefile_dir/makefile_c makefile
cp makefile_dir/AMake ./
cp makefile_dir/BMake ./

查看makefile文件,内容如下:

#this is a makefile reload example main part
.PHONY:AAim BAim
AAim:
    make -f AMake intro
BAim:
    make -f BMake intro

当目标为AAim时,会执行“make -f AMake intro”。 也就是会重载AMake作为makefile文件并执行intro规则。 BAim的处理方式也类似。

现在测试一下执行效果,执行:

make AAim

终端打印:

make -f AMake intro
make[1]: Entering directory '/root/study/make_example/chapter2'
Hello, this is AMake
make[1]: Leaving directory '/root/study/make_example/chapter2'

可见AMake下的intro规则的确被执行到了。
再执行BAim规则:

make BAim

终端打印:

make -f BMake intro
make[1]: Entering directory '/root/study/make_example/chapter2'
Hello, this is BMake
make[1]: Leaving directory '/root/study/make_example/chapter2'

BMake的规则也被顺利执行。 上述部分是基本的重载方式。
现在我们在多一条需求,希望其它未定义规则都要执行另一条intro规则,此规则定义在CMake文件中。 为了匹配其它所有的未定义规则,我们需要用到通配符“%”。

修改makefile在文件最后加入“所有匹配模式”规则:

%:
    make -f CMake intro

并将makefile_dir/CMake文件拷贝到当前目录下:

cp makefile_dir/CMake ./

随便执行一条规则AAA

make AAA

终端打印:

make -f CMake intro
make[1]: Entering directory '/root/study/make_example/chapter2'
Hello, this is CMake
make[1]: Leaving directory '/root/study/make_example/chapter2'

说明这条未定义的规则最后会重载CMake并执行其intro规则。

实验效果如图所示: Makefile 基础入门 - 图29 Makefile 基础入门 - 图30 Makefile 基础入门 - 图31

实验总结

本实验验证了makefile的自动推导规则,一些环境变量的使用,include指示符的使用和限制,以及makefile重载的技巧。

Make 的处理阶段及条件执行

课程简介

实验简介

本实验重点介绍make的两个处理阶段和条件执行语句。

  • 验证make的两个处理阶段。
  • 测试make目标指令的执行细节。
  • 测试make的条件执行语句。

    实验知识点

    本课程项目完成过程中将学习:

  • make 读取 makefile 文件,执行更新和重建

  • makefile 使用反斜线和 $$
  • makefile 中条件语句的基本格式
  • makefile ifeq,ifneq,ifdef,ifndef 的使用

    实验环境

  • Xfce 终端

  • GCC
  • Gedit

    适合人群

    本课程难度为简单,属于入门级别课程,适合有代码编写能力的用户,熟悉和掌握make的一般用法。

    代码获取

    你可以通过下面命令将本课程里的所有源代码下载到实验楼环境中,作为参照对比进行学习。
    $ wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip
    
    请尽量按照实验步骤自己写出 C 语言程序。

    实验步骤

    本章节的源代码位于 /home/shiyanlou/Code/make_example/chapter3/目录下。
    项目文件结构

    makefile:make工程文件。

make 的两个执行阶段

  • 新增makefile并测试执行状况

源代码中已经存在makefile文件,查看文件内容:

# this is a makefile example
vari_a = "vari a from makefile"
vari_b = "vari b from makefile"
.PHONY:all
all:
    @echo $(vari_a)
    @echo $(vari_b)

此文件定义了两个变量vari_a,vari_b,并在执行all规则时打印它们的值。 执行make

make

终端打印如下:

vari a from makefile
vari b from makefile
  • 新增被包含文件并修改makefile

现在新增一个文件inc_a,并在其中将vari_b变量改为“vari b from inc_a”

源代码中已有此文件,内容如下:

# this is a include file for make
vari_b = "vari b from inc_a"

修改makefile在最后一行包含inc_a档案。

include inc_a
  • 测试include文件是否在第一阶段被包含

我们知道make是按照顺序一行行读入makefile。 前面介绍make的第一阶段是读入所有makefileinclude文档,内建变量。 所以解析新修改的makefile时,inc_a应该在第一阶段被解析完毕,vari_b变量也被inc_a修改掉。

执行make进行验证:

make

终端打印:

vari a from makefile
vari b from inc_a

说明vari_b已经被修改,与include指示符在makefile的位置无关。 实验过程如下图所示: Makefile 基础入门 - 图32

make目标指令的执行细节

  • makefile目标下指令的执行过程

到目前为止,我们看到makefile中的指令都是shell指令,那么make是怎样执行目标对应指令呢? 答案还是shell。make会调用shell去执行每一条指令。 但需要注意的是,即便在同一个目标下,每一条指令都是相互独立的。 也就是说make会分别调用shell去执行每一条指令,而非使用一个shell进程按顺序将所有指令都执行一遍。

  • 新建makefile测试命令pwdcd的执行效果。

现在使用cd命令和pwd命令查看两条相邻的命令能否相互产生影响。 源文件代码中已经有cd_test.mk文件,内容如下:

# this is a makefile to test cd and pwd cmd
.PHONY:all
all:
    @pwd
    cd ..
    @pwd

all 规则由三条命令构成,其中“@pwd”表示打印当前绝对路径,但不要显示“pwd”命令,“cd ..”表示回到上一层目录。 因此,若三条指令是在一个 shell进程中顺序执行,应该会先打印当前目录的绝对路径, 再返回上一层目录并打印上一层目录的绝对路径。

执行make -f cd_test.mk 进行测试:

make -f cd_test.mk

终端打印:

/root/study/make_example/chapter3
cd ..
/root/study/make_example/chapter3

可见实际执行状况中cd命令并不会对下一条指令产生影响。

  • 在每一条指令中打印进程id确认指令会被不同的进程执行。

还有一个更简单的方法是打印执行当前命令的进程 id。 源文件代码中已经有cmd_test.mk文件,内容如下:

#this is a command test makefile
.PHONY:all
all:
    @echo "cmd1 process id is :" $$$$
    @echo "cmd2 process id is :" $$$$

其中代表的是当前进程id。 所以cmd_test.mk的命令执行过程就是分别打印all目标下两条命令的进程id

执行make -f cmd_test.mk进行测试:

make -f cmd_test.mk

终端打印:

cmd1 process id is : 298
cmd2 process id is : 299

可以看出两条命令执行的进程id并不相同。

  • 在同一行中使用多条命令

有些状况下,用户希望能够使用cd命令来控制命令执行时所在的路径, 比如cd到某个目录下,编译其中的源代码,要如何实现呢? 此时必须在一行中写入多条指令。 先修改cd_test.mk文件,将三条指令都放在一行,并用“;”隔开。 请注意第三条“@pwd”的指令中,“@”符号要删掉,此符号只用于每一行的开头。

修改后的cd_test.mk内容如下:

# this is a makefile to test cd and pwd cmd
.PHONY:all
all:
    @pwd; cd .. ; pwd

再次执行 cmd_test.mk 文件:

make -f cd_test.mk

终端打印:

/root/study/make_example/chapter3
/root/study/make_example

说明现在三条语句已经在同一个进程中被执行到了。
同样,我们也对cmd_test.mk文件进行修改,再确认进程号是否一致。

修改后的cmd_test.mk文件内容如下:

#this is a command test makefile
.PHONY:all
all:
    @echo "cmd1 process id is :" $$$$;echo "cmd2 process id is :" $$$$

使用 make 执行此文件:

make -f cmd_test.mk

终端打印:

cmd1 process id is : 666
cmd2 process id is : 666

由此可见同一行的指令的确会被同一个进程执行到。

  • 使用反斜线分割命令

在同一行中书写多条指令是一件比较麻烦的事情,尤其是指令较长时,非常不方便阅读和修改。 makefile 中可以使用反斜线“\”来将一行的内容分割成多行。

源文件中有一个multi_test.mk脚本,用于测试反斜线的作用,内容如下:

#this is a command test makefile
.PHONY:all
all:
    @echo "cmd1 process \
    id is :" $$$$; \
    echo "cmd2 process id is :" $$$$

此文件将一条指令分割成3行,其中第一行和第二行组成一条完整的指令,内容与第三行指令相似。 两条指令的作用也是打印当前执行进程的 id号。

使用make 执行此文件:

make -f multi_test.mk

终端打印如下:

cmd1 process id is : 675
cmd2 process id is : 675

执行效果与修改后的 cmd_test.mk文件一致,说明反斜杠的确能起到连接多行指令的作用。

实验过程如下图所示: Makefile 基础入门 - 图33 Makefile 基础入门 - 图34

条件执行语句

  • 条件语句的基本格式

makefile 中不包含else 分支条件判断语句的语法格式为:

CONDITIONAL-DIRECTIVE
TEXT-IF-TRUE
endif

TEXT-IF-TRUE可以为若干任何文本行,当条件为真时它被 make 作为需要执行的一部分。

包含 else 的格式为:

CONDITIONAL-DIRECTIVE
TEXT-IF-TRUE
else
TEXT-IF-FALSE
endif

make 在条件为真时执行TEXT-IF-TRUE,否则执行TEXT-IF-FALSE

  • ifeq 语句

ifeq 用于判断条件是否相等,可以支持以下几种格式: ifeq (ARG1, ARG2) ifeq ‘ARG1’ ‘ARG2’ ifeq “ARG1” “ARG2” ifeq “ARG1” ‘ARG2’ ifeq ‘ARG1’ “ARG2” 请注意:ifeq/ifneq 等关键字后面一定要接一个空格,否则make会因为无法识别关键字而报错!

源文件代码中已有eq.mk文件,内容如下:

#this is a makefile to test ifeq
.PHONY:all
b="ifeq default"
ifeq ($(a),1)
b="ifeq a 1"
endif
ifeq '$(a)' '2'
b="ifeq a 2"
endif
ifeq "$(a)" "3"
b="ifeq a 3"
endif
ifeq "$(a)" '4'
b="ifeq a 4"
endif
ifeq '$(a)' "5"
b="ifeq a 5"
endif
all:
        @echo $(b)

使用make重建目标all时,将会根据a的值重新定义b的值并将其打印出来。

使用make命令执行此文件,指令及打印内容如下:

shiyanlou:chapter3/ (master*) $ make -f eq.mk
ifeq default
shiyanlou:chapter3/ (master*) $ make -f eq.mk a=1
ifeq a 1
shiyanlou:chapter3/ (master*) $ make -f eq.mk a="1"
ifeq a 1
shiyanlou:chapter3/ (master*) $ make -f eq.mk a=2  
ifeq a 2
shiyanlou:chapter3/ (master*) $ make -f eq.mk a=3
ifeq a 3
shiyanlou:chapter3/ (master*) $ make -f eq.mk a=4
ifeq a 4
shiyanlou:chapter3/ (master*) $ make -f eq.mk a=5
ifeq a 5
shiyanlou:chapter3/ (master*) $ make -f eq.mk a=6
ifeq default
  • ifneq语句

ifneq支持的格式与ifeq相同, 源文件代码中已经有neq.mk,内容如下:

#this is a makefile to test ifneq
.PHONY:all
ifneq ($(a),)
b=$(a)
else
b="null"
endif
all:
    @echo "value b is:" $(b)

neq.mk中使用了ifneq ... else ... endif结构。 当a不为空时,b的值与a相同,否则b为默认值“null”

执行make时会打印b的值,指令及打印内容如下:

shiyanlou:chapter3/ (master*) $ make -f neq.mk
value b is: null
shiyanlou:chapter3/ (master*) $ make -f neq.mk a=1
value b is: 1
shiyanlou:chapter3/ (master*) $ make -f neq.mk a=2
value b is: 2
shiyanlou:chapter3/ (master*) $ make -f neq.mk a="hello"
value b is: hello
  • ifdef语句

ifdef语句的语法格式如下: ifdef VARIABLE-NAME ifdef 只会判断变量是否有值,而不关心其值是否为空。
现在我们测试ifdef的用法,以及要怎样理解变量值为空和变量未定义的差别。
源文件代码中已经存在def.mk文件,内容如下:

#this is a makefile to test ifdef
.PHONY:all
a=
b=$(a)
ifdef a
c="a is defined"
else
c="a is not defined"
endif
ifdef b
d="b is defined"
else
d="b is not defined"
endif
all:
    @echo "vari a is:" $(a)
    @echo "vari b is:" $(b)
    @echo "vari c is:" $(c)
    @echo "vari d is:" $(d)

def.mk文件中先声明了一个变量a,但并未给其赋值,这相当于是未定义变量。 变量a又被赋给了变量b,由于a是未定义变量,因此b为空值。

make执行此文件时分别打印变量a``b``c``d的值。 执行make

make -f def.mk

终端打印:

vari a is:
vari b is:
vari c is: a is not defined
vari d is: b is defined

可见对make 来说,它认为 a 属于未定义变量,b 则属于已定义变量。

  • ifndef语句

ifneq格式与ifeq相同,逻辑上与ifneq相反。 源文件代码中已有ndef.mk文件,内容与def.mk相似:

#this is a makefile to test ifndef
.PHONY:all
a=
b=$(a)
ifndef a
c="a is not defined"
else
c="a is defined"
endif
ifndef b
d="b is not defined"
else
d="b is defined"
endif
all:
    @echo "vari a is:" $(a)
    @echo "vari b is:" $(b)
    @echo "vari c is:" $(c)
    @echo "vari d is:" $(d)

执行 make:

make -f ndef.mk

终端打印:

vari a is:
vari b is:
vari c is: a is not defined
vari d is: b is defined

实验过程如下图所示: Makefile 基础入门 - 图35

实验总结

本实验测试了make执行的两个阶段,目标指令的执行细节和条件执行语句的编写。

Makefile 规则进阶(1)

课程简介

实验简介

本次实验介绍 make 目标认定的细节,包括终极目标如何认定,目标重建的条件,目标依赖的类型以及如何使用文件名通配符。

  • 验证 make 终极目标认定的优先级。
  • 验证 make 终极目标的重建条件。
  • 测试不同依赖类型的区别。
  • 验证文件名通配符的使用。

    实验知识点

    本课程项目完成过程中将学习:

  • makefile 终极目标的定义

  • makefile 不能作为终极目标的情况
  • 目标重建的条件
  • makefile 目标可以有的两种依赖
  • 文件名可以使用的通配符匹配

    实验环境

  • Xfce 终端

  • GCC
  • Gedit

    适合人群

    本课程难度为中等,适合已经初步了解 makefile 规则的学员进行学习。

    代码获取

    你可以通过下面命令将本课程里的所有源代码下载到实验楼环境中,作为参照对比进行学习。
    $ wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip
    
    请尽量按照实验步骤自己写出 C 语言程序。

    实验步骤

    本章节的源代码位于 /home/shiyanlou/Code/make_example/chapter4/ 目录下。

项目文件结构

makefile:用于测试终极目标的 makefile 文件 envir_make:用于测试环境变量的 makefile 文件 order_make:用于测试 order-only 依赖项的 makefile 文件 pattern_make:用于测试匹配模式的 makefile 文件 phony_make:用于测试 .PHONY 规则的 makefile 文件 rebuild_make:用于测试目标重建时机和过程的 makefile 文件 wildcard_make:用于测试通配符的 makefile 文件

make 对终极目标的认定

  • 确定 makefile 的终极目标

一般情况下 makefile 的终极目标是第一条规则的目标。

源代码中已经有一个名为 makefile 的文件可以验证其终极目标的选定,内容如下:

# this is a makefile to verify the default aim
aim_1:
    @echo "this is " $@
aim_2:
    @echo "this is " $@
aim_3:
    @echo "this is " $@

makefile 中指定了三个目标,执行的动作相同:打印当前执行的目标名称。 自动化变量 $@会被 make 自动展开为目标名称。

顺序执行这三个规则:

make aim_1;make aim_2;make aim_3

PS:shell中可以使用分号隔开一系列指令并顺序执行。 终端打印:

this is  aim_1
this is  aim_2
this is  aim_3

而不指定目标时,make 默认执行的目标就是终极目标,如下所示:

make

终端打印:

this is  aim_1

这就表示第一个目标 aim_1 为终极目标。 感兴趣的同学可以自己调换一下 aim_1`` aim_2`` aim_3 的顺序看看终极目标是否会随之改变。

  • 终极目标认定的例外(1)

当目标名以“.”开头且其后不是斜线符号“/”(即不被解析为目录符号),此目标无法作为终极目标。 如前面所述的“.PHONY:”规则,代表声明伪目标,这则规则可以被执行,却无法作为终极目标。

源文件中有一份名为 phony_make的文件,内容如下:

# this is a makefile to verify the default aim
.PHONY: aim_2
    @echo "this is " $@
aim_1:
    @echo "this is " $@
aim_2:
    @echo "this is " $@
aim_3:
    @echo "this is " $@

相比 makefile 文件,它在最前面多了一条 .PHONY目标。

执行 .PHONY 目标进行验证:

make -f phony_make .PHONY

终端打印:

this is  aim_2
this is  .PHONY

说明 .PHONY可以被正确执行。

再执行 make 试试终极目标:

make -f phony_make

终端打印:

this is  aim_1

可见终极目标仍然是 aim_1。

  • 终极目标认定的例外(2)

第二个例外是模式目标无法被认定为终极目标。

请参考 pattern_make 文件:

# this is a makefile to verify the default aim
%:
    @echo "this is " $@
aim_1:
    @echo "this is " $@
aim_2:
    @echo "this is " $@
aim_3:
    @echo "this is " $@

相比 makefile 文件,其第一条目标变成了模式规则,“%”符号可以匹配任何未显示定义的目标。

先测试模式匹配的效果:

make -f pattern_make 123

终端打印:

this is  123

“123”为未定义的目标,测试说明模式匹配已经生效。

再执行 make:

make -f pattern_make

终端打印:

this is  aim_1

由此说明模式规则无法作为终极目标。

  • 终极目标认定的例外(3)

使用 MAKEFILES 指定的文件会被make 首先读入,但其中的目标无法作为终极目标。

参考 envir_make文件:

# this is a makefile to verify the default aim
envir_1:
    @echo "this is " $@

此文件中有一个 envir_1目标。 现在修改 makefile,在aim_2后面增加依赖项envir_1,来确认envir_make 是否被正常读入。

修改后 makefile中的目标 aim_2 内容如下:

aim_2:envir_1
    @echo "this is " $@

执行make aim_2

make aim_2

终端打印:

make: *** No rule to make target 'envir_1', needed by 'aim_2'.  Stop.

这是因为 envir_1 还没被加入到 MAKEFILES环境变量中,现在将其加入:

export MAKEFILES=envir_make

再次执行:

make aim_2

终端打印:

this is  envir_1
this is  aim_2

说明 envir_make 已经被正确包含。

再执行 make,看 envir_make 的目标能否得到执行:

make

终端打印:

this is  aim_1

说明aim_1依然是终极目标。

  • include指定文件中的目标可以作为终极目标

MAKEFILES环境变量不同,使用include 包含的文件目标则可以被认定为终极目标。

现在清空 MAKEFILES环境变量:

export MAKEFILES=

再次修改 makefileaim_1 目标之前加入对 envir_make 文件的包含,增加的makefile内容如下:

include envir_make

执行 make:

make

终端打印:

this is  envir_1

可见envir_make 中的 envir_1目标被作为终极目标进行重建了。

实验过程截图如下: Makefile 基础入门 - 图36 Makefile 基础入门 - 图37 Makefile 基础入门 - 图38

Makefile 规则进阶(2)

课程简介

实验简介

本次实验将介绍 makefile 中 wildcardVPATHvpathGPATH-lNAME 的使用方法及文件路径保存方法。

  • 函数 wildcard 的使用
  • VPATH 和 vpath 的使用
  • 文件路径的保存及 GPATH 的使用
  • -lNAME 文件的使用

    实验知识点

    本课程项目完成过程中将学习:

  • 1.3 实验环境

  • 在变量定义或者函数引用时不能直接使用通配符,而要用 wildcard 函数来代替
  • VPATH 变量可以指定依赖文件的搜索路径,使用空格或冒号将多个路径分开
  • vpath关键字比 VPATH 更灵活,可以为符合模式匹配的文件指定搜索路径,还可以清除搜索路径
  • make 的文件路径保存算法如下:
    1)在当前工作目录搜索目标文件,若不存在则执行目录搜索;
    2)依赖文件也使用同样的处理方式;
    3)决定目标是否需要重建时做出如下选择:
    3.1)不需要重建时规则中所有文件的完整路径名有效;
    3.2)需要重建时,目标的完整路径失效,目标在会在当前工作目录下重建,而非目录搜索时得到的目录,但依赖文件的完整路径依然有效;
  • 若目标文件的完整路径存在于GPATH变量列表中,make 会使用完整路径来重建目标,而非当前工作目录
  • 当出现 -lNAME 形式的文件名时,make会先查找 libNAME.so,文件不存在时查找libNAME.a文件,路径搜索顺序为:当前目录 > VPATH or vpath 指定目录 > /lib/ > /usr/lib/ > /usr/local/lib/
  • -lNAME 被展开成libNAME.solibNAME.so 是由变量".LIBPATTERNS"指定的

    适合人群

    本课程难度为中等,适合已经初步了解 makefile 规则的学员进行学习。

    代码获取

    你可以通过下面命令将本课程里的所有源代码下载到实验楼环境中,作为参照对比进行学习。

    $ wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip
    

    请尽量按照实验步骤自己写出 C 语言程序。

    实验步骤

    本章节的源代码位于 /home/shiyanlou/Code/make_example/chapter5/目录下。
    项目文件结构:

    .
    ├── gpath_code:用于测试 GPATH 
    │   ├── main
    │   └── makefile
    ├── lib_code:用于测试 -lNAME 的使用方法
    │   ├── lib
    │   │   ├── foo_dynamic.c
    │   │   └── foo_static.c
    │   ├── main.c
    │   └── makefile
    ├── vpath_code:用于测试 VPATH 和 vpath
    │   ├── main.c
    │   ├── makefile
    │   └── vpath.mk
    └── wild_code:用于测试 wildcard 函数
      ├── foo1.c
      ├── foo1.h
      ├── foo2.c
      ├── foo2.h
      ├── main.c
      ├── makefile
      └── pat_make.mk
    

    函数 wildcard 的使用


  • wildcard 的使用时机

前面章节介绍了文件通配符的使用,在规则中通配符会被自动展开,但在变量定义和函数引用时,通配符将会失效。 此时如果需要使用通配符就要使用 wildcard 函数。 它的语法格式为:$(wildcard PATTERN...)。 在 makefile 中,它被展开为已经存在的、使用空格分开的、匹配此模式的所有文件列表。

  • 使用 wildcard 来匹配当前目录下所有的 .c 文件

此处我们用代码编译流程来进行测试,先进入 wild_code 目录:

cd wild_code

wild_code 目录下文件如下:

├── foo1.c
├── foo1.h
├── foo2.c
├── foo2.h
├── main.c
└── makefile

foo1.c定义了 foo1() 函数,打印“Hello foo1!”foo2.c定义了foo2() 函数,打印“Hello foo2!”。 main.c 依次调用 foo1() 和 foo2()。

makefile 文件内容如下:

#this is a makefile for wildcard code test
.PHONY:all clean
code=$(wildcard *.c)
aim=wildtest
all:$(code)
    @echo "objs inlude : " $(code)
    $(CC) -o $(aim) $(code)
clean:
    $(RM) $(aim)

它的终极目标 all 依赖于当前目录下所有的 .c 文件。 重建目标 all 时会打印依赖文件并使用 cc 将其链接为 wildtest 文件。

执行 make 看看效果:

make

终端打印:

objs inlude :  foo1.c main.c foo2.c
cc -o wildtest foo1.c main.c foo2.c

执行 wildtest:

./wildtest

终端打印:

Hello foo1!
Hello foo2!

可见 wildtest 程序符合预期流程。

  • 更复杂的用法

在实际的项目管理中,我们通常用 .o 文件作为依赖,而非 .c 文件,此时需要用到函数的嵌套调用。 我们可以使用 $(patsubst SRC_PATTERN,DEST_PATTERN,FULL_STR) 来进行字符串替换,将 .c 文件替换为 .o文件:

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

这样就可以将当前目录下的 .c 文件列表转换为 .o 文件列表,再利用 make 的隐含规则自动编译。

具体使用方法可以参考 pat_make.mk 文件:

#this is a makefile for wildcard code test
.PHONY:all clean
objs=$(patsubst %.c,%.o,$(wildcard *.c))
aim=wildtest2
all:$(objs)
    @echo "objs inlude : " $(objs)
    $(CC) -o $(aim) $(objs)
clean:
    $(RM) $(objs)
    $(RM) $(aim)

执行 make:

make -f pat_make.mk

终端打印:

cc -c -o foo1.o foo1.c
cc -c -o foo2.o foo2.c
cc -c -o main.o main.c
objs inlude :  foo1.o foo2.o main.o
cc -o wildtest2 foo1.o foo2.o main.o

可见 foo1.cmain.cfoo2.c都已经被替换为对应的.o 文件。

实验过程如下图所示: Makefile 基础入门 - 图39

VPATH 变量和 vpath 关键字的使用


  • VPATH 变量测试

VPATH 变量可以指定文件的搜索路径,若规则的依赖文件或目标文件在当前目录不存在时,make 会在此变量指定的目录下去寻找依赖文件。 VPATH 可以定义多个目录,目录间用“:”隔开,目录搜索顺序与 VPATH 中定义的顺序一致。

进入到 vpath_code 目录下:

cd ../vpath_code

这里有一份 main.c 文件,内容如下:

#include <stdio.h>
extern void foo1(void);
extern void foo2(void);
int main(void)
{
        foo1();
        foo2();
        return 0;
}

main 函数的实现与 2.1 中的 mian 函数一致,都是分别调用 foo1() 和 foo2()。 但此处用 extern 声明了 foo1() 和 foo2() 是外部函数,而不再通过包含头文件来声明这两个函数。 这使得 main.c 本身不需要关注这两个函数的头文件放在什么位置,只要链接时的 .o 文件能够包含它们的实现即可。 foo1() 和 foo2() 的函数实现位于chapter5/wild_code/目录下,我们可以通过 VPATH 变量告知 makefile 它们的路径。

makefile 文件内容如下:

#this is a makefile for VPATH test
.PHONY:all clean
depen=main.o foo1.o foo2.o
aim=main
all:$(depen)
    @echo "objs inlude : " $(depen)
    $(CC) -o $(aim) $(depen)
clean:
    $(RM) $(depen)
    $(RM) $(aim)

它只是单纯指定了三个依赖项分别为 main.ofoo1.ofoo2.o,并在重建 all 目标时将三者链接为 main 文件。 直接执行 make 会怎样呢?

make

终端打印:

cc -c -o main.o main.c
make: *** No rule to make target 'foo1.o', needed by 'all'.  Stop.

由于 make 找不到 foo1.o,当前路径下也没有 foo1.c 文件,无法依靠隐含规则自动重建 foo1.o,因此 make 报错并退出执行。

现在我们设定 VPATH 为 ../wild_code/ 并传给 make 执行:

make VPATH=../wild_code/

预期终端打印:

cc -c -o main.o main.c
cc -c -o foo1.o ../wild_code/foo1.c
cc -c -o foo2.o ../wild_code/foo2.c
objs inlude :  main.o foo1.o foo2.o
cc -o main main.o foo1.o foo2.o

但在实验楼环境中打印如下:

objs inlude :  main.o foo1.o foo2.o
cc -o main main.o foo1.o foo2.o
cc: error: main.o: No such file or directory
cc: error: foo1.o: No such file or directory
cc: error: foo2.o: No such file or directory
cc: fatal error: no input files
compilation terminated.
make: *** [all] Error 4

不但找不到 foo1.o 和 foo2.o,而且连 main.o 的隐含规则都没有被执行到。 实验楼用的 make 版本为 3.81,换成 make 4.1 版本即可出现预期打印,这表明 3.81 版本的 make 工具无法正确支持 VPATH 变量。但没有关系,接下来我们使用更灵活,更受推荐的方式来指定搜索目录。

  • vpath 关键字的使用

vpath 关键字的作用与 VPATH 变量相似,可以指定依赖文件或目标文件的目录。
但 vpath 的用法更加灵活,其用法如下:

1)vpath PATTERN DIR:为匹配 PATTERN 模式的文件指定搜索目录。 2)vpath PATTERN:清除匹配 PATTERN 模式的文件设置的搜索目录。 3)vpath:清除全部搜索目录。

参考 vpath.mk 的内容,与 makefile 一致,只是多出一行:

vpath %.c ../wild_code/

这一行指定了所有 .c 文件的搜索目录。 先 clean 掉上次的编译结果,再执行新的 makefile 进行测试:

make clean; make -f vpath.mk

终端打印:

rm -f main.o foo1.o foo2.o
rm -f main
cc -c -o main.o main.c
cc -c -o foo1.o ../wild_code/foo1.c
cc -c -o foo2.o ../wild_code/foo2.c
objs inlude :  main.o foo1.o foo2.o
cc -o main main.o foo1.o foo2.o

可知 vpath.mk 的执行效果与使用 VPATH 一致。 有兴趣的同学可以再自行实验 vpath 清除搜索目录的功能。

实验过程如下图所示: Makefile 基础入门 - 图40

文件路径的保存及 GPATH 的使用


  • 文件路径的保存

如前面实验所展示,有时候某些依赖文件或目标文件需要搜索 VPATH 或 vpath 指定目录才能得到。 因此后续的流程中需要决定目录搜索得到的完整路径是要保留还是废弃。 make 在解析 makefile 文件时对文件路径的保存/废弃算法如下:

1)在当前目录查找文件,若不存在则搜索指定路径。 2)若目录搜索成功则将完整路径作为临时文件名保存。 3)依赖文件直接使用完整路径名。 4)目标文件若不需要重建则使用完整路径名,否则完整路径名被废弃。

比较难理解的是第四点,简单来说意思就是目标文件会在当前路径被进行重建。

  • 文件路径规则验证

下面进行规则验证,先清除掉上次实验的结果并切换目录:

make -f vpath.mk clean; cd ../gpath_code/

gpath_code 目录下有一份 makefile 文件,内容如下:

#this is a makefile for gpath test
.PHONY:all clean
vpath %.c ../wild_code/
depen=main.o foo1.o foo2.o
aim=main
all:$(depen)
    @echo "objs inlude : " $(depen)
    $(CC) -o $(aim) $^
clean:
    $(RM) $(depen)
    $(RM) $(aim)

相比之前的 makefile 此处同时指定了.c.o 文件的搜索路径。 此外,还在重建 all 目标时使用自动化变量$^代替$(depen)$^ 变量会将指定的目标文件展开为完整路径名。

但此次所有的 .c文件都在 ../wild_code/ 目录下,根据文件路径的保存规则,其对应的.o文件要在当前路径下生成。 执行 make 进行测试:

make;ls

终端打印:

cc -c -o main.o ../wild_code/main.c
cc -c -o foo1.o ../wild_code/foo1.c
cc -c -o foo2.o ../wild_code/foo2.c
objs inlude :  main.o foo1.o foo2.o
cc -o main main.o foo1.o foo2.o
foo1.o  foo2.o  main  main.o  makefile

可以发现foo1.o foo2.o`` main.o ``main 全部都在当前路径被生成。

  • GPATH 变量的使用

如果不希望在当前目录下生成目标文件,可以使用 GPATH 变量。 若目标文件与 GPATH 变量指定目录相匹配,其完整路径名不会被废弃,此时目标文件会在搜寻到的目录中被重建。 为了测试 GPATH 变量的效果,我们先清除掉上一次测试产生的文件,并切换到 ../wild_code/ 目录编译得到对应的 .o 文件:

make clean;ls;cd ../wild_code;
cc -c foo1.c;touch foo1.c
cc -c foo2.c;touch foo2.c
cc -c main.c;touch main.c

现在wild_code 目录下已经存在 foo1.o foo2.o main.o文件了。 切回 gpath_code 目录并在执行 make 时使用 GPATH 变量:

cd ../gpath_code/; make GPATH=../wild_code/;ls

终端打印:

cc -c -o main.o ../wild_code/main.c
cc -c -o foo1.o ../wild_code/foo1.c
cc -c -o foo2.o ../wild_code/foo2.c
objs inlude :  main.o foo1.o foo2.o
cc -o main main.o foo1.o foo2.o
foo1.o  foo2.o  main  main.o  makefile

从 ls 的结果来看.o文件依然在当前路径下生成,不符合预期,为什么?
检查 makefile 文件,发现:

vpath %.c ../wild_code/

但我们并没有为 .o 目标文件指定目录。
修改 makefile,在 vpath 关键字下面一行再增加一条规则:

vpath %.o ../wild_code/

清除掉上次的执行结果,再执行一次:

touch ../wild_code/main.c
touch ../wild_code/foo1.c
touch ../wild_code/foo2.c
make clean;ls;make GPATH=../wild_code/;ls

终端打印:

rm -f main.o foo1.o foo2.o
rm -f main
makefile
cc -c -o ../wild_code/main.o ../wild_code/main.c
cc -c -o ../wild_code/foo1.o ../wild_code/foo1.c
cc -c -o ../wild_code/foo2.o ../wild_code/foo2.c
objs inlude :  main.o foo1.o foo2.o
cc -o main ../wild_code/main.o ../wild_code/foo1.o ../wild_code/foo2.o
main  makefile

可见这一次只有 main 文件在当前路径下生成,其余 .o 文件都在 ../wild_code/ 中被重建。

实验过程如下图所示: Makefile 基础入门 - 图41 Makefile 基础入门 - 图42

-lNAME 文件的使用

  • -lNAME 的搜索

makefile 中可以使用 -lNAME 来链接共享库和静态库。文件列表中的 -lNMAE 将被解析为名为 libNAME.so 或 libNAME.a 文件。

make 搜索 -lNAME 的过程如下:

1)在当前目录搜索名为libNAME.so 的文件; 2)若不存在则搜索 VPATH 或 vpath 定义的路径; 3)若仍然不存在,make 将搜索系统默认目录,顺序为/lib , /usr/lib , /usr/local/lib; 4)若依然无法找到文件,make 将按照以上顺序查找名为 libNAME.a的文件。

库文件搜索规则验证:

本次实验步骤如下:

1)编写同名的动态库文件和静态库文件,使用相同的 api 内部打印不同信息; 2)编写 main 文件调用库文件 api; 3)编译库文件生成静态库和动态库; 4)makefile 中使用 -lNAME 依赖项进行链接,验证使用的哪个库文件; 5)删除之前链接到的库文件再次执行 make 确认另一个库文件能否被成功链接;

示例代码已经在 chapter5/lib_code/ 目录下,文件如下:

.
├── lib
│   ├── foo_dynamic.c
│   └── foo_static.c
├── main.c
└── makefile

lib/ 下有两个 .c 文件 foo_dynamic.cfoo_static.c,定义了同一个函数 foo(),分别返回 1 和 2,这两份代码会被分别用于生成动态库和静态库文件。 主目录下的 main.c 调用 foo()函数并打印得到的结果。

makefile 中提供了生成库文件和链接 main.o 的方法,内容如下:

#this is a makefile for -lNAME test
.PHONY: all clean static_lib dynamic_lib
VPATH=lib/
all: main.o -lfoo
    $(CC) -o main $^
static_lib: foo_static.o
    $(AR) rc libfoo.a $^;\
    mv libfoo.a lib/
dynamic_lib: foo_dynamic.o
    $(CC) $^ -fPIC -shared -o libfoo.so;\
    mv libfoo.so lib/
clean:
    $(RM) *.o *.a *.so main
    $(RM) lib/*.a lib/*.soso

动态库和静态库的链接,我们在 chapter0 已经测试过了,现在先分别生成动态库和静态库文件。

执行:

cd ../lib_code
make static_lib;make dynamic_lib;ls lib/

终端打印:

cc -c -o foo_static.o lib/foo_static.c
ar rc libfoo.a foo_static.o;\
mv libfoo.a lib/
cc -c -o foo_dynamic.o lib/foo_dynamic.c
cc foo_dynamic.o -fPIC -shared -o libfoo.so;\
mv libfoo.so lib/
foo_dynamic.c  foo_static.c  libfoo.a  libfoo.so

再执行 make 看看终极目标链接的是哪个库文件:

make; ./main

终端打印:

cc -c -o main.o main.c
cc -o main main.o lib/libfoo.so
get i=1

可见 -lNAME 优先被解析为动态库。

删除动态库再次编译执行:

rm lib/libfoo.so 
make; ./main

终端打印:

cc -o main main.o lib/libfoo.a
get i=2

可见动态库文件不存在时,make 会尝试查找和链接静态库文件。
-lNAME 的展开是由变量.LIBPATTERNS来指定的,其值默认为“lib%.so lib%.a”。 感兴趣的同学可以自己尝试打印和修改此变量。

实验过程如下图所示: Makefile 基础入门 - 图43

实验总结

本实验测试了wildcardVPATHvpathGPATH-lNAME 的使用方法及文件路径保存算法。

课后习题

尝试测试使用 make 4.1 以后版本的 VPATH 功能。

Makefile 规则进阶(3)

课程简介

实验简介

本次实验将介绍强制目标、多规则目标、多目标规则、静态模式、双冒号规则的使用。

  • 强制目标的使用
  • 多规则目标的使用
  • 多目标规则的使用
  • 静态模式的使用
  • 双冒号规则的使用

    实验知识点

    本课程项目完成过程中将学习:

  • 强制更新依赖

  • 一个目标文件对应到多个规则目标
  • 依赖文件和重建指令对应多条不同的目标
  • 静态模式的规则和目标
  • 双冒号规则

    实验环境

  • Xfce 终端

  • GCC
  • Gedit

    适合人群

    本课程难度为中等,适合已经初步了解 makefile 规则的学员进行学习。

    代码获取

    你可以通过下面命令将本课程里的所有源代码下载到实验楼环境中,作为参照对比进行学习。

    $ wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip
    

    请尽量按照实验步骤自己写出 C 语言程序。

    实验步骤

    本章节的源代码位于 /home/shiyanlou/Code/make_example/chapter6/ 目录下。
    项目文件结构

    .
    ├── force:强制目标的使用测试
    │   ├── aim
    │   ├── dep_a
    │   ├── makefile
    │   └── normal.mk
    ├── multi:多规则目标和多目标规则的使用测试
    │   ├── aim.mk
    │   ├── file_1
    │   └── makefile
    ├── readme.md:实验说明文件
    ├── rule:双冒号规则使用测试
    │   ├── err.mk
    │   └── makefile
    └── static:静态规则使用测试
      └── makefile
    

    强制目标的使用

  • 普通目标使用测试

为了验证强制目标的功能,我们先使用一个普通目标测试其被make执行的状况。

源代码的force/normal.mk内容如下:

#this is a makefor for foce aim test
aim:dep_a
    date >> aim
dep_a:
    touch dep_a

终极目标aim依赖于dep_a文件,dep_a文件会在执行时被touch指令更新时间戳。

进入force目录并连续执行两次此makefile文件:

cd force;make -f normal.mk;make -f normal.mk

终端打印:

touch dep_a
date >> aim
make: `aim' is up to date.

可见终极目标 aim 只会重建一次,之后因为依赖文件没有更新,aim 目标 也不会再被重建。

  • 强制目标使用测试

如果希望终极目标每次都能够被重建,一种方法是将其声明为伪目标,另一种方法是使用强制目标。 强制目标是指没有依赖或者命令行的目标,此目标每次被执行时都会被更新到,因此依赖于此目标的目标也会被强制更新。

源代码的force/makefile中有一个强制目标的示例,内容如下:

#this is a makefor for foce aim test
aim:FORCE
    date >> aim
FORCE:

本例中FORCE就是一个强制目标,由于FORCE没有依赖项和命令行,且搜索目录下不存在同名的FORCE文件, 因此FORCE目标每次被执行时都会无条件更新,导致aim也会被强制更新。

直接执行三次make,检查终端打印和aim文件内容:

make;make;make;cat aim

终端打印如下:

date >> aim
date >> aim
date >> aim
Tue Jul 18 12:57:50 CST 2017
Tue Jul 18 12:59:15 CST 2017
Tue Jul 18 12:59:15 CST 2017
Tue Jul 18 12:59:15 CST 2017

可见aim目标被连续重建三次,aim文件中第一个时间是使用normal.mk时加入的,后面三次时间是使用makefile加入的。

实验过程如下图所示: Makefile 基础入门 - 图44

多规则目标的使用

多规则目标是指一个目标可以对应到多个规则,每条规则有不同的依赖。 make 对多规则目标的处理方式是:

1)合并依赖文件列表。 2)不断重写目标的重建规则,由新的规则直接覆盖旧的规则。

源代码目录下有multi/makefile文件,用于测试多规则目标,内容如下:

#this is a makefile for multi-command test
.PHONY: multi prepare
multi:dep_a dep_b
    @echo $^ "[cmd1]"
multi:dep_c dep_d
    @echo $^ "[cmd2]"
multi:prepare
    @echo $^ "[cmd3]"
multi:
    @echo $^ "[cmd4]"
prepare:
    @touch dep_a dep_b dep_c dep_d

可见multi目标对应了四条规则,依赖文件,执行的指令都不一样。

先看看make会怎样处理这个文件,切换目录并执行 make:

cd ../multi;make

终端打印:

makefile:9: warning: overriding commands for target `multi'
makefile:6: warning: ignoring old commands for target `multi'
makefile:12: warning: overriding commands for target `multi'
makefile:9: warning: ignoring old commands for target `multi'
makefile:15: warning: overriding commands for target `multi'
makefile:12: warning: ignoring old commands for target `multi'
prepare dep_a dep_b dep_c dep_d [cmd4]

可以看见multi规则指令一直在被重写,并忽略之前的指令。 最后使用cmd4来重建multi目标,其它指令都没有得到执行。 最后依赖文件列表值得思考: 注意:使用make 4.13.8产生的依赖项摆放顺序不一样,感兴趣的同学可以自己测试。 make 4.1的排列是按照makefile规则的顺序从后到前,从左到右。 而make3.81则是最后的依赖项在前,其它依赖项从前往后合并。

实验过程如下图所示: Makefile 基础入门 - 图45

多目标规则的使用

makefile 的规则书写格式允许一个规则对应多个目标,规则的依赖关系和命令对所有目标有效。

我们使用aim.mk来测试多目标规则,aim.mk内容如下:

#this is a makefile for multi-aim test
.PHONY:clean
file_1 file_2 file_3:depen_1 depen_2
    @echo "this is a multi-aim rule for " $@
    touch file_1 file_2 file_3
depen_1:
    touch depen_1
depen_2:
    touch depen_2
clean:
    $(RM) depen_* file_*

file_1``file_2``file_3共用一条规则,并共同依赖于depen_1``depen_2两个文件。 规则中可以使用自动化变量$@来区分具体执行哪一条规则。

现在分别执行三条规则看看效果如何。

cd ../multi/
make -f aim.mk file_1;make -f aim.mk file_2;make -f aim.mk file_3

终端打印:

touch depen_1
touch depen_2
this is a multi-aim rule for  file_1
touch file_1
this is a multi-aim rule for  file_2
touch file_2
this is a multi-aim rule for  file_3
touch file_3

说明处理符合预期,$@能够识别到当前的执行目标。 另一点需要说明的地方是多目标规则同样能支持终极目标,多目标中的第一个目标就是终极目标。

执行make验证一下:

make -f aim.mk clean;make -f aim.mk

终端打印:

rm -f depen_* file_*
touch depen_1
touch depen_2
this is a multi-aim rule for  file_1
touch file_1

表明file_1就是aim.mk的终极目标。

实验过程如下图所示: Makefile 基础入门 - 图46

静态模式的使用

多目标规则可以利用自动化变量来区分不同目标并做出相应的处理,但所有目标的依赖文件必须相同。 如果某种情况下我们希望多目标规则中,不同目标能对应不同的依赖文件,可以使用静态模式规则来实现。

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

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

TARGETS表示目标文件列表,RARGET-PATTERN为目标模式,它提取出与TARGETS中相匹配的部分作为“茎”, 替换PREREQ-PATTERNS中相应的部分来产生依赖文件。这样不同的目标就可以通过模式匹配来依赖不同的文件。
chapter6/static/ 目录下的 makefile 文件用于验证静态模式规则,文件内容如下:

#this is a makefile for static mode
.PHONY:clean aim_1 aim_2 aim_3
aim_1 aim_2 aim_3:aim_%:depen_%
    @echo "target:"$@ " depen:" $^
depen_%:
    touch $@
clean:
    $(RM) aim_* depen*

aim_1 aim_2 aim_3是规则目标,根据静态模式的语法可以看出他们分别依赖于depen_1 depen_2 depen_3文件。 这三个依赖文件也使用了模式匹配规则,通过touch命令生成或更新自己。

分别执行三个目标,命令如下:

cd ../static/
make aim_1;make aim_2;make aim_3

终端打印:

touch depen_1
target:aim_1  depen: depen_1
touch depen_2
target:aim_2  depen: depen_2
touch depen_3
target:aim_3  depen: depen_3

可见它们分别依赖于各自特有的依赖文件。

实验过程如下图所示: Makefile 基础入门 - 图47

双冒号规则的使用

  • 双冒号规则

双冒号规则使用“::”代替普通规则中的“:”。当一个文件作为多个双冒号规则的目标时,这些不用的规则会被独立处理,分别执行,而不是像普通规则一样被新的处理命令覆盖。
chapter6/rule/目录下的makefile文件可以测试双冒号规则,内容如下:

#this is a test makefile
.PHONY:clean
aim::depen_a
    @echo $@ " : " $^ " [cmd1]"
aim::depen_b depen_c
    @echo $@ " : " $^ " [cmd2]"
aim::depen_d
    @echo $@ " : " $^ " [cmd3]"
aim::
    @echo $@ " : " $^ " [cmd4]"
depen_%:
    touch $@
clean:
    $(RM) depen_*

aim是多个双冒号规则的目标,这些规则的依赖和命令都不一样。

现在来测试aim的重建过程,执行make

cd ../rule/
make

终端打印:

touch depen_a
aim  :  depen_a  [cmd1]
touch depen_b
touch depen_c
aim  :  depen_b depen_c  [cmd2]
touch depen_d
aim  :  depen_d  [cmd3]
aim  :   [cmd4]

可见四个规则都被执行到,并且每一条规则都是独立执行,依赖项并不想普通多规则目标那样发生合并。

  • 双冒号规则的限制

一个目标可以出现在多个规则中,但这些规则类型必须相同,要么都是普通规则,要么都是双冒号规则。

err.mk 演示了普通规则和双冒号规则混合使用的情况,它将makefile中的第四条双冒号规则修改为普通规则,如下:

aim:
    @echo $@ " : " $^ " [cmd4]"

执行此文件:

make -f err.mk

终端打印:

err.mk:14: *** target file 'aim' has both : and :: entries.  Stop.

可知make不允许不同类型规则混用的情况出现。

实验过程如下图所示: Makefile 基础入门 - 图48

实验总结

本次实验测试了强制目标、多规则目标、多目标规则、静态模式、双冒号规则的使用方法。

Makefile 规则命令

课程简介

实验简介

本次实验将介绍make对规则命令的执行,命令执行过程中的错误处理以及命令包的使用。

  • make对规则命令的执行
  • make的多线程执行
  • make的错误忽略选项
  • make的异常结束

    实验知识点

    本课程项目完成过程中将学习:

  • $(SHELL) 执行规则命令

  • -j 选项进行多线程执行
  • -、-i、-k 参数的作用
  • make 异常结束
  • define

    实验环境

  • Xfce 终端

  • GCC
  • Gedit

    适合人群

    本课程难度为中等,适合已经初步了解 makefile 规则的学员进行学习。

    代码获取

    你可以通过下面命令将本课程里的所有源代码下载到实验楼环境中,作为参照对比进行学习。

    $ wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip
    

    请尽量按照实验步骤自己写出 C 语言程序。_

    实验步骤

    本章节的源代码位于 /home/shiyanlou/Code/make_example/chapter7/ 目录下。
    项目文件结构:

    .
    ├── cancel:make 的异常结束处理
    │   └── makefile
    ├── error:make 命令的错误处理
    │   ├── iopt.mk
    │   ├── kopt.mk
    │   └── makefile
    ├── joption:make 的并行使用
    │   └── makefile
    ├── pack:命令包使用测试
    │   └── makefile
    ├── Readme.md
    └── shell_vari:验证`$(SHELL)`环境变量的传递
      └── shell.mk
    

    make对规则命令的执行

  • SHELL环境变量的传递

make 使用环境变量SHELL指定的程序来处理规则命令行,GNU make 中默认的程序是/bin/sh; 与其它环境变量不同的是,SHELL 变量会由 GNU make 自行定义,而不会使用当前系统的同名变量。 这样做的理由是:make 认为系统的 SHELL 变量适用于定义人机交互接口,make 没有交互过程,因此不适用。

shell_vari 目录下的shell.mk文件演示了系统环境变量SHELLmake使用的环境变量的差异, 文件内容如下:

#this is a makefile for $(SHELL) test
.PHONY:all
all:
    @echo "\$$SHELL environment is $$SHELL"
    @echo "\$$SHELL in makefile is " $(SHELL)

由于符号$是变量引用的起始字符,因此要使用$本身这个字符时需要使用进行转义。 all规则的第一条指令是打印系统环境变量SHELL代表$字符,所以在终端上印出来的内容相当于:

"\$SHELL environment is $SHELL"

\符号是终端下的转义字符,$符号在终端下同样是变量引用的起始字符,因此$SHELL会被系统环境变量SHELL的内容代替,而\$SHELL会被打印为“$SHELL”all规则的第二条指令是打印当前make使用的SHELL变量。
进入shell_vari目录,并查看当前系统SHELL变量:

cd shell_vari/;echo $SHELL

终端打印:

/usr/bin/zsh

再执行make看看SHELL变量定义是什么:

make -f shell.mk

终端打印:

$SHELL environment is /usr/bin/zsh
$SHELL in makefile is  /bin/sh

可见make使用的是自己默认的变量,与系统SHELL变量无关。

  • SHELL变量传参

下面再实验在执行make时,传入SHELL变量为abc

make -f shell.mk SHELL=abc

终端打印:

make: abc: Command not found
make: *** [all] Error 127

可见make尝试用我们传入的abc来执行规则结果因为找不到abc导致执行失败。 这说明make自身的SHELL变量也是可以通过传参进行修改的。

实验过程如下图所示: Makefile 基础入门 - 图49

make的多线程执行

make 也可以使用多线程进行并发执行,使用方法为执行make时加入命令行选项-jN,N为一个数字,表示要执行的线程数。 当make的每个线程会执行一个规则的重建,每条规则只由一个线程执行。 不使用-j选项时为单线程编译。
chapter7/joption/makefile文件给出了测试方法,内容如下:

#this is a makefile for -j option
.PHONY:all
all:aim1 aim2 aim3 aim4
    @echo "build final finish!"
aim%:
    @echo "now build " $@
    @sleep 2
    @echo "build " $@ " finish!"

终极目标allaim1aim4四个依赖项,每个依赖项的规则一致,打印信息并睡眠两秒。 进入joption目录,并执行make,完成编译需要8秒。

cd ../joption/;make

终端打印:

now build  aim1
build  aim1  finish!
now build  aim2
build  aim2  finish!
now build  aim3
build  aim3  finish!
now build  aim4
build  aim4  finish!
build final finish!

现在使用两个线程执行makefile

make -j2

可以看到终端先同时印出aim1 aim2的执行信息,两秒后再打印aim3 aim4的执行信息,用时为4秒,比单线程缩短一倍,内容如下:

now build  aim2
now build  aim1
build  aim2  finish!
build  aim1  finish!
now build  aim3
now build  aim4
build  aim3  finish!
build  aim4  finish!
build final finish!

大家可以再测试一下三线程和四线程的并行执行过程,并尝试在目标中加入依赖项来限制并行编译的顺序。

实验过程如下图所示: Makefile 基础入门 - 图50

make的错误忽略选项

  • make执行过程出错的简单测试

下面我们来看一下make执行出错的状况。 chapter7/error/目录下的makefile文件演示了rm命令的执行状况,内容如下:

#this is a makefile for error handle test
.PHONY:all
all:pre_a pre_b pre_c
    $(RM) pre_a
    $(RM) pre_b
    $(RM) pre_c
    $(RM) d
    -rm e
    rm f
    rm g
pre_%:
    touch $@

前面三条指令是删除生成的文件,后面四条指令则是删除不存在的文件,在shell使用rm会直接运行失败。

现在执行此makefile并解释执行状况:

cd ../error/;make

终端打印:

touch pre_a
touch pre_b
touch pre_c
rm -f pre_a
rm -f pre_b
rm -f pre_c
rm -f d
rm e
rm: cannot remove 'e': No such file or directory
make: [all] Error 1 (ignored)
rm f
rm: cannot remove 'f': No such file or directory
make: *** [all] Error 1

make在运行规则命令结束后会检测命令执行的返回状态,返回成功则启动另外一个子shell来执行下一条命令。 在makefile执行过程中,先生成pre_a pre_b pre_c三个文件,再使用rm -f或者rm删除它们,这个过程没有问题。 第四条指令是删除不存在的文件d,由于使用了-f参数,因此shell也不会返回错误。 第五条指令是删除不存在的文件e,由于命令行起始处使用了符号“-”make会忽略此命令的执行错误,所以shell虽然返回并打印错误,但make继续往下执行。 第六条指令是删除不存在的文件f,由于只使用了rm命令,shell返回错误,make收到错误后不再往下执行, 因此第七条指令已经没机会执行到。

  • 忽略命令执行错误

在某些状况下,用户希望make遇上错误可以继续往下执行。在多人维护的庞大工程中,makefile文件随时可能出现错误,这时用户希望它能继续执行下去方便测试自己的模块,而不是被其他人的错误阻塞住。 此时可以使用-i选项,-i选项会让make忽略所有的错误。

iopt.mk文件可以演示-i选项的用法,内容如下:

#this is a makefile for error handle test
.PHONY:all
all:
    rm a
    rm b
    rm c
    rm d

make会直接调用rm删除四个不存在的文件,每一条指令都会返回错误。

先看正常执行结果,直接使用make

make -f iopt.mk

终端打印:

rm a
rm: cannot remove 'a': No such file or directory
make: *** [all] Error 1

现在加入 -i 选项:

make -f iopt.mk -i

终端打印:

rm a
rm: cannot remove 'a': No such file or directory
make: [all] Error 1 (ignored)
rm b
rm: cannot remove 'b': No such file or directory
make: [all] Error 1 (ignored)
rm c
rm: cannot remove 'c': No such file or directory
make: [all] Error 1 (ignored)
rm d
rm: cannot remove 'd': No such file or directory
make: [all] Error 1 (ignored)

现在 make 可以执行到每一条指令了。

  • 忽略依赖项错误

make遇上依赖项不存在时,-i选项就不管用了,因为它不属于命令行错误。

kopt.mk文件用于演示依赖文件错误的状况,内容如下:

#this is a makefile for error handle test
.PHONY:all
all: h i j
        @echo "exe OK!"

分别使用makemake -i执行此文件:

make -f kopt.mk ; make -f kopt.mk -i

终端打印:

make: *** No rule to make target 'h', needed by 'all'.  Stop.
make: *** No rule to make target 'h', needed by 'all'.  Stop.

说明在依赖项错误中-i选项没有任何作用。此时可以使用-k选项让其忽略依赖项错误并继续执行:

make -f kopt.mk -k

终端打印:

make: *** No rule to make target 'h', needed by 'all'.
make: *** No rule to make target 'i', needed by 'all'.
make: *** No rule to make target 'j', needed by 'all'.
make: Target 'all' not remade because of errors.

-k选项可以让make继续检查其它依赖项,但并不会执行终极目标的指令。 若有多个依赖项被修改过后,可以使用此选项测试哪些依赖项的修改有问题。
请谨慎使用-i-k选项,以免产生预期外的错误。

实验过程如下图所示: Makefile 基础入门 - 图51 Makefile 基础入门 - 图52

make的异常结束

make 若收到致命信号被终止时,它会删除此过程中已经重建的目标文件,以免目标文件出现预期外的错误。 例如某个目标规则需要对目标文件进行多次处理,处理到一半时make被终止,导致目标文件处于异常状态, 因此make会删除此文件以免产生难以察觉的问题。
chapter7/cancel/目录下目录下的makefile用于演示make异常结束的状况,内容如下:

#this is a makefile for cancel handle
.PHONY:all clean
all:clean pre_a pre_b pre_c
    sleep 1
    @echo "exe target all!"
clean:
    $(RM) pre_*
pre_%:
    @echo "\n"
    touch $@
    @echo "generate " $@
    @ls -l $@
    @echo "sleep 5s before finish..."
    sleep 5

终极目标all依赖于pre_a pre_b pre_c文件,这三个文件在建立过程中会sleep五秒钟,方便用户结束make命令。 先正常执行一次:

cd ../cancel/;make

终端显示执行成功,并在当前目录下生成pre_a pre_b pre_c三个目标文件。 现在在产生pre_b的过程中使用ctrl+c结束make进程,终端打印如下:

rm -f pre_*
touch pre_a
generate  pre_a
-rw-rw-r-- 1 shiyanlou shiyanlou 0 Jul 24 21:44 pre_a
sleep 5s before finish...
sleep 5
touch pre_b
generate  pre_b
-rw-rw-r-- 1 shiyanlou shiyanlou 0 Jul 24 21:44 pre_b
sleep 5s before finish...
sleep 5
^Cmake: *** Deleting file `pre_b'
make: *** [pre_b] Interrupt

pre_b规则命令使用ls命令查看到当前目录下已经有pre_b文件生成。

make被强制结束后,再次使用ls命令查看当前目录文件:

ls -l pre*

文件内容如下:

-rw-r--r-- 1 root root 0 Jul 24 08:12 pre_a

可见pre_b已经被make自行删除。从make的打印内容中也可以看出它有执行删除pre_b的动作。

实验过程如下图所示: Makefile 基础入门 - 图53 Makefile 基础入门 - 图54

命令包的使用

书写makefile时,可能有多个规则会使用一组相同的命令,就像c语言要调用函数一样, 我们可以使用define来完成这项功能。define“define”开头,以“endef”结束,作用与c语言的宏定义类似。 chapter7/pack/目录下演示了命令包的使用方法,其内容如下:

#this is a makefile for define test
.PHONY:all
define echo-target
@echo "now rebuilding target : " $@
touch $@
endef
all:pre_a pre_b pre_c
    @echo "final target finish!"
pre_%:
    $(echo-target)

终极目标all的依赖项都会调用同一组命令包。 进入pack目录并测试执行效果:

cd ../pack/;make

终端打印:

now rebuilding target :  pre_a
touch pre_a
now rebuilding target :  pre_b
touch pre_b
now rebuilding target :  pre_c
touch pre_c
final target finish!

实验过程如下图所示: Makefile 基础入门 - 图55

实验总结

本次实验测试了make对规则命令的执行,命令执行过程中的错误处理以及命令包的使用方式。

课后习题

  1. 请使用目标依赖来控制并行编译的顺序。
  2. 请尝试在命令包中使用变量控制参数的输入和输出。

Make 递归执行

课程简介

实验简介

本次实验将介绍 make 的递归执行及其过程中变量、命令行参数的传递规则。

  • make的递归执行示例
  • 递归执行过程中变量的传递
  • 测试MAKELEVEL环境变量
  • 命令行参数和变量的传递

    实验知识点

    本课程项目完成过程中将学习:

  • make 的 -w 选项

  • makefile 中使用 $(MAKE)
  • 递归执行过程中的变量传递
  • export 和 unexport

    实验环境

  • Xfce 终端

  • GCC
  • Gedit

    适合人群

    本课程难度为中等,适合已经初步了解 makefile 规则的学员进行学习。

    代码获取

    你可以通过下面命令将本课程里的所有源代码下载到实验楼环境中,作为参照对比进行学习。

    $ wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip
    

    请尽量按照实验步骤自己写出 C 语言程序。

    实验步骤

    本章节的源代码位于 /home/shiyanlou/Code/make_example/chapter8/ 目录下。
    项目文件结构

    .
    ├── flags:用于测试 MAKEFLAGS 变量
    │   ├── dir_a
    │   │   └── makefile
    │   └── makefile
    ├── level:用于测试 MAKELEVEL 变量
    │   ├── dir_a
    │   │   ├── dir_b
    │   │   │   └── makefile
    │   │   └── makefile
    │   └── makefile
    ├── Readme.md
    ├── recur:用于测试 make 的递归调用
    │   ├── dir_a
    │   │   └── makefile
    │   ├── dir_b
    │   │   └── makefile
    │   └── makefile
    └── vari:用于测试 make 递归调用的变量传递
      ├── dir_a
      │   └── makefile
      ├── dir_b
      │   └── makefile
      ├── dir_c
      │   └── makefile
      ├── export.mk
      ├── makefile
      └── spec.mk
    

    make的递归执行示例

  • 测试make的递归调用

make 的递归过程是指:在makefile中使用make作为命令来执行本身或其它makefile文件。 在实际项目中,我们经常需要用到make的递归调用,这样可以简化每个模块的makefile设计与调试,便于项目管理。 chapter8/recur/目录演示了一个简单的递归调用过程,主目录makefile内容如下:

#this is a makefile for recursion test
subdir := dir_a dir_b
.PHONY:clean all $(subdir)
all:$(subdir)
    @echo "final target finish!"
$(subdir):
    cd $@;$(MAKE)

终极目标all依赖于两个依赖项,依赖项的规则命令是进入子目录并调用底层的makefile。 这里有三个地方需要说明:

1)subdir变量使用了:=进行赋值,这是直接展开赋值,即make读到此行时就直接将subidr文本固定为后面赋值的内容。我们会在后续章节继续讲解:==的差别。对简单的makefile而言,推荐大家尽量使用:=的赋值方式。 2).PHONY中也引用了变量subdir,这样做没有任何问题。 3)观察依赖项的规则命令,进入子目录后使用了$(MAKE)而非make,这样做的好处是保证执行上下层makefilemake都是同一个程序。

两个子目录下的makefile文件内容相同,都是:

.PHONY:show
show:
    @echo "target " $@ "@" $(PWD)

所以子目录的执行过程是更新目录时间戳,并打印当前执行路径。

现在进入recur目录并执行make

cd recur/;make

终端打印:

cd dir_a;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/recur/dir_a'
target  show @ /home/shiyanlou/Code/make_example/chapter8/recur/dir_a
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/recur/dir_a'
cd dir_b;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/recur/dir_b'
target  show @ /home/shiyanlou/Code/make_example/chapter8/recur/dir_b
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/recur/dir_b'
final target finish!

分析打印内容可知,make 先后进入dir_adir_b目录执行目录下的makefile,并完成终极目标的重建。 除了预期打印之外,终端上还多出了几行make进出目录的打印信息,这其实是由-w选项来控制的。 make在进行递归调用时会自动传递-w选项给下层make

我们在顶层make也加上-w确认一下效果:

make -w

终端打印:

make: Entering directory `/home/shiyanlou/Code/make_example/chapter8/recur'
cd dir_a;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/recur/dir_a'
target  show @ /home/shiyanlou/Code/make_example/chapter8/recur/dir_a
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/recur/dir_a'
cd dir_b;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/recur/dir_b'
target  show @ /home/shiyanlou/Code/make_example/chapter8/recur/dir_b
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/recur/dir_b'
final target finish!
make: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/recur'

可知make在执行顶层makefile和执行完毕之后都会打印进出当前目录的信息。

实验过程如下图所示: Makefile 基础入门 - 图56

递归执行过程中变量的传递

  • 变量传递

make的递归执行过程中,上层的变量默认状态下是不会传递给下层makefile的。 chapter8/vari/目录下的内容演示了变量传递的过程,先看看chapter8/vari/makefile文件:

#this is a makefile for recursion test
subdir := dir_a dir_b
.PHONY:clean all $(subdir)
all:$(subdir)
    @echo "final target finish!"
$(subdir):
    cd $@;$(MAKE)

与上一个实验中的内容一致,进入两个子目录并调用$(MAKE)之后打印完成消息。

dir_a/makefile的内容如下:

.PHONY:show
show:
    @echo "target " $@ "@" $(PWD)
    @echo "vari subdir is " $(subdir)

打印当前目录,并打印subdir变量的内容。

dir_b/makefile内容相似,但多出了一个subdir的定义:

.PHONY:show
subdir := none
show:
    @echo "target " $@ "@" $(PWD)
    @echo "vari subdir is " $(subdir)

执行打印,看看子目录的subdir内容:

cd ../vari/;make

终端打印:

cd dir_a;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_a'
target  show @ /home/shiyanlou/Code/make_example/chapter8/vari/dir_a
vari subdir is 
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_a'
cd dir_b;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_b'
target  show @ /home/shiyanlou/Code/make_example/chapter8/vari/dir_b
vari subdir is  none
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_b'
final target finish!

可见上层的subdir变量没有传递到下层makefile中。

  • 使用export传递变量

如果想要将上层变量往下层传递,需要用到export关键字,格式为:

export VARIABLE ...

export.mk文件演示了export的用法,它的内容与makefile一致,只是在定义完subdir之后多了一行:

export subdir

执行 export.mk 文件:

make -f export.mk

终端打印:

cd dir_a;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_a'
target  show @ /home/shiyanlou/Code/make_example/chapter8/vari/dir_a
vari subdir is  dir_a dir_b
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_a'
cd dir_b;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_b'
target  show @ /home/shiyanlou/Code/make_example/chapter8/vari/dir_b
vari subdir is  none
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_b'
final target finish!

这次dir_a目录下的makefile可以成功继承上层传递的subdir变量, 但dir_b目录下的makefile打印出来依然为“none”,这是因为低层的makefile对变量定义具有更高的优先级。

可以使用-e选项来取消低层的高优先级:

make -f export.mk -e

终端打印:

cd dir_a;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_a'
target  show @ /home/shiyanlou/Code/make_example/chapter8/vari/dir_a
vari subdir is  dir_a dir_b
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_a'
cd dir_b;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_b'
target  show @ /home/shiyanlou/Code/make_example/chapter8/vari/dir_b
vari subdir is  dir_a dir_b
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_b'
final target finish!

可以看出,现在subdir可以顺利传递给每个子目录的makefile。 如果不希望变量再往下传递,可以使用unexport关键字,格式与export一致。 当unexportexport作用于同一个变量时,以最后声明的unexport/export为准,请大家课后自行测试unexport的用法。

  • 两个默认传递的环境变量

需要注意的是有两个变量默认会传递给下层的makefile,它们是$(SHELL)$(MAKEFLAGS)变量。 之前的实验已经说明了$(SHELL)的作用:指明要使用何种shell程序执行规则命令。 $(MAKEFLAGS)则是用来传递make的命令行选项和参数,实验2.4会对这个变量做进一步说明。

spec.mk文件演示了这两个变量的传递,内容如下:

#this is a makefile for special vari in recursion test
subdir := dir_c
.PHONY:all $(subdir)
all:$(subdir)
    @echo "finished!"
$(subdir):
    cd $@;$(MAKE)

spec.mk会进入dir_c为子目录并执行$(MAKE)。 子目录makefile内容如下:

.PHONY:show
show:
    @echo "SHELL is " $(SHELL)
    @echo "MAKEFLAGS is " $(MAKEFLAGS)
    @echo "subdir is " $(subdir)

因此子目录的执行内容就是打印三个变量的值。 执行spec.mk 进行测试:

make -f spec.mk

终端打印:

cd dir_c;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_c'
SHELL is  /bin/sh
MAKEFLAGS is  w
subdir is 
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_c'
finished!

可见SHELLMAKEFLAGS变量默认是有值的,如何证明他们是从上层传递下来的而不是make的默认值呢?
只需要在顶层调用make时修改他们的值即可:

make -f spec.mk SHELL=bash -i -k

终端打印:

cd dir_c;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_c'
SHELL is  bash
MAKEFLAGS is  wki -- SHELL=bash
subdir is 
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/vari/dir_c'
finished!

上个章节的实验已经测试过SHELL变量和-i``-k选项的作用,大家可以自行复习一下。 此时两个变量都得到了更新。

实验过程如下图所示: Makefile 基础入门 - 图57 Makefile 基础入门 - 图58

测试MAKELEVEL环境变量

变量MAKELEVEL表明当前的调用深度,每一级的递归调用中,MAKELEVEL的值都会发生变化, 最上层值为0,每往下一层加1chapter8/level/makefile演示了MAKELEVEL的变化,level/目录下还有两层子目录dir_adir_b, 每一层的makefile都会打印当前MAKELEVEL的值。

进入level目录并执行makefile,确认MAKELEVEL的变化:

cd ../level/;make

终端打印:

cd dir_a;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/level/dir_a'
cd dir_b;make
make[2]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/level/dir_a/dir_b'
this is level: 2
dir_b finished!
make[2]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/level/dir_a/dir_b'
this is level: 1
dir_a finished!
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/level/dir_a'
this is level: 0
root dir finished!

由打印可以看出dir_b目录下MAKELEVEL值为2dir_a值为1level目录值为0。 有些项目中需要利用MAKELEVEL变量的特性来进行条件编译,大家可以自己设计实验使用MAKELEVEL

实验过程如下图所示: Makefile 基础入门 - 图59

命令行参数和变量的传递

make的递归执行过程中,最上层make的命令行选项会被自动通过环境变量MAKEFLAGS传递给子make进程。 如前面的实验,通过命令行指定了-i-k选项,MAKEFLAGS的值会被自动设定为“ki”

需要注意的是:

1)-w选项默认会被传递给子make; 2)有几个特殊的命令行选项不会被记录进变量,它们是-C -f -o-W

顶层makefile内容如下:

#this is a makefile for MAKEFLAGS test
subdir := dir_a
.PHONY:all $(subdir)
all:$(subdir)
    @echo "root dir finished!"
$(subdir):
    @echo "MAKEFLAGS before subdir is : " $(MAKEFLAGS)
    cd $@;$(MAKE)

它会在进入subdir之前打印MAKEFLAGS变量,底层makefile也同样会打印MAKEFLAGS变量。

执行make

cd ../flags/;make

终端打印:

MAKEFLAGS before subdir is : 
cd dir_a;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/flags/dir_a'
MAKEFLAGS in dir_a is : w
dir_a finished!
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/flags/dir_a'
root dir finished!

可见顶层MAKEFLAGS变量为空,底层自动添加了-w选项。 现在可以利用这些打印测试传入不同参数。

先将makefile拷贝为test.mk,再用-f选项指定执行test.mk,并添加其它参数:

cp makefile test.mk;make -f test.mk -i -k SHELL=bash

终端打印如下:

MAKEFLAGS before subdir is :  ki -- SHELL=bash
cd dir_a;make
make[1]: Entering directory `/home/shiyanlou/Code/make_example/chapter8/flags/dir_a'
MAKEFLAGS in dir_a is : wki -- SHELL=bash
dir_a finished!
make[1]: Leaving directory `/home/shiyanlou/Code/make_example/chapter8/flags/dir_a'
root dir finished!

可见顶层makefile中,MAKEFLAGS变量为“ik -- SHELL=bash”-f选项没有添加进来。 底层makefile中,MAKEFLAGS变量为“ikw -- SHELL=bash”,除了多出默认需要传递的-w选项外,其它部分与顶层传参一致。

实验过程如下图所示: Makefile 基础入门 - 图60

实验总结

本次实验测试了make的递归执行及其过程中变量、命令行参数的传递规则。

课后习题

  • 请测试exportunexport作用于同一个变量时,make的处理方式。
  • 请使用MAKELEVEL变量进行编译流程控制。
  • 请查找资料并测试$(MAKE)make在命令行参数的传递处理上有何区别并设计实验进行测试。

    Makefile 变量

    课程简介

实验简介

本次实验将介绍make的变量定义风格,变量的替换引用,环境变量、命令行变量、目标指定变量的使用及自动化变量的使用。

  • 不同的变量风格和赋值风格
  • 变量的替换引用,环境变量、命令行变量的使用
  • 目标指定变量的使用
  • 自动化变量的使用

实验知识点

本课程项目完成过程中将学习:

  • 变量的定义及展开时机
  • 递归展开变量使用 = 或 define
  • 变量的替换引用
  • 系统环境变量和文件中的同名变量
  • 命令行变量
  • 目标指定变量
  • 自动化变量

    实验环境

  • Xfce 终端

  • GCC
  • Gedit

    适合人群

    本课程难度为中等,适合已经初步了解 makefile 规则的学员进行学习。

    代码获取

    你可以通过下面命令将本课程里的所有源代码下载到实验楼环境中,作为参照对比进行学习。
    $ wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip
    
    请尽量按照实验步骤自己写出 C 语言程序。

实验步骤

本章节的源代码位于 /home/shiyanlou/Code/make_example/chapter9/目录下。
项目文件结构

.
├── auto:自动化变量的使用
│   ├── add.c
│   ├── makefile
│   └── minus.c
├── Readme.md
├── rep:变量的替换
│   ├── envi.mk
│   ├── makefile
│   └── override.mk
├── style:变量和赋值风格
│   ├── append.mk
│   ├── direct.mk
│   └── makefile
└── target:目标指定变量
    └── makefile

make 的递归执行示例

  • 递归展开式变量

makefile 变量就是一个名字,代表一个文本字符串。变量有两种定义方式:递归展开式变量和直接展开式变量。变量在makefile的读入阶段被展开成字符串。 递归展开式变量可以通过"=""define"进行定义,在变量定义过程中,对其它变量的定义不会立即展开,而是在变量被规则使用到时才进行展开。 chapter9/style/目录下的makefile文件演示了递归展开式变量的定义和使用方式。 文件内容如下:

#this makefile is for recursively vari test
.PHONY:recur loop
a1 = abc
a2 = $(a3)
a3 = $(a1)
b1 = $(b2)
b2 = $(b1)
recur:
    @echo "a1:"$(a1)
    @echo "a2:"$(a2)
    @echo "a3:"$(a3)
loop:
    @echo "b1:"$(b1)
    @echo "b2:"$(b2)

文件中recur规则用到3个变量,a1是直接定义字符串,a2引用后面才定义到的a3,a3则引用a1loop规则用到b1,b22个变量,二者相互引用。 进入style目录,测试recur规则:

cd style;make recur

终端打印:

a1:abc
a2:abc
a3:abc

可见a1 a2 a3的值是一致的,变量的展开与定义顺序无关。 再测试loop命令:

make loop

终端打印:

makefile:9: *** Recursive variable 'b1' references itself (eventually).  Stop.

make 因为两个变量的无限递归而报错退出。 从上面测试可以看出递归展开式的优点:此变量对引用变量的定义顺序无关。缺点则是:多个变量在互相引用时可能导致无限递归。 除此之外,递归展开式变量中若有函数引用,每次引用该变量都会导致函数重新执行,效率较低。

  • 直接展开式变量

直接展开式变量通过”:=”进行定义,对其它变量的引用和函数的引用都将在定义时被展开。

文件direct.mkmakefile中的"="替换为":=",重新执行recurloop规则:

make -f direct.mk recur;make -f direct.mk loop

终端打印:

a1:abc
a2:
a3:abc
b1:
b2:

从测试结果可以看出,由于a2,b1都引用了尚未定义的变量,因此被展开为空。 使用直接展开式变量可以避免无限递归问题和函数重复展开引发的效率问题,并且更符合一般的程序设计逻辑,便于调试问题,因此推荐用户尽量使用直接展开式变量。

  • 变量追加和条件赋值

使用+=赋值符号可以对变量进行追加,变量追加时的赋值风格与变量定义时一致,若追加的是未定义变量,则默认以递归展开式风格进行赋值。 使用?=赋值符号可以对变量进行条件赋值,若变量未被定义则会对变量进行赋值,否则不改变变量的当前定义。

append.mk文件演示了追加赋值和条件赋值的使用方式,内容如下:

#this makefile is for += test
.PHONY:dir recur
a1 := aa1
a1 += _a1st
a2 := _a2
a1 += $(a2)
a1 += $(a3)
a3 += $(a1)
b1 = bb1
b1 += _b1st
b2 = _b2
b1 += _b2
b1 += $(b3)
b3 += $(b1)
c1 += $(c2)
c2 += $(c1)
d1 ?= dd1
d2 = dd2
d2 ?= dd3
dir:
    @echo "a1:"$(a1)
recur:
    @echo "b1:"$(b1)
def:
    @echo "c1:"$(c1)
cond:
    @echo "d1:"$(d1)
    @echo "d2:"$(d2)

dirrecur规则演示了递归展开式变量和直接展开式变量使用追加赋值的区别。 def规则演示了未定义变量追加赋值的默认风格。 cond演示了条件赋值的使用。

分别执行四条规则:

make -f append.mk dir;make -f append.mk recur;make -f append.mk def;make -f append.mk cond

终端打印:

a1:aa1 _a1st _a2
append.mk:16: *** Recursive variable 'b1' references itself (eventually).  Stop.
append.mk:19: *** Recursive variable 'c1' references itself (eventually).  Stop.
d1:dd1
d2:dd2

请自行分析每一行打印与其原因。

实验过程如下图所示: Makefile 基础入门 - 图61
_

变量的替换

  • 替换引用

对于已经定义的变量,可以使用”替换引用”对其指定的字符串进行替换。 替换引用的格式为$(VAR:A=B),它可以将变量VAR中所有A结尾的字符替换为B结尾的字符。 也可以使用模式符号将符合A模式的字符替换为B模式。

chapter9/rep/makefile演示了变量的替换引用,内容如下:

.PHONY:all
vari_a := fa.o fb.o fc.o f.o.o
vari_b := $(vari_a:.o=.c)
vari_c := $(vari_a:%.o=%.c)
vari_d := $(vari_a:f.o%=f.c%)
all:
    @echo "vari_a:" $(vari_a)
    @echo "vari_b:" $(vari_b)
    @echo "vari_c:" $(vari_c)
    @echo "vari_d:" $(vari_d)

文件中分别对不同的变量进行替换引用和模式替换引用,进入rep目录并测试:

cd ../rep;make

终端打印:

vari_a: fa.o fb.o fc.o f.o.o
vari_b: fa.c fb.c fc.c f.o.c
vari_c: fa.c fb.c fc.c f.o.c
vari_d: fa.o fb.o fc.o f.c.o

vari_b中的.o后缀被替换成了.c后缀,f.o.o被替换为f.o.c,这表明只有后缀会被替换,字符串的其它部分保持不变。 vari_c则是使用模式符号替换后缀,结果与vari_b一致。 vari_d使用模式符号将前缀f.o替换为f.c

  • 环境变量的使用

对于makefile来说,系统下的环境变量都是可见的。若文件中的变量名与环境变量名一致,默认引用文件中的变量。

文件envi.mk演示了变量CC与环境变量CC发生冲突时的执行情况:

.PHONY:all
CC := abc
all:
    @echo $(CC)

文件定义一个CC变量并赋值为abc,执行终极目标时打印CC变量的内容。

我们先export一个环境变量CC,再执行envi.mk观察两个变量是否有区别:

export CC=def;echo $CC;make -f envi.mk

终端打印:

def
abc

说明makefile自定义变量优先级高于环境变量。我们也可以在makefile中取消CC变量的定义或者修改PATH变量定义看看会发生什么状况。

  • 防止环境变量被覆盖

可以使用-e选项防止环境变量被同名变量覆盖,如上述实验加入-e选项:

make -f envi.mk -e

终端打印:

def
  • 命令行变量

与环境变量不同,在执行make时指定的命令行变量会覆盖makefile中同名的变量定义, 如果希望变量不被覆盖则需要使用override关键字。

override.mk文件演示了命令行参数的覆盖和override关键字的使用:

.PHONY:all
vari_a = abc
vari_b := def
override vari_c = hij
override vari_d := lmn
vari_c += xxx
vari_d += xxx
override vari_c += zzz
override vari_d += zzz
all:
    @echo "vari_a:" $(vari_a)
    @echo "vari_b:" $(vari_b)
    @echo "vari_c:" $(vari_c)
    @echo "vari_d:" $(vari_d)
    @echo "vari_e:" $(vari_e)

vari_a和 vari_c是递归展开式变量,vari_b和 vari_d是直接展开式变量,vari_e是未定义变量。

现在从命令行传入vari_avari_e并查看变量最终的展开值:

make -f override.mk vari_a=va vari_b=vb vari_c=vc vari_d=vd vari_e=ve

终端打印:

vari_a: va
vari_b: vb
vari_c: hij zzz
vari_d: lmn zzz
vari_e: ve

从打印可以看出无论哪种风格的变量,都需要使用override指示符才能防止命令行定义的同名变量覆盖。

同时,用override定义的变量在进行修改时也需要使用override,否则修改不会生效,验证方法如下:

make -f override.mk

终端打印:

vari_a: abc
vari_b: def
vari_c: hij zzz
vari_d: lmn zzz
vari_e:

可见命令行没有传入变量,但vari_cvari_d仍然无法追加不用override指示符时的"+= xxx"

实验过程如下图所示: Makefile 基础入门 - 图62

目标指定变量和模式指定变量

makefile 中定义的变量通常时对整个文件有效,类似于全局变量。除了普通的变量定义以外,还有一种目标指定变量,定义在目标依赖项处,仅对目标上下文可见。这里的目标上下文也包括了目标依赖项的规则。 目标指定变量还可以定义在模式目标中,称为模式指定变量。 当目标中使用的变量既在全局中定义,又在目标中定义时,目标定义优先级更高,但需注意:目标指定变量与全局变量是两个变量,它们的值互不影响。

chapter9/target/makefile演示了目标指定变量的用法,内容如下:

.PHONY:all
vari_a=abc
vari_b=def
all:vari_a:=all_target
all:pre_a pre_b file_c
    @echo $@ ":" $(vari_a)
    @echo $@ ":" $(vari_b)
pre_%:vari_b:=pat
    pre_%:
    @echo $@ ":" $(vari_a)
    @echo $@ ":" $(vari_b)
file_%:
    @echo $@ ":" $(vari_a)
    @echo $@ ":" $(vari_b)

makefile中定义了vari_avari_b两个全局变量,目标all指定了一个同名的vari_a变量,模式目标pre_%指定了一个同名的vari_b变量。 每个目标的规则中都打印它们能看到的vari_avari_b的值,大家可以根据前面所述的规则推测每个目标分别会打印什么信息。

进入target目录,执行make

cd ../target;make

终端打印:

pre_a : all_target
pre_a : pat
pre_b : all_target
pre_b : pat
file_c : all_target
file_c : def
all : all_target
all : def

由于终极目标all指定了vari_a"all_target",因此在整个目标重建过程中vari_a都以目标指定变量的形式出现。vari_b仅在模式目标pre_%中被定义,因此对pre_apre_b来说,vari_bpat,但对file_%all目标而言,vari_b是全局变量,展开后为def

我们也可以单独以pre_afile_c为目标,看看内容有什么区别:

make pre_a

终端打印:

pre_a : abc
pre_a : pat

再执行:

make file_c

终端打印:

file_c : abc
file_c : def

由于此时并非处于all目标的上下文中,所以all指定的vari_a变量失效,取而代之的是原有的值"abc",而pre_%指定了vari_b变量,所以对pre_a来说,vari_b变量依然是"pat"

实验过程如下图所示: Makefile 基础入门 - 图63

自动化变量

在模式规则中,一个模式目标可以匹配多个不同的目标名,但工程重建过程中经常需要指定一个确切的目标名,为了方便获取规则中的具体的目标名和依赖项,makefile 中需要用到自动化变量,自动化变量的取值是根据具体所执行的规则来决定的,取决于所执行规则的目标和依赖文件名。

总共有七种自动化变量:

$@:目标名称 $%:若目标名为静态库,代表该静态库的一个成员名,否则为空 $<:第一个依赖项名称 $?:所有比目标文件新的依赖项列表 $^:所有依赖项列表,重名依赖项被忽略 $+:包括重名依赖项的所有依赖项列表 $*:模式规则或静态模式规则中的茎,也即”%”所代表的部分

chapter9/auto/makefile 演示了七种自动化变量的用法,文件内容如下:

# $@ $^ $% $< $? $* $+
.PHONY:clean
PRE:=pre_a pre_b pre_a pre_c
all:$(PRE) lib -ladd
    @echo "$$""@:"$@
    @echo "$$""^:"$^
    @echo "$$""+:"$+
    @echo "$$""<:"$<
    @echo "$$""?:"$?
    @echo "$$""*:"$*
    @echo "$$""%:"$%
    @touch $@
$(PRE):pre_%:depen_%
    @echo "$$""*(in $@):"$*
    touch $@
depen_%:
    @echo "use depen rule to build:"$@
    touch $@
lib:libadd.a(add.o minus.o)
    @echo "$$""?(in $@):" $?
    touch $@
libadd.a(add.o minus.o):add.o minus.o
    @echo "$$""?(in $@):" $?
    @echo "$$""%(in $@):" $%
    $(AR) r $@ $%

clean:
    $(RM) pre_* depen_* *.a *.o lib all

终极目标all的依赖项包括pre_a pre_b pre_c lib和库文件libadd.a,其中重复包含了一次pre_a依赖项。 模式规则pre_%利用静态模式依赖于对应的depen_%规则,打印匹配到的茎,并生成目标文件,库文件规则打印$%并打包生成libadd.a

由于此处会用到$(CC)进行编译,而我们之前将环境变量CC赋值为"def",现在需要将其修改回来:

export CC=gcc

现在进入 auto 目录并执行 make:

cd ../auto;make

终端打印:

makefile:17: target `pre_a' given more than once in the same rule.
use depen rule to build:depen_a
touch depen_a
$*(in pre_a):a
touch pre_a
use depen rule to build:depen_b
touch depen_b
$*(in pre_b):b
touch pre_b
use depen rule to build:depen_c
touch depen_c
$*(in pre_c):c
touch pre_c
gcc    -c -o add.o add.c
gcc    -c -o minus.o minus.c
$?(in libadd.a): add.o minus.o
$%(in libadd.a): add.o
ar r libadd.a add.o
ar: creating libadd.a
$?(in libadd.a): add.o minus.o
$%(in libadd.a): minus.o
ar r libadd.a minus.o
$?(in lib): add.o minus.o
touch lib
$@:all
$^:pre_a pre_b pre_c lib libadd.a
$+:pre_a pre_b pre_a pre_c lib libadd.a
$<:pre_a
$?:pre_a pre_b pre_c lib libadd.a
$*:
$%:

make首先重建pre_a pre_b pre_c依赖项,并打印匹配到的茎a b c,接下来重建lib规则,libadd.a在重建过程中打印$%,从打印和打包命令可以看出$%展开后仅为add.o这一项文件,但静态文件目标会依据给定的文件列表展开多次。最后,make执行终极目标all的命令列表,分别打印其自动化变量,并生成all文件。 请大家仔细观察不同规则下自动化变量的变化。由于这是初次建立终极目标,因此$?得到的依赖项列表是全部的依赖项。

使用touch命令更新pre_a pre_b再次测试:

touch pre_a pre_b;make

终端打印:

makefile:17: target `pre_a' given more than once in the same rule.
$@:all
$^:pre_a pre_b pre_c lib libadd.a
$+:pre_a pre_b pre_a pre_c lib libadd.a
$<:pre_a
$?:pre_a pre_b
$*:
$%:

由于pre_a pre_b被手动更新过,现在打印的$?内容为pre_a pre_b。 上述七个自动化变量除了直接引用外,还可以在其后增加D或者F字符获取目录名和文件名, 如:$(@D)表示目标文件的目录名,$(@F)表示目标文件的文件名。这种用法非常简单,也适用于所有的自动化变量,请大家自行实验测试。

实验过程如下图所示: Makefile 基础入门 - 图64 Makefile 基础入门 - 图65

实验总结

本本次实验介绍了make的变量定义风格,变量的替换引用,环境变量、命令行变量、目标指定变量的使用及自动化变量的使用。

课后习题

  • 请自行设计实验测试自动化变量的目录名和文件名的获取。

    Make 内建函数

    课程简介

    实验简介

    本实验将make的内建函数分为三类,并介绍它们的使用方法。

  • 测试字符串处理函数的使用方式

  • 测试make控制函数的使用方式
  • 测试文件名处理函数的使用方式

    实验知识点

    本课程项目完成过程中将学习:

  • 替换字符串函数

  • 简化空格函数
  • 字符串查找
  • 过滤
  • 排序
  • 单词查找
  • 统计单词数量
  • 单词连接
  • 取目录/文件
  • 取前后缀
  • 加前后缀
  • 文件名匹配
  • 循环
  • 条件控制
  • make控制
  • 函数调用
  • 调用 shell
  • 获取变量展开前的值
  • 二次展开
  • 查询变量出处

    实验环境

  • Xfce 终端

  • GCC
  • Gedit

    适合人群

    本课程难度为中等,适合已经初步了解 makefile 规则的学员进行学习。

    代码获取

    你可以通过下面命令将本课程里的所有源代码下载到实验楼环境中,作为参照对比进行学习。

    $ wget http://labfile.oss.aliyuncs.com/courses/849/make_example-master.zip
    

    请尽量按照实验步骤自己写出 C 语言程序。

    实验步骤

    本章节的源代码位于 /home/shiyanlou/Code/make_example/chapter10/目录下。
    项目文件结构:

    .
    ├── control:控制相关的内建函数
    │   ├── cond.mk
    │   ├── eval.mk
    │   └── vari.mk
    ├── files:文件名相关的内建函数
    │   └── files.mk
    └── strings:字符串处理相关的内建函数
      ├── rep.mk
      └── word.mk
    

    字符串处理函数

  • 函数的使用规则

GNU make 函数的调用格式与变量引用相似,基本格式如下:

$(FUNCTION ARGUMENTS)

FUNCTION 为函数名,ARGUMENTS 为函数的参数,参数以”,”进行分割。 函数处理参数时,若参数中存在其它变量或函数的引用,则先展开参数再进行函数处理,展开顺序与参数的先后顺序一致。 函数中的参数不能直接出现逗号和空格,前导空格会被忽略,若需要使用逗号和空格则需要将它们赋值给变量。

  • 文本替换函数

substpatsubst可以对字符串进行替换,其中patsubst可以使用模式替换,函数格式如下:

$(subst FROM,TO,TEXT) $(patsubst PATTERN,REPLACEMENT,TEXT)

strip函数可以简化字符串中的空格,将多个连续空格合并成一个,函数格式如下:

$(strip STRING)

文件chapter10/strings/rep.mk演示了函数的用法,内容如下:

#test function subst patsubst strip 
.PHONY:raw sub patsub
str_a := a.o b.o c.o f.o.o abcdefg
str_b := $(subst .o,.c,$(str_a))
str_c := $(patsubst %.o,%.c,$(str_a))
str_d := $(patsubst .o,.c,$(str_a))
str_e := $(patsubst a.o,a.c,$(str_a))
str_1 := abc.o  def.o     gh.o    i.o     #end
str_2 := $(strip $(str_1))
sub:raw
    @echo "str_b=" $(str_b) #replace all match char for per word
patsub:raw
    @echo "str_c=" $(str_c) #replace match pattern
    @echo "str_d=" $(str_d) #replace nothing
    @echo "str_e=" $(str_e) #replace all-match word
strip:
    @echo "str_1=" $(str_1) #looks like auto strip by make4.1
    @echo "str_2=" $(str_2)
raw:
    @echo "str_a=" $(str_a)

str_a是原字符串,str_b使用subst函数将所有的.o字符替换成.c字符。 str_c使用patsubst用模式替换将.o后缀替换为.c后缀。 str_dstr_e演示在没有通配符的情况下,patsubst需要匹配整个字符串。 str_1str_2演示strip的字符串简化功能。

进入strings目录并执行rep.mk文件:

cd strings; make -f rep.mk sub; make -f rep.mk patsub; make -f rep.mk strip

终端打印:

str_a= a.o b.o c.o f.o.o abcdefg
str_b= a.c b.c c.c f.c.c abcdefg
str_a= a.o b.o c.o f.o.o abcdefg
str_c= a.c b.c c.c f.o.c abcdefg
str_d= a.o b.o c.o f.o.o abcdefg
str_e= a.c b.o c.o f.o.o abcdefg
str_1=       a      b         c        
str_2=  a b c
  • 单词处理函数

单词处理函数包括:

$(findstring FIND,IN):查找字符串,若存在返回字符串,否则返回空
$(filter PATTERN...,TEXT):去除指定模式的字符串
$(filter-out PATTERN...,TEXT):保留指定模式的字符串,去除其它字符串
$(sort LIST):按首字母顺序进行排序
$(word N,TEXT):获取第 N 个单词
$(wordlist S,E,TEXT):获取从 S 位置到 E 位置的单词
$(words TEXT):统计字符串中的单词数量
$(firstword NAMES...):获取第一个单词
$(join LIST1,LIST2):将 LIST1 和 LIST2 中的单词按顺序逐个连接

文件word.mk演示了以上函数的用法,由于篇幅较长,请自行阅读。

测试findstring函数:

make -f word.mk find

终端打印:

str_a= cxx.o n.o fxx.o xy.c fab.o zy.py jor.py abc.o
str_b= xx
str_c= .o x
str_d=

str_b是匹配字符串”xx”的结果,str_c匹配".o x"str_d匹配不存在的字符串"nothing"
测试filterfilter-out函数:

make -f word.mk filt;make -f word.mk filt_out

终端打印:

str_a= cxx.o n.o fxx.o xy.c fab.o zy.py jor.py abc.o
str_e= zy.py jor.py
str_a= cxx.o n.o fxx.o xy.c fab.o zy.py jor.py abc.o
str_f= cxx.o n.o fxx.o xy.c fab.o abc.o

str_estr_f分别过滤和反过滤.py结尾的字符串。

测试sort函数:

make -f word.mk sort

终端打印:

str_a= cxx.o n.o fxx.o xy.c fab.o zy.py jor.py abc.o
str_g= abc.o cxx.o fab.o fxx.o jor.py n.o xy.c zy.py

str_g是对str_a中单词首字母进行排序的结果,若首字母相同则以第二个字母排序,以此类推。

测试 wordlist 函数:

make -f word.mk word_list

终端打印:

str_a= cxx.o n.o fxx.o xy.c fab.o zy.py jor.py abc.o
str_j= fxx.o xy.c fab.o
str_k= fxx.o xy.c fab.o zy.py jor.py abc.o

str_j 和 str_k 的定义如下:

str_j := $(wordlist 3,5,$(str_a)) #list 3rd to 5rd words
str_k := $(wordlist 3,99,$(str_a)) #list our of range

str_j打印第3,4,5个单词 str_k则打印从3开始的所有单词,当end位置超出界限时,wordlist会取到str_a的最后一个单词处。

利用words函数统计str_a的单词数量:

str_m := $(words $(str_a)) #cacu words num

测试 words 函数:

make -f word.mk words

终端打印:

str_a= cxx.o n.o fxx.o xy.c fab.o zy.py jor.py abc.o
str_m= 8

利用join函数会逐个连接下面两个字符串中对应同一位置的单词:

str_a := cxx.o n.o fxx.o xy.c fab.o zy.py jor.py abc.o 
str_join := ./dira/ ./dirb/ ./dirc/ ./dird/ ./dire/ ./dirf/
str_n := $(join $(str_join),$(str_a))

测试join函数:

make -f word.mk join

终端打印:

str_a= cxx.o n.o fxx.o xy.c fab.o zy.py jor.py abc.o
str_join= ./dira/ ./dirb/ ./dirc/ ./dird/ ./dire/ ./dirf/
str_n= ./dira/cxx.o ./dirb/n.o ./dirc/fxx.o ./dird/xy.c ./dire/fab.o ./dirf/zy.py jor.py abc.o

利用call函数反转前四个单词的位置,并舍弃其它参数:

part_rev = $(4) $(3) $(2) $(1)
str_o = $(call part_rev,a,b,c,d,e,f,g) #must use "="

$(1)$(4)分别代表传给call4个参数a b c d,$(0)代表part_rev函数。 测试call函数:

make -f word.mk call

终端打印:

str_o= d c b a

可见a b c d的位置发生了翻转,且e f g三个参数被丢弃。

实验过程如下图所示: Makefile 基础入门 - 图66 Makefile 基础入门 - 图67 Makefile 基础入门 - 图68

文件名相关函数

文件名处理相关的函数包括:

$(dir NAMES...):获取目录
$(notdir NAMES...):获取文件名
$(suffix NAMES...):获取后缀
$(basename NAMES...):获取前缀
$(addsuffix SUFFIX,NAMES...):增加后缀
$(addprefix PREFIX,NAMES...):增加前缀
$(wildcard PATTERN):获取匹配的文件名

chapter10/files/files.mk文件演示了文件名相关函数的用法, 进入目录并执行init规则会自动生成用于函数测试的目录和文件:

cd ../files;make -f files.mk init

终端打印生成的文件树:

.
├── dir_a
│   ├── file_a.c
│   ├── file_b.s
│   └── file_c.o
└── files.mk

由于files.mk内容较长,请大家自行阅读。 detect_files变量利用foreach函数(后面会介绍此函数的用法)和wildcard函数获取dir_a目录下的文件,并在每个目录前增加换行符后赋值给show 变量方便打印和观察:

detect_files := $(foreach each,$(dirs),$(wildcard $(each)/*))
detect_files := $(foreach each,$(detect_files),$(PWD)"/"$(each))
show := $(patsubst %,"\n"%,$(detect_files)) #add '\n' for view

dirnotdir函数测试代码如下:

vari_dir := $(dir $(detect_files))
show_dir := $(patsubst %,"\n"%,$(vari_dir))
vari_files := $(notdir $(detect_files))

vari_dirvari_files分别利用dirnotdir函数取得文件目录和文件名。 由于文件目录过长,show_dir变量在每个目录前加入换行符便于观察。 测试dirnotdir函数:

make -f files.mk dir ; make -f files.mk notdir

终端打印:

detected files: 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_a.c 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_b.s 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_c.o
get dir: 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/ 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/ 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/
detected files: 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_a.c 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_b.s 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_c.o
get files:
file_a.c file_b.s file_c.o

获取文件名前后缀函数测试代码如下:

vari_base := $(basename $(detect_files))
show_base := $(patsubst %,"\n"%,$(vari_base))
vari_suffix := $(suffix $(detect_files))

vari_baseshow_base得到文件的前缀名,vari_suffix得到文件的后缀名。

测试basenamesuffix函数:

make -f files.mk base ; make -f files.mk suffix

终端打印如下:

detected files: 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_a.c 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_b.s 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_c.o
file base name: 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_a 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_b 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_c
detected files: 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_a.c 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_b.s 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_c.o
file suffix: .c .s .o

测试增加前后缀函数的代码如下:

vari_addprefix := $(addprefix "full name:",$(detect_files))
show_addprefix := $(patsubst %,"\n"%,$(vari_addprefix))
vari_addsuffix := $(addsuffix ".text",$(detect_files))
show_addsuffix := $(patsubst %,"\n"%,$(vari_addsuffix))

vari_addprefixvari_addsuffix 分别利用 addprefix函数和addsuffix函数为文件名增加前缀"full name:",后缀".text" 测试 addprefixaddsuffix 函数:

make -f files.mk addprefix ; make -f files.mk addsuffix

终端打印:

detected files: 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_a.c 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_b.s 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_c.o
file add prefix: 
full nname:/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_a.c 
full nname:/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_b.s 
full nname:/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_c.o
detected files: 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_a.c 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_b.s 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_c.o
file add suffix: 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_a.c.text 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_b.s.text 
/home/shiyanlou/Code/make_example/chapter10/files/dir_a/file_c.o.text

实验过程如下图所示: Makefile 基础入门 - 图69 Makefile 基础入门 - 图70

控制和变量相关的函数

控制和变量相关的函数包括:

$(foreach VAR,LIST,TEXT):把 LIST 中的单词依次赋给 VAR,并执行 TEXT 中的表达式
$(if CONDITION,THEN-PART[,ELSE-PART]):如果满足 CONDITION 条件,执行 THEN-PART 语句,否则执行 ELSE-PART 语句
$(error TEXT...):产生致命错误并以 TEXT 内容进行提示
$(warning TEXT...):产生警告并以 TEXT 内容进行提示
$(shell CMD...):调用 shell 并传入 CMD 作为参数
$(value VARIABLE):返回 VARIABLE 未展开前的定义值,即 makefile 中定义变量时所书写的字符串
$(origin VARIABLE):返回变量的初始定义方式,包括:
    undefined,default,environment,environment override,file,command line,override,automatic
$(eval TEXT...):将 TEXT 内容展开为 makefile 的一部分,可用于将字符串展开为规则供 make 解析

chapter10/control/cond.mk 文件演示了 foreach if error warning shell 这几个函数的用法。

其中 init 规则利用 foreach 函数遍历需要生成的文件,并与 $(dir) 路径结合生成文件全名:

files_a := $(foreach each,$(files),$(word 1,$(dirs))"/"$(each)) #get all files under a dir by foreach & word func
files_b := $(foreach each,$(files),$(word 2,$(dirs))"/"$(each))
files_c := $(foreach each,$(files),$(word 3,$(dirs))"/"$(each))
files_d := $(foreach each,$(files),$(word 4,$(dirs))"/"$(each))

detect_files 变量则使用foreach函数遍历全部目录,并获取目录下的文件名。

detect_files := $(foreach each,$(dirs),$(wildcard $(each)/*))

执行init 规则生成测试所需的文件:

cd ../control; make -f cond.mk init

终端打印当前的文件树:

.
├── cond.mk
├── dir_a
│   ├── file_a
│   ├── file_b
│   └── file_c
├── dir_b
│   ├── file_a
│   ├── file_b
│   └── file_c
├── dir_c
│   ├── file_a
│   ├── file_b
│   └── file_c
├── dir_d
│   ├── file_a
│   ├── file_b
│   └── file_c
├── eval.mk
└── vari.mk

其中dir_a dir_b dir_c dir_d就是测试中需要用到的目录。

测试foreach函数:

make -f cond.mk for_loop

终端打印:

files= 
dir_a/file_b 
dir_a/file_c 
dir_a/file_a 
dir_b/file_b 
dir_b/file_c 
dir_b/file_a 
dir_c/file_b 
dir_c/file_c 
dir_c/file_a 
dir_d/file_b 
dir_d/file_c 
dir_d/file_a

接下来测试if函数,测试代码如下:

vari_a :=
vari_b := b
vari_c := $(if $(vari_a),"vari_a has value:"$(vari_a),"vari_a has no value")
vari_d := $(if $(vari_b),"vari_b has value:"$(vari_b),"vari_b has no value")

vari_cvari_d根据vari_a vari_b的定义与否来得到不同的值。

执行测试:

make -f cond.mk if_cond

终端打印:

vari_a=
vari_b= b
vari_c= vari_a has no value
vari_d= vari_b has value:b

可见vari_c因为vari_a没有定义,所以取值为参数$(3),而vari_d因为vari_b有定义,取值为$(2)

warningerror的测试代码如下:

err_exit := $(if $(vari_e),$(error "you generate a error!"),"no error defined") #define vari_e to enable error
warn_go := $(if $(vari_f),$(warning "you generate a warning!"),"no warning defined") #define vari_f to enalbe warning

如果有定义vari_e变量,会产生一条错误信息并使make停止执行,如果有定义vari_f变量,会产生一条警告信息,make继续执行。

执行测试:

make -f cond.mk warn

终端打印:

no warning defined

这是一条普通信息,再执行:

make -f cond.mk warn vari_f=1

终端打印:

cond.mk:23: "you generate a warning!"

这是一条make抛出的警告信息,error的测试方法也类似:

make -f cond.mk err vari_e=1

终端打印:

cond.mk:22: *** "you generate a error!".  Stop.

make在抛出错误信息后退出执行。

shell函数的测试代码如下:

shell_cmd := $(shell date)

make 调用 shell 执行 date 程序打印当前时间。

执行测试:

make -f cond.mk shell

终端打印:

Sun Aug 6 14:36:55 CST 2017

此处的时间是变量展开时的时间,而不是执行规则时的时间,请自行设计实验证明。
接下来测试valueorigin函数,测试代码位于vari.mk文件中。

定义五个变量如下:

vari_a = abc
vari_b = $(vari_a)
vari_c = $(vari_a) "+" $(vari_b)
override vari_d = vari_a
vari_e = $($(vari_d))

使用value函数得到他们的定义字符串并打印:

vari_1 = $(value vari_a)
vari_2 = $(value vari_b)
vari_3 = $(value vari_c)
vari_4 = $(value vari_d)
vari_5 = $(value vari_e)
value:
    @echo "vari_1=" '$(vari_1)'
    @echo "vari_2=" '$(vari_2)'
    @echo "vari_3=" '$(vari_3)'
    @echo "vari_4=" '$(vari_4)'
    @echo "vari_5=" '$(vari_5)'

执行测试:

make -f vari.mk value

终端打印:

vari_1= abc
vari_2= $(vari_a)
vari_3= $(vari_a) "+" $(vari_b)
vari_4= vari_a
vari_5= $($(vari_d))

可见打印内容与其定义一致。

origin 函数测试代码如下:

origin:
    @echo "origin vari_a:" $(origin vari_a)
    @echo "origin vari_b:" $(origin vari_b)
    @echo "origin vari_c:" $(origin vari_c)
    @echo "origin vari_d:" $(origin vari_d)
    @echo "origin vari_e:" $(origin vari_e)
    @echo 'origin $$@:' $(origin @)
    @echo "origin vari_f:" $(origin vari_f)
    @echo "origin PATH:" $(origin PATH)
    @echo "origin MAKE:" $(origin MAKE)

其中vari_avari_e已经在vari.mk中定义,我们将vari_e导出为环境变量,并在命令行中添加vari_a的定义,观察打印的变量出处:

export vari_e=1;make -f vari.mk origin vari_a=1 -e

终端打印:

origin vari_a: command line
origin vari_b: file
origin vari_c: file
origin vari_d: override
origin vari_e: environment override
origin $@: automatic
origin vari_f: undefined
origin PATH: environment
origin MAKE: default

请对照每个变量的定义查看出处与定义是否一致。
eval 函数是一个二次解析函数,函数先将其变量做一次展开,展开的结果将会作为makefile规则的一部分被make做第二次解析,这样就可以定义一些规则模板,增强makefile灵活性。

eval 的测试文件为 eval.mk,内容如下:

#this is a eval func test
PROGRAMS = server client
server_OBJS = server.o server_pri.o server_access.o
server_LIBS = priv protocol
client_OBJS = client.o client_api.o client_mem.o
client_LIBS = protocol
.PHONY:all
define PROGRAM_template
$(1):
    touch $$($(1)_OBJS) $$($(1)_LIBS)
    @echo $$@ " build finished!"
endef
$(foreach prog,$(PROGRAMS),$(eval $(call PROGRAM_template,$(prog))))
$(PROGRAMS):
clean:
    $(RM) *.o $(server_LIBS) $(client_LIBS)

其中PROGRAM_template被定义为一个模板,根据传入的参数产生不同的规则。 此实验中serverclient被传入模板中产生serverclient规则。 请注意由于make需要读入展开的规则模板,因此作为make解析和重构规则的文本中变量引用要使用, 会被转义成$,这样才能使得变量引用生效,否则make在读入时就会展开变量产生预期外的效果。

现在分别测试这两条规则:

make -f eval.mk server; make -f eval.mk client

终端打印:

touch server.o server_pri.o server_access.o priv protocol
server  build finished!
touch client.o client_api.o client_mem.o protocol
client  build finished!

可见使用规则模板后,server规则和client规则行为类似,依赖文件却不一样。

实验过程如下图所示: Makefile 基础入门 - 图71 Makefile 基础入门 - 图72 Makefile 基础入门 - 图73

实验总结

本次实验测试了make各个内建函数的使用方式。

课后习题

请自行设计实验测试各个函数的使用和验证方式。