agentsclimarketplace

Makefile best practices

Skill air-gapped/skills/.claude/skills/makefile-best-practices

Makefile best practices, patterns, and templates for GNU Make 4.x — dependency graphs, task-runner workflows, parallel-safe recipes, self-documenting help targets, and language-specific patterns (Go, Python, Node, Docker, Helm, POSIX).From its SKILL.md

Install
npx -y skills add air-gapped/skills --skill makefile-best-practices

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

11.1 KB, ~3.0k tokens by cl100k_base, as published. Nobody here has run it

Makefile Best Practices

Target: GNU Make 4.x. Covers Make as both a build system (dependency-driven compilation) and a task runner (developer workflow automation).

On the dev plugin. Its members are grouped by "general development tooling", not by a shared workflow — this skill has no dependency on baml-expert, jinja-expert, or transformers-config-tokenizers-expert, and none of them on it. Don't hunt for a pipeline that isn't there. The one overlap worth knowing: a Makefile that shells out to render templates is still a Makefile question here, but the template body itself is jinja-expert.

Golden Rules

0. Simplicity First

  • Start with the minimum viable solution; each target does ONE thing well.
  • Default to <=10 focused targets; expand only on explicit request.

1. Make is a Dependency Graph, Not a Script

Targets represent outputs; prerequisites represent inputs; recipes transform inputs → outputs. Think graph-first.

# WRONG: Script thinking - order-dependent, breaks with -j
build:
	compile src/a.c
	compile src/b.c
	link

# RIGHT: Graph thinking - declares real dependencies
program: a.o b.o
	$(CC) -o $@ $^

%.o: %.c
	$(CC) -c $< -o $@

2. Correctness Under make -j is the Real Bar

If it breaks with parallel builds, it's broken. Always declare real dependencies.

# WRONG: Hidden dependency, races under -j
generated.h:
	./generate-header.sh > $@

main.o: main.c  # Missing: generated.h
	$(CC) -c $< -o $@

# RIGHT: Explicit dependency
main.o: main.c generated.h
	$(CC) -c $< -o $@

Validate dependency correctness with make --shuffle=random -j (GNU Make 4.4+). Randomizing prerequisite order exposes missing edges that a fixed order hides.

3. Phony vs File Targets Drive Behavior

Use .PHONY for commands, not for artifacts. Understanding timestamps is 80% of Make proficiency.

.PHONY: clean test lint help  # Commands - always run
# Don't mark file-producing targets as phony

4. Variable Expansion Rules Matter

# := immediate (evaluated when defined) - use for $(shell), most cases
FILES := $(shell find src -name '*.c')

# = deferred (evaluated when used) - use when referencing later-defined vars
CFLAGS = $(BASE_FLAGS) $(EXTRA_FLAGS)

# ?= conditional (set only if undefined) - use for user-overridable defaults
PREFIX ?= /usr/local
CC ?= gcc

$(shell ...) with = re-runs the command every time the variable is expanded — always use := for shell captures unless repeated execution is intentional.

5. Pattern Rules + Automatic Variables Enable Elegance

VariableMeaning
$@Target name
$<First prerequisite
$^All prerequisites (deduped)
$?Prerequisites newer than target
$*Stem matched by %
$(@D)Directory part of target
$(BUILD_DIR)/%.o: src/%.c | $(BUILD_DIR)
	@mkdir -p $(@D)
	$(CC) $(CFLAGS) -c $< -o $@

Minimal Skeleton

The essential hygiene directives plus a self-documenting help target. For a production-ready template with verbosity toggle, color output, and a GNU Make 4.0+ compatibility check, see references/Makefile.gnumake-template.

Each line of the preamble matters:

DirectiveEffect
SHELL := bashUse bash (not /bin/sh/dash) for richer recipe syntax
.SHELLFLAGS := -eu -o pipefail -cUnset vars, errors, and pipe failures all abort the recipe
.DELETE_ON_ERROR:Remove the target file on recipe failure — prevents stale half-built artifacts
--warn-undefined-variablesCatch typos in variable names at parse time
--no-builtin-rulesStrip implicit rules for faster parsing and explicit semantics
.DEFAULT_GOAL := helpBare make prints help instead of building the first target
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables --no-builtin-rules
.DEFAULT_GOAL := help

.PHONY: build test clean help

build: ## Build the project
	go build ./...

test: ## Run tests
	go test ./...

clean: ## Remove build artifacts
	rm -rf build/

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

Essential Patterns

Order-Only Prerequisites for Directories

# BAD: Rebuilds when ANY file added to dir (timestamp changes)
$(objs): $(BUILD_DIR)

# GOOD: Only checks existence, not timestamp
$(objs): | $(BUILD_DIR)

$(BUILD_DIR):
	mkdir -p $@

Auto-Generated Dependencies (C/C++)

CPPFLAGS += -MMD -MP
-include $(deps)

Flags: -MMD generates .d files, -MP adds phony targets for headers (prevents errors if deleted).

Grouped Targets (GNU Make 4.3+)

For rules producing multiple outputs, use &: instead of sentinel files:

# Modern: grouped target
parser.c parser.h &: parser.y
	bison -d $<

# Legacy: sentinel file pattern
.parser.sentinel: parser.y
	bison -d $<
	touch $@
parser.c parser.h: .parser.sentinel

Target-Specific Variables

# Different flags for different targets
debug: CFLAGS += -g -O0 -DDEBUG
debug: all

release: CFLAGS += -O3 -DNDEBUG
release: all

test: CFLAGS += -DTEST --coverage
test: $(target)
	./run-tests

Non-Interactive Guards (CI-Safe)

# BAD: Breaks in CI
confirm:
	@read -p "Are you sure? [y/N] " ans && [ "$$ans" = y ]

# GOOD: Environment variable guard
deploy: guard-CONFIRM ## Deploy (requires CONFIRM=1)
	./deploy.sh

guard-%:
	@if [ -z '${${*}}' ]; then \
		echo "ERROR: Variable $* is not set"; \
		exit 1; \
	fi

Color Output (Respecting NO_COLOR)

ifdef NO_COLOR
  CYAN :=
  GREEN :=
  RESET :=
else
  CYAN := \033[36m
  GREEN := \033[32m
  RESET := \033[0m
endif

.PHONY: build
build:
	@echo "$(CYAN)Building...$(RESET)"
	$(MAKE) all
	@echo "$(GREEN)Done$(RESET)"

Anti-Patterns to Avoid

1. Recursive Make as Architecture

# AVOID: Incomplete dependency graph, poor -j performance
all:
	$(MAKE) -C lib
	$(MAKE) -C src  # Can't see lib's deps!

# PREFER: Non-recursive with includes
include lib/module.mk
include src/module.mk

If recursion is necessary, always use $(MAKE) not make (preserves jobserver).

2. Multi-Line Recipe cd Bug

# WRONG: Each line runs in separate shell
install:
	cd /usr/local
	cp myapp bin/  # Runs in original directory!

# RIGHT: Chain commands
install:
	cd /usr/local && cp myapp bin/

# OR: Use .ONESHELL (changes all recipes)

3. Silencing Everything

# BAD: CI failures are impossible to debug
build:
	@$(CC) -o $@ $^

# GOOD: Verbosity toggle
build:
	$(Q)$(CC) -o $@ $^
# Run: make V=1 for verbose

4. Non-Portable Shell Assumptions

# BAD: Bashisms without declaring bash
build:
	[[ -f config ]] && source config  # Fails on /bin/sh

# GOOD: Declare shell or use POSIX
SHELL := bash
# OR use POSIX: [ -f config ] && . config

Debugging Makefile Issues

Essential Flags

FlagPurpose
make -nDry run (print commands, don't execute)
make -BForce rebuild all targets
make -dDebug output (why did it rebuild?)
make --tracePrint each target as it runs
make -pPrint database (all rules and variables)
make -rRDisable built-in rules and variables

Diagnostic Functions

# Print variable value
$(info DEBUG: CFLAGS = $(CFLAGS))

# Warning (continues execution)
$(warning Something looks wrong)

# Error (stops execution)
$(error FATAL: Missing required variable)

Common Symptoms

SymptomLikely Cause
"Nothing to be done"Target exists and is up-to-date, or missing .PHONY
Rebuilds every timeMissing dependency, or .PHONY on file target
Breaks with -jHidden dependencies between targets
"missing separator"Spaces instead of tabs in recipe
Variable emptyWrong expansion timing (= vs :=) or typo

Portability Notes

GNU Make vs BSD Make

FeatureGNU MakeBSD MakePOSIX 2024 (Issue 8)
:= assignmentYesYesNo — use ::=
::= / :::= assignment4.4+YesYes
?= / += assignmentYesYesYes
.PHONYYesYesYes
.WAIT / .NOTPARALLEL.NOTPARALLEL onlyYesYes
$(shell ...)Yes!= syntax!= assignment only
$(wildcard ...)YesNoNo
.DELETE_ON_ERRORYesNoNo
Pattern rules %YesLimitedNo — reserved, not specified
Grouped targets &:4.3+NoNo

POSIX caught up in 2024. Issue 8 (IEEE Std 1003.1-2024) standardized .PHONY, .WAIT, .NOTPARALLEL, and the ::= / :::= / ?= / += / != assignment operators — all previously "common extension, not standard". The one trap: plain := is still not standard. Issue 8 spells immediate expansion ::=, and the rationale asks implementations to keep := as a compatibility extension while telling portable makefiles not to rely on it under .POSIX:. Pattern rules stay unspecified — % is reserved for possible future use, and metarules were considered and rejected.

Shell Portability

  • Default SHELL is /bin/sh (often dash on Debian, not bash)
  • Avoid bashisms unless SHELL := bash is declared
  • sed -i differs between GNU and BSD
  • echo -e is non-portable; use printf

Helm & Kubernetes Patterns

For Helm chart and Kubernetes Makefile patterns, consult references/Makefile.helm-k8s (452 lines, 47 targets). Key principles:

  • Artifact-first: define chart identity once (CHART_NAME, VERSION), derive all artifact names — eliminates hardcoding
  • Cluster safety guards: verify-context target that checks kubectl config current-context against an allowed list; all mutating targets depend on it
  • Air-gapped image extraction: render with all features enabled, grep images
  • Resource ordering: CRDs first, delete in reverse
  • Do NOT set KUBECTL_EXTERNAL_DIFF in Makefiles — users have their own diff viewers

Resources

Reference files are catalogs of patterns to pick from, not templates to copy wholesale. Real-world Makefiles typically use 10-20 targets.

  • Makefile.gnumake-template - Modern GNU Make skeleton
  • Makefile.go - Go project patterns
  • Makefile.python - Python development workflow
  • Makefile.node - Node.js with npm integration
  • Makefile.docker - Docker build/push patterns
  • Makefile.helm-k8s - Helm charts & Kubernetes operations
  • Makefile.portable - Cross-platform POSIX compatible
  • ci-integration.md - CI/CD usage patterns

What ships with it: 10 files

55.6 KB alongside SKILL.md

Gives 0 of the 12 instructions most containers cloud skills give in ~3.0k tokens

Counted across 607 of the 705 authors here whose files we hold, read 2026-09-06

  • Run as non-root userin 34 of 607, across 27 files
  • Use multi-stage buildsin 29 of 607
  • Set resource requests and limitsin 24 of 607, across 20 files
  • Configure liveness and readiness probesin 18 of 607, across 14 files
  • Use named volumes for persistent datain 14 of 607, across 9 files
  • Pin base image versionsin 14 of 607
  • Set up environment variablesin 14 of 607, across 10 files
  • Pin provider versionsin 14 of 607
  • Apply least privilege RBAC permissionsin 10 of 607, across 7 files
  • Create a dockerignore filein 10 of 607
  • Use remote state with lockingin 9 of 607
  • Pin base images by digestin 9 of 607, across 8 files

Said here and by no other author read

  • Start with the minimum viable solution
  • Think graph-first
  • Always declare real dependencies
  • Use phony for commands not artifacts
  • Use immediate expansion for shell captures
  • Use order-only prerequisites for directories

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 623,721. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.