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.
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 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?
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.
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 flock
ing 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.