GNU Make

Changing defaults

# Use strict mode Bash instead of /bin/sh
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c

# Run each recipe in single shell session instead of a shell per line.
# Lets you do bash loops, variable assignments etc
.ONESHELL

# Delete target file if rule fails.
# Lets you properly re-run failed rule, guards against broken files.
.DELETE_ON_ERROR

# Warns you if you try to use a non-existent variable
MAKEFLAGS += --warn-undefined-variables

# Disable magic
MAKEFLAGS += --no-builtin-rules

# Use `>` instead of tabs
ifeq ($(origin .RECIPEPREFIX), undefined)
  $(error This Make does not support .RECIPEPREFIX. Please use GNU Make 4.0 or later)
endif
.RECIPEPREFIX = >

Writing Makefile

# Default rule
.PHONY: build
build: out/image-id

# Clean up the output directories;
# since all the sentinel files go under tmp,
# this will cause everything to get rebuilt
.PHONY: clean
clean:
> rm -rf tmp
> rm -rf out

# 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