Making an XML bill of materials in GNU Make


run in the Makefile.  And at the time that $(SHELL) is expanded the per-rule automatic variables (such as $@) have already been set.   Thus by modifying SHELL it's possible to perform some task for every rule in the Makefile as it runs.

Line 3 stores the original value of SHELL in bom-old-shell using an immediate assignment (:=) and then redefines SHELL to be the expansion of $(bom-run) (which does all the work for this article) and the original shell.   Since $(bom-run) actually expands to an empty string the affect is that bom-run is expanded for each rule in the Makefile, but the actual shell used is unaffected.

bom-run itself is defined on line 13.   All it does is use $(eval) to store the relationship between the current target being built (the $(if) ensures that $@ is defined) and its prerequisites.   

For example, when building foo a call will be made to bom-run with $@ set to foo and $^ (the list of all prerequisites) set to baz.  bom-run will set the value of bom-prereq-foo to baz.  Later the values of these bom-prereq-X variables is used to dump out the XML tree.

Line 5 shows the definition of the pattern rule that handles the bom-% target.  Since its prerequisite is % this has the affect of building the target matching the % and then building bom-%.  In the example above, running make bom-all matches against this pattern rule to build all and then run the commands associated with the bom-% with %* set to all.

bom-%'s commands first delete the bom-file and then recursively dump out the XML starting from $*.  In the example, above where the user did make bom-all the bom-% commands calls bom-dump with the argument all.

Line 11 shows the definition of bom-dump.  It's fairly routine, it uses a helper function (bom-write) to echo fragments of XML to the bom-file
and calls itself for each of the targets in the prerequisites of the
target it is dumping.   Prerequisites are extracted from the bom-prereq-X variables created by bom-run.


There are a few gotchas with this technique.

Firstly, it can end up producing enormous amounts of output.  That's because it
will print the entire tree above any target.  If a target appears multiple times in the tree then a large tree can be repeated many times in the output.  Even for small projects this can make the dump time for the XML very long.

To workaround that it's possible to change the definition of bom-dump to just dump the prerequisite information once for each target.  This has much faster than the original above, and could be processed by a script to understand the structure of the Make.

bom-%: %

   @$(shell rm -f $(bom-file))$(call bom-write,<bom>)$(call bom-dump,$*)$(call bom-write,</bom>)

bom-write = $(shell echo '$1' >> $(bom-file))

= $(if $(bom-prereq-$1),$(call bom-write,<rule
target="$1">)$(call bom-write,<prereq>)$(foreach
p,$(bom-prereq-$1),$(call bom-write,<rule target="$p" />))$(call
bom-write,</prereq>)$(call bom-write,</rule>),$(call
bom-write,<rule target="$1" />))$(foreach
p,$(bom-prereq-$1),$(call bom-dump,$p))$(eval bom-prereq-$1 := )

For the example Makefile the XML document now looks like:


  <rule target="all">


      <rule target="foo" />

      <rule target="bar" />



  <rule target="foo">


      <rule target="baz" />



  <rule target="baz" />

  <rule target="bar" />


Another gotcha is that if the Makefile includes rules with no commands those rules will cause a break in the tree output by this technique.  For example, if the example Makefile is:

all: foo bar

    @echo Making $@

foo: baz


    @echo Making $@


    @echo Making $@

The resulting XML will not mention baz at all

About the author

AgileConnection is a TechWell community.

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