Dynamic Breakpoints in the GNU Make Debugger

[article]
Summary:

The most recent change (taking GMD from v1.0.0 to v1.0.1) is the addition of dynamic target breakpoints. At the GMD prompt it's now possible to set and remove breakpoints on the name of a file (in GNU Make language a target) that the Makefile will build.

The most recent change (taking GMD from v1.0.0 to v1.0.1) is the addition of dynamic target breakpoints. At the GMD prompt it's now possible to set and remove breakpoints on the name of a file (in GNU Make language a target) that the Makefile will build.

It's no longer necessary to insert the $(__BREAKPOINT) string in a Makefile. Typing a simple set breakpoint command has the same effect. And another keystroke list all breakpoints currently in effect.
Adding this functionality wasn't easy. In this article I'll show their use of the new breakpoints, but also how they are coded. 


Getting the breakpoints to activate required a little bit of GNU Make magic, but first here's an example.

Dynamic Breakpoints In Action

Before showing you how the debugger works, let's take a look at how to use it. The debugger and these examples all assume that you are using GNU Make 3.80 or above.

Here's an example Makefile that builds all from prerequisites foo and bar.

To illustrate the use of the debugger I've set a breakpoint in the Makefile by inserting a line at the end of the Makefile that consists of just the variable $(__BREAKPOINT). $(__BREAKPOINT) will get expanded when the Makefile is parsed and this will cause the debugger to break execution before any rules are run and prompt for input. (The debugger is included here with the include gmd command at the start; you can get the GMD files from the GMD web site; it's all open source code).

include gmd  
MYVAR1 = hello 
MYVAR2 = $(MYVAR1) everyone 
all: MYVAR3 = $(MYVAR2) 
all: foo bar    
	@echo Finally making $@ 
foo bar:     
	@echo Building $@  
$(__BREAKPOINT)

Here's what happens when this Makefile is executed with no existing files called all, foo or bar. The debugger immediately breaks and waits for input. The first thing to do is type h to see the help text and the three new commands: b (to set a breakpoint), r (to remove a breakpoint) and l (to list current breakpoints).

Makefile:11: GNU Make Debugger Break 
1> h 
Makefile:11: c:     continue 
Makefile:11: q:     quit 
Makefile:11: v VAR: print value of $(VAR) 
Makefile:11: o VAR: print origin of $(VAR) 
Makefile:11: d VAR: print definition of $(VAR) 
Makefile:11: b TAR: set a breakpoint on target TAR 
Makefile:11: r TAR: unset breakpoint on target TAR 
Makefile:11: l:     list all target breakpoints 
2>

We'll set two breakpoints in the Makefile: one when foo gets built and one for all.


After setting the breakpoints we'll verify with the l command that they are set.

2> b foo Makefile:11: Breakpoint set on `foo' 
3> b all Makefile:11: Breakpoint set on `all' 
4> l Makefile:11: Current target breakpoints: `all' `foo' 
5>

Continuing execution by typing c causes the foo breakpoint to be hit immediately. foo is the first target that the Makefile will build (followed by bar and finally all). The breakpoint indicates that the rule for foo is at line 9.

5> c 
Makefile:9: GNU Make Debugger Break 
Makefile:9: - Building 'foo' 
1>

Continuing on we first see the output generated when bar is created and then hit the all breakpoint which prints out much more information than foo because all has prerequisites. (Once gain, for more information on this read my previous article).

1> c 
Building foo 
Building bar 
Makefile:7: GNU Make Debugger Break 
Makefile:7: - Building 'all' from 'foo bar' 
Makefile:7: - First prerequisite is 'foo' 
Makefile:7: - Prequisites 'foo bar' are newer than 'all' 
1>

The Easy Part

To add the breakpoint functions to the GNU Make Debugger I first altered the code that handles the keyboard to recognize the b, r and l commands and call GNU Make macros called __BP_SET, __BP_UNSET and __BP_LIST.

The targets for which breakpoints are defined is simply a GMSL set of target names. Initially there are no breakpoints and so the list, called _ BREAKPOINTS is empty:

__BREAKPOINTS := $(empty_set)

Setting and removing breakpoints is a matter of calling the GMSL functions set_insert and set_remove to add or remove an element from
__BREAKPOINTS:

__BP_SET =  $(eval __BREAKPOINTS := $(call set_insert,$1,
	    $(__BREAKPOINTS)))  \              
	    $(warning Breakpoint set on `$1')  
__BP_UNSET =$(if $(call set_is_member,$1,$(__BREAKPOINTS)),\       
    	    $(eval __BREAKPOINTS := $(call set_remove,$1,
	    $(__BREAKPOINTS)))\                
            $(warning Breakpoint on `$1' removed), \                
            $(warning Breakpoint on `$1' not found)) 

Both these macros use the GNU Make function $(eval) to reset the value of __BREAKPOINTS ($(eval FOO) will evaluate its argument FOO as if it were a piece of text during parsing the Makefile: this means that at run time you can change variable values or define new rules).

__BP_UNSET used the GMSL function set_is_member to determine if the breakpoint being removed was actually defined and output a helpful message in the case that the user tries to remove a non-existent breakpoint (which may be caused by a typing error on their part).

Listing the current breakpoints is simply a matter of outputting the contents of the set stored in __BREAKPOINTS. Since that set is just a list with no duplicates __BP_LIST feeds its value into the GNU Make function $(addprefix) and $(addsuffix) to put quotation marks around the target names:

__BP_LIST = $(if $(__BREAKPOINTS),  		       \               
	      $(warning Current target breakpoints:    \                   
	 	$(addsuffix ',$(addprefix `,
	      $(__BREAKPOINTS)))),                     \               
	      $(warning No target breakpoints set))  

__BP_LIST uses the GNU Make $(if) function to choose between listing the breakpoints if there are any, or saying No target breakpoints set is the __BREAKPOINTS set is empty. $(if) will evaluate its second argument if $(__BREAKPOINTS) is a non-empty string and its third is there are no breakpoints. That's because GNU Make considers 'true' to be any non-empty string and 'false' the empty string.

The Trick

To get GNU Make to break into the debugger it has to expand the $(__BREAKPOINT) variable (which outputs information about the breakpoint and prompts for commands). But for that to happen we need a way to check which breakpoints are defined every time a rule is about to run and then expand $(__BREAKPOINT) if necessary.

Luckily it's possible to do this by modifying GNU Make's own SHELL variable. SHELL stores the name of the shell that should be used to execute commands (it's usually something like /bin/sh on Linux), and is defined by default. But it can be overriden like any other variable by writing SHELL = /my/shell in a Makefile; all commands will be run using that shell.

The SHELL variable is also expanded every time a command is about to run inside a rule. That makes it ideal for the purpose of checking breakpoints. Here's the actual code in the GNU Make Debugger that uses SHELL for breakpoint handling:

__BP_OLD_SHELL := $(SHELL) 
__BP_NEW_SHELL = $(if $(call seq,$(__BP_FLAG),$@),      \                      
	$(call $1,),                                    \                 
	$(__BP_CHECK))$(__BP_OLD_SHELL) 
SHELL = $(call __BP_NEW_SHELL,$1)

First the real value of SHELL is stored in __BP_OLD_SHELL (you'll note that the GNU Make := operator is used to capture the value, and not the definition, of SHELL). Then SHELL itself is redefined to call the __BP_NEW_SHELL macro.

__BP_NEW_SHELL is where the interesting work is done. The last part of it is $(__BP_OLD_SHELL) which is the value of the original SHELL variable. After all, once we are done checking breakpoints, we'd like GNU Make to use the original shell. Before that there's a rather complex $(if). Concentrate for a moment on the call to $(__BP_CHECK). That's the macro that will actually check to see whether the macro needs to execute a breakpoint. It's defined like this:

__BP_CHECK = $(if $(call set_is_member,$@,$(__BREAKPOINTS)),\                
	$(eval __BP_FLAG := $@)                             \                
	$(eval __IGNORE := $(call SHELL,__BREAKPOINT)))  
__BP_FLAG :=

__BP_CHECK checks to see if the current target being built (stored in the standard GNU Make automatic variable $@) is present in the list of breakpoints. If does this using the new GMSL function set_is_member. If the target is present then it does two things: it sets an internal variable called

__BP_FLAG to be the target with the breakpoint and then proceeds to $(call) a macro and throw away the result by setting storing it in a variable called __IGNORE. That's done so that __BP_CHECK's return value will be empty; it's used, after all, in the definition of SHELL which ultimately needs to be just the name of the shell to execute.

Experienced GNU Make users will be scratching their heads wondering why I wrote the odd syntax $(call SHELL,__BREAKPOINT). That's where some GNU Make rocket science comes in.

Rocket Science

Instead of writing $(call SHELL,__BREAKPOINT) I would have wanted to write $(__BREAKPOINT) to get the breakpoint to activate. But I can't.

I can't because doing so would cause a fatal GNU Make error. If you follow the chain of macros up from __BP_CHECK you'll see that it's been expanded because SHELL was being expanded (because a rule was about to run). If you follow into __BREAKPOINT you'd have the nasty surprise that there's a call to $(shell) (you can see this in the GMD code or in my previous article) which is going to cause SHELL to be expanded.

Eek! SHELL is defined in terms of SHELL. This causes GNU Make to spot the recursion and give up. The $(call SHELL,__BREAKPOINT) syntax lets us play with fire. Any time a variable is $(call)ed in GNU Make the flag that is used to check for recursion is disabled. So doing $(call SHELL,__BREAKPOINT) means that the recursion flag on SHELL is turned off (avoiding the error) and if you examine the definition of SHELL you'll see that it calls

__BP_NEW_SHELL with one argument. The argument is the word __BREAKPOINT. __BP_NEW_SHELL checks to see if __BP_FLAG is set to the same value as $@ (which it does using the GMSL seq function) and then proceeds to $(call) its first argument (which is __BREAKPOINT) and the breakpoint fires and the prompt appears.

You might be worrying now that some horrible infinite recursion will occur when the $(shell) gets executed and SHELL is expanded again. Two things prevent that: firstly __BP_FLAG is still the same as $@ (so that __BP_CHECK is not called again) and this time SHELL has no argument (i.e. the value in $1 is empty) and so that $(call $1,) does nothing and recursion stops.

Conclusion

And with that v1.0.1 of the GNU Make Debugger is released. It can be downloaded from gmd.sf.net. See you next time.

About the author

CMCrossroads is a TechWell community.

Through conferences, training, consulting, and online resources, TechWell helps you develop and deliver great software every day.