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.
About eleven years ago, GNU Make
introduced
the “special target”,
.ONESHELL:
.
Here we explore it.
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.
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.
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
.
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.
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
.
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.
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
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.