22.2. Code Generator Example for Make

The derived-code-generator build item in doc/example/general/user/derived/code-generator contains a trivial code generator. All it does is generate a source and header file that define a function that returns a constant, which is read from a file. We have modified derived-main to use the code generator. The code generator itself is implemented in the derived-codegen build item, which provides a rule set called code-generator. The build item is making these rules available for build items of type object-code. The file that implements the rules is therefore called rules/object-code/code-generator.mk.

First, we'll look at the code generator itself. Notice that the only abuild file contained in the derived-code-generator build item directory at user/derived/code-generator is Abuild.conf. There is no Abuild.interface or Abuild.mk, though these files could exist if the build item itself had something that it needed to build.

Before we get to the rules themselves, observe that there is another file in rules/object-code: the help file, rules/object-code/code-generator-help.txt, the contents of which are presented here:

general/user/derived/code-generator/rules/object-code/code-generator-help.txt

You must set these variables:
  DERIVED_CODEGEN_SRC -- name of a C source file to generate
  DERIVED_CODEGEN_HDR -- name of a C header file to generate
  DERIVED_CODEGEN_INFILE -- a file containing a numerical value

These rules will generate a source file called DERIVED_CODEGEN_SRC
which will contain a function called getNumber().  That function will
return the number whose value you have placed in the file named in
DERIVED_CODEGEN_INFILE.  The file DERIVED_CODEGEN_HDR will contain a
prototype for the function.

You must include DERIVED_CODEGEN_SRC in the list of sources for one of
your build targets in order to have it included in that target.

Whenever you provide a rule implementation for rule rulename, you should create a rulename-help.txt file in the same directory. This is the file that will be displayed when the user types abuild --help rules rule:rulename.

The most important part of this example is the rule implementation file, so we'll study it carefully. Here is the rules/object-code/code-generator.mk file:

general/user/derived/code-generator/rules/object-code/code-generator.mk

# Make sure that the user has provided values for all variables
_UNDEFINED := $(call undefined_vars,\
                DERIVED_CODEGEN_HDR \
                DERIVED_CODEGEN_HDR \
                DERIVED_CODEGEN_INFILE)
ifneq ($(words $(_UNDEFINED)),0)
$(error The following variables are undefined: $(_UNDEFINED))
endif

all:: $(DERIVED_CODEGEN_HDR) $(DERIVED_CODEGEN_SRC)

$(DERIVED_CODEGEN_SRC) $(DERIVED_CODEGEN_HDR): $(DERIVED_CODEGEN_INFILE) \
                                $(abDIR_derived-code-generator)/gen_code
        @$(PRINT) Generating $(DERIVED_CODEGEN_HDR) \
                and $(DERIVED_CODEGEN_SRC)
        perl $(abDIR_derived-code-generator)/gen_code \
                $(DERIVED_CODEGEN_HDR) \
                $(DERIVED_CODEGEN_SRC) \
                $(SRCDIR)/$(DERIVED_CODEGEN_INFILE)

The first thing that happens is that we have some code that checks for undefined variables. This isn't strictly necessary, but it can save your users a lot of trouble if you detect when variables they are supposed to provide are not there. We use the function undefined_vars to do this check. This function is provided by abuild and appears in the file make/global.mk relative to the top of the abuild installation. If you expect to write a lot of make rules, it will be in your best interest to familiarize yourself with the functions offered by this file. Even if we hadn't done this, abuild invokes GNU Make in a manner that causes it to warn about any undefined variables. This is useful because undefined variables are a common cause of makefile errors.

Then we add our source file to the all:: target to make sure it gets built. Note that we use all::, not all:, since there are multiple all:: targets throughout various rules files. Adding our source files to the all:: target is not strictly necessary in this case since, by listing the source file as a source file for one of our targets (in the build item that uses this code generator), it will be built in the proper sequence. It's still good practice to do this though, but remember that in a parallel build, dependencies of the all:: target may not necessarily be built in the order in which they are listed.

Next, we have the actual make rules that create the automatically generated files. In this case, we make the output files depend on both the input file and the code generator. Although abuild is running the rules from the depending build item's output directory, we don't need to put any prefix in front of the name of the input file: abuild sets make's VPATH variable so that the file can be found. By creating a dependency on the code generator as well, we can be sure that if the code generator itself is modified, any code that it generates will also be updated.

In the commands for generating our files, notice that we don't need an @ sign before the generation command to prevent it from echoing since abuild doesn't echo its output by default. Not putting an @ sign there means that the user will see the command if he/she runs abuild with the --verbose option. So that the user knows something is happening, we generate a useful output message using @$(PRINT). The use of @$(PRINT) ensures that we don't see the actual echo command even when running with --verbose (since otherwise, we'd see the echo command immediately followed by the output of the echo command), and that we don't see the output at all when we're running with --silent. All informational messages should be generated this way. Note also that we invoke the code generator with perl rather than assuming that it is executable (since some version control systems or file systems make it hard to preserve execute permissions) and that we specify the path to the code generator in terms of $(abDIR_derived-code-generator). Also notice that we have to prefix the name of the input file with $(SRCDIR) when we pass it to the code generator since we are running from the abuild output directory. The VPATH variable tells make where to look, but it doesn't tell our code generator anything! However, the special GNU Make variables like $< and $^ will contain the full paths to prerequisites and can often be used instead.

To use this code generator from derived-main.src, all we have to do is define the required variables in Abuild.mk, add derived-code-generator as a dependency in Abuild.conf, and add code-generator to the RULES variable in Abuild.mk. Note that we have modified main.cpp to include auto.h and to call getNumber(), thus testing the use of the code generator. Since this application effectively contains the only test suite for the code generator, we declare it as a tester of the code generator in Abuild.conf using traits. Here are the relevant files from derived-main.src:

general/user/derived/main/src/Abuild.conf

name: derived-main.src
platform-types: native
deps: common-lib2 project-lib derived-code-generator world-peace
traits: tester -item=derived-main.src -item=derived-code-generator

general/user/derived/main/src/Abuild.mk

TARGETS_bin := main
DERIVED_CODEGEN_SRC := auto.c
DERIVED_CODEGEN_HDR := auto.h
DERIVED_CODEGEN_INFILE := number
SRCS_bin_main := main.cpp $(DERIVED_CODEGEN_SRC)
RULES := ccxx code-generator

Here is the modified main.cpp file:

general/user/derived/main/src/main.cpp

#include <ProjectLib.hpp>
#include <CommonLib2.hpp>
#include <iostream>
#include <world_peace.hh>
#include "auto.h"

int main(int argc, char* argv[])
{
    std::cout << "This is derived-main." << std::endl;
    ProjectLib l;
    l.hello();
    CommonLib2 cl2(6);
    cl2.talkAbout();
    cl2.count();
    std::cout << "Number is " << getNumber() << "." << std::endl;
    // We don't have to know or care whether this is the stub
    // implementation or the real implementation.
    create_world_peace();
    return 0;
}