1

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?

Edgar Bonet
  • 3,416
  • 15
  • 18

2 Answers2

3

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.

MadScientist
  • 92,819
  • 9
  • 109
  • 136
  • Thanks, but I do not quite understand the issue about “expanded before parsing“: the rule _does work_ as intended if `TARGETS` does not contain `index.html`. As for the `create_html` variable, I may fallback to this solution if there is no other choice, but I find it clumsy if there are many commands. – Edgar Bonet Jun 16 '20 at 18:34
  • I'll explain in the answer – MadScientist Jun 16 '20 at 19:01
0

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.

Andreas
  • 5,086
  • 3
  • 16
  • 36
  • Special thanks to the people pointing out the errors in my previous answer. Encore! – Andreas Jun 16 '20 at 19:37
  • 1
    I feared that this intermediate copy would mess up with make figuring out whether the file has to be re-rendered, but it turns out it handles this quite smartly. Adopted the solution, with `cp` replaced by `ln -s`. – Edgar Bonet Jun 16 '20 at 21:09