makefilegnu-makegnu

Why is the last file not recompiled?


Suppose the project's root directory is as follows:

|-> Makefile
|-> obj         // Empty folder)
|-> src         // Contains a.c, b.c, c.c, d.c, e.c source files

and the content of Makefile is as follows:

CC=gcc
RM=rm -rf

DIRSRC=./src
DIROBJ=./obj
SRCS=a.c b.c c.c d.c e.c
OBJS=$(patsubst  %.c,%.o,$(SRCS))
OBJX=$(addprefix  $(DIROBJ)/,$(OBJS))

app: $(OBJX)
        $(CC) -Wall -o app $(OBJX)

$(DIROBJ)/%.o: $(DIRSRC)/%.c  $(DIROBJ)   // (line 13)
         $(CC) -Wall -c -o $@ $<

$(DIROBJ):
         mkdir $(DIROBJ)

.PHONY: clean
clean:
         $(RM) $(OBJX)
         $(RM) $(DIROBJ)
         $(RM) ./app

At line 13 of Makefile, instead of using an order-only prerequisite, I intentionally used a normal prerequisite.

When I execute make for the first time, the output is:

mkdir ./obj
gcc -Wall -c -o obj/a.o src/a.c
gcc -Wall -c -o obj/b.o src/b.c
gcc -Wall -c -o obj/c.o src/c.c
gcc -Wall -c -o obj/d.o src/d.c
gcc -Wall -c -o obj/e.o src/e.c
gcc -Wall -o app ./obj/a.o ./obj/b.o ./obj/c.o ./obj/d.o ./obj/e.o

Again I execute make, and now the output is as follows:

gcc -Wall -c -o obj/a.o src/a.c
gcc -Wall -c -o obj/b.o src/b.c
gcc -Wall -c -o obj/c.o src/c.c
gcc -Wall -c -o obj/d.o src/d.c
gcc -Wall -o app ./obj/a.o ./obj/b.o ./obj/c.o ./obj/d.o ./obj/e.o

My question is: in the second, third, etc execution, why is the last .c file {in this example e.c) not compiled?


Solution

  • The question is not why that one file was not rebuilt. The question is why all the other files were rebuilt. If you run make twice in a row then, with a properly-written makefile, the second invocation should do nothing because everything is up to date.

    The answer to all the questions is exactly what @tripleee suggests: you have listed a directory as a prerequisite of your object files.

    To make, directories are not handled specially. They are considered as targets and their last modification time is used to decide whether any targets that depend on them are out of date.

    But to the filesystem, the last modification time of a directory is not handled the way you might expect. Although, it makes perfect sense when you think about it: the directory's mod time is set whenever the directory's contents are modified. That is, when a new file is added, or a file is removed, or a file is renamed. Changing a file without doing one of those actions, doesn't change the directory so the mod time of the directory is not updated.

    So, when make goes to check an object file to see if it's out of date it compares the modification time of the file with the modification time of the directory. Then when you create a new file in that directory (for example) it changes the mod time and so when you run make again, the target is out of date.

    So why isn't every .o file rebuilt? One possibility is this: the last object file is created, which updates the directory's mod time. Then the compiler writes content into the object file and closes it, which updates the mod time of the last object file. So the mod time of the last object file is newer than the mod time of the directory and it doesn't need to be rebuilt.

    Why doesn't the second rebuilding of object files update the directory's timestamp? There are two things to consider: first is that maybe the compiler doesn't delete or rename the object files: if they already exist the compiler simply truncates them and overwrites them. That won't change the directory's mod time.

    The other thing is that make only considers the timestamp of a given target once, not for every target that lists it as a prerequisite. After the mod time is discovered make will only update it if make thinks that target might have been rebuilt. Since the mod time update of a directory is a side effect of creating other files, make doesn't think that the directory target's mod time has changed and won't re-fetch it.

    Note that the guesses about how the compiler behaves are just that: guesses. Exactly how this will work depends completely on how the compiler is written, so you should not write your makefiles to rely on a specific behavior.

    You can see what make thinks by adding the -d option. It will show you:

       Finished prerequisites of target file 'obj/d.o'.
       Prerequisite 'src/d.c' is older than target 'obj/d.o'.
       Prerequisite 'obj' is newer than target 'obj/d.o'.
      Must remake target 'obj/d.o'.
    

    but:

       Finished prerequisites of target file 'obj/e.o'.
       Prerequisite 'src/e.c' is older than target 'obj/e.o'.
       Prerequisite 'obj' is older than target 'obj/e.o'.
      No need to remake target 'obj/e.o'.