Search code examples
cunit-testingmakefilecompilationlinker

Struggling to understand Makefile implicit rules


This is my Makefile, the goal is simply to compile a bunch of test files and link them with another file called tap.c which contains my testing functions.

SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
EXES = $(filter-out tap,$(basename $(SRCS)))

all: $(EXES)

%: %.o tap.o
        gcc -o $@ $^

%.o: %.c
        gcc -c $<

tap.o: tap.c tap.h
        gcc -c $<

clean:
        rm -f $(OBJS)

fclean: clean
        rm -f $(EXES)

This is very basic.

.
├── isalnum.c
├── isalpha.c
├── isascii.c
├── isdigit.c
├── isprint.c
├── Makefile
├── tap.c
└── tap.h

1 directory, 8 files

The issue is when I try to run make I get a linker error.

cc     isalnum.c   -o isalnum
/usr/bin/ld: /tmp/ccnuuKzZ.o: in function `main':
isalnum.c:(.text+0x14): undefined reference to `tap_plan'
/usr/bin/ld: isalnum.c:(.text+0x4a): undefined reference to `ok_at_loc'
/usr/bin/ld: isalnum.c:(.text+0x80): undefined reference to `ok_at_loc'
/usr/bin/ld: isalnum.c:(.text+0xb8): undefined reference to `ok_at_loc'
/usr/bin/ld: isalnum.c:(.text+0xf0): undefined reference to `ok_at_loc'
/usr/bin/ld: isalnum.c:(.text+0x128): undefined reference to `ok_at_loc'
/usr/bin/ld: /tmp/ccnuuKzZ.o:isalnum.c:(.text+0x160): more undefined references to `ok_at_loc' follow
/usr/bin/ld: /tmp/ccnuuKzZ.o: in function `main':
isalnum.c:(.text+0x3d9): undefined reference to `exit_status'
collect2: error: ld returned 1 exit status
make: *** [<builtin>: isalnum] Error 1

My tap.c file contains all these symbols and the rule for building my target seems to be bypassed because the cc command executed is different from the rule I defined in my Makefile.

Surely enough, after some research, I found that when I use make -r everything seems to work properly.

gcc -c tap.c
gcc -c isalnum.c
gcc -o isalnum isalnum.o tap.o
gcc -c isalpha.c
gcc -o isalpha isalpha.o tap.o
gcc -c isascii.c
gcc -o isascii isascii.o tap.o
gcc -c isdigit.c
gcc -o isdigit isdigit.o tap.o
gcc -c isprint.c
gcc -o isprint isprint.o tap.o
rm isascii.o isprint.o isalnum.o isdigit.o isalpha.o

What is happening here? Please tell me what is wrong about my workflow, I am certainly missing something crucial about Makefiles, linking and compilation. I expected my initial Makefile to work without any trouble.


Solution

  • GNU Make has a number of built-in rules. One of these rules knows how to build an executable directly from a source file.

    It is quite common that there are lots of different implicit rules that might be used to create one target: for example, a %.o could be compiled from a C file, C++ file, etc. So, GNU Make also has a set of rules to decide the order in which it will check and apply implicit rules.

    One of those rules says that if there are two ways to chain rules to make a target, the shorter chain wins. So if make can build a program using foo.c -> foo.o -> foo and it can also build the program using foo.c -> foo, then the latter chain will be used.

    That's what you're seeing here: make is using the built-in implicit rule, not the longer set of rules that your implicit rule provides.

    You have a lot of options. The simplest one is to disable the built-in implicit rules; this will work if you have GNU Make 4.0 or better:

    MAKEFLAGS += -r
    

    if you don't this will also work although it's not quite as good:

    .SUFFIXES:
    

    (you can find info on .SUFFIXES and what this does in the GNU Make manual).

    But, in general it's a bad idea idea to add "extra" prerequisites to a pattern rule like this:

    %: %.o tap.o
             gcc -o $@ $^
    

    A pattern rule is a template not a promise. So, if that rule is not chosen for some reason then the prerequisite tap.o won't apply.

    Better is to add the prerequisite explicitly, like this:

    %: %.o
             gcc -o $@ $^
    
    $(EXES): tap.o
    

    Now you know those executables all must have tap.o.

    Another option is to use a static pattern rule. In general "match-anything" rules (pattern rules with a target of %) are bad for performance in makefiles. So instead it's better to declare a static pattern rule like this:

    $(EXES): %: %.o tap.o
             gcc -o $@ $^
    

    Although the name is confusing, a static pattern rule is not an implicit rule at all: this is a shorthand for creating an explicit rule for each of the targets in $(EXES).