12.2 Writing Code for Debugging

     

As you can see, there aren't too many tools for debugging makefile s, just a few ways to dump make 's internal data structures and a couple of print statements. When it comes right down to it, it is up to you to write your makefile s in ways that either minimize the chance of errors or provide your own scaffolding to help debug them.

The suggestions in this section are laid out somewhat arbitrarily as coding practices, defensive coding, and debugging techniques. While specific items, such as checking the exit status of commands, could be placed in either the good coding practice section or the defensive coding section, the three categories reflect the proper bias. Focus on coding your makefile s well without cutting too many corners. Include plenty of defensive coding to protect the makefile against unexpected events and environmental conditions. Finally, when bugs do arise, use every trick you can find to squash them.

The "Keep It Simple" Principle (http://www.catb.org/~esr/jargon/html/K/KISS-Principle.html) is at the heart of all good design. As you've seen in previous chapters, makefile s can quickly become complex, even for mundane tasks , such as dependency generation. Fight the tendency to include more and more features in your build system. You'll fail, but not as badly as you would if you simply include every feature that occurs to you.

12.2.1 Good Coding Practices

In my experience, most programmers do not see writing makefile s as programming and, therefore, do not take the same care as they do when writing in C++ or Java. But the make language is a complete nonprocedural language. If the reliability and maintainability of your build system is important, write it with care and use the best coding practices you can.

One of the most important aspects of programming robust makefile s is to check the return status of commands. Of course, make will check simple commands automatically, but makefile s often include compound commands that can fail quietly :

 do:         cd i-dont-exist; \         echo *.c 

When run, this makefile does not terminate with an error status, although an error most definitely occurs:

 $  make  cd i-dont-exist; \ echo *.c /bin/sh: line 1: cd: i-dont-exist: No such file or directory *.c 

Furthermore, the globbing expression fails to find any .c files, so it quietly returns the globbing expression. Oops. A better way to code this command script is to use the shell's features for checking and preventing errors:

 SHELL = /bin/bash do:         cd i-dont-exist && \         shopt -s nullglob &&         echo *.c 

Now the cd error is properly transmitted to make , the echo command never executes, and make terminates with an error status. In addition, setting the nullglob option of bash causes the globbing pattern to return the empty string if no files are found. (Of course, your particular application may prefer the globbing pattern.)

 $  make  cd i-dont-exist && \ echo *.c /bin/sh: line 1: cd: i-dont-exist: No such file or directory make: *** [do] Error 1 

Another good coding practice is formatting your code for maximum readability. Most makefile s I see are poorly formatted and, consequently, difficult to read. Which do you find easier to read?

 _MKDIRS := $(shell for d in $(REQUIRED_DIRS); do [[ -d $$d \ ]]  mkdir -p $$d; done) 

or:

 _MKDIRS := $(shell                            \              for d in $(REQUIRED_DIRS);       \              do                               \                [[ -d $$d ]]  mkdir -p $$d;  \              done) 

If you're like most people, you'll find the first more difficult to parse, the semicolons harder to find, and the number of statements more difficult to count. These are not trivial concerns. A significant percentage of the syntax errors you will encounter in command scripts will be due to missing semicolons, backslashes, or other separators, such as pipe and logical operators.

Also, note that not all missing separators will generate an error. For instance, neither of the following errors will produce a shell syntax error:

 TAGS:         cd src \         ctags --recurse disk_free:         echo "Checking free disk space..." \         df .  awk '{ print $ }' 

Formatting commands for readability will make these kinds of errors easier to catch. When formatting user -defined functions, indent the code. Occasionally, the extra spaces in the resulting macro expansion cause problems. If so, wrap the formatting in a strip function call. When formatting long lists of values, separate each value on its own line. Add a comment before each target, give a brief explanation, and document the parameter list.

The next good coding practice is the liberal use of variables to hold common values. As in any program, the unrestrained use of literal values creates code duplication and leads to maintenance problems and bugs. Another great advantage of variables is that you can get make to display them for debugging purposes during execution. I show a nice command line interface in Section 12.2.3, later in this chapter.

12.2.2 Defensive Coding

Defensive code is code that can execute only if one of your assumptions or expectations is wrong ” an if test that is never true, an assert function that never fails, or tracing code. Of course, the value of this code that never executes is that occasionally (usually when you least expect it), it does run and produce a warning or error, or you choose to enable tracing code to allow you to view the inner workings of make .

You've already seen most of this code in other contexts, but for convenience it is repeated here.

Validation checking is a great example of defensive code. This code sample verifies that the currently executing version of make is 3.80:

 NEED_VERSION := 3.80 $(if $(filter $(NEED_VERSION),$(MAKE_VERSION)),,              \   $(error You must be running make version $(NEED_VERSION).)) 

For Java applications, it is useful to include a check for files in the CLASSPATH .

Validation code can also simply ensure that something is true. The directory creation code from the previous section is of this nature.

Another great defensive coding technique is to use the assert functions defined in Section 4.2.4 in Chapter 4. Here are several versions:

 # $(call assert,condition,message) define assert   $(if ,,$(error Assertion failed: )) endef # $(call assert-file-exists,wildcard-pattern) define assert-file-exists   $(call assert,$(wildcard ), does not exist) endef # $(call assert-not-null,make-variable) define assert-not-null   $(call assert,$(),The variable "" is null) endef 

I find sprinkling assert calls around the makefile to be a cheap and effective way of detecting missing and misspelled parameters as well as violations of other assumptions.

In Chapter 4, we wrote a pair of functions to trace the expansion of user-defined functions:

 # $(debug-enter) debug-enter = $(if $(debug_trace),\                 $(warning Entering 
 # $(debug-enter) debug-enter = $(if $(debug_trace),\ $(warning Entering $0($(echo-args)))) # $(debug-leave) debug-leave = $(if $(debug_trace),$(warning Leaving $0)) comma := , echo-args = $(subst ' ','$(comma) ',\ $(foreach a,1 2 3 4 5 6 7 8 9,'$($a)')) 
($(echo-args)))) # $(debug-leave) debug-leave = $(if $(debug_trace),$(warning Leaving
 # $(debug-enter) debug-enter = $(if $(debug_trace),\ $(warning Entering $0($(echo-args)))) # $(debug-leave) debug-leave = $(if $(debug_trace),$(warning Leaving $0)) comma := , echo-args = $(subst ' ','$(comma) ',\ $(foreach a,1 2 3 4 5 6 7 8 9,'$($a)')) 
)) comma := , echo-args = $(subst ' ','$(comma) ',\ $(foreach a,1 2 3 4 5 6 7 8 9,'$($a)'))

You can add these macro calls to your own functions and leave them disabled until they are required for debugging. To enable them, set debug_trace to any nonempty value:

 $ make debug_trace=1 

As noted in Chapter 4, these trace macros have a number of problems of their own but can still be useful in tracking down bugs.

The final defensive programming technique is simply to make disabling the @ command modifier easy by using it through a make variable, rather than literally:

 QUIET := @ ... target:         $(QUIET)   some command   

Using this technique, you can see the execution of the silent command by redefining QUIET on the command line:

 $ make QUIET= 

12.2.3 Debugging Techniques

This section discusses general debugging techniques and issues. Ultimately, debugging is a grab-bag of whatever works for your situation. These techniques have worked for me, and I've come to rely on them to debug even the simplest makefile problems. Maybe they'll help you, too.

One of the very annoying bugs in 3.80 is that when make reports problems in makefile s and includes a line number, I usually find that the line number is wrong. I haven't investigated whether the problem is due to include files, multiline variable assignments, or user-defined macros, but there it is. Usually the line number make reports is larger than the actual line number. In complex makefile s, I've had the line number be off by as much as 20 lines.

Often the easiest way to see the value of a make variable is to print it during the execution of a target. Although adding print statements using warning is simple, the extra effort of adding a generic debug target for printing variables can save lots of time in the long run. Here is a sample debug target:

 debug:         $(for v,$(V), \           $(warning $v = $($v))) 

To use it, just set the list of variables to print on the command line, and include the debug target:

 $ make V="USERNAME SHELL" debug makefile:2: USERNAME = Owner makefile:2: SHELL = /bin/sh.exe make: debug is up to date. 

If you want to get really tricky, you can use the MAKECMDGOALS variable to avoid the assignment to the variable V :

 debug:         $(for v,$(V) $(MAKECMDGOALS), \           $(if $(filter debug,$v),,$(warning $v = $($v)))) 

Now you can print variables by simply listing them on the command line. I don't recommend this technique, though, because you'll also get confusing make warnings indicating it doesn't know how to update the variables (since they are listed as targets):

 $  make debug PATH SHELL  makefile:2: USERNAME = Owner makefile:2: SHELL = /bin/sh.exe make: debug is up to date. make: *** No rule to make target USERNAME.  Stop. 

In Chapter 10, I briefly mentioned using a debugging shell to help understand some of the activities make performs behind the scenes. While make echos commands in command scripts before they are executed, it does not echo the commands executed in shell functions. Often these commands are subtle and complex, particularly since they may be executed immediately or in a deferred fashion, if they occur in a recursive variable assignment. One way to see these commands execute is to request that the subshell enable debug printing:

 DATE := $(shell date +%F) OUTPUT_DIR = out-$(DATE) make-directories := $(shell [ -d $(OUTPUT_DIR) ]  mkdir -p $(OUTPUT_DIR)) all: ; 

When run with sh 's debugging option, we see:

 $ make SHELL="sh -x" + date +%F + '[' -d out-2004-05-11 ']' + mkdir -p out-2004-05-11 

This even provides additional debugging information beyond make warning statements, since with this option the shell also displays the value of variables and expressions.

Many of the examples in this book are written as deeply nested expressions, such as this one that checks the PATH variable on a Windows/Cygwin system:

 $(if $(findstring /bin/,                               \        $(firstword                                     \          $(wildcard                                    \            $(addsuffix /sort$(if $(COMSPEC),.exe),     \              $(subst :, ,$(PATH)))))),,                \   $(error Your PATH is wrong, c:/usr/cygwin/bin should \     precede c:/WINDOWS/system32)) 

There is no good way to debug these expressions. One reasonable approach is to unroll them, and print each subexpression:

 $(warning $(subst :, ,$(PATH))) $(warning /sort$(if $(COMSPEC),.exe)) $(warning $(addsuffix /sort$(if $(COMSPEC),.exe),     \              $(subst :, ,$(PATH)))) $(warning $(wildcard                                   \             $(addsuffix /sort$(if $(COMSPEC),.exe),    \               $(subst :, ,$(PATH))))) 

Although a bit tedious , without a real debugger, this is the best way (and sometimes the only way) to determine the value of various subexpressions .



Managing Projects with GNU make
Managing Projects with GNU Make (Nutshell Handbooks)
ISBN: 0596006101
EAN: 2147483647
Year: 2003
Pages: 131

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net