Search code examples
foreachmakefilewildcardgeneric-programmingprecompile

Precompile headers in a makefile automatically


Goal of my Makefile is to create in the end a static library *.a out of Fortran77 files and some *.c's + *.h's whereas a specific part of the headers have to be precompiled with a special company internal precompiler which is provided via executable and all you have to hand over is the pathname+filename.

Let's call the Precompiler CPreComp.

The files needing the precompilation *_l.h .

So I want first to collect all the headers I need to precompile and then hand it over to a script which does some magic (env variables blubb blubb) and calls the precompiler.

Here you go with my Makefile:

SHELL=/usr/bin/bash
.SHELLFLAGS:= -ec

SOURCE_PATH = ./src
CPRECOMP = ./tools/cprecomp.exe
DO_CPreComp = $(SOURCE_PATH)/do_cprec
HDREXT = .h
PREC_HEADERS = $(foreach d, $(SOURCE_PATH), $(wildcard $(addprefix $(d)/*, $(HDREXT))))

.PHONY: all prereq

all:: \
      prereq \
      lib.a

prereq: chmod 777 $(DO_CPreComp)
        echo $(PREC_HEADERS) >> makefileTellMeWhatYouHaveSoFar.txt

lib.a: \
     obj/file1.o \
     obj/file2.o
     ar -r lib.a $?

obj/file1.o:

# do some fortran precompiling stuff here for a specific file

obj/file2.o: $(SOURCE_PATH)/*.h precomp_path/*.h $(SOURCE_PATH)/file2.c precomp_path/%_l.h
         cc -c -g file2.c

precomp_path/%_l.h : DatabaseForPreComp.txt

precomp_path/%_l.h : 
       $(foreach i , $(PREC_HEADERS) , $(DO_CPreComp) $(i) $(CPRECOMP);)

So that is my Makefile, the script for the DO_CPreComp looks as follows:

#!/bin/bash
filename="(basename "$1")"
dir="$(dirname "$1")"
cprecomptool="$2"

echo ${dir} ${filename} ${cprecomptool} >> scriptTellMeWhatYouKnow.txt     

"${cprecomptool}" "precomp_path/${filename}.1" >&cprecomp.err
cp "precomp_path/${filename}.1" "precomp_path/${filename}"

So according to the makefileTellMeWhatYouHaveSoFar.txt I collect all the headers, obviously also the ones not specified with _l.h . This has space for improvement but the precompiler is smart enough to skip the files which are not suitable. So makefileTellMeWhatYouHaveSoFar.txt looks like that:

header1.h header2.h header2_l.h headerx_l.h headery_l.h headerz.h

The Error tells me:

path_to_here/do_cprec : line xy: $2: unbound variable
make[2]: *** [precomp_path/%_l.h] Error 1
make[1]: *** [lib.a] Error 2

scriptTellMeWhatYouKnow.txt shows me the script knows nothing and it is not even created. If I modify cprecomptool and directly add it in the script hardcoded the scriptTellMeWhatYouKnow.txt shows me the argument $(CPRECOMP) twice as file name and path name and the hardcoded precompiler. And ofc it ends up with Segmentation fault, so the header name was never handed over.

Additionally: If I do not call the script in the second foreach but let $(i) be printed out with echo in another file it is empty.

Perhaps I am just too blind. And please if you are able to help me , explain it to me for dumb people, such that for the next time I stumble over a problem I am smarter because I know what I am doing. :)


Solution

  • OK, now that the main issue is solved, let's have a look at make coding styles. The make way of accomplishing what you want is not exactly using foreach in recipes. There are several drawbacks with this approach like, for instance, the fact that make cannot run parallel jobs, while it is extremely good at this. And on modern multi-core architectures, it can really make a difference. Or the fact that things are always redone while they are potentially up to date.

    Assuming the result of the pre-compilation of foo_l.h file is a foo.h (we will look at other options later), the make way is more something like:

    SOURCE_PATH         := ./src
    CPRECOMP            := ./tools/cprecomp.exe
    DO_CPreComp         := $(SOURCE_PATH)/do_cprec
    HDREXT              := .h
    PREC_HEADERS        := $(wildcard $(addsuffix /*_l.$(HDREXT),$(SOURCE_PATH)))
    PRECOMPILED_HEADERS := $(patsubst %_l.h,%.h,$(PREC_HEADERS))
    
    $(PRECOMPILED_HEADERS): %_l.h: %.h DatabaseForPreComp.txt
        $(DO_CPreComp) $@ $(CPRECOMP)
    

    ($@ expands as the target). This is a static pattern rule. With this coding style only the headers that need to be pre-compiled (because they are older than their prerequisites) are re-built. And if you run make in parallel mode (make -j4 for 4 jobs in parallel) you should see a nice speed-up factor on a multi-core processor.

    But what if the pre-compilation modifies the foo_l.h file itself? In this case you need another dummy (empty) file to keep track of when a file has been pre-compiled:

    SOURCE_PATH         := ./src
    CPRECOMP            := ./tools/cprecomp.exe
    DO_CPreComp         := $(SOURCE_PATH)/do_cprec
    HDREXT              := .h
    PREC_HEADERS        := $(wildcard $(addsuffix /*_l.$(HDREXT),$(SOURCE_PATH)))
    PREC_TAGS           := $(patsubst %,%.done,$(PREC_HEADERS))
    
    $(PREC_TAGS): %.done: % DatabaseForPreComp.txt
        $(DO_CPreComp) $< $(CPRECOMP) && \
        touch $@
    

    ($< expands as the first prerequisite). The trick here is that the foo_l.h.done empty file is a marker. Its last modification time records the last time foo_l.h has been pre-compiled. If foo_l.h or DatabaseForPreComp.txt has changed since, then foo_l.h.done is out of date and make re-builds it, that is, pre-compiles foo_l.h and then touch foo_l.h.done to update its last modification time. Of course, if you use this, you must tell make that some other targets depend on $(PREC_TAGS).