tl;dr - Some tips for writing better makefiles (using preambles, generating help text), and why you might want to use just
instead.
This preamble is online @ davis-hansson.com
SHELL := bash
.ONESHELL:
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
ifeq ($(origin .RECIPEPREFIX), undefined)
$(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or later)
endif
.RECIPEPREFIX = >
# Default - top level rule is what gets ran when you run just `make`
build: out/image-id
.PHONY: build
# Clean up the output directories; since all the sentinel files go under tmp, this will cause everything to get rebuilt
clean:
> rm -rf tmp
> rm -rf out
.PHONY: clean
# Tests - re-ran if any file under src has been changed since tmp/.tests-passed.sentinel was last touched
tmp/.tests-passed.sentinel: $(shell find src -type f)
> mkdir -p $(@D)
> node run test
> touch $@
# Webpack - re-built if the tests have been rebuilt (and so, by proxy, whenever the source files have changed)
tmp/.packed.sentinel: tmp/.tests-passed.sentinel
> mkdir -p $(@D)
> webpack ..
> touch $@
# Docker image - re-built if the webpack output has been rebuilt
out/image-id: tmp/.packed.sentinel
> mkdir -p $(@D)
> image_id="example.com/my-app:$$(pwgen -1)"
> docker build --tag="$${image_id}
> echo "$${image_id}" > out/image-id
While of course you don’t have to follow all of these guides, the preamble (and stuff like switching to “>” has been massively helpful for my own projects).
I use a slightly smaller version of the above preamble from file to file:
# Makefile preamble (https://tech.davis-hansson.com/p/make/)
SHELL := bash
.ONESHELL:
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
ifeq ($(origin .RECIPEPREFIX), undefined)
$(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or later)
endif
.RECIPEPREFIX = >
ifndef VERBOSE
MAKEFLAGS += --no-print-directory
endif
.PHONY: your phony targets go here \
> and here
I’ve seen some recipes for help text generation for makefiles and this is one of the best ones I’ve seen so far, since it’s so concise:
help: ## Display this help
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_\-.*]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
One I’ve personally used that’s a bit more involved (and has some additions related to my environment) is the following:
# COLORS
RED := $(shell tput -Txterm setaf 1)
GREEN := $(shell tput -Txterm setaf 2)
YELLOW := $(shell tput -Txterm setaf 3)
VIOLET := $(shell tput -Txterm setaf 5)
AQUA := $(shell tput -Txterm setaf 6)
WHITE := $(shell tput -Txterm setaf 7)
RESET := $(shell tput -Txterm sgr0)
## Show help
help:
@echo ''
@echo 'Makefile for infrastructure that powers vadosware.io'
@echo ''
@echo 'Usage:'
@echo ' ${YELLOW}make${RESET} ${GREEN}<target>${RESET}'
@echo ''
@echo 'Pulumi Variables:'
# @printf " ${YELLOW}_PLUGIN${RESET}\tSet storage plugin, [*rook-ceph-lvm*, linstor-drbd9, ...]\n"
@echo ''
@echo 'Ansible Variables:'
@printf " ${YELLOW}TARGET${RESET}\tTargets (ex. node.cluster.dns.vadosware.io)\n"
@printf " ${YELLOW}NODE_NAME${RESET}\tNode name for a single target (ex. example.target)\n"
@printf " ${YELLOW}STORAGE_STRATEGY${RESET}\tWay to slice storage on machine (ex. mdadm+zfs)\n"
@echo ''
@echo 'Targets:'
@awk '/^[a-zA-Z\-_0-9]+:/ { \
helpMessage = match(lastLine, /^## (.*)/); \
if (helpMessage) { \
helpCommand = substr($$1, 0, index($$1, ":")-1); \
helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
printf " ${YELLOW}%-$(TARGET_MAX_CHAR_NUM)s${RESET}\t${GREEN}%s${RESET}\n", helpCommand, helpMessage; \
} \
} \
{ lastLine = $$0 }' $(MAKEFILE_LIST)
Here are just some of the places that there are great Makefile tips:
Jacob’s post on how your makefiles are wrong
Alex Tan has a great Make 101 post on Medium
There’s always the official documentation
just
Lately on most projects (especially those written in Rust) I’ve started to use just
– it’s a command runner that is fashioned after Make but avoids a ton of the foot guns.
Remember how hard making help text was? Here it is in Justfile
:
default:
just --list
just
has many of the same creature comforts that the preamble gets you, and a bunch of useful functions – check out the README and consider switching.
Like most Rust CLI projects it’s also really easy to adopt – just download the appropriate binary for your $PLATFORM
and you’re ready to go.
Another thing I like doing with Justfile
s is running bash scripts a bit more conscientiously:
# Log in to Docker registry in order to pull RSS API
registry-rss-api-login:
#!/bin/env -S bash -euo pipefail
if [ ! -f "{{rss_api_registry_username_secret_path}}" ] ; then
echo -e "\n[info] Please enter a username for pulling the rss-api docker container:";
read REGISTRY_USERNAME;
echo -e "[info] registry username to => [${REGISTRY_USERNAME}]";
echo "[info] Writing registry username to relevant secret files...";
echo -e "${REGISTRY_USERNAME}" | tr -d '[:space:]' > {{rss_api_registry_username_secret_path}}; \
fi
if [ ! -f "{{rss_api_registry_password_secret_path}}" ] ; then
echo -e "\n[info] Please enter a password for pulling the rss-api docker container:";
read REGISTRY_PASSWORD;
echo -e "[info] registry password to => [${REGISTRY_PASSWORD}]";
echo "[info] Writing registry password to relevant secret files...";
echo -e "${REGISTRY_PASSWORD}" | tr -d '[:space:]' > {{rss_api_registry_password_secret_path}}; \
fi
if [ -f "{{rss_api_registry_docker_json_path}}" ] ; then
echo -e "[info] file @ [{{rss_api_registry_docker_json_path}}] already exists, skipping...";
else
echo "\n[info] logging into docker...";
export DOCKER_CONFIG={{rss_api_registry_docker_secret_folder}};
export DOCKER_USERNAME=$(cat {{rss_api_registry_username_secret_path}});
cat {{rss_api_registry_password_secret_path}} | {{docker}} login {{container_registry}} -u ${DOCKER_USERNAME} --password-stdin;
cp {{rss_api_registry_docker_json_path}} {{rss_api_registry_docker_secret_folder}}/.dockerconfigjson;
fi
(NOTE: remember, if you’re storing secrets in your repo, use a tool like git-crypt
or SOPS!)
This is mostly the same as how you would write it in make
, except you’ve got the nicety of being able to use any shebang there – so you could write your target in NodeJS or Python, for example.
Less footguns, the ability to write targets that execute in common languages – just
is probably the future for me (and maybe it should be for you, as well).