Skip to content

Latest commit

 

History

History
202 lines (168 loc) · 6.59 KB

make.md

File metadata and controls

202 lines (168 loc) · 6.59 KB

Make

make runs recipes from a Makefile and rebuilds only what is out of date. Outside of compiling C it is a handy task runner for homelab and repo chores (make up, make lint, make deploy). Scripts that a Makefile calls are covered in Bash; linting in ShellCheck and shfmt.

Documentation: GNU Make Manual

Command Line

COMMAND DESCRIPTION
make Build the first (default) target
make <TARGET> Build a specific target
make -n Dry run: print recipes without running them
make -j<N> / make -j$(nproc) Run N jobs in parallel
make -k Keep going after errors
make -B Rebuild everything regardless of timestamps
make -C <DIR> Run in another directory
make -f <FILE> Use a different makefile
make VAR=value Override a variable
make -s Silent, do not echo recipes
make -p -q Print the rules and variables make knows
make --warn-undefined-variables Catch typos in variable names

Makefile Syntax

# target: prerequisites
#<TAB>recipe
build/app: src/main.c src/util.c
	$(CC) -o $@ $^

.PHONY: all clean
all: build/app

clean:
	rm -rf build

Recipe lines must start with a TAB, not spaces. The error *** missing separator almost always means spaces were used (or a line is not a rule or assignment). Set .RECIPEPREFIX = > to use another character, or configure your editor to keep tabs in Makefile.

Variables

SYNTAX DESCRIPTION
NAME = value Recursive: expanded each time it is used
NAME := value Simple: expanded once, at assignment (use this by default)
NAME ?= value Set only if not already set (environment or command line wins)
NAME += more Append
NAME != cmd Assign the output of a shell command
$(NAME) or ${NAME} Expand
$(shell cmd) Run a shell command and use its output
export NAME Pass to recipes' environment
$$VAR A literal $ so the shell sees $VAR
override NAME = value Beat a command-line override

Automatic Variables

VARIABLE MEANING
$@ Target name
$< First prerequisite
$^ All prerequisites (deduplicated)
$+ All prerequisites (with duplicates)
$? Prerequisites newer than the target
$* The stem matched by % in a pattern rule
$(@D), $(@F) Directory and file part of the target

Phony Targets

Targets that are actions, not files. Without .PHONY, a file named clean would make make clean do nothing.

.PHONY: up down logs test lint
up:
	docker compose up -d
down:
	docker compose down
logs:
	docker compose logs -f --tail=100

Pattern Rules and Functions

# Build every .o from its .c
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

SRC := $(wildcard src/*.c)
OBJ := $(SRC:.c=.o)
OBJ2 := $(patsubst src/%.c,build/%.o,$(SRC))
FUNCTION DESCRIPTION
$(wildcard *.c) Expand a glob
$(patsubst %.c,%.o,$(SRC)) Pattern substitution
$(SRC:.c=.o) Shorthand substitution
$(subst a,b,text) Plain replace
$(foreach f,$(LIST),cmd $(f)) Loop
$(filter %.c,$(FILES)), $(filter-out ...) Keep / drop matching words
$(addprefix pre-,$(LIST)), $(addsuffix .o,$(LIST)) Add prefix / suffix
$(dir ...), $(notdir ...), $(basename ...) Path pieces
$(if cond,then,else) Conditional
$(call fn,arg) Call a user-defined define
$(error message), $(warning message) Stop or warn at parse time

Conditionals and Includes

ifeq ($(ENV),prod)
  COMPOSE_FILES := -f compose.yml -f compose.prod.yml
else
  COMPOSE_FILES := -f compose.yml
endif

ifndef VERSION
  $(error VERSION is required, run: make release VERSION=1.2.3)
endif

-include .env          # leading - ignores a missing file
include config.mk

Recipes

SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help
.DELETE_ON_ERROR:

deploy:
	@echo "Deploying to $(HOST)"        # @ hides the command line
	-rm -f stale.lock                   # - ignores a failure
	cd app && ./build.sh                # each line is a separate shell!
	cd app; \
	  ./build.sh && ./test.sh           # one shell via backslash continuation

.ONESHELL:                              # run the whole recipe in one shell

Each recipe line runs in its own shell, so cd and variable assignments do not carry to the next line. Use &&, ; with a backslash, or .ONESHELL. Make does not use bash by default; set SHELL := bash before using Bash features.

Self-Documenting Help Target

Put a ## comment after each target and generate the help from them:

.DEFAULT_GOAL := help
.PHONY: help up down lint

help: ## Show this help
	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

up: ## Start the stack
	docker compose up -d

down: ## Stop the stack
	docker compose down

lint: ## Run all linters
	pre-commit run --all-files

make then prints a coloured, aligned list of targets. The awk program is explained in AWK; note the doubled $$ so make passes $1 and $2 through to awk.

Example: Project Task Runner

SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help

COMPOSE ?= docker compose
SERVICE ?=

.PHONY: help up down restart logs ps pull update lint fmt

help: ## Show this help
	@grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "} {printf "%-12s %s\n", $$1, $$2}'

up: ## Start services
	$(COMPOSE) up -d $(SERVICE)

down: ## Stop services
	$(COMPOSE) down

restart: ## Restart (make restart SERVICE=web)
	$(COMPOSE) restart $(SERVICE)

logs: ## Follow logs
	$(COMPOSE) logs -f --tail=100 $(SERVICE)

update: pull up ## Pull images and recreate

pull:
	$(COMPOSE) pull

lint: ## shellcheck and shfmt
	git ls-files -z '*.sh' | xargs -0 -r shellcheck -x
	git ls-files -z '*.sh' | xargs -0 -r shfmt -d

fmt: ## Format shell scripts
	git ls-files -z '*.sh' | xargs -0 -r shfmt -w

Pitfalls

PROBLEM CAUSE AND FIX
*** missing separator Recipe indented with spaces; use a TAB
make: 'x' is up to date. A file called x exists; mark the target .PHONY
$VAR empty in recipe make expanded it; write $$VAR for a shell variable
cd has no effect on the next line Separate shells; chain with && or use .ONESHELL
Recipe runs even when it should not Prerequisite is a phony target or a file that always changes
Editing .env does nothing Make variables are read at start; export and -include the file
Spaces in filenames break rules Make splits on whitespace; avoid spaces in paths
make -j output interleaves Add --output-sync=target (GNU Make 4+)
$(shell ...) runs on every parse Cache it with :=