Turn Make Options into Tool Flags

Often times when developing programs there is a need to build the program in/for multiple configurations. Many times, autoconf and its resulting configure script do what you need. Other times you can just change a #define in your code. But sometimes, autoconf isn't an option and changing a define doesn't quite work (say you need to pass your defines/undefines to m4 or some other tool that can't handle include files). The solution is probably to change your makefile. The method presented here results in a fairly compact change to your makefile.

Let's start with the following simple makefile:

# Makefile

all: a.out

a.out: test.o
    gcc test.o -o a.out

test.o: test.c
    gcc -c test.c

Now let's suppose that we have 3 different configuration options that we would like to be able to enable at build time: OPTION_A, OPTION_B, and OPTION_C. So, we want to be able to configure the options by specifying the options on the make command line:

  $ make OPTION_B=1 OPTION_C=1
We also, want to be able to have some of these options on by default in the makefile and then disable them when we invoke make:
  $ make OPTION_A=
Inside the makefile we want to be able to use the option flags as variables:
  ifdef OPTION_A
  ...
  endif
We also want to setup flags for our tool-chain for all the options:
  $ make OPTION_A= OPTION_B=1 OPTION_C=1
  # ... will result in:
  gcc -UOPTION_A -DOPTION_B -DOPTION_C

The modified makefile is:
# Makefile

CONFIG_OPTIONS=\
    OPTION_A \
    -OPTION_B \
    -OPTION_C

SET_CONFIG_OPTIONS=$(filter-out -%,$(CONFIG_OPTIONS))
UNSET_CONFIG_OPTIONS=$(patsubst -%,%,$(filter -%,$(CONFIG_OPTIONS)))
ALL_CONFIG_OPTIONS=$(SET_CONFIG_OPTIONS) $(UNSET_CONFIG_OPTIONS)
#$(info Set: $(SET_CONFIG_OPTIONS))
#$(info Unset: $(UNSET_CONFIG_OPTIONS))

# Turn config options into make variables.
$(foreach cfg,$(SET_CONFIG_OPTIONS),$(eval $(cfg)=1))
$(foreach cfg,$(UNSET_CONFIG_OPTIONS),$(eval $(cfg)=))

# Make sure none of the options are set to anything except 1 or blank.
# Using "make OPTION=0" doesn't work, since "0" is set, you need "make OPTION=".
$(foreach cfg,$(ALL_CONFIG_OPTIONS), \
    $(if $(patsubst %1,%,$(value $(cfg))), \
        $(error Use "$(cfg)=1" OR "$(cfg)=" not "$(cfg)=$(value $(cfg))")))

# Turn them into tool flags (-D).
TOOL_DEFINES+=$(foreach cfg,$(ALL_CONFIG_OPTIONS),$(if $(value $(cfg)),-D$(cfg),-U$(cfg)))
#$(info $(TOOL_DEFINES))

# Configuration options are turned into make variables above.
ifdef OPTION_A
$(info Building with option-a)
endif
ifdef OPTION_B
$(info Building with option-b)
endif
ifdef OPTION_C
$(info Building with option-c)
endif

all: a.out

a.out: test.o
    gcc $(TOOL_DEFINES) test.o -o a.out

test.o: test.c
    gcc $(TOOL_DEFINES) -c test.c

The allowable configuration options are added to a make variable at the beginning of the makefile:

  CONFIG_OPTIONS=\
          OPTION_A \
          -OPTION_B \
          -OPTION_C
Option names that start with a minus sign are disabled by default, option names without a minus sign are enabled by default.

After setting the config options we set three variables SET_CONFIG_OPTIONS, UNSET_CONFIG_OPTIONS, and ALL_CONFIG_OPTIONS which respectively contain the names of the set options, the unset options, and all the options (all without minus signs):

  SET_CONFIG_OPTIONS=$(filter-out -%,$(CONFIG_OPTIONS))
  UNSET_CONFIG_OPTIONS=$(patsubst -%,%,$(filter -%,$(CONFIG_OPTIONS)))
  ALL_CONFIG_OPTIONS=$(SET_CONFIG_OPTIONS) $(UNSET_CONFIG_OPTIONS)
In the case of the set options we filter out all the words that start with a minus sign. In the case of unset options we filter "in" all the words that start with a minus sign and then remove the minus sign. All the options are simply resulting set and unset options combined.

Next we turn all the options into make variables:

  $(foreach cfg,$(SET_CONFIG_OPTIONS),$(eval $(cfg)=1))
  $(foreach cfg,$(UNSET_CONFIG_OPTIONS),$(eval $(cfg)=))
This simply loops through each of the set/unset variable names and then sets the value to "1" (using eval) for set variables and blank for unset variables. Remember that any attempt to set a variable that was specified on the make command line is ignored by make, so the value on the command line will override the value in the makefile.

After that, the next way-too-complex make statement checks to make sure that all the configuration varialbes have a value of either "1" or blank:

  $(foreach cfg,$(ALL_CONFIG_OPTIONS), \
          $(if $(patsubst %1,%,$(value $(cfg))), \
                  $(error Use "$(cfg)=1" OR "$(cfg)=" not "$(cfg)=$(value $(cfg))")))
To do this we loop through all the configuation options, the if test does a pttern substitution changing "%1" "%" to remove a "1" from the value of the variable. If there's anything left after the pattern substitution then the configuration variable has an invalid value and we print out a error.

Finally, we turn all of our configuration options into tool flag defines and undefines:

  TOOL_DEFINES+=$(foreach cfg,$(ALL_CONFIG_OPTIONS),$(if $(value $(cfg)),-D$(cfg),-U$(cfg)))
and modify our tool invocations to include the flags:
  a.out: test.o
          gcc $(TOOL_DEFINES) test.o -o a.out

  test.o: test.c
          gcc $(TOOL_DEFINES) -c test.c

This sample program test.c is:

#include <stdio.h>

main()
{
#ifdef OPTION_A
    printf("Hello option-a\n");
#endif
#ifdef OPTION_B
    printf("Hello option-b\n");
#endif
#ifdef OPTION_C
    printf("Hello option-c\n");
#endif
}

Some output from different builds follows:

  # Default is OPTION_A on:
  $ rm -f a.out test.o; make; ./a.out
  Building with option-a
  gcc -DOPTION_A -UOPTION_B -UOPTION_C -c test.c
  gcc -DOPTION_A -UOPTION_B -UOPTION_C test.o -o a.out
  Hello option-a

  # Turn off OPTION_A and turn on OPTION_B:
  $ rm -f a.out test.o; make OPTION_A= OPTION_B=1; ./a.out
  Building with option-b
  gcc -UOPTION_A -DOPTION_B -UOPTION_C -c test.c
  gcc -UOPTION_A -DOPTION_B -UOPTION_C test.o -o a.out
  Hello option-b

  # Turn off OPTION_A and turn on OPTION_B and OPTION_C:
  $ rm -f a.out test.o; make OPTION_A= OPTION_B=1 OPTION_C=1; ./a.out
  Building with option-b
  Building with option-c
  gcc -UOPTION_A -DOPTION_B -DOPTION_C -c test.c
  gcc -UOPTION_A -DOPTION_B -DOPTION_C test.o -o a.out
  Hello option-b
  Hello option-c

  # Turn off OPTION_A and OPTION_B and turn on OPTION_C:
  $ rm -f a.out test.o; make OPTION_A= OPTION_B= OPTION_C=1; ./a.out
  Building with option-c
  gcc -UOPTION_A -UOPTION_B -DOPTION_C -c test.c
  gcc -UOPTION_A -UOPTION_B -DOPTION_C test.o -o a.out
  Hello option-c

  # Try to get tricky:
  $ rm -f a.out test.o; make OPTION_C=on
  Makefile.mak:20: *** Use "OPTION_C=1" OR "OPTION_C=" not "OPTION_C=on".  Stop.

At first glance one might say that this is not really a particularly "compact" solution given the size of the resulting makefile compared to the original. However, the original makefile is fairly simple and isn't likely to be representative of anything you'd actually be using in a real world program. On top of that, if the original makefile included the ability to build with the different configuration options it would be much longer. Plus, if you take out the comments, line continuations, the info prints, and blank lines, the modification only results in 8 additional lines. You might want to take the lines after the CONFIG_OPTIONS=... line and put them in an include file that you could then reuse.

Mitch Frazier is an embedded systems programmer at Emerson Electric Co. Mitch has been a contributor to and a friend of Linux Journal since the early 2000s.

Load Disqus comments