make 3.81
既有的项目中, 通过 make -C 来递归处理每个子目录下的Makefile.
目标: 为make test 目标自动调用各个子目录下的Makefile, 并显示成功失败的自动测试, 只调用含有test目标的makefile
项目顶级目录中的 Makefile:
COLUMNS:=$(shell stty -a | sed -n '/columns/s/.* columns \([0-9]*\).*/\1/p')
export OK_BEGIN=$(shell [ -f ~/test_OK_BEGIN.txt ] && cat ~/test_OK_BEGIN.txt)
export FAIL_BEGIN=$(shell [ -f ~/test_FAIL_BEGIN.txt ] && cat ~/test_FAIL_BEGIN.txt)
export TEST_END=$(shell [ -f ~/test_END.txt ] && cat ~/test_END.txt)
export OK=$(shell printf "%*s[ %sOK%s ]\n" $$(( $(COLUMNS) - 30)) "" "$(OK_BEGIN)" "$(TEST_END)" )
export FAILED=$(shell printf "%*s[ %sFAILED%s ]\n" $(( $(COLUMNS)-30)) "" "$(FAIL_BEGIN)" "$(TEST_END)" )
export TEST_TITLE_BEGIN=$(shell [ -f ~/test_TITLE_BEGIN.txt ] && cat ~/test_TITLE_BEGIN.txt)
export TEST_TITLE_END=$(TEST_END)
第一行, 得到当前console的行宽, 每个 [ OK ] 或 [ FAILED ] 显示在固定行宽的位置
注意, $COLUMNS对bash来说, 并非是环境变量, 所以不能简单通过echo $COLUMNS得到.
接下来的 OK_BEGIN, 是一个颜色代码, 根据当前标准输出是否是终端, 决定是输出终端驱动的特定颜色, 还是一个空串.
如通过make test
调用, 则对每个test 的描述文字输出为黄色, 对成功的OK输出为绿色, 对失败的FAILED输出为红色,
如果通过make test | vi -
来调用, 标准输出不是终端, 则输出普通的ASCII文字, 没有颜色代码.
程序的这种功能可以通过测试标准输出是否为一个终端设备来实现, 如
ls --color=auto | less
通过 less查看时, ls就不会输出颜色代码, 即使指定了--color=auto
注意, 这个测试不能想当然地用下面的办法来实现:
is_atty:=$(shell [ -t 1 ] && echo 1)
这是因为make 在实现$(shell ..)这个调用时, 会把bash子进程的标准输出置为一个管道, 捕获其输出.
所以在其中运行[ -t 1 ]并非是测试到make 进程本身的标准输出, 而是特定的bash进程的标准输出.
上面其实就是我一开始时想当然的做法.
最终的做法是通过一个test_tty的伪目标来实现
test_tty:
@if [ -t 1 ] ; then \
echo -ne "\033[0;37;40m" > ~/test_END.txt; \
echo -ne "\033[0;32;40m" > ~/test_OK_BEGIN.txt; \
echo -ne "\033[0;31;40m" > ~/test_FAIL_BEGIN.txt; \
echo -ne "\033[1;33;40m" > ~/test_TITLE_BEGIN.txt; \
else \
echo -n "" > ~/test_END.txt; \
echo -n "" > ~/test_OK_BEGIN.txt; \
echo -n "" > ~/test_FAIL_BEGIN.txt; \
echo -n "" > ~/test_TITLE_BEGIN.txt; \
fi
不同的颜色代码保存在中间文件中. 的确代价不菲.
然后, 再回头看上面的导出变量赋值.
都是简单地把对应的文件的内容赋给相应的变量, 根据标准输出是否为终端, 对应文件中要么包括相应的终端驱动颜色代码, 要么是空串.
关键之处是赋值不能用 :=这种立即求值的方式, 必需在真正引用这个变量时, 才去求值, 这可以保证伪目标test_tty所生成的文件总是被正确赋值到相应的make变量.
关于为什么每个文件在cat 之前都先用[ -f filename ] 测试其存在性, 原因很怪: 被export的变量, 即使没有真正的子make 过程被调用从而继承它, 也没有真正引用其变量, 也会被至少求值一次, 这会造成相应文件不存在时报告一个错误信息, 很扎眼.
这些导出变量中, 其实真正被各个子目录下的Makefile所使用的, 也就是
TEST_TITLE_BEGIN, 和 TEST_TITLE_END, 就是黄色和白色的颜色代码.
可以显示出如上图中的黄色标题.
接下来是项目顶级目录中的test目标的定义了:
test: test_tty
@# Declare number of sucessed test and failed test
@# Find out all Makefile contains a [test] target, excluding this Makefile itself
@# Calling the make test and count number of success and failed test
@declare -i ok=0;\
declare -i fail=0;\
while read i ; do \
if (cd "$${i%/*}" ; make test ) ; then ok=ok+1;echo "$(OK)"; else fail=fail+1;echo "$(FAILED)"; fi ; \
done < <(grep -lr --include=Makefile '^test:' . | grep -v "^\./Makefile$$" ) ; \
total=$$((ok+fail));\
printf "%s%d%s [%6.2f%%] tests passed\n" "$(OK_BEGIN)" $$ok "$(TEST_END)" $$(echo "100*$$ok/$$total" | bc -l) ; \
printf "%s%d%s [%6.2f%%] tests failed\n" "$(FAIL_BEGIN)" $$fail "$(TEST_END)" $$(echo "100-100*$$ok/$$total" | bc -l) ; \
echo "total tests: " $$((ok+fail))
@rm -f ~/test_END.txt ~/test_OK_BEGIN.txt ~/test_FAIL_BEGIN.txt ~/test_TITLE_BEGIN.txt
为test指定test_tty作为依赖目标, 可以保证前述的所有工作都先于test目标的执行已经完成.
之所以为#.. 注释也加上@, 是因为注释也一样被make作为命令传递给bash. 也一样会有子shell, 所以放在命令中的注释为make文件自己的注释是不一样的. 不过为了让注释最贴近要注释的内容, 我还是选择把它放在命令区.
另外, 对于用\ 连接的多个命令, 只需要第一个以@前辍, 即可实现不输出这些命令本身. @是由make来处理, 不是bash.
用\来连接多个命令作为一个, 而不是分散的多个命令是有原因的: 我们要统计出成功测试和失败测试的个数, 这要用到bash变量, 而要能对变量赋值和引用, 必需保证是在同一个bash进程中, 出于同样这一个原因, 那个while 循环不能通过管道来读取输入, 而必需通过文件重定向.
说到这, 简单说一下这个特殊一点的语法
< <(...)
是把(...)命令运行的输出写到一个临时文件中, 因为第一个< 的关系, 这个临时文件再作为重定向的标准输入.
这个语法是bash特有的, 所以, 在make的最开头,
export SHELL=/bin/bash
是必需的, 用
ls -l /bin/sh
看, /bin/sh不过是一个指向/bin/bash的符号链接, 实质为同一个程序, 但程序的名字关系重大, 如果通过/bin/sh来调用, 它就表现出兼容sh的行为, 类似的程序如 egrep, fgrep
接下来的不值得细说, printf, bash变量, bc 计算成功/失败的测试, 输出.
这样一个简单的实现保证了每个子Makefile不需要做什么事情, 只需要添加自己的test测试即可.
阅读(1928) | 评论(0) | 转发(0) |