This URL hosts a Git repository (ie. git clone https://steady.supply/git/make will work). For your convenience, the file README.md from this repository is rendered below.

GNU Make for fun and profit

About eleven years ago, GNU Make introduced the “special target”, .ONESHELL:. Here we explore it.

Makefile 0

A:
	x=y
	echo $$x

where make -f0 gives

x=y
echo $x

Because each line of a recipe is executed in its own shell, the shell variable $x is not defined when we come to echo it. An approach to “working around” this behaviour in sh or bash is to tack the symbols ;\ to the end of each line, where the ; instructs the shell that the statement is ended and the \ instructs GNU Make to escape the newline. For over a decade, this little dance has been unecessary.

Makefile 1

.ONESHELL:

A:
	x=y
	echo $$x

where make -f1 gives

x=y
echo $x
y

By writing .ONESHELL: somewhere in our Makefile, we are able to define and access shell variables on different lines without littering our recipes with ;\ symbols. All the lines of a recipe will be sent to a single shell instance.

Makefile 2

.ONESHELL:
SHELL=python3

A:
	x = 'y'
	print(x)

where make -f2 gives

x = 'y'
print(x)
y

We can use Python as our shell by setting SHELL.

Makefile 3

.ONESHELL:
SHELL=docker
.SHELLFLAGS=run --rm python:3-alpine python3 -c

A:
	x = 'y'
	print(x)

where make -f3 gives

x = 'y'
print(x)
y

With a modicum of prancing, we can use Docker as our shell.

Makefile 4

NAME=makefile-4
.ONESHELL:

A: SHELL=docker
A: .SHELLFLAGS=run --rm $(NAME) python3 -c
A: B
	import art
	art.aprint('hug me')

define DOCKERFILE
FROM alpine
RUN apk add py3-pip
RUN pip3 install art
endef

B: SHELL=bash
B: .SHELLFLAGS=-c
B: ; echo '$(DOCKERFILE)' | docker build -t $(NAME) -

where make -f4 gives

echo 'FROM alpine
RUN apk add py3-pip
RUN pip3 install art' | docker build -t makefile-4 -
import art
art.aprint('hug me')
(っ◕‿◕)っ 

We can use different shells for different recipes. Here we specify a Dockerfile inline, build its image (which uses Python packages from PyPI) and use the resulting container as our shell.

Note that the “run” target A depends on the “build” target B.

Makefile 5

.ONESHELL:
UUID=$(shell uuidgen | awk -F- '{ print tolower($$5) }')

# -----------------------------------------------------------------------------

A: PROGRAM=import sys; print(sys.version)
A: B C

# -----------------------------------------------------------------------------

B: DOCKERFILE=FROM python:2-slim
B: RUN=python2 -c
B: run-$(UUID)

C: DOCKERFILE=FROM python:3-slim
C: RUN=python3 -c
C: run-$(UUID)

# -----------------------------------------------------------------------------

run-%: SHELL=docker
run-%: .SHELLFLAGS=run --rm $* $(RUN)
run-%: build-% ; $(PROGRAM)

build-%: SHELL=bash
build-%: .SHELLFLAGS=-c
build-%: ; echo '$(DOCKERFILE)' | docker build -t $* -

where make -f5 gives

echo 'FROM python:2-slim' | docker build -t 5cef834d169c -
import sys; print(sys.version)
2.7.18 (default, Apr 20 2020, 19:34:11) 
[GCC 8.3.0]
echo 'FROM python:3-slim' | docker build -t 2960fc0c064c -
import sys; print(sys.version)
3.9.6 (default, Jul 22 2021, 15:24:21) 
[GCC 8.3.0]

The targets from Makefile 4 can be generalised, here as build-% and run-%. Our target A defines a program and, by depending on B and C, indicates that the program should be run in two different shells.

Refer to 6.2 The Two Flavors of Variables to understand when and how the Make variable UUID gets expanded (the “why” is left as an excercise to the reader).

Using the example in Makefile 5 we can define the shell to be used by a recipe inline, right up to OS-level dependencies.

Punchline

This article is generated by the following Makefile

.ONESHELL:
SHELL=bash

S=0 1 2 3 4 5

all: README.md

session/%.md: %.md % 
	rm -f $@
	e () { echo "$$1" >> $@ ; }
	c () { cat $$1 >> $@ ; }
	e '## Makefile `$*`' ; e '<pre>' ; c $* ; e '</pre>'
	e 'where `make -f$*` gives'; e '<pre>' ; 
	$(MAKE) --no-print-directory -f$* >> $@
	e '</pre>' ; c $*.md

README.md: $(foreach name,$(S),session/$(name).md)
	cat head.md > $@ ; for x in $^ ; do cat $$x >> $@ ; done
	echo '## Punchline' >> $@
	echo 'This article is generated by the following `Makefile`' >> $@
	echo '```' >> $@
	cat Makefile >> $@
	echo '```' >> $@
	echo 'Clone the repo to see for yourself' >> $@

clean:
	rm -f session/* README.md ; touch $(S)

Clone the repo to see for yourself

LICENSE

           DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
                   Version 2, December 2004
 
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>

Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
 
           DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
  TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

 0. You just DO WHAT THE FUCK YOU WANT TO.