Search code examples
cmakefilegnu-make

Makefile: not detecting that target executable has already been built (gnu make)


Introduction

I have a simple project in plain C, which I am building with a handwritten Makefile and gcc (in linux).

It works (almost) perfectly fine, but there is a small issue: It doesn't detect that the target has already been built.

Makefile + Project Structure

It consists of a single .c and .h file, which compiles to an executable.

comms_test    <-- resulting executable 
comms_test.c  
comms_test.h  
comms_test.o

My makefile looks like this:


PROJ := comms_test

CSRCS = $(PROJ).c

CC := cc
LD := gcc

INCLUDES := \
        -I. 

LDFLAGS = 

CPPFLAGS = 

CFLAGS := \
      -O0 \
      -Wpedantic


OBJS += $(CSRCS:%.c=%.o)


.PHONY: all
all: options $(PROJ)

.PHONY: options 
options: 
    @echo $(PROJ) build options:
    @echo "CC           = $(CC)"
    @echo "CFLAGS       = $(CFLAGS)"
    @echo "CPPFLAGS     = $(CPPFLAGS)"
    @echo "INCLUDES     = $(INCLUDES)"
    @echo "LDFLAGS      = $(LDFLAGS)"

%.o: %.c
    ${CC} ${CPPFLAGS} ${CFLAGS} ${INCLUDES} -c $< -o $@

.PHONY: ${PROJ}
${PROJ}: ${OBJS}
    ${LD} -o $@ ${OBJS} ${LDFLAGS}

.PHONY: clean
clean:
    rm -rf ${OBJS} ${PROJ}

.PHONY: echo
echo:
    @echo ${CSRCS}
    @echo ${OBJS}

.PHONY: test 

To build, I just call make or (to skip the CC options printout) make comms_test. make clean etc pp works too just fine. The options target is a holdover from other projects where I suppress the printing of the compiler flags on each single .c file (might be dozens), so I just print them once at the top for better readability.

The Issue

The linking step doesn't "understand" that the target executable has already been built. When I call make comms_test, it always runs gcc -o comms_test comms_test.o, even though the resulting executable already exists, and the input .o file has not changed.

How can I fix this issue? I am clearly writing my make rule for that incorrectly. Any suggestion?


EDIT:

I just remembered, If I remove the .PHONY: ${PROJ} it won't relink the executable, but it still will do the options/all target. What I want, is to detect that the executable already exists, and then decide that the requirements for the options target are already satisfied. If nothing was actually compiled/linked, I also do NOT want the options output, but no output (or "nothing to be done"), otherwise this is a bit confusing.


Solution

  • Marking a target .PHONY instructs (GNU) make to consider that target out of date at the beginning of every run, regardless of the existence or timestamp of any same-named file. As discussed in comments and your self-answer, that's why you are observing program comms_test being re-linked unnecessarily (but not comms_test.o being re-compiled unnecessarily).

    That's also part of why you are seeing the options output whenever you make or make all but not when you make comms_test. The options target is marked .PHONY (as it should be), and it is designated a prerequisite of the the default target, all. Thus, the recipe for options will run whenever you build all, whether explicitly or as the default target.

    What I want, is to detect that the executable already exists, and then decide that the requirements for the options target are already satisfied. If nothing was actually compiled/linked, I also do NOT want the options output, but no output (or "nothing to be done"), otherwise this is a bit confusing.

    A bona fide file that you want make to build should never be marked .PHONY. A target that does not correspond to an actual file to build should be marked .PHONY, but that just protects against misbehavior in the event that a corresponding file is created by some other means. If you have behavior that you want to be executed only in the event that a given target is built on a given run, before that target itself is built, then that behavior should be triggered from the recipe for that target. If you make it a prerequisite of the target then that will force the target to be rebuilt on every run, because the phony target is always initially out of date.

    To trigger behavior from a recipe, you need to take control of the recipe -- that is, provide your own rule. You cannot amend the recipe of an existing rule, only replace the whole rule with a different one.

    But there's another problem: behavior exercised by a rule's recipe happens after all the rule's prerequisites are brought up to date. You want the options to be evaluated / printed before anything is built if they are printed at all. You may be able to make that work in this case, by building the executable directly from the C sources, but inasmuch as you complain about unnecessary linking of object files, that does not seem to be your actual case.

    The options target is a holdover from other projects where I suppress the printing of the compiler flags on each single .c file (might be dozens), so I just print them once at the top for better readability.

    In the general case, you do need to control this in the recipes for all the contributing .o files (in addition to in the recipe for the executable). Typically, however, this would be accomplished by providing your own implicit rule covering all the .o files, instead of writing a separate rule for every one. You can also use a sub-make to perform the conditional logic for you. Maybe something like this:

    # ...
    
    TS_FILE = build_ts
    
    .PHONY: all
    
    all:
        rm -f $(TS_FILE)
        $(MAKE) $(PROJ)
    
    $(PROJ): $(OBJS)
        flock $(TS_FILE) $(MAKE) options
        $(CC) -o $@ $(CFLAGS) $(LDFLAGS) $^
    
    %.o: %.c
        flock $(TS_FILE) $(MAKE) options
        $(CC) -o $@ $(CPPFLAGS) $(INCLUDES) $(CFLAGS) $<
    
    # Not .PHONY; produces an actual file
    options: $(TS_FILE)
        @echo $(PROJ) build options: | tee $@
        @echo "CC           = $(CC)" | tee -a $@
        @echo "CFLAGS       = $(CFLAGS)"  | tee -a $@
        @echo "CPPFLAGS     = $(CPPFLAGS)"  | tee -a $@
        @echo "INCLUDES     = $(INCLUDES)"  | tee -a $@
        @echo "LDFLAGS      = $(LDFLAGS)"  | tee -a $@
    

    Explanation:

    • The all target removes any pre-existing timestamp file, then uses a sub-make to build the executable. This gets the timestamp file removed before anything else is done, even with parallel make.

    • The options target is an ordinary target, producing a file a transcript of the options in addition to displaying them in the output. Because it has the timestamp file as a prerequisite (and that is not .PHONY either), make will run the recipe only if the options transcript does not already exist or is older than the timestamp file.

    • The recipe for linking the executable and the pattern rule by which all the .o files are built each use a sub-make to build the options target. But that leaves open the possibility that in a parallel make, the options would be printed more than once. To prevent that, the sub-make is gated by flocking the timestamp file, which both causes it to be created if it does not already exist, and serializes all the make options executions, so that only the first can find options out of date.

    • No rule for $(TS_FILE) is needed or wanted.

    Additional notes:

    • If you build the executable or any of the object files explicitly then you might or might not get the options printed, depending on whether the timestamp and options files from a previous build are still present.

    • If your filesystem has low timestamp resolution then you might see the options printed more than once on a given run.

    • Overall, it would be much easier and more reliable to just print the options first on every run (or every build of the all target), regardless of whether anything else is to be done. You said that's not what you want, but it's what I would do in the unlikely event that I did anything at all of this sort.