8 make 的进阶操作

5,052 阅读4分钟

知识点

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

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

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

  • $(SHELL) 执行规则命令
  • -j 选项进行多线程执行
  • -、-i、-k 参数的作用
  • make 异常结束
  • define

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


项目结构

项目文件结构:

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

1️⃣ make 对规则命令的执行

SHELL环境变量的传递

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

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

#this is a makefile for $(SHELL) test

.PHONY:all

all:
	@echo "\$$SHELL environment is $$SHELL"
	@echo "\$$SHELL in makefile is " $(SHELL)

由于符号$是变量引用的起始字符,因此要使用$本身这个字符时需要使用$$进行转义。 all规则的第一条指令是打印系统环境变量SHELL$$代表$字符,所以它的执行结果与下面这条命令的执行结果是相同的。

echo "\$SHELL environment is $SHELL"

makefile 中 @echo 与 echo 区别:echo 会在终端中先输出 echo 命令本身,再输出结果,而 @echo 是直接输出结果。详见 Difference between echo and @echo in unix shells

「\」符号是终端下的转义字符,「」符号在终端下同样是变量引用的起始字符,因此」符号在终端下同样是变量引用的起始字符,因此 `SHELL会被系统环境变量SHELL的内容代替,而SHELL会被打印为「SHELL` 会被打印为「SHELL」。all 规则的第二条指令是打印当前 make 使用的 SHELL 变量。

输入下面的命令进入 shell_vari 目录,并查看当前系统 SHELL 变量。

cd shell_vari/;echo $SHELL

Terminal 的输出结果如下图:

图片.png

执行下面的命令查看,make 在执行 makefile 规则时所使用到的 shell。

make -f shell.mk

Terminal 的输出结果如下图:

图片.png

从输出结果上看到 make 在执行 makefile 的时候所使用到的 shell 程序是不同于系统默认的 shell 程序。


SHELL变量传参

接下来测试实验在执行 make 时,传入值为 abc 的 SHELL 变量。

make -f shell.mk SHELL=abc

Terminal 的输出结果如图所示:

可见make尝试用我们传入的abc来执行规则结果因为找不到abc导致执行失败。

这说明 make自身的SHELL变量也是可以通过传参进行修改的。\color{red}{`make`自身的`SHELL`变量也是可以通过传参进行修改的。}


2️⃣ make 的多线程执行

make 也可以使用多线程进行并发执行,使用方法为执行 make 时加入命令行选项 -jN,其中 N 为一个数字,表示要执行的线程数。而 make 的每个线程会执行一个规则的重建,每条规则只由一个线程执行。当不使用 -j 选项时 make 的执行为单线程编译。

通过 chapter7/joption 文件夹中的 makefile 文件可对 make 的多线程进行验证,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 秒。

运行截图如下:

图片.png


单线程执行

为了能更直观的看到时间变化过程我们可以修改 makefile 的内容,我们可以为每一个输出过程添加一个时间戳,修改后的内容如下:

#this is a makefile for -j option

.PHONY:all

all:aim1 aim2 aim3 aim4
    @echo "build final finish!"

aim%:
    @echo "now build $$(date +%T)" $@
    @sleep 2
    @echo "build " $@ " finish! $$(date +%T)"

先执行 make 命令查看在单线程下编译的情况,Terminal 的输出如下图:

图片.png

从输出结果可以看出,从开始编译 aim1aim4 编译结束,总共耗时 8 秒。


多线程执行

接下来,通过添加 -j2 选项查看在多线程下的执行情况。

make -j2

Terminal 的输出结果如图:

可以看出 从开始编译 aim1aim4 编译完成总共耗时 4 秒比单线程所消耗的时间减少了一半。

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

参考:# 在Makefile中强制依赖项顺序

关于Makefile的一个注意事项:多线程编译出错总结


然后总结出来的3线程顺序:

#this is a makefile for -j option

.PHONY:all

all:all2 all4 all1 all3
	@echo "build final finish!"

all2:aim1
all4:aim2
all1:aim3
all3:aim4

aim%:
	@echo "now build $$(date +%T)" $@
	@sleep 2
	@echo "build " $@ " finish! $$(date +%T)"

图片.png


总结出来的四线程顺序: 想不出好办法了,用的依赖

#this is a makefile for -j option

.PHONY:all

all:aim1 aim2 aim3 aim4
	@echo "build final finish!"
        
aim4:aim1 aim2 aim3
aim3:aim1 aim2
aim2:aim1

aim%:
	@echo "now build $$(date +%T)" $@
	@sleep 2
	@echo "build " $@ " finish! $$(date +%T)"

图片.png


3️⃣ 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 会直接运行失败。

现在进入到 error 文件夹下并执行此 makefile 并观察执行状况:

cd ../error/;make

Terminal 的输出结果如图:

从输出结果上进行分析 make 在运行规则命令结束后会检测命令执行的返回状态,返回成功则启动另外一个子 shell 来执行下一条命令。在 makefile 执行过程中,先生成 pre_apre_bpre_c 三个文件,再使用 rm -f 或者 rm 命令来删除它们,这个过程是没有问题的。

然而第四条指令是删除不存在的文件 d,由于使用了 -f 参数,因此 shell 也不会返回错误。

第五条指令是删除不存在的文件 e由于命令行起始处使用了符号「-」,所以 make 会忽略此命令的执行错误,所以 shell 虽然返回并打印错误,但 make 继续往下执行。

第六条指令是删除不存在的文件 f,由于只使用了 rm 命令,shell 返回错误,make 收到错误后不再往下执行,因此第七条指令已经没机会执行到。


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

通过提供的 iopt.mk 文件可以对 -i 选项的用法进行演示,iopt.mk 文件内容如下:

#this is a makefile for error handle test

.PHONY:all

all:
    rm a
    rm b
    rm c
    rm d

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

现在执行 make 命令先并观察不使用 -i 选项时文件执行的结果。

make -f iopt.mk

Terminal 的输出结果如下图:

接下来执行加上 -i 选项再执行 iopt.mk 文件。

make -f iopt.mk -i

Terminal 的输出结果如下图:

可以看到虽然前一条指令执行时发生了错误,但是后面的指令依然得到了执行,并没有就此终止执行。

注意:make遇上依赖项不存在时,i选项就不管用了,它不属于命令行错误。\color{red}{注意:当 make 遇上依赖项不存在时,`-i` 选项就不管用了,它不属于命令行错误。}


提供的 kopt.mk 文件可以用来对依赖文件错误的状况进行验证,其内容如下:

#this is a makefile for error handle test

.PHONY:all

all: h i j
        @echo "exe OK!"

执行 make -f kopt.mk -i 时 Terminal 的输出结果如下图:

执行 make -f kopt.mk 时 Terminal 的输出结果如下图:

对比两种情况的输出结果,说明在依赖项错误中 -i 选项没有任何作用。


对于这种情况可以使用 -k 选项让其忽略依赖项错误并继续执行,现执行 make 命令。

make -f kopt.mk -k

Terminal 的输出结果如图所示:

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

请谨慎使用-i-k选项,以免产生预期外的错误。


4️⃣ 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_apre_bpre_c 文件,这三个文件在建立过程中会 sleep 五秒钟,方便用户结束 make 命令。

现在进入到 cancel 文件夹中执行 make 命令不作其他任何操作并观察输出结果。

cd ../cancel;make

Terminal 的输出结果如下图:

此时通过 ls 命令查看当前目录下新文件的产生情况。

可见 pre_apre_bpre_c 三个文件被被成功的生成。


手动模拟 make 执行过程中异常产生

接下来手动模拟 make 执行过程中异常的产生,在 pre_b 文件被生成的过程中,手动键入 ctrl + c 来终止程序的继续执行。

现在通过 ls 命令查看当目录中文件的情况。

从输出结果可以看出,pre_b 已经被删除,从上一张 make 的执行过程的图片中也可以看出在异常中断的产生过程中 pre_b 文件就被删除了。


5️⃣ 命令包的使用(c函数)

书写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

Terminal 的输出结果如图所示:

可以看出在 all 的每个依赖的命令部分都执行了 define 定义的命令包。


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