Makefiles are the main and most common “build system” used by me. A Makefile
is the first file I’m creating in every project. With a reasonable default rule
(usually “build and run tests”) they make switching between different
projects less time-consuming (cause I don’t have to check, which cargo
flags
I usually use or what name a docker image I want build should have) and, also,
the things around the code more uniform (e.g. usually my CI configuration has
jobs calling simply make check
, make
, make test
etc.).
Python
Last week, when reading why my Makefiles are wrong I’ve learnt that I should:
Tell Make “this Makefile is written with Bash as the shell” by adding this to the top of the Makefile:
SHELL := bash
Which is a thing I’ve spotted many times before during years of using Makefiles, but when I’ve been using Make as “a tool for writing tab-indented named shell snippets that depend on each other” I’ve never thought it’s possible to use a non-shell interpreter for the recipes:
The key message here, of course, is to choose a specific shell. If you’d rather use ZSH, or Python or Node for that matter, set it to that.
What does it mean is that one can both use all the goodies supplied by Makefile
(so well-understood target/prerequisites/recipe model and ubiquitousness of
make
) with one’s favourite language’s syntax and libraries. Wow!
So by using setting SHELL
and .ONESHELL
one can use Python to generate a
target file by downloading (and, possibly, parsing) a website:
# Makefile
SHELL := python
.ONESHELL:
index.html:
import requests
with open('$@', 'w') as f:\
f.write(requests.get('https://hryni.uk').text)
Running make
($@
refers to the target file) will create the index.html
file (if it doesn’t exist) with the content of the site under https://hryni.uk.
Go
It seems make
just calls passed interpreter with -c
flag and the recipe’s
source, we can check it in (docs or) a fancy way by running strace -f make
(-f
makes strace
trace child processes):
$ strace -f make
...
[pid 28099] execve("/usr/sbin/python", ["python", "-c", "import requests\nwith open('index"...], 0x55a94d29aca0 /* 42 vars */ <unfinished ...>
...
…so it’s possible to run whatever script one could think of there. So, why
not, let’s write Makefile
in Go. One way to do so could be to save recipe’s
code in a file and then go run
this file. Here’s the Makefile
:
# Makefile
SHELL := ./rungo.sh
.ONESHELL:
index.html:
c := make(chan string)
go func () { c <- "Makefile in go!" }()
fmt.Println(<-c)
rungo.sh
is a script (without most of the things that make your Bash scripts
right to make it shorter) that
takes source code of the recipe above, wraps it with func main()
, saves to a
temporary file, then compiles and runs:
#!/bin/bash
# rungo.sh
# $0 == "./rungo.sh"
# $1 == "-c"
# $2 == "c := make(chan string) go func () { c <- "Makefile in go!" }() fmt.Println(<-c)"
SRC="$2"
TMP_FILE="$(mktemp)"
echo -e "package main\nfunc main() { ${SRC} }" > "${TMP_FILE}"
goimports "${TMP_FILE}" > "${TMP_FILE}.go"
go run "${TMP_FILE}.go"
Of course it can be run like a “normal” Makefile
:
$ make
Makefile in go!
Ta-da!