waferthin
waferthin

Reputation: 1602

Makefile where target names unknown

I'm trying to write a Makefile where multiple source files (in my case they are markdown) create multiple target files (pdfs). However, the target files generated have extra characters in the file name that can't be predicted (it happens to be a version number encoded in the source), but ideally the Makefile would not have to read the source itself.

So, for example:

file1.md => file1-v1.pdf
file2.md => file2-v2.pdf
...

I can calculate source name given a target name (by excluding anything after the hyphen and adding .md), but cannot calculate target name given the source.

Is it possible to write a Makefile that builds only the targets where the source have been updated?

Upvotes: 2

Views: 3055

Answers (4)

8c6b5df0d16ade6c
8c6b5df0d16ade6c

Reputation: 2584

maybe try this ? or something along those lines

# makefile

SRCS=$(wildcard *.md)
PDFS=$(shell printf *.pdf)

$(PDFS): $(SRCS)
    command ...

the printf *.pdf is meant to either expand to the first of the pdf files if they exist, else fail if they don't and that will signal to make that it should build. if this doesn't work i suggest maybe experimenting with find, ls or other listing tools (e.g. compgen, complete), maybe even in combination with xargs to get everything on one line.

Upvotes: 0

Vercingatorix
Vercingatorix

Reputation: 1884

@Beta's answer is informative and helpful, but I needed a solution (using GNU Make 4.1) that worked when the destination filename bears no resemblance to the input filename, for example, if it is generated from its content. I came up with the following, which takes every file matching *.in, and creates a file by reading the contents of the source file, appending a .txt, and using it as a filename to create. (For example, if test.in exists and contains foo, the makefile will create a foo.txt file.)

SRCS := $(wildcard *.in)
.PHONY: all 

all: all_s

define TXT_template =
$(2).txt: $(1)
    touch $$@
ALL += $(2).txt
endef

$(foreach src,$(SRCS),$(eval $(call TXT_template, $(src), $(shell cat $(src)))))

.SECONDARY_EXPANSION:
all_s: $(ALL)

The explanation:

The define block defines the recipe needed to make the text file from the .in file. It's a function that takes two parameters; $(1) is the .in. file and $(2) is the contents of it, or the base of the output filename. Replace touch with whatever makes the output. We have to use $$@ because eval will expand everything once, but we want $@ to left after this expansion. Since we have to collect all the generated targets so we known what all the make, the ALL line accumulates the targets into one variable. The foreach line goes through each source file, calls the function with the source filename and the contents of the file (i.e. what we want to be the name of the target, here you'd normally use whatever script generates the desired filename), and then evaluates the resulting block, dynamically adding the recipe to make. Thanks to Beta for explaining .SECONDARY_EXPANSION; I needed it for reasons not entirely clear to me, but it works (putting all: $(ALL) at the top doesn't work). The all: at the top depends on the secondary expansion of all_s: at the bottom and somehow this magic makes it work. Comments welcome.

Upvotes: 2

Beta
Beta

Reputation: 99172

This will be ugly, but it will work.

As it often is with Make, our problem divides into these two problems:
1. construct a list of targets
2. build them

Suppose we have five md files which map to pdf files (whose names we don't know beforehand):

file1.md => file1-v1.pdf
file2.md => file2-v1.pdf
file3.md => file3-v1.pdf
file4.md => file4-v1.pdf
file5.md => file5-v1.pdf

We can't use the real output file names as targets, because we don't know them beforehand, but we see five input files and know that we must build one output file for each. For now, a fake target name will do:

file1-dummy.pdf: file1.md
    zap file1.md

When Make executes this rule, it produces the file file1-v1.pdf. The fact that it doesn't produce a file named file1-dummy.pdf is disquieting, but not a serious problem. We can turn this into a pattern rule:

%-dummy.pdf: %.md
    zap $<

Then all we have to do is turn the list of existing input files (file1.md, file2.md, ...) into a list of dummy targets (file1-dummy.pdf, file2-dummy.pdf, ...), and build them. So far, so good.

But suppose some of the output files already exist. If file2-v2.pdf already exists -- and is newer than file2.md -- then we would prefer that Make not rebuild it (by attempting to build file2-dummy.pdf). In that case we would prefer that file2-v2.pdf be in the target list, with a rule that worked like this:

file2-v2.pdf: file2.md
    zap $<

This is not easy to turn into a pattern rule, because Make does not handle wildcards very well, and cannot cope with multiple wildcards in a single phrase, not without a lot of clumsiness. But there is a way to write one rule that will cover both cases. First note that we can obtain the part of a variable before the hyphen with this kludge:

$(basename $(subst -,.,$(VAR)))

Armed with this, and with secondary expansion, we can write a pattern rule that will work with both cases, and construct a target list that will exploit it:

# There are other ways to construct these two lists, but this will do.
MD  := $(wildcard *.md)
PDF := $(wildcard *.pdf)

PDFROOTS := $(basename $(subst -,.,$(basename $(PDF))))
MDROOTS := $(filter-out $(PDFROOTS), $(basename $(MD)))

TARGETS:= $(addsuffix -foo.pdf, $(MDROOTS)) $(PDF)

.SECONDEXPANSION:
%.pdf: $$(basename $$(subst -,., $$*)).md
    # perform actions on $<

Upvotes: 2

MadScientist
MadScientist

Reputation: 101081

Make's algorithm always starts with the final output product and works its way backwards to the source files, to see what needs to be updated.

Therefore, you HAVE to be able to enumerate the final output product as a target name and correlate that back to the inputs that generate that output, for make to work.

This is also why make is not a great tool for building Java, for example, since the output filenames don't map easily to the input file names.

So, you must have at least one target/prerequisite pair which is derivable (for implicit rules), or state-able (for explicit rules)--that is, known at the time you write the makefile. If you don't then a marker file is your only alternative. Note you CAN add extra generated, non-derivative prerequisites (for example, in compilers you can add header files as prerequisites that are not related to the source file name), in addition to the known prerequisite.

Upvotes: 1

Related Questions