Checking ‘dynamic’ conditions in Makefile’s

How can you test ‘dynamic’ conditions in a gnu Makefile?

The net is full of GNU-Makefile advise explaining how to define variables and then later use them at compile time to run different commands or different targets. This is all very useful but often time you want to run a command and test it’s result for certain (dynamic) conditions.

What do I mean by checking ‘dynamic’ conditions in a Makefile?
A “dynamic” condition is one whose outcome depends on the result of building one of your targets (or running some other command within the body of one of your build rules).

Example (integrating unittests into your build process)

In the example which I will give we will want to integrate testing into our project’s build procedures and have the output make reflect whether the tests failed or passed. So we will want to build and run our test apps and then echo to standard output whether the test passed or failed.

Let’ suppose that all of our test apps are designed such that when they run ok they don’t produce any output and when some of the tests fail there will be error messages generated on stderr or stdout. Then, inside our makefile, we can test for this dynamic condition (whether the unit-test passed of failed) and print different messages or take some other action.

Here’s the contents of our sample Makefile:

define report_results_func
    if [ -s $(1) ]; \
    then \
        echo FAILURE; \
    else \
        echo OK; \
    fi
endef

unittest :
    @echo "Compiling and running UNITTEST"

    # your compilation command and your flags go below...
    @gcc $(YOUR_C_FLAGS) unittest.c -o unittest

    # run the test, redirect output and errors to a file
    @./unittest &> unittest.out

    @$(call report_results_func,unittest.out)

The most interesting parts of this Makefile (highlighted in blue) are of course the user defined function checking the test results:
    define report_results_func

…and how we call this function from within the ‘unittest’ build rule:
    @$(call report_results_func,unittest.out)

The function report_results_func, (which is really a Makefile variable, but since its contents will be dynamically evaluated whenever it is used it is in effect a “function” and so we will call it a function below) – this function will test whether a file (represented by the first param – $1, which must evaluate to a known existing filename) is of size 0.

The if statement and everything else within the body of the function is written in a bash shell script syntax, so this example assumes that your shell is bash. The [ -s ] is a standard bash construct for testing if a size of a file is 0 (see the section on CONDITIONAL EXPRESSIONS in bash man page).

So the function will echo “OK” when the size of the file (whose name is passed in as param $1) is 0 and “FAILURE” when this file size is not 0. Since we expect correct tests to produce no output this is exactly what we need.

The best part? You can reuse the above code in your projects – just call the function with a different output filename as a parameter, e.g.

    # run another test, redirect output and errors to a file
    @./test-1 &> test-1.out
    @$(call report_results_func, test-1.out)

…The above will run another test, called test-1, redirect its output to a file named test-1.out and then call our report_results_func function to report results of the test (assuming test-1 has been successfully compiled already, of course…).

One thought on “Checking ‘dynamic’ conditions in Makefile’s

  1. Another type of ‘dynamic’ condition that you might need to handle in your Makefile is setting a variable on a per-target basis (target-specific variables).

    Suppose the following setup:
    1. In your project Makfiles you need to be able to build your main project app(s) as well as some unittest suites/apps. So you need a rule for building the main app (main_app:) and for building unittests (unittests:).
    2. Based on an environment variable DEBUG you either build your app and unittests with debug symbols and asserts enabled (-g) or you build for release, i.e. no debug info and no asserts (-DNDEBUG). One exception to the above is that when building the unittests you never disable the assert macros, even DEBUG=0 because your unittests are based on asserts.

    3. So you need 2 targets main_app: and unittest: and you need to handle the DEBUG=1 or DEBUG=0 condition differently depending on whether you’re building main_app or unittest…

    You do this by using target-specific variable assignment. Here’s one way to do it:

    ifneq “$(DEBUG)” “”
    ifneq “$(DEBUG)” “0”
        all: CFLAGS += -g
    else
        all: CFLAGS += -DNDEBUG
    endif
    else
        all: CFLAGS += -DNDEBUG
    endif

    all:
    $(CC) $(CFLAGS) $(other_flags) main_app.c -o main_app

    ifneq “$(DEBUG)” “”
    ifneq “$(DEBUG)” “0”
        all: CFLAGS += -g
    endif
    endif

    unittests:
    $(CC) $(CFLAGS) $(other_flags) unittest.c -o unittest

Leave a Reply

Your email address will not be published. Required fields are marked *