agentsclimarketplace

Makefile best practices

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

Claude Code plugin marketplace — 58 installable reference skills across vLLM/SGLang inference, Kubernetes & Harvester, GPU host bring-up, observability, security, and agent workflows.

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

  • 3 stars3 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.

What its author says it does

Copied from the file, not written here

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).

SKILL.md

11.1 KB, 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

Keep looking

Skills are one crate of 328,083. 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.