Search code examples
makefilegnu-makeexpansionindirection

Secondary expansion in a Makefile is causing unnecessary targets to be run


I am trying to write a Makefile that builds PDF outputs with LaTeX, using Latexmk. The outputs have basically the same rule, with different prerequisites, so I tried generalising my original Makefile using GNU Make's "secondary expansion". (I also created .PHONY targets, also with secondary expansion, to make it more user-friendly.) However, this is causing the prerequisite rules to always be run, even when they don't need to be. Fortunately, Latexmk is clever enough to avoid doing unnecessary work, but I wonder if I'm doing anything wrong...

To try to abstract what I'm attempting:

      ,-> foo -> build/foo.pdf
all -{
      `-> bar -> build/bar.pdf

That is, the all target builds foo and bar. These targets open the respective PDF file, which have a prerequisite of build/X.pdf (where X is foo or bar). These are genuine targets which build the appropriate PDF file.

This is what I've come up with:

TARGETS   = foo bar
BUILD_DIR = build
OUTPUTS   = $(TARGETS:%=$(BUILD_DIR)/%.pdf)

commonSRC = src/preamble.tex src/header.tex # etc...
fooSRC    = src/foo.tex $(commonSRC) # etc...
barSRC    = src/bar.tex $(commonSRC) # etc...

all: $(TARGETS)

.SECONDEXPANSION:
$(TARGETS): $(BUILD_DIR)/[email protected]
    open $<

# FIXME This isn't quite right: This rule is still getting called by the
# above rule, even when it doesn't need to be. Latexmk is clever enough
# not to do any extra work, but it shouldn't run at all.
.SECONDEXPANSION:
$(OUTPUTS): $$($$(subst .pdf,SRC,$$(@F))) $(BUILD_DIR)
    latexmk -outdir=$(BUILD_DIR) -auxdir=$(BUILD_DIR) -pdf $<

$(BUILD_DIR):
    mkdir $@

clean:
    rm -rf $(BUILD_DIR)

.PHONY: all $(TARGETS) clean

Just to be clear: The rule for build/X.pdf should run whenever the files enumerated in XSRC (again, where X is foo or bar) are newer than the PDFs, or the PDFs don't exist; but it should not run otherwise.


Solution

  • I believe that this got somewhat complex, more than it needs to be. Part of these second expansion statements can be just replaced with static pattern rules. The other thing is that .SECONDEXPANSION: makes all further Makefile contents to be subject to second expansion, so you don't need to explicitly state it before every target (it would be much clearer to mark .PHONY targets this way to quickly see if a target is phony or not).

    Nevertheless, I believe that most important issue here is that you mention a directory as a prerequisite. Remember that make decides on whether to rebuild the target based on dependencies timestamp, and a directory gets its timestamp always updated whenever a file in this directory is updated. Therefore, whenever you write $(BUILD_DIR)/foo.pdf, $(BUILD_DIR) timestamp gets updated and the next call will build again since the directory is newer. You can avoid it by specifying a directory as an order-only prerequisite (which means: build if it doesn't exist, but do not check timestamp).

    Putting it all together I would make it this way:

    TARGETS   = foo bar
    BUILD_DIR = build
    
    commonSRC = src/preamble.tex src/header.tex # etc...
    fooSRC    = src/foo.tex $(commonSRC) # etc...
    barSRC    = src/bar.tex $(commonSRC) # etc...
    
    .SECONDEXPANSION:
    
    .PHONY: all
    all: $(TARGETS)
    
    .PHONY: $(TARGETS)
    $(TARGETS): %: $(BUILD_DIR)/%.pdf
            echo open $<
    
    $(BUILD_DIR)/%.pdf: $$($$*SRC) | $(BUILD_DIR)
            echo latexmk -outdir=$(BUILD_DIR) -auxdir=$(BUILD_DIR) -pdf $< > $@
    
    $(BUILD_DIR):
            mkdir -p $@
    
    .PHONY: clean
    clean:
            rm -rf $(BUILD_DIR)
    

    Output:

    $ make all
    mkdir -p build
    echo latexmk -outdir=build -auxdir=build -pdf src/foo.tex > build/foo.pdf
    echo open build/foo.pdf
    open build/foo.pdf
    echo latexmk -outdir=build -auxdir=build -pdf src/bar.tex > build/bar.pdf
    echo open build/bar.pdf
    open build/bar.pdf
    
    $ make all
    echo open build/foo.pdf
    open build/foo.pdf
    echo open build/bar.pdf
    open build/bar.pdf
    

    Note that subsequent call did not attempt to build anything, just open the pdf. It still reacts on the file change however:

    $ touch src/foo.tex
    $ make all
    echo latexmk -outdir=build -auxdir=build -pdf src/foo.tex > build/foo.pdf
    echo open build/foo.pdf
    open build/foo.pdf
    echo open build/bar.pdf
    open build/bar.pdf
    
    $ touch src/header.tex
    $ make all
    echo latexmk -outdir=build -auxdir=build -pdf src/foo.tex > build/foo.pdf
    echo open build/foo.pdf
    open build/foo.pdf
    echo latexmk -outdir=build -auxdir=build -pdf src/bar.tex > build/bar.pdf
    echo open build/bar.pdf
    open build/bar.pdf