Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Makefile: shell function in lazy variable is not lazy

Tags:

makefile

Consider the following Makefile.

$(shell touch /tmp/example.txt)
FILE := /tmp/example.txt
CONTENTS = $(shell cat $(FILE); bash -c 'echo [debugging id: $$RANDOM]')

.PHONY: all
all:
    @cat $(FILE)
    @echo '$$(CONTENTS):' $(CONTENTS)
    bash -c 'echo file-contents-$$RANDOM' > $(FILE)
    @cat $(FILE)
    @echo '$$(CONTENTS):' $(CONTENTS) # This line outputs the old contents. Why?

It prints the contents of the file, overwrites with new contents and prints the contents again. It shows as (after second shots of make):

file-contents-1543
$(CONTENTS): file-contents-1543 [debugging id: 15172]
bash -c 'echo file-contents-$RANDOM' > /tmp/example.txt
file-contents-22441
$(CONTENTS): file-contents-1543 [debugging id: 151]

The old content is file-contents-1543 and new content is file-contents-22441 (the numbers are random), but the last line echo $(CONTENTS) does not print the new contents. I think the command is actually called twice as debugging ids show but shell function in the lazy variable seems to be executed before writing the new contents to the file.

I expect that lazy variable in Makefile is evaluated every time it is referred, the echo $(CONTENTS) command always prints the latest file contents. What am I wrong?

By the way, I found that using CONTENTS = $$(cat $(FILE)) works as I expect. I will using this instead of shell function but is it ok?

like image 910
itchyny Avatar asked Mar 05 '23 05:03

itchyny


1 Answers

I expect that lazy variable in Makefile is evaluated every time it is referred, the echo $(CONTENTS) command always prints the latest file contents. What am I wrong?

First of all, in make's slang these variables are called recursive, not lazy. And, yes, they get expanded (i.e. recursively substituted) each time they are referred with $(CONTENTS). Considering that $(eval...) and $(shell...) (as pretty much anything looking as $(...)) also went through the same (recursive) expansion procedure (albeit, with some "side-effects"), each expansion of such variable could also result in some sort of "evaluation" or "execution".

Next, the order of expansion in make is a bit specific. In particular, the recipes (i.e. the lines starting with [tab]) are expanded after the whole makefile was (pre-)processed, but before the first line of the recipe gets executed by shell. Which is the main source of your confusion, I suppose.

I found that using CONTENTS = $$(cat $(FILE)) works as I expect

$$ is a way to get a single literal $ after an expansion procedure. So $$(cat $(FILE)) when expanded becomes $(cat /tmp/example.txt) which is a legal syntax for command substitution in bash. This means it will work only as part of a bash command (recipe line). If that is what you want then it's okay.

like image 168
Matt Avatar answered May 02 '23 01:05

Matt