如何调试makefile e# x6 w5 a9 A: l
' f1 U1 j8 M9 j1 O1 _/ L+ u5 x
# J9 t: i+ U3 K; s+ I2 w2 |makefile 的调试有点像魔法。可惜,并不存在makefile 调试器之类的东西可用来查看特定规则是如何被求值的,或某个变量是如何被扩展的。相反,大部分的调试过程只是在执 行输出的动作以及查看makefile。事实上,GNU make 提供了若干可以协助调试的内置函数以及命令行选项。 用来调试makefile 的一个最好方法就是加入调试挂钩以及使用具保护的编程技术,让你能够在事情出错时恢复原状。我将会介绍若干基本的调试技术以及我所发现的最有用的具保 护能力的编码习惯。 1.make 的调试功能: m+ [! W9 m8 V* S/ P7 T# \- @
warning函数非常适合用来调试难以捉摸的makefile。因为warning函数会被扩展成空字符串,所以它可以放在makefile 中的任何地方:开始的位置、工作目标或必要条件列表中以 及命令脚本中。这让你能够在最方便查看变量的地方输出变量的值。例如: $(warning A top-level warning) FOO := $(warning Right-hand side of a simple variable)bar% f4 n2 c7 C, K3 t+ \
BAZ = $(warning Right-hand side of a recursive variable)boo $(warning A target)target: $(warning In a prerequisite list)makefile" i$ K' V. B; X# T7 L, z! l
$(BAZ)
: h( r) q7 d+ f; W0 [! W, j! r$(warning In a command script): i4 b; |/ K8 Z
ls* Q% `& i& ~* c; ?8 Q8 c
$(BAZ): 这会产生如下的输出: $ make' A. N- `) s3 K9 ]5 P; G, Z
makefile:1: A top-level warning. g: }1 `# z9 {
makefile:2: Right-hand side of a simple variable7 m; B. o e4 J% W3 F
makefile:5: A target
- V1 o! i% P+ M! ymakefile:5: In a prerequisite list2 N4 O$ y5 X/ c% r* a$ E5 H( E
makefile:5: Right-hand side of a recursive variable
+ b8 L/ ]* [( r0 u j' pmakefile:8: Right-hand side of a recursive variable
0 }9 E0 J5 X. i( s; O& g3 kmakefile:6: In a command script, G& y3 O$ ?0 T4 }
ls+ I2 }' X2 b, v' J# A. @$ Z
makefile 请注意,warning函数的求值方式是按照make标准的立即和延后求值算法。虽然对BAZ的赋值动作中包含了一个warning函数,但是直到BAZ在必要条件列表中被求值后,这个信息才 会被输出来。 “可以在任何地方安插warning调用”的这个特性,让它能够成为一个基本的调试工具。 2.命令行选项! d+ z# [- D: {8 [
我找到了三个最适合用来调试的命令行选项:
& `* c% x2 ^ b. w5 H `8 b--just-print(-n)
6 d$ p; } S$ Y) x1 V, r--print-database(-p)# M2 Q5 o: n) t9 Q, [
--warn-undefined-variables。 2.1 --just-print
/ F6 S1 t- w8 a5 V在一个新的makefile 工作目标上,我所做的第一个测试就是以--just-print(-n)选项来调用make。这会使得make读进makefile并且输出它更新工作目标时将会执行的命令,但是 不会真的执行它们。GNU make 有一个方便的功能,就是允许你为将被输出的命令标上安静模式修饰符(@)。 这个选项被假设可以抑制所有命令的执行动作,然而这只在特定的状况下为真。实际上,你必须小心以对。尽管make不会运行命令脚本,但是在立即的语境之中,它会对shell函数 调用进行求值动作。例如: REQUIRED_DIRS = ...
, N. `% E- G' K1 p9 M) f, p_MKDIRS := $(shell for d in $(REQUIRED_DIRS); \
$ f, ~; T5 D* X" o do \1 ?, \( \% \( V3 ?
[[ -d $$d ]] || mkdir -p $$d; \5 p. a+ v5 Q" G5 d4 x/ @! |0 H
done) $(objects) : $(sources) 正如我们之前所见,_MKDIRS 简单变量的目的是触发必要目录的创建动作。如果这个makefile 是以--just-print 选项的方式运行的,那么当make 读进makefile 时,shell命令将 会一如往常般被执行。然后,make 将会输出(但不会执行)更新$(objects)文件列表所需要进行的每个编译命令。 2.2 --print-data-base
% v7 b$ x( f6 v9 e--print-data-base(-p)是另一个你常会用到的选项。它会运行makefile,显示GNU版权信息以及make 所运行的命令,然后输出它的内部数据库。数据库里的数据将会依种类划分 成以下几个组:variables、directories、implicit rules、pattern-specific variables、files(explicit rules)以及vpath earch path。如下所示: # GNU Make 3.80
* i# v* y6 V) m# Copyright (C) 2002 Free Software Foundation, Inc.
* t) J+ w' s# S" R# This is free software; see the source for copying conditions./ p0 `0 Z) T, s, ?/ V
# There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A9 T! a$ y3 ^& p+ S( }
# PARTICULAR PURPOSE.
7 j: ^: L2 g( I, j* _5 Z正常的命令将会在此处执行 # Make data base, printed on Thu Apr 29 20:58:13 2004
) a4 O' A0 ~9 H9 e9 @* a& b# Variables
, [& N: f% w! i6 K) r. q...
7 S3 N" ^% `4 N" r' _# c# Directories
0 s/ m$ ?' ]9 ~) D7 ~( m8 q...
1 s+ L( q# E J1 l2 v# Implicit Rules
& O) Q O! c: x( { {% V...' W- {/ [" }+ A! J% _0 S9 b7 t6 E; [2 F
# Pattern-specific variable values
6 A+ ]. n- H% H6 p+ N/ ~2 X! O...' t# x. z0 r2 s- C& Y( Z. ^* p- L
# Files
2 ^9 E' a* h7 y5 W...
! M: H- o; n5 Y& |5 _" ^9 ]# VPATH Search Paths 让我们更详细地查看以上这几个区段。7 B6 x& ?; Q9 D0 x
变量区段(variable)将会列出每个变量以及具描述性的注释: # automatic9 X, Y$ c1 e/ Q' {% |1 C+ r' w- S% D- g
<d =="" $(patsubst="" %="" ,%,$(dir="" $<))# S6 K) m3 Y; m% A- G, N' v- F5 E
# environment
, w# Z# y( i/ qEMACS_DIR = C:/usr/emacs-21.3.50.7
+ j& C8 E3 _: c) I& ^# l# default* h4 O1 w. `9 m* t
CWEAVE = cweave) P0 o& Z# |; q2 {+ m
# makefile (from `../mp3_player/makefile', line 35)3 ~) `" s' j. Q( H+ w% K' s( o
CPPFLAGS = $(addprefix -I ,$(include_dirs))
# v; C0 `. ^' G, d: J: }6 R. V# makefile (from `../ch07-separate-binaries/makefile', line 44)
) W ?- L5 a: v+ D/ DRM := rm -f. d/ Z' T# r5 W J( K
# makefile (from `../mp3_player/makefile', line 14)1 Y6 v& S: Q- y7 N% S& M
define make-library0 Y4 A, S0 o' q6 O$ z! |6 l
libraries += $1
6 J# s% y6 {* a i- D! g S) _ sources += $2 $1: $(call source-to-object,$2)( |1 T4 Y4 ~% G) r" d( \& q7 Q' Z
$(AR) $(ARFLAGS) $$@ $$^$ o5 i$ H0 n' k" ?2 g# X9 G# w
endef 自动变量不会被显示出来,但是通过它们可以方便变量的获得,像$(<d)。注释所指出的是origin 函数所返回的变量类型(参见“较不重要的杂项函数”一节)。如果变量被定义="" 在一个文件中,则会在注释中指出其文件名以及该定义所在的行号。简单变量和递归变量的差别在于赋值运算符。简单变量的值将会被显示成右边部分被求值的形式。 下一个区段标示为Directories,它对make 开发人员比对make 用户有用。它列出了将会被make 检查的目录,包括可能会存在的SCCS 和RCS 子目录,但它们通常不存在。对每个目 录来说,make 会显示实现细节,比如设备编号、inode 以及文件名模式匹配的统计数据。 接着是Implicit Rules 区段。这个区段包含了make 数据库中所有的内置的和用户自定义的模式规则。此外,对于那些定义在文件中的规则,它们的注释将会指出文件名以及行号 : %.c %.h: %.y; M$ O, C+ @7 E# g* j( w$ {
# commands to execute (from `../mp3_player/makefile', line 73):, R, p( r9 s2 j+ X# C5 Q [4 G# W
$(YACC.y) --defines $< T5 [ i5 B3 y: G1 Y7 {! Y
$(MV) y.tab.c $*.c% ]) o4 }9 T$ H
$(MV) y.tab.h $*.h %: %.c+ K5 l6 [: N! s. [# P& e
# commands to execute (built-in):# O7 n2 O9 A: G" F
$(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@ %.o: %.c
* T4 E( W2 m+ {# commands to execute (built-in):
% | }% K: k% o. b, v$(COMPILE.c) $(OUTPUT_OPTION) $< 查看这个区段,是让你能够熟悉make 内置规则的变化和结构的最佳方法。当然,并非所有的内置规则都会被实现成模式规则。如果你没有找到你想要的规则,可以查看Files区段 ,旧式后缀规则就列在该处。 下一个区段被标示为Pattern-specific variables,此处所列出的是定义在makefile 里的模式专属变量。所谓模式专属变量,就是变量定义的有效范围被限定在相关的模式规则执 行的时候。例如,模式变量YYLEXFLAG 被定义成: %.c %.h: YYLEXFLAG := -d2 `1 t# M# G1 u4 M$ N! ?+ K9 }4 ]
%.c %.h: %.y
9 c% n3 D% q6 G# `+ O/ k3 h$(YACC.y) --defines $<
' l- P5 I( X, t* r# y' N$(MV) y.tab.c $*.c
8 g4 \7 d e. l9 A$(MV) y.tab.h $*.h 将会被显示成: # Pattern-specific variable values7 b5 S5 T* I* X) z
%.c :
2 B, F# d; V: `# makefile (from `Makefile', line 1)
, F$ V4 S: s( N1 F7 c# YYLEXFLAG := -d
, J( U5 J$ |3 @& E: ^! d7 ~# variable set hash-table stats:
. E9 Q* L9 J( [$ Z {# Load=1/16=6%, Rehash=0, Collisions=0/1=0%7 a, S* ?* j. Q
%.h :
8 G5 N' F# Q6 U! a( |9 H. ]# Q# makefile (from `Makefile', line 1)
: @" i+ n5 l9 }1 y# YYLEXFLAG := -d3 y- w7 A' i; M% ?: ^
# variable set hash-table stats:
# h# Q1 [, s! x8 ?+ C( @7 L, _# o# Load=1/16=6%, Rehash=0, Collisions=0/1=0%4 f9 @3 d3 O# W$ I' f) }
# 2 pattern-specific variable values 接着是Files 区段,此处所列出的都是与特定文件有关的自定义和后缀规则: # Not a target:6 @5 A) {4 Z) Y$ Z" Q
.p.o:
9 ^* `' Z+ `; O& }# j& d" Q# Implicit rule search has not been done.
7 G7 P% b) r5 w. U# Modification time never checked.
( f' j' Y5 ]6 s$ K5 e" ]# File has not been updated.
, b2 f& Y$ J7 ?8 Y# commands to execute (built-in):
3 c9 K! F, i8 d5 i$(COMPILE.p) $(OUTPUT_OPTION) $< lib/ui/libui.a: lib/ui/ui.o+ ]; v; q; m: r' v/ y3 ~9 D- {1 T
# Implicit rule search has not been done.1 ~5 ]# r3 S; u `- @! y* @
# Last modified 2004-04-01 22:04:09.5156259 N# b3 ^/ h- R# n8 C. I
# File has been updated.2 U& r, ?. f# a
# Successfully updated.
; M* _5 o$ a* D! |: @+ v: a6 ?% x# commands to execute (from `../mp3_player/lib/ui/module.mk', line 3):
. R$ }9 j9 V' E. W5 V& ]ar rv $@ $^ lib/codec/codec.o: ../mp3_player/lib/codec/codec.c ../mp3_player/lib/codec/codec.c ../mp3_player/include/codec/codec.h# w* n7 O; f" x) L8 w
# Implicit rule search has been done.
3 a' j9 }" _; A" i4 R# Implicit/static pattern stem: `lib/codec/codec'
% F* b3 L- ]% G3 J# Last modified 2004-04-01 22:04:08.40625
6 {! M. u9 s$ I# N% G& y# a# File has been updated.
- \0 E8 @# @$ J# v; I+ g; L$ c# Successfully updated.
2 e8 ^3 p5 n7 P* |) G9 c# commands to execute (built-in):& ?( [% e: g4 N. W7 G
$(COMPILE.c) $(OUTPUT_OPTION) $< 中间文件与后缀规则会被标示为Not a target,其余是工作目标。每个文件将会包含注释,用以指出make 是如何处理此规则的。被找到的文件在被显示的时候将会通过标准的 vpath 搜索来找出其路径。 最后一个区段被标示为VPATH Search Paths,列出了VPATH 的值以及所有的vpath模式。 对于大规模使用eval 以及用户自定义函数来建立复杂的变量和规则的makefile 来说,查看它们的输出结果通常是确认宏是否已被扩展成预期值的唯一方法。 2.3 --warn-undefined-variables
7 v4 d# w0 ?2 V& z E* {这个选项会使得make 在未定义的变量被扩展时显示警告信息。因为未定义的变量会被扩展成空字符串,这常见于变量名称打错而且很长一段时间未被发现到。这个选项有个问题, 这也是为什么我很少使用这个选项的原因,那就是许多内置规则都会包含未定义的变量以作为用户自定义值的挂钩。所以使用这个选项来运行make必然会产生许多不是错误的警告 信息,而且对用户的makefile 没有什么用处。例如: $ make --warn-undefined-variables -n
4 K3 @( C# y" N( ]' w; rmakefile:35: warning: undefined variable MAKECMDGOALS
# R8 i" S& ]/ lmakefile:45: warning: undefined variable CFLAGS
k$ j& V4 e2 T' F( z& |- Xmakefile:45: warning: undefined variable TARGET_ARCH
1 H) C H( I# ?...
% ]( |7 V- G5 A5 s% N/ t2 M& [8 `8 vmakefile:35: warning: undefined variable MAKECMDGOALS# f& _; a# m% `
make: warning: undefined variable CFLAGS
$ P/ d5 R5 D' x' y" @- q- k0 Jmake: warning: undefined variable TARGET_ARCH5 o% ]7 d1 ?- y7 F9 f6 @" a6 R
make: warning: undefined variable CFLAGS
" X# j: ~. l1 Kmake: warning: undefined variable TARGET_ARCH+ S; ^4 Z3 \. z4 O4 A# ?: m
...
. U6 f2 T* \5 q% H9 Gmake: warning: undefined variable LDFLAGS7 s% D/ ~6 P/ W m* j: L
make: warning: undefined variable TARGET_ARCH1 Y1 F; s) \/ q0 A4 e
make: warning: undefined variable LOADLIBES
. I/ Q! f7 V1 b( L5 c( qmake: warning: undefined variable LDLIBS 不过,此命令在需要捕获此类错误的某些场合上可能非常有用。 3.--debug 选项, r# U* u* ~+ p. p3 s2 n8 b
当你需要知道make 如何分析你的依存图时,可以使用--debug 选项。除了运行调试器,这个选项是让你获得最详细信息的另一个方法。你有五个调试选项以及一个修饰符可用,分 别是:basic、verbose、implicit、jobs、all 以及makefile。 如果调试选项被指定成--debug,就是在进行basic 调试;如果调试选项被指定成-d,就是在进行all调试;如果要使用选项的其他组合,则可以使用--debug=option1,option2 这 个以逗号为分隔符的列表,此处的选项可以是下面任何一个单词(实际上,make 只会查看第一个字母): 3.1 basic( ]5 N _! r9 M6 Q5 o" E" }+ O8 O
这是所提供的信息最不详细的基本调试功能。启用时,make会输出被发现尚未更新的工作目标并更新动作的状态。它的输出会像下面这样: File all does not exist.
: S) Q7 {0 `/ j7 {File app/player/play_mp3 does not exist.: f8 j5 p' N9 i6 b8 m
File app/player/play_mp3.o does not exist.
2 P2 F, O! N0 w8 a8 V; E% KMust remake target app/player/play_mp3.o.
; Y# V9 @. F, p6 ? x6 N5 ogcc ... ../mp3_player/app/player/play_mp3.c% F- b$ @, q5 o1 }: f
Successfully remade target file app/player/play_mp3.o. 3.2 verbose9 U" ?7 e2 X6 Z1 p0 Z& ~
这个选项会设定basic 选项,以及提供关于“哪些文件被分析、哪些必要条件不需要重建等”的额外信息: File all does not exist.
- b, J H E/ T: g, QConsidering target file app/player/play_mp3./ {% M8 I1 ~7 d. Q0 H$ M
File app/player/play_mp3 does not exist.! c7 i5 d9 v6 v q9 l
Considering target file app/player/play_mp3.o.
& A3 v" i, g. [2 Y" O" S$ r+ {* MFile app/player/play_mp3.o does not exist.
q( G2 ~ ~. O$ l1 w1 i8 E, }Pruning file ../mp3_player/app/player/play_mp3.c.
1 w- N; F! k$ E' f, b \Pruning file ../mp3_player/app/player/play_mp3.c.
! M0 D* C; \" S( w! h' yPruning file ../mp3_player/include/player/play_mp3.h.* s1 Y9 ]5 z8 ^, K2 t. p
Finished prerequisites of target file app/player/play_mp3.o.
6 o9 a% O: \5 u$ [% s, f W: KMust remake target app/player/play_mp3.o.; K% |; p9 _' p+ S
gcc ... ../mp3_player/app/player/play_mp3.c
$ ?) e( p* e/ d+ N, z' H: NSuccessfully remade target file app/player/play_mp3.o.
& Y. u+ {* T- I# K' C: Z1 PPruning file app/player/play_mp3.o. 3.3 implicit
% }8 ]2 i+ Q8 s J这个选项会设定basic 选项,以及提供关于“为每个工作目标搜索隐含规则”的额外信息: File all does not exist.9 _( E6 l: V2 W$ E: R* u, o1 R
File app/player/play_mp3 does not exist.
( F2 \4 T& B- w! F+ _) w) mLooking for an implicit rule for app/player/play_mp3.
! }: h& v" F( v# q- h4 w- QTrying pattern rule with stem play_mp3. N# S5 Q3 Z1 P9 t- H. ?
Trying implicit prerequisite app/player/play_mp3.o.% @; n# Q! b/ t8 {
Found an implicit rule for app/player/play_mp3.
' ~( j7 U: W1 R5 eFile app/player/play_mp3.o does not exist.. U& ~+ h4 z3 ~+ |: A: M4 G
Looking for an implicit rule for app/player/play_mp3.o./ s, v: }" Z! U: s7 V' U3 }
Trying pattern rule with stem play_mp3.7 }* s4 @8 P) h% O& I
Trying implicit prerequisite app/player/play_mp3.c.) @" W' I0 d' V( E
Found prerequisite app/player/play_mp3.c as VPATH ../mp3_player/app/
! B8 O) N' y# tplayer/play_mp3.c4 |0 ^& P% g$ }
Found an implicit rule for app/player/play_mp3.o.
& s% p9 r# |* ` D, ?3 j4 p! KMust remake target app/player/play_mp3.o.+ D: D; G3 r) D4 f7 I
gcc ... ../mp3_player/app/player/play_mp3.c
- }+ f. ~# E9 c- Q' D/ O2 PSuccessfully remade target file app/player/play_mp3.o. 3.4 jobs2 e5 d2 o3 X2 b; P# U" H. S$ w
这个选项会输出被make 调用的子进程的细节,它不会启用basic 选项的功能。 Got a SIGCHLD; 1 unreaped children.
& G- n4 M/ |& ^) p( q- h2 dgcc ... ../mp3_player/app/player/play_mp3.c
6 P7 @4 N7 Z6 R2 I1 d6 O/ c0 Y: S5 kPutting child 0x10033800 (app/player/play_mp3.o) PID 576 on the chain., }. T# D* t6 j, Y+ {
Live child 0x10033800 (app/player/play_mp3.o) PID 576
9 @. y) c: u0 c, ^8 p4 `Got a SIGCHLD; 1 unreaped children.
5 L1 v4 ]2 }/ MReaping winning child 0x10033800 PID 576
' P. Z u5 F% q \; O: k4 a& ?/ jRemoving child 0x10033800 PID 576 from chain. 3.5 all; J5 x" |6 L& p G. K4 [" |% f
这会启用前面的所有选项,当你使用-d 选项时,默认会启用此功能。 3.6 makefile: {3 T' L1 O! F
它不会启用调试信息,直到makefile 被更新—— 这包括更新任何的引入文件。如果使用此修饰符,make 会在重编译makefile 以及引入文件的时候,输出被选择的信息。这个选 项会启用basic 选项,all 选项也会启用此选项。 4.编写用于调试的代码1 F6 C" K2 X* } C
如你所见,并没有太多的工具可用来调试makefile,你只有几个方法可以输出若干可能有用的信息。当这些方法都不管用时,你就得将makefile 编写成可以尽量减少错误发生的机 会,或是可以为自己提供一个舞台来协助你进行调试。 这一节所提供的建议被我(有点随意地)分类成:良好的编码习惯、具保护功能的编码以及调试技术等部分。然而一些特殊的项目,像是检查命令的结束状态,可能会被放在良好 的编码习惯中或是具保护功能的编码中,做这样的分类适当地反映出了趋势所在。将焦点好好地放在makefile 上,尽量避免简单行事。采用具保护的编码以避免makefile被非预期 的事件和环境状态所影响。最后,当缺陷出现时,使用你可以找到的用来压制它们的每个诀窍。 makefile 马上就会变得很复杂—— 即使是一般的工作,比如依存关系的产生。要对抗“在你的编译系统中加入越来越多的功能”的潮流,你将会失败,但如果你只是不经思索地 加入你% u3 N# b0 r# B0 O: p2 h/ l2 b% L" Q
所发现的每个功能,失败并不会比你这么做的后果还糟。 4.1 良好的编码习惯
0 e$ `5 C( s k* N" }2 F% D以我的经验来说,大部分的程序员都不会把makefile 作为程序来写,因此,他们不会像编写C++或Java 时那样细心。事实上,make 语言是一个完整的非程序语言。如果可靠性和 可维护性对你的编译系统来说很重要,那么请小心编写你的makefile,并且尽量遵守良好的编码习惯。 编码健全的makefile 的重点之一就是检查命令的返回状态。当然,make 将会自动检查简单的命令,但是makefile 通常会使用可能不会处理失败状态的复合命令:
9 m( X& V8 n/ t) Zdo:9 S0 G( y/ `1 C8 L! |; u- q% O
cd i-dont-exist; \
5 I; Q( w0 S: i! kecho *.c 运行时,此makefile 并不会因为有错误发生而终止运行,尽管这是一个必然会发生的错误: $ make( u. R! D$ R' y9 d1 B8 N9 a. y! n. e
cd i-dont-exist; \
. H. r- J/ j) a4 p# _2 Q8 Techo *.c3 Y( m1 A3 M/ T! c) g: r' p
/bin/sh: line 1: cd: i-dont-exist: No such file or directory; P, \; N" f1 \& Y8 a- C8 b2 r
*.c 此外,当文件名匹配表达式(globbing expression)找不到任何的.c 文件时,它会不动声色地返回文件名匹配表达式。一个比较好的做法,就是在你编码此命令脚本时,使用 shell 的功能来检查以及防止错误: SHELL = /bin/bash' V# |5 |" A# `6 q: G
do:8 N! N9 l2 X: w( W
cd i-dont-exist && \6 |- G$ B+ ~6 p1 k2 Y2 W8 ~
shopt -s nullglob &&9 D% e+ w7 Y7 T. [# ^ ]
echo *.c 现在cd 的错误会被正确传送到make,所以echo 命令不会被执行,而且make 会因为有错误发生而终止运行。此外,设定bash的nullglob选项,将会使得文件名匹配模式在找不到文 件时返回空字符串。(当然,你的应用程序可能比较喜欢文件名匹配模式。) $ make' A1 ?: G! d( F& f: V% A, z
cd i-dont-exist && \
K8 |0 s5 J, {7 O! x6 {+ kecho *.c: P7 M5 b5 d: I( s8 V+ r8 C
/bin/sh: line 1: cd: i-dont-exist: No such file or directory, N b) |# v3 X" P) M& m
make: *** [do] Error 1 另一个良好的编码习惯,就是将你的代码编排成最具可读性的形式。我所看过的makefile,多半编排得很差,这必然会造成难以阅读的情况。下面这两段代码哪一个比较容易阅读 ? _MKDIRS := $(shell for d in $(REQUIRED_DIRS); do [[ -d $$d \, o# e- d6 K' L3 C8 T
]] || mkdir -p $$d; done) 或: _MKDIRS := $(shell \
6 _" z) Z+ c. k1 A& Y for d in $(REQUIRED_DIRS); \
8 u* K/ L0 [+ L e9 T& U do \6 z0 J! ^( M' T5 _
[[ -d $$d ]] || mkdir -p $$d; \6 F& f; z/ i1 M* O! u' I# K% n
done) 如果你像大部分人那样,你将会觉得第一段代码比较难分析,不容易找到分号,很难计算有几句语句。这些都是必须注意到的地方。在命令脚本中,你会遇到的语法错误,多半是 由于漏掉了分号、反斜线或是其他的分隔符,比如管道(pipe)和逻辑运算符。 此外请注意,并非任何分隔符被漏掉都会产生错误。例如,下面的错误都不会产生shell的语法错误: TAGS:% h( ?8 R% \1 D. _
cd src \
' a* Q; b# s' @# v6 M. Lctags --recurse disk_free:) e/ _: l* `$ t5 [( S; j) L$ o
echo "Checking free disk space..." \# h: k* c+ F- _* Q& O
df . | awk '{ print $$4 }' 把命令编排得具有可读性,将会让以上所提到的错误很容易被发现。编排用户自定义函数的时候可以采用内缩的做法。有时候,宏扩展后的结果中,额外的空格将会造成问题。如 果是这样,你可以将它的编排结果封装在strip 函数的调用中。编排一长串值时,你可以让每个值自成一行。在每个工作目标的前面加上注释,可以提供简介以及说明参数列表。 下一个良好的编码习惯就是大量使用变量来保存常用的值。如同在程序中一样,过度使用文字值将会造成重复的程序代码,以及导致维护困难与缺陷。变量的另一个优点是在执行 期间,你可以基于调试的目的,让make 把它们给显示出来。稍后你将会在“调试技术”一节中看到一个不错的命令行界面。 4.2 具保护功能的编码, z- W# U2 ?. a1 a/ \) e" E, G$ ?
具保护功能的代码,就是如果你的假设或预计有一个是错误的(if 测试结果永远为假、assert 函数决不会失败或追踪代码)才会执行的代码,这让你能够查看make 内部工作的状 态。 事实上,你已经在本书其他地方看到过此类代码,不过为了方便起见,此处会重复加以描述。 确认检查就是具保护功能代码的最佳范例。如下的代码范例可用来确认当前所运行的make 版本是否为3.80: NEED_VERSION := 3.801 }6 ^( p2 y) ]) r* D# r( N
$(if $(filter $(NEED_VERSION),$(MAKE_VERSION)),, \8 R* o( e1 D7 C3 N: K/ U7 @
$(error You must be running make version $(NEED_VERSION).)) 对Java 应用程序来说,它可用来检查CLASSPATH 中的文件。 进行确认的代码还可以用来确认某个东西是否为真,比如前一节用来创建目录的代码就是这样。 另一个重要的具保护功能的编码技术,就是使用“流程控制”一节所定义的assert 函数。下面是其中的若干版本: # $(call assert,condition,message)
% R* C) v& G/ X. _" ?- jdefine assert
7 z1 _" [. H- D. t4 I $(if $1,,$(error Assertion faiLED: $2))
# K1 Z! ?& T; n7 I H: wendef # $(call assert-file-exists,wildcard-pattern)
1 u' A: q2 A2 f3 Mdefine assert-file-exists
! N) X% i" z$ b7 _ $(call assert,$(wildcard $1),$1 does not exist)
; r# e5 T9 n, mendef # $(call assert-not-null,make-variable)( J8 R: t' |! P- p9 [
define assert-not-null+ f7 L, ^1 S2 k: I- v
$(call assert,$($1),The variable "$1" is null)
1 C: K6 d1 }( N" a0 i, C; X: ?/ j) Qendef 我发现在makefile 中到处声明assert的调用,是找出漏掉和打错的参数以及违反其他假定的既便宜又有效的方法。 我曾在第四章中编写了一对可用来追踪用户自定义函数扩展过程的函数: # $(debug-enter), G) l: ^. Y* v+ b' @. ~/ J' F. j
debug-enter = $(if $(debug_trace),\0 x/ i2 f) @1 {
$(warning Entering $0($(echo-args)))) # $(debug-leave)* M- l* S. G6 i+ |- R9 q2 Q
debug-leave = $(if $(debug_trace),$(warning Leaving $0)) comma := ,
1 {5 m9 o5 T eecho-args = $(subst ' ','$(comma) ',\
" D0 }; h: Q" A' b) a $(foreach a,1 2 3 4 5 6 7 8 9,'$($a)')) 你可以把这些宏调用到自己的函数里,并让它们处在停用状态,直到你需要进行调试。要启用它们时,请将debug_trace 设定成任何非空值: $ make debug_trace=1 正如第四章所说,这些追踪宏本身存在一些问题,不过仍然可用来追踪缺陷。 最后要介绍的具保护功能的编码技术,就是通过make 变量让@ 命令修饰符的禁用更容易进行: QUIET := @) }$ z- O2 y/ g- f
...! u" z1 |5 m" F3 K# G5 D9 w$ i5 X# W
target:3 }$ M' L# k; J
$(QUIET) some command 使用此技术时,如果想看到安静模式命令的执行,你可以在命令行上以如下的方式重新定义QUIET: $ make QUIET= 5.调试技术1 n+ Q: ^2 G4 y% W- y9 j* G
这一节将会探讨一般的调试技术与相关主题。最后你会觉得,调试就好像是一个装了各种你需要的东西的幸运袋。这些技术对我来说都很实用,即使是最简单的makefile问题,我 也是靠着它们来进行调试的,或许它们也能协助你。 3.80版中一个非常恼人的缺陷是,当make汇报makefile中的问题时还会包含一个行号,我发现那个行号通常是错的。我并未调查出是否此问题是由于引入文件、多行变量赋值或用 户自定义宏的关系,但是它的确是存在的。make 所汇报的行号通常会比实际的行号还大,在复杂的makefile 中,我发现行号差了20 行之多。 通常,查看make 变量值的最简单方法,就是在工作目标的执行期间输出它。尽管使用warning加入输出语句很简单,而为了在长期运行中节省时间你会想要加入通用的debug工作目 标,但是必须多费一番工夫。下面是一个简单的debug 工作目标: debug:" w* T7 R" e0 m; k* @
$(for v,$(V), \
0 W9 I+ W% u* x# Z( S$(warning $v = $($v))) 要使用此功能,只需要在命令行上将一份需要输出的变量的列表赋值给变量V以及指定debug 工作目标: $ make V="USERNAME SHELL" debug0 T. T4 j+ Q, q) ]) h
makefile:2: USERNAME = Owner
1 }# N) U0 F) e' ]! Y; E! Emakefile:2: SHELL = /bin/sh.exe
8 O0 v2 F- a% C) zmake: debug is up to date. 如果你觉得这样很麻烦,只要使用MAKECMDGOALS就可以避免对变量V进行赋值的动作: debug:
' G1 {, h7 O. x' ?) Y7 A. c3 z$ Z$(for v,$(V) $(MAKECMDGOALS), \3 x. v! H: i8 {
$(if $(filter debug,$v),,$(warning $v = $($v)))) 现在,你只需要在命令行上直接指定需要输出的变量即可。但是我并不建议使用这个技术,因为当make 的警告信息指出它不知道如何更新变量时(因为它们是以工作目标的形式出 现在命令行上的),你可能会产生混淆: $ make debug PATH SHELL! `( p8 ^, D9 u1 c2 W$ F/ n
makefile:2: USERNAME = Owner* [7 {1 z7 H( p8 C
makefile:2: SHELL = /bin/sh.exe& _1 Y4 @- R$ P& H
make: debug is up to date.
2 H6 d1 Q1 D" `6 B/ L' Pmake: *** No rule to make target USERNAME. Stop. 我在第十章曾简单提到过,使用开启调试功能的shell可协助我们了解make在后台所进行的活动。尽管make 在执行命令之前会输出命令脚本中的命令,但是它并不会输出shell函数 中所执行的命令。通常这些命令是既微妙且复杂的,尤其是因为它们可能会被立即执行或是延后执行(如果它们出现在递归变量中)。查看这些命令如何执行的一个方法,就是要 求subshell 启用调试的功能: DATE := $(shell date +%F)0 r- o$ R8 a: c4 b i8 E
OUTPUT_DIR = out-$(DATE) make-directories := $(shell [ -d $(OUTPUT_DIR) ] || mkdir -p
2 h, u5 N; U4 r" `$ m/ z' ?$(OUTPUT_DIR)) all: ; 如果运行时指定了sh 的调试选项,我们将会看到: $ make SHELL="sh -x"7 m% B4 ~- F* U- e: E: T
+ date +%F
9 L/ F7 B0 l1 J- o. }7 a& i+ '[' -d out-2004-05-11 ']'
1 F$ |7 v. A6 {. M h5 k) h8 F, T+ mkdir -p out-2004-05-11 这么做,你不仅可以看到make 的警告信息,也可以看到额外的调试信息,因为开启调试功能的shell 还会显示变量和表达式的值。 本书所举过的许多范例都用到了嵌套层极深的表达式,比如下面这个用来在Windows/Cygwin 系统上检查PATH 变量的表达式: $(if $(findstring /bin/, \5 ~! k9 I K* e d; ]
$(firstword \1 p1 l9 S* ~# ?
$(wildcard \
3 s6 R: `, H) t D2 G3 a $(addsuffix /sort$(if $(COMSPEC),.exe), \6 C& L) `( e4 I, z- c) r
$(subst :, ,$(PATH)))))),, \/ \5 L, |6 H7 F# h2 P7 }
$(error Your PATH is wrong, c:/usr/cygwin/bin should \ C# c# t4 E& O) E4 W+ B% W7 o p: x
precede c:/WINDOWS/system32)) 要对这些表达式进行调试并没有什么好办法。一个可行的办法就是将它们拆开,输出每个子表达式(subexpression): $(warning $(subst :, ,$(PATH)))
: U! O" |, s$ d! K9 m8 v$(warning /sort$(if $(COMSPEC),.exe))3 j1 O7 L! M- _/ A' F4 v0 B
$(warning $(addsuffix /sort$(if $(COMSPEC),.exe), \ g/ V4 v- l3 b" c
$(subst :, ,$(PATH)))) $(warning $(wildcard \: K0 C. R, y/ Z0 p9 n# m
$(addsuffix /sort$(if $(COMSPEC),.exe), \
2 x: n3 [/ V2 o8 Z $(subst :, ,$(PATH))))) 尽管这有点烦人,但是在没有调试器可用的状况下,这或许是确定各个子表达式值的最好办法(有时是唯一的办法)。 6.常见的错误信息( M' i k- y/ Y- m9 T
3.81 版的GNU make 在线使用手册列有make 的错误信息以及它们产生的原因。我们在此只会介绍若干最常见的错误。此处所提到的问题中的部分并非完全是make 的错误,比如命 令脚本中的语法错误,但是它们仍然是开发人员常会遇到的问题。至于完整的make 错误列表,请参考make 在线使用手册。 make 所输出的错误信息具有如下的标准格式: makefile:n: *** message. Stop. 或: make:n: *** message. Stop. makefile 部分是发生错误的makefile 或引入文件的名称,下一个部分是发生错误的行号,接着是三个星号,最后是错误信息。 请注意,make的工作就是运行其他的程序,如果发生错误,即使问题出在你的makefile上,也非常可能会让人觉得错误是来自其他程序。例如,shell 发生错误有可能是命令脚本 形式不正确的结果,编译器发生错误有可能是因为命令行参数不正确。找出错误信息产生自哪个程序,是你解决此问题时所必须进行的第一项工作。幸好,make 的错误信息相当具 有说明性。 6.1 语法错误+ K" g+ B9 H2 Y3 y
这些通常是打字上的错误:漏掉圆括号、以空格代替跳格等。 make 的新用户最常会遇到的一个错误,就是漏掉变量名称的圆括号: foo:
8 K @8 w4 n8 Zfor f in $SOURCES; \
( I. i9 J) C/ U/ Q& S9 [" Odo \, r0 l, R* F+ N/ Z
... \
# t$ m$ I* z% Ndone 这可能会使得make 把$S 扩展成空无一物,而且shell 只会以值为OURCES 的f 执行循环一次。你可能会看到如下适当的shell 错误信息: OURCES: No such file or directory 不过也可能看不到任何信息,这取决于你处理f 的方式。所以,别忘了为你的make 变量加上圆括号。 6.2 missing separator 如下的错误信息: makefile:2:missing separator. Stop. 或: makefile:2:missing separator (did you mean TAB instead of 8 spaces?). Stop. 通常代表你的命令脚本以空格代替了跳格。 以文字来解释的话,就是make 想要查找一个make 分隔符,比如:、= 或一个跳格符,但是找不到。它所找到的是它不了解的东西。 6.3 commands commence before first target 跳格符的问题又出现了! 此信息首次出现在“分析命令”一节中。当命令脚本之外的文本行以一个跳格符开头时,此错误似乎通常会出现在makefile的中间。make将会尽可能消除此模糊不清的状态,但如 果该文本行无法被确定为变量赋值、条件表达式或多行宏定义,make 就会认为这代表命令放错地方了。 6.4 unterminated variable reference
% R) s9 J8 c# K$ Z0 f5 p这是一个简单但常见的错误,代表你没有为变量引用或函数调用加上适当数目的右圆括号。当函数调用和变量引用嵌套很多层时,make 文件看起来很像Lisp!使用能够检查圆括号 是否完整的编辑器,比如Emacs,是避免此类错误最可靠的方法。 6.5 命令脚本中的错误! E1 j: v. R" i3 g5 F* Q7 a3 k
脚本中有三种常见的错误:在多行命令中漏掉一个分号,一个不完整或不正确的路径变量,或是一个“执行时会遇到问题的”命令。 我们已经在“良好的编码习惯”一节中探讨过漏掉分号的问题,所以此处不再做进一步的说明。 当shell 无法找到foo 命令时,将会显示如下的典型错误信息: bash: foo: command not found 这表示shell 已经搜索过PATH变量中的每个变量,但是找不到相符的可执行文件。要修正此错误,你必须更新你的PATH变量,它通常被放在你的.profile 文件(Bourne shell)、 .bashrc 文件(bash)或.cshrc 文件(C shell)中。当然,它也有可能设定在makefile 文件中的PATH 变量里,并且从make 导出PATH 变量。 最后,当shell命令执行失败的时候,它会以非零的结束状态终止执行。在此状况下,make1 i1 @, @" M1 b. q" U7 B0 P$ R* o
将会以如下的信息汇报此错误: $ make: S% I0 B+ J! v/ G! v9 Z9 z6 q) \
touch /foo/bar
4 ]1 `- s/ z! `: p) H! ^- xtouch: creating /foo/bar: No such file or directory$ T4 G# a/ E7 ]/ j' V4 k8 ?
make: *** [all] Error 1 此处执行失败的命令是touch,它会输出自己的错误信息以说明此状态。下一行是make的错误摘要。执行失败的makefile 工作目标会被显示在中括号里,后面还会跟着运行失败的 程序的结束值。如果程序结束运行是因为信号的缘故,make 将会输出比较详细的信息,而不会只显示非零的结束状态。 并请注意,因为@ 修饰符而安静执行的命令也会执行失败。在此状况下,所显示的错误信息好像到处都是。 不管是以上哪种状况,错误信息皆来自make 所运行的程序,而不是make 本身。 6.6 No Rule to Make Target 此信息有两种形式: make: *** No rule to make target XXX. Stop. 以及: make: *** No rule to make target XXX, needed by YYY. Stop. 这代表make 判断文件XXX 需要更新,但是make 找不到执行此工作的任何规则。在放弃和输出此信息之前,make 将会在它的数据库中搜索所有的隐含和具体规则。 此项错误的理由可能有三个:
4 C8 ^! C/ `* R; \? 你的makefile 漏掉了更新此文件所需要的一个规则。在此状况下,你必须加入描述如何建立此工作目标的规则。 ? 在makefile 中打错了字。不是make 找错了文件,就是更新此文件的规则指定了错误的文件。因为make 变量的使用,你很难在makefile 中发现打错字的问题。有时候,要确定 复杂文件名的值是否正确唯有将它输出:你可以直接输出变量,或是查看make 的内部数据库。 ? 这个文件应该存在,但是make 就是找不到它,可能是因为把它漏掉了,或是因为make不知道要到哪里找它。当然,有时make是绝对正确的,文件缺失的原因或许是你忘了将它从 CVS调出。较常见的状况是,make找不到源文件只是因为文件放错地方了。有时是因为源文件放在独立的源文件树中,或是文件产生自另一个程序
6 }+ B1 W( P2 p& m2 R且所产生的文件放在二进制文件树中。 6.7 Overriding Commands for Target
/ B/ n- |6 h. Y" d1 Mmake只允许一个工作目标拥有一个命令脚本(双冒号规则除外,但是很少使用)。如果一个工作目标被指定了一个以上的命令脚本,make 将会输出如下的警告信息: makefile:5: warning: overriding commands for target foo 它也可能会显示如下的警告信息: makefile:2: warning: ignoring old commands for target foo 第一个警告信息指出,make 在第5 行找到了第二个命令脚本;第二个警告信息指出,位于第2 行的最初命令脚本被覆盖掉了。 在复杂的makefile 中,一个工作目标通常会被定义许多次,每一次都会加入它自己的必要条件。这些工作目标中通常会有一个被指定命令脚本,但是在开发或调试期间,你很容易 会加入另一个命令脚本而忘记这么做会覆盖掉现有的命令脚本。 例如,我们可能会在一个引入文件中定义一个通用的工作目标: # 建立一个jar 文件。
9 X2 I# R! y8 Y( b ~2 I$(jar_file):
! }# O6 f- Q8 e5 ~6 h& S( O- Q1 N$(JAR) $(JARFLAGS) -f $@ $^ 这使得其他的makefile 可以加入自己的必要条件。然后我们可能会在某个makefile 文件中这么做: # 为jar 的建立设定工作目标并且加入必要条件
8 d; Z$ o5 V" j% J3 |' cjar_file = parser.jar
/ w: @$ Y5 f' x$(jar_file): $(class_files) 如果我们不小心将一个命令脚本加入此makefile,make可能会产生overriding的警告信 ' D0 L6 m0 w7 \1 E, }5 [
|