Silverleaf7
Silverleaf7

Reputation: 41

Conditional compilation in C not working as expected

Here is my main program:

#include <stdio.h>

int main(void)
{
    #ifdef MONO
    printf("MONO is defined\n");

    #elif DEBUG
    printf("DEBUG is defined\n");

    #else
    printf("Nothing is defined\n");
    #endif

    return 0;
}

and my makefile:

# Makefile Variables
CC = gcc
EXEC     =
CFLAGS = -Wall -g -ansi -pedantic -Werror
OBJ      = test.o

# Add MONO to the CFLAGS and recompile the program
ifdef
CFLAGS += -D MONO
FANCY : clean $(EXEC)
endif

# Add DEBUG to the CFLAGS and recompile the program
ifdef
CFLAGS += -D DEBUG
DEBUG : clean $(EXEC)
endif

$(EXEC) : $(OBJ)
        $(CC) $(OBJ) -o $(EXEC)

test.o : test.c
        $(CC) $(CFLAGS) test.c -c

clean:
        rm -f $(EXEC) $(OBJ)

Basically, I'm trying to use DEBUG and MONO for conditional compilation. The problem is that when both are undefined in my Makefile, as you can see above, it still prints "Testing... MONO is defined". I compile by typing "make" which gives me "gcc -Wall -g -ansi -pedantic -Werror test.c -c" and run by ./test

Upvotes: 0

Views: 826

Answers (3)

raspy
raspy

Reputation: 4261

I see a bunch of potential problems with this approach. The most important thing is that you may have part of your objects compiled with one define and others without it, which may be desirable, but only if you are fully aware of it. Consider the following:

$ cat Makefile
CC = gcc
CFLAGS = -Wall -g -ansi -pedantic -Werror
OBJ = test.o ident.o

EXEC = test

ifdef FANCY
CFLAGS += -DFANCY
FANCY : clean $(EXEC)
endif

ifdef DEBUG
CFLAGS += -DDEBUG
DEBUG : clean $(EXEC)
endif

ifdef MONO
CFLAGS += -DMONO
MONO : clean $(EXEC)
endif

$(EXEC): $(OBJ)
        $(CC) $(OBJ) -o $(EXEC)

clean:
        rm -f $(EXEC) $(OBJ)


$ cat test.c
#include <stdio.h>
#include "ident.h"

int main(int argc, char *argv[]) {
  printf("%s\n", ident());
  return 0;
}


$ cat ident.c
#include "ident.h"

const char *ident(void) {
  #ifdef MONO
  return "MONO is defined";

  #elif DEBUG
  return "DEBUG is defined";

  #else
  return "Nothing is defined";
  #endif
}

Since there is a DEBUG target defined, I would imagine that I can call make DEBUG to compile it in debug mode, right?

$ make DEBUG
make: *** No rule to make target 'DEBUG'.  Stop.

Nope. Target DEBUG does not exist until additional variable is defined:

$ make DEBUG DEBUG=1
rm -f test test.o ident.o
gcc -Wall -g -ansi -pedantic -Werror -DDEBUG   -c -o test.o test.c
gcc -Wall -g -ansi -pedantic -Werror -DDEBUG   -c -o ident.o ident.c
gcc test.o ident.o -o test

Another tricky thing is that defining DEBUG changes default target from $(EXEC) to DEBUG, which is counterintuitive - suddenly by defining a variable you change what make is going to build, not only how. This is supposed to be determined by targets, not variables. Note also that since DEBUG is now default, it triggers clean which may not be desirable (and in fact is not enforced when building without DEBUG). Therefore whenever I work on debug version, I need to call make every time with DEBUG=1 and it always cleans everything (might be quite annoying with hundreds of files to compile).

The greatest thing is that when you switch from debug mode back to release, clean is not enforced, leaving some objects from debug version linked with others built in release mode. See for yourself:

$ make DEBUG=1
rm -f test test.o ident.o
gcc -Wall -g -ansi -pedantic -Werror -DDEBUG   -c -o test.o test.c
gcc -Wall -g -ansi -pedantic -Werror -DDEBUG   -c -o ident.o ident.c
gcc test.o ident.o -o test

$ touch test.c
$ make
gcc -Wall -g -ansi -pedantic -Werror   -c -o test.o test.c
gcc test.o ident.o -o test

$ ./test
DEBUG is defined

In this example ident.c was not recompiled and it was reused from debug version. Quite easy to overlook in regular development.

Personally, for such cases I would compile every supported mode in a separate directory, like this:

$ cat Makefile.new
CC = gcc
CFLAGS = -Wall -g -ansi -pedantic -Werror
OBJ = test.o ident.o

EXEC = test

MODES := RELEASE FANCY DEBUG MONO

define mode_template
.PHONY: all
all: $(1)

.PHONY: $(1)
$(1): $(1)/$(EXEC)

$(1)/$(EXEC): $(addprefix $(1)/,$(OBJ))
        $$(LINK.o) $$(OUTPUT_OPTION) $$^

$(addprefix $(1)/,$(OBJ)): CFLAGS += -D$(1)
$(addprefix $(1)/,$(OBJ)): $(1)/%.o: %.c | $(1)/.
        $$(COMPILE.c) $$(OUTPUT_OPTION) $$<

$(1)/.:
        mkdir -p $$(@D)

.PHONY: clean
clean::
        -rm -f $(1)/$(EXEC) $(addprefix $(1)/,$(OBJ))
endef

$(foreach mode,$(MODES),$(eval $(call mode_template,$(mode))))

This allows to build all supported modes at once (useful to check if they all still build):

$ make -f Makefile.new
mkdir -p RELEASE
gcc -Wall -g -ansi -pedantic -Werror -DRELEASE   -c -o RELEASE/test.o test.c
gcc -Wall -g -ansi -pedantic -Werror -DRELEASE   -c -o RELEASE/ident.o ident.c
gcc   -o RELEASE/test RELEASE/test.o RELEASE/ident.o
mkdir -p FANCY
gcc -Wall -g -ansi -pedantic -Werror -DFANCY   -c -o FANCY/test.o test.c
gcc -Wall -g -ansi -pedantic -Werror -DFANCY   -c -o FANCY/ident.o ident.c
gcc   -o FANCY/test FANCY/test.o FANCY/ident.o
mkdir -p DEBUG
gcc -Wall -g -ansi -pedantic -Werror -DDEBUG   -c -o DEBUG/test.o test.c
gcc -Wall -g -ansi -pedantic -Werror -DDEBUG   -c -o DEBUG/ident.o ident.c
gcc   -o DEBUG/test DEBUG/test.o DEBUG/ident.o
mkdir -p MONO
gcc -Wall -g -ansi -pedantic -Werror -DMONO   -c -o MONO/test.o test.c
gcc -Wall -g -ansi -pedantic -Werror -DMONO   -c -o MONO/ident.o ident.c
gcc   -o MONO/test MONO/test.o MONO/ident.o

Every version is compiled differently:

$ ./RELEASE/test
Nothing is defined
$ ./FANCY/test
Nothing is defined
$ ./DEBUG/test
DEBUG is defined
$ ./MONO/test
MONO is defined

It does not erase everything on each call:

$ make -f Makefile.new
make: Nothing to be done for 'all'.

It recompiles only changed files without mixing them up or erasing everything:

$ touch test.c
$ make -f Makefile.new
gcc -Wall -g -ansi -pedantic -Werror -DRELEASE   -c -o RELEASE/test.o test.c
gcc   -o RELEASE/test RELEASE/test.o RELEASE/ident.o
gcc -Wall -g -ansi -pedantic -Werror -DFANCY   -c -o FANCY/test.o test.c
gcc   -o FANCY/test FANCY/test.o FANCY/ident.o
gcc -Wall -g -ansi -pedantic -Werror -DDEBUG   -c -o DEBUG/test.o test.c
gcc   -o DEBUG/test DEBUG/test.o DEBUG/ident.o
gcc -Wall -g -ansi -pedantic -Werror -DMONO   -c -o MONO/test.o test.c
gcc   -o MONO/test MONO/test.o MONO/ident.o

It supports parallel compilation (which you cannot be sure when having clean and $(EXEC) on the same dependency list):

$ make -f Makefile.new -j4
mkdir -p RELEASE
mkdir -p FANCY
mkdir -p DEBUG
mkdir -p MONO
gcc -Wall -g -ansi -pedantic -Werror -DRELEASE   -c -o RELEASE/test.o test.c
gcc -Wall -g -ansi -pedantic -Werror -DRELEASE   -c -o RELEASE/ident.o ident.c
gcc -Wall -g -ansi -pedantic -Werror -DFANCY   -c -o FANCY/test.o test.c
gcc -Wall -g -ansi -pedantic -Werror -DFANCY   -c -o FANCY/ident.o ident.c
gcc -Wall -g -ansi -pedantic -Werror -DDEBUG   -c -o DEBUG/test.o test.c
gcc -Wall -g -ansi -pedantic -Werror -DDEBUG   -c -o DEBUG/ident.o ident.c
gcc -Wall -g -ansi -pedantic -Werror -DMONO   -c -o MONO/test.o test.c
gcc -Wall -g -ansi -pedantic -Werror -DMONO   -c -o MONO/ident.o ident.c
gcc   -o RELEASE/test RELEASE/test.o RELEASE/ident.o
gcc   -o FANCY/test FANCY/test.o FANCY/ident.o
gcc   -o DEBUG/test DEBUG/test.o DEBUG/ident.o
gcc   -o MONO/test MONO/test.o MONO/ident.o

And you can still build only one config at a time:

$ make -f Makefile.new DEBUG
mkdir -p DEBUG
gcc -Wall -g -ansi -pedantic -Werror -DDEBUG   -c -o DEBUG/test.o test.c
gcc -Wall -g -ansi -pedantic -Werror -DDEBUG   -c -o DEBUG/ident.o ident.c
gcc   -o DEBUG/test DEBUG/test.o DEBUG/ident.o

Upvotes: 0

Read documentation of GNU make. You are not using ifdef correctly.

Read also the documentation about invoking gcc

If on Linux, consider debugging your Makefile with remake invoked with -x. It is very helpful.

You could also consider switching to some other build automation, like ninja.

Upvotes: 1

Silverleaf7
Silverleaf7

Reputation: 41

The problem was with my makefile. As per everyone's suggestions, I edited it so ifdef wasn't by itself in the makefile and ran it like "make MONO=1" and "make DEBUG=1". The working makefile is:

# Makefile Variables
CC = gcc
EXEC     = test
CFLAGS = -Wall -g -ansi -pedantic -Werror
OBJ      = test.o

# Add FANCY to the CFLAGS and recompile the program
ifdef FANCY
CFLAGS += -D FANCY
FANCY : clean $(EXEC)
endif

# Add DEBUG to the CFLAGS and recompile the program
ifdef DEBUG
CFLAGS += -D DEBUG
DEBUG : clean $(EXEC)
endif

# Add DEBUG to the CFLAGS and recompile the program
ifdef MONO
CFLAGS += -D MONO
DEBUG : clean $(EXEC)
endif

$(EXEC) : $(OBJ)
        $(CC) $(OBJ) -o $(EXEC)

test.o : test.c
        $(CC) $(CFLAGS) test.c -c

clean:
        rm -f $(EXEC) $(OBJ)

Upvotes: 0

Related Questions