Skill

makefile-conventions

14 ↓ | .tar.gz

Structures Makefiles as a universal command interface using modular includes and self-documenting help. Use when the user wants to add a make target, create a Makefile, organize build commands, set up a make-based workflow, or asks how to structure make targets. Also activate before modifying any Makefile or mk/ file in this project.

Use Make as the universal command interface. All project operations are accessible via make, regardless of the underlying implementation language.

Workflow

  1. Investigate the project — Before writing targets, check:

    • Does a Makefile already exist? Read it.
    • What scripts, tools, or build steps does the project use?
    • Is there an existing mk/ directory?
  2. Decide placement — If the target belongs to an existing concern group, add it there. If it starts a new concern, create a new modular makefile. See makefile-structure for the full modular pattern.

  3. Write the target — Follow these conventions:

    • Declare every target .PHONY unless it produces a file
    • Add a ## Description comment for the help system
    • Delegate complex logic to scripts
    • Use @ prefix for clean output
    • Quote variable references: "$(VAR)"
  4. Update help — Add the new target to the appropriate concern group in make help output.

  5. Verify — Run make help to confirm the target appears. Run the target to confirm it works.

Complete Example

Root Makefile pattern — includes domain modules and provides grouped help:

.DEFAULT_GOAL := help

include mk/test.mk
include mk/deploy.mk

.PHONY: help
help: ## Show this help
	@echo "Project - Make Commands"
	@echo "======================"
	@echo ""
	@echo "Testing:"
	@echo "  make test       - Show test help"
	@echo "  make test sh    - Run shellcheck"
	@echo ""
	@echo "Development:"
	@echo "  make check      - Check prerequisites"

For the full modular delegation pattern (domain .mk files, subdirectory Makefiles, subcommand delegation), see makefile-structure.

Conventions

Structure

  • One main Makefile at the project root
  • Modular makefiles in mk/ for each concern domain
  • Domains with subcommands get mk/<domain>/Makefile
  • .DEFAULT_GOAL := help so bare make shows help
  • Never use %: @: catch-all patterns. Use conditional explicit subcommand declarations instead (see Subcommand Delegation below).

Naming

  • Targets: lowercase, hyphenated for multi-word
  • Subcommands: space separation (make test sh)
  • Variables: UPPER_CASE

Help system

  • make or make help shows all commands grouped by concern
  • make <domain> shows domain-specific help
  • When a domain has 2 or more subcommands, bare make <domain> must show subcommand help. Do not make it execute a default action.

Delegation

Makefiles are the interface, not the implementation:

deploy:
	@./scripts/deploy.sh "$(ENV)"

Subcommand delegation

When a domain uses $(MAKE) -C for space-separated subcommands (make db migrate), Make sees migrate as a separate goal and errors. Solve this with conditional explicit targets — not a catch-all:

.PHONY: db
db:
	@$(MAKE) -C mk/db $(filter-out $@,$(MAKECMDGOALS))

# Only activate when 'db' is on the command line
ifneq ($(filter db,$(MAKECMDGOALS)),)
.PHONY: migrate rollback
migrate rollback:
	@:
endif

This ensures:

  • make db migrate — works (migrate is a known no-op)
  • make db typo — errors (typo has no rule)
  • make typo — errors (conditional is inactive)

Never use %: @: — it swallows all unknown targets silently, making typos invisible.

Example Scenario

User: "Add a make target for running database migrations"

  1. Reads existing Makefile — finds mk/ structure
  2. No existing db.mk — creates mk/db.mk and mk/db/Makefile with migrate, rollback, status
  3. Adds include mk/db.mk to main Makefile
  4. Adds Database section to make help output
  5. Verifies: make db migrate works

Common Failures

  • No help text — targets without descriptions are undiscoverable. Always update make help.
  • Logic in Makefiles — complex bash in a target is hard to debug. Delegate to scripts.
  • Catch-all swallows errors%: @: silently succeeds for ANY unknown target, including typos within domains (make skills typo). Never use a blanket catch-all. Instead, declare valid subcommands explicitly inside conditional blocks.
├── references/
│ └── makefile-structure.md
└── SKILL.md
SKILL.md | | Raw

Makefile Conventions

Use Make as the universal command interface. All project operations are accessible via make, regardless of the underlying implementation language.

Workflow

  1. Investigate the project — Before writing targets, check:

    • Does a Makefile already exist? Read it.
    • What scripts, tools, or build steps does the project use?
    • Is there an existing mk/ directory?
  2. Decide placement — If the target belongs to an existing concern group, add it there. If it starts a new concern, create a new modular makefile. See makefile-structure for the full modular pattern.

  3. Write the target — Follow these conventions:

    • Declare every target .PHONY unless it produces a file
    • Add a ## Description comment for the help system
    • Delegate complex logic to scripts
    • Use @ prefix for clean output
    • Quote variable references: "$(VAR)"
  4. Update help — Add the new target to the appropriate concern group in make help output.

  5. Verify — Run make help to confirm the target appears. Run the target to confirm it works.

Complete Example

Root Makefile pattern — includes domain modules and provides grouped help:

.DEFAULT_GOAL := help

include mk/test.mk
include mk/deploy.mk

.PHONY: help
help: ## Show this help
	@echo "Project - Make Commands"
	@echo "======================"
	@echo ""
	@echo "Testing:"
	@echo "  make test       - Show test help"
	@echo "  make test sh    - Run shellcheck"
	@echo ""
	@echo "Development:"
	@echo "  make check      - Check prerequisites"

For the full modular delegation pattern (domain .mk files, subdirectory Makefiles, subcommand delegation), see makefile-structure.

Conventions

Structure

  • One main Makefile at the project root
  • Modular makefiles in mk/ for each concern domain
  • Domains with subcommands get mk/<domain>/Makefile
  • .DEFAULT_GOAL := help so bare make shows help
  • Never use %: @: catch-all patterns. Use conditional explicit subcommand declarations instead (see Subcommand Delegation below).

Naming

  • Targets: lowercase, hyphenated for multi-word
  • Subcommands: space separation (make test sh)
  • Variables: UPPER_CASE

Help system

  • make or make help shows all commands grouped by concern
  • make <domain> shows domain-specific help
  • When a domain has 2 or more subcommands, bare make <domain> must show subcommand help. Do not make it execute a default action.

Delegation

Makefiles are the interface, not the implementation:

deploy:
	@./scripts/deploy.sh "$(ENV)"

Subcommand delegation

When a domain uses $(MAKE) -C for space-separated subcommands (make db migrate), Make sees migrate as a separate goal and errors. Solve this with conditional explicit targets — not a catch-all:

.PHONY: db
db:
	@$(MAKE) -C mk/db $(filter-out $@,$(MAKECMDGOALS))

# Only activate when 'db' is on the command line
ifneq ($(filter db,$(MAKECMDGOALS)),)
.PHONY: migrate rollback
migrate rollback:
	@:
endif

This ensures:

  • make db migrate — works (migrate is a known no-op)
  • make db typo — errors (typo has no rule)
  • make typo — errors (conditional is inactive)

Never use %: @: — it swallows all unknown targets silently, making typos invisible.

Example Scenario

User: "Add a make target for running database migrations"

  1. Reads existing Makefile — finds mk/ structure
  2. No existing db.mk — creates mk/db.mk and mk/db/Makefile with migrate, rollback, status
  3. Adds include mk/db.mk to main Makefile
  4. Adds Database section to make help output
  5. Verifies: make db migrate works

Common Failures

  • No help text — targets without descriptions are undiscoverable. Always update make help.
  • Logic in Makefiles — complex bash in a target is hard to debug. Delegate to scripts.
  • Catch-all swallows errors%: @: silently succeeds for ANY unknown target, including typos within domains (make skills typo). Never use a blanket catch-all. Instead, declare valid subcommands explicitly inside conditional blocks.
references/makefile-structure.md | | Raw

Makefile Structure

Directory layout

project/
├── Makefile              # Entry point, includes mk/*.mk
└── mk/
    ├── test.mk           # Delegates to mk/test/Makefile
    ├── deploy.mk         # Delegates to mk/deploy/Makefile
    ├── test/
    │   └── Makefile      # Subcommands: sh, yaml, md, html
    └── deploy/
        └── Makefile      # Subcommands: staging, production

Main Makefile

The root Makefile includes all domain modules and provides the top-level help:

.DEFAULT_GOAL := help

include mk/test.mk
include mk/deploy.mk

.PHONY: help
help: ## Show this help
	@echo "Project Name - Make Commands"
	@echo "============================"
	@echo ""
	@echo "Testing:"
	@echo "  make test       - Show test help"
	@echo "  make test sh    - Run shellcheck"
	@echo "  make test yaml  - Run yamllint"
	@echo ""
	@echo "Deployment:"
	@echo "  make deploy     - Show deploy help"
	@echo "  make deploy production - Deploy to production"

Domain module (mk/test.mk)

Each .mk file delegates to a subdirectory Makefile:

.PHONY: test

test:
	@$(MAKE) -C mk/test $(filter-out $@,$(MAKECMDGOALS))

# Valid subcommands for 'make test <sub>'
ifneq ($(filter test,$(MAKECMDGOALS)),)
.PHONY: sh yaml md html
sh yaml md html:
	@:
endif

The filter-out pattern passes subcommands through: make test sh calls $(MAKE) -C mk/test sh.

The conditional block declares valid subcommands as explicit no-op targets, active only when test is on the command line. This prevents "No rule to make target" for valid subcommands while still erroring on typos like make test typo.

Domain Makefile (mk/test/Makefile)

Contains the actual targets and domain-specific help:

.DEFAULT_GOAL := help

.PHONY: help sh yaml

help:
	@echo "Testing"
	@echo "======="
	@echo ""
	@echo "Commands:"
	@echo "  make test sh   - Run shellcheck"
	@echo "  make test yaml - Run yamllint"

sh:
	@./scripts/test-shell.sh

yaml:
	@yamllint .

Simple targets

Not every target needs the delegation pattern. Simple, standalone targets go directly in the main Makefile or a flat .mk file:

.PHONY: check
check: ## Check development prerequisites
	@./scripts/check-prereqs.sh

.PHONY: clean
clean: ## Remove build artifacts
	@rm -rf dist/ build/

Use the modular pattern when a domain has multiple subcommands. Use flat targets when there is only one command for the concern.

makefile-conventions structures Makefiles as a universal command interface with modular includes and self-documenting help.

Why Make is the interface and scripts are the implementation

Makefiles become unreadable when they contain logic. Delegating to scripts keeps make targets as a discoverable command menu while scripts handle complexity. Each can be tested independently.

Why the catch-all target pattern is banned

%: @: silently swallows typos. make tset succeeds with no output instead of failing. Explicit targets fail loud on typos, which is correct behavior.

Why bare domain commands show help

When make chat has multiple subcommands, running it bare should show what's available — not silently do nothing or pick a default. Discoverability over convenience.

Why targets are split into modular mk/ files

A single Makefile grows unwieldy past 100 lines. Splitting by domain (chat.mk, dev.mk, skills.mk) lets teams own their section without merge conflicts. The indirection cost is low; the organization value is high.

[1.0.1] - 2026-06-11

Changed

Added

[1.0.0] — 2026-05-24

Added