makefile

Recursive Makefile Does Not Recognize Dependency Changes in Sub-Directory


I am aware that there is a lot of debate around recursive makefiles. That being understood, I'm still trying to get one to work.

Context: I have the following project setup:

quendor
  src
    zmachine
      Makefile
      main.c
      zmachine.h
  Makefile

So I use the root-level Makefile to call another Makefile in a directory. (My actual project has other directories that generate other library files but, for now, the above showcases the problem with the minimum amount of context.

The makefile setup I have in place does work in that it generates .o and .d files in the zmachine directory. Then a library (zmachine.a) is built correctly.

Problem: The problem I have is that when I run the makefile at the root level again, it will not pick up if there have been changes to either the .c or .h files in the subdirectory. I just get this message:

make: Nothing to be done for `zmachine_lib'.

Example of My Code:

Here is the root level Makefile:

CC := gcc

ifeq ($(CC), gcc)
    CFLAGS += -std=c11 -Wall -Wextra -Wpedantic -Wconversion -Wmissing-prototypes -Wshadow -MMD -MP
    LDFLAGS +=
    OPT +=
endif

AR ?= $(shell which ar)
RANLIB ?= $(shell which ranlib)

export CC
export AR
export RANLIB
export CFLAGS

SRC_DIR = src

ZMACHINE_DIR = $(SRC_DIR)/zmachine
ZMACHINE_LIB = $(ZMACHINE_DIR)/zmachine.a

SUB_DIRS = $(ZMACHINE_DIR)

SUB_CLEAN = $(SUB_DIRS:%=%-clean)

# Targets

zmachine_lib : $(ZMACHINE_LIB)

$(ZMACHINE_LIB):
    $(MAKE) -C $(ZMACHINE_DIR)

clean : $(SUB_CLEAN)

$(SUB_CLEAN):
    -$(MAKE) -C $(@:%-clean=%) clean

Here is the Makefile from the zmachine directory:

SOURCES := $(wildcard *.c)
HEADERS = $(wildcard *.h)
OBJECTS := $(SOURCES:%.c=%.o)

DEPS := $(OBJECTS:%.o=%.d)

TARGET = zmachine.a

ARFLAGS = rc

$(TARGET): $(OBJECTS)
    $(AR) $(ARFLAGS) $@ $?
    $(RANLIB) $@
    @echo "** Finished building Z-Machine architecture."

%.o: %.c
    $(CC) $(CFLAGS) -fPIC -c $< -o $@

clean:
    $(RM) $(TARGET) $(OBJECTS) $(DEPS)

-include $(DEPS)

Again, the logic of the Makefiles works in terms of generating the correct output and library files. So I seem to have got that part right.

What I can't get to work is having the combined Makefiles, working together, recognize that changes have been made and thus recompiling is necessary.

For example, if I change main.c or zmachine.h, running make again won't recognize that the change has happened, thus no recompilation is trigered.

What I Tried:

I did some searching for "recursive makefiles dependencies" and related searches but I couldn't find anything that showed me how to handle dependency changes in this specific context.

I did try running make -d to look at the files and dates that make was using. It gave me this output, which wasn't as helpful to me:

 No implicit rule found for `zmachine_lib'.
  Considering target file `src/zmachine/zmachine.a'.
   Finished prerequisites of target file `src/zmachine/zmachine.a'.
  No need to remake target `src/zmachine/zmachine.a'.
 Finished prerequisites of target file `zmachine_lib'.
Must remake target `zmachine_lib'.
Successfully remade target file `zmachine_lib'.
make: Nothing to be done for `zmachine_lib'.

I realize this must be because I do not have the file set up to recognize what depends on what. But I feel I do with this:

zmachine_lib : $(ZMACHINE_LIB)

$(ZMACHINE_LIB):
    $(MAKE) -C $(ZMACHINE_DIR)

Here zmachine_lib is the only target in this example. It depends on $(ZMACHINE_LIB) which, as you can see, I have set up to call the Makefile in the subdirectory.

So I'm guessing it's this bit of calling $(MAKE) that is perhaps obfuscating changes made at the subdirectory level since, for the $(ZMACHINE_LIB) target would appear to be already satisfied (from the perspective of the top-level Makefile).

I did find this makefile with dependency on a shared library sub project, which does seem like it's very close to my issue. But I can't see how to utilize that solution in my case because of the fact that the logic is distributed over the two Makefiles. I did try to change my target in the sub-Makefile to this:

$(TARGET): $(SOURCES)
  ...

Basically changing $(OBJECTS) to $(SOURCES), which is what it seems like that other solution was suggesting. But that does not work for me; dependency changes are still not recognized.


Solution

  • I am aware that there is a lot of debate around recursive makefiles.

    I'm not sure there is so much debate, really. Recursive make has some pretty well known limitations. Non-recursive make has different, largely complementary, limitations.

    That being understood, I'm still trying to get one to work.

    Lots of people do. But although you may understand that recursive make has known issues, you do not seem to understand their nature, because you are asking about a manifestation of one of main ones.

    Problem: The problem I have is that when I run the makefile at the root level again, it will not pick up if there have been changes to either the .c or .h files in the subdirectory.

    No, it doesn't. That is to be expected with your makefile.

    Recursive make serves large projects by dividing build information into more manageable pieces and keeping it close to the sources being built. Among the main costs of doing so is that details, especially dependency details, are compartmentalized. In your case, the top-level make knows that a target src/zmachine/zmachine.a is to be built, but no dependencies for that target are known to it. As a result, that make considers the target out of date only if it does not exist. So yes, it will not recognize that target as being out of date relative to its actual sources. That a sub-make would update it if run is irrelevant, because the sub-make never runs.

    The usual, more practicable approach to recursive make is to recurse unconditionally. Something like this:

    SUBDIRS = src doc
    
    all: $(SUBDIRS)
    
    $(SUBDIRS):
            $(MAKE) -C $@
    

    Where there are dependencies among the targets managed by different make runs, those still need to be modeled somehow, else you will sometimes need multiple runs of the overall make for everything to be updated. Ideally, that's handled on a directory-by-directory basis, not a specific-target basis.

    But that's a bit oversimplified, I'm afraid, because one generally wants recursion to work for most or all top-level targets, not just for the default target. You should consider looking at someone else's working recursive make build system to see how they do it.