Edgar Bonet
Edgar Bonet

Reputation: 3566

Static patterns and conditionals with make

I have a problem with GNU Make 4.2.1. It seems there is some interaction between the syntax of static pattern rules and conditional functions that I do not quite understand.

The context: I have a project documented by a set of Markdown files, and I would like to render these files to HTML in order to check them locally. The directory structure should end up looking like this:

some_project/
├── README.md       # entry page of documentation
├── doc/            # extra docs
│   ├── foo.md
│   ├── bar.md
│   └── ...         # and some more
└── doc_html/       # HTML rendering of the docs
    ├── Makefile    # the Makefile I am trying to write
    ├── index.html  # rendered from README.md
    ├── foo.html    # ............. doc/foo.md
    ├── bar.html    # ............. doc/bar.md
    └── ...         # etc.

Without the special case of index.html, I could write something like:

%.html: ../doc/%.md
    some list of commands $< $@

The problem is that the prerequisite of index.html (namely ../README.md) does not match the pattern. I would like to handle this special case without having to repeat the whole list of commands. This is what I have so far:

DOC_PAGES = $(wildcard ../doc/*.md)
TARGETS = index.html $(patsubst %.md,%.html,$(notdir $(DOC_PAGES)))

# Function to find the source for page $(1)
source = $(if $(findstring index.html,$(1)), \
    ../README.md, \
    $(patsubst %.html,../doc/%.md,$(1)) \
)

all: $(TARGETS)

$(TARGETS): %.html: $(call source,%.html)
    @echo some list of commands $< $@

# Check the TARGETS variable and the `source' function
test:
    @echo TARGETS = $(TARGETS)
    @echo "source(index.html)" = $(call source,index.html)
    @echo "source(foo.html)" = $(call source,foo.html)

My source function seems to work:

$ make test
TARGETS = index.html bar.html foo.html
source(index.html) = ../README.md
source(foo.html) = ../doc/foo.md

However, it doesn't behave properly in the static rule

$ make
make: *** No rule to make target '../doc/index.md', needed by 'index.html'.  Stop.

Note that the rule does work if I remove index.html from $(TARGETS).

An idea of what I am doing wrong?

Upvotes: 1

Views: 179

Answers (2)

MadScientist
MadScientist

Reputation: 100866

This line:

$(TARGETS): %.html: $(call source,%.html)

cannot work because you're expanding the source macro with an argument of the literal string %.html. You can't use patterns or automatic variables in macros in prerequisite lists: macros are expanded first, before parsing or expanding patterns.

However, it seems to me that you're making this way more complicated than it needs to be. If most of your targets are built one way but a few are built a different way, then just create a pattern rule for the "most" and write explicit rules for the "some":

%.html: ../doc/%.md
        some list of commands $< $@

index.html : ../README.md
        commands to build index.html

If the set of commands is identical and you don't want to repeat them, put them in a variable:

create_html = some list of commands $< $@

%.html: ../doc/%.md
        $(create_html)

index.html : ../README.md
        $(create_html)

(be sure to create the variable with = not := if you want to include $< and $@ in the script).

ETA You asked about why things seemed to work: when make expands this it will substitute the literal string %.html. You can prove this to yourself by adding an $(info...) call like this:

source = $(info 1=$(1)) $(if $(findstring index.html,$(1)), \
    ../README.md, \
    $(patsubst %.html,../doc/%.md,$(1)) \
)

and you'll see that it will print (one time only because the rule is only expanded once) 1=%.html.

What happens next? Well this means that your macro expands to this:

$(if $(findstring index.html,%.html), ../README.md, $(patsubst %.html,../doc/%.md,%.html))

(again, using the literal string %.html). The findstring always returns empty because index.html can't be found in the string %.html, so you expand the else-clause:

$(patsubst %.html,../doc/%.md,%.html)

Clearly %.html matches %.html with a stem of %, so the substitution is made and returns ../doc/%.md. So after all that your rule looks like this:

$(TARGETS): %.html: ../doc/%.md
        @echo some list of commands $< $@

This exactly the same thing you had before with your simple pattern rule.

Upvotes: 3

Andreas
Andreas

Reputation: 5301

This would seem to be a solution:

DOC_PAGES := $(wildcard ../doc/*.md)
TARGETS := index.html $(patsubst %.md,%.html,$(notdir $(DOC_PAGES)))

all: $(TARGETS)

.INTERMEDIATE: ../doc/index.md
../doc/index.md: ../README.md
    cp $< $@

%.html: ../doc/%.md
    @echo some list of commands $< $@

test:
    @echo $(TARGETS)

With output:

$ make
cp ../README.md ../doc/index.md
some list of commands ../doc/index.md index.html
some list of commands ../doc/bar.md bar.html
some list of commands ../doc/foo.md foo.html
rm ../doc/index.md

index.html looks for ../doc/index.md through the pattern rule, leading to the recipe copying ../README.md.

Prerequisites to the .INTERMEDIATE special target are removed when make completes. Optional.

Upvotes: 0

Related Questions