When creating or modifying Makefiles, follow these principles to ensure they are self-documenting and user-friendly.
- Default Target Must Be help
Always set help as the default target so users can discover available commands:
.DEFAULT_GOAL := help
This ensures running make without arguments shows available targets instead of executing an arbitrary first target.
- Self-Documenting Help Target
The help target should automatically build itself from comments in the Makefile. Use the ## comment pattern:
target-name: ## Description of what this target does @command here
help: ## Show this help @awk 'BEGIN {FS = ":.##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
Pattern Explanation
-
target: ## Description
-
The ## marks the description for that target
-
##@ Section
-
Creates optional section headers in the help output
-
The awk command parses these patterns and formats the output
Benefits
-
Documentation stays with the code
-
Adding new targets automatically updates help
-
No manual maintenance of help text
-
Consistent format across all Makefiles
Audit Mode
When asked to audit a Makefile, check for the following issues and report findings:
Required
-
.DEFAULT_GOAL := help is set
-
help target exists
-
help target uses the self-documenting awk pattern
-
All targets have ## Description comments
Warnings
-
Targets without descriptions (missing ## )
-
Missing .PHONY declarations for non-file targets
-
help target is not using $(MAKEFILE_LIST) (won't work with includes)
Report Format
Makefile Audit: <path>
PASS: .DEFAULT_GOAL set to help PASS: help target exists FAIL: 3 targets missing ## descriptions: build, test, deploy WARN: Missing .PHONY for: build, test, clean
Summary: 2 passed, 1 failed, 1 warning
Provide specific line numbers and suggested fixes for each issue found.
- Install/Uninstall Pattern for Scripts and Binaries
For one-off shell scripts or binaries that should be available in ~/bin , use this symlink pattern:
Variables
SCRIPT_NAME := my-script.sh INSTALL_DIR := $(HOME)/bin INSTALL_PATH := $(INSTALL_DIR)/$(SCRIPT_NAME) SOURCE_PATH := $(CURDIR)/$(SCRIPT_NAME)
Install Target
install: ## Install symlink to ~/bin
@mkdir -p $(INSTALL_DIR)
@if [ -L "$(INSTALL_PATH)" ]; then
echo "Removing existing symlink...";
rm -f "$(INSTALL_PATH)";
elif [ -e "$(INSTALL_PATH)" ]; then
echo "Error: $(INSTALL_PATH) exists and is not a symlink";
exit 1;
fi
@ln -s "$(SOURCE_PATH)" "$(INSTALL_PATH)"
@echo "Installed: $(INSTALL_PATH) -> $(SOURCE_PATH)"
Uninstall Target
uninstall: ## Remove symlink from ~/bin
@if [ -L "$(INSTALL_PATH)" ]; then
rm -f "$(INSTALL_PATH)";
echo "Removed: $(INSTALL_PATH)";
elif [ -e "$(INSTALL_PATH)" ]; then
echo "Error: $(INSTALL_PATH) exists but is not a symlink (not removing)";
exit 1;
else
echo "Nothing to remove: $(INSTALL_PATH) does not exist";
fi
Check Target (Optional)
check: ## Check installation status
@echo "Source: $(SOURCE_PATH)"
@if [ -L "$(INSTALL_PATH)" ]; then
echo "Status: Installed (symlink)";
echo "Target: $$(readlink "$(INSTALL_PATH)")";
elif [ -e "$(INSTALL_PATH)" ]; then
echo "Status: Exists but NOT a symlink";
else
echo "Status: Not installed";
fi
Key Principles
-
Always use symlinks - Never copy files; symlinks ensure updates are automatic
-
Safe removal - Only remove if it's a symlink to prevent accidental deletion
-
Idempotent install - Running install multiple times should work
-
Fail on conflicts - If a non-symlink file exists, error rather than overwrite
-
Create ~/bin if needed - mkdir -p ensures the directory exists
For Sourced Scripts
If the script needs to be sourced (not executed), add post-install instructions:
install: ## Install symlink to ~/bin @mkdir -p $(INSTALL_DIR) # ... symlink creation ... @echo "" @echo "Add to your shell config:" @echo " source ~/bin/$(SCRIPT_NAME)"
Complete Example
my-tool Makefile
SCRIPT_NAME := my-tool.sh INSTALL_DIR := $(HOME)/bin INSTALL_PATH := $(INSTALL_DIR)/$(SCRIPT_NAME) SOURCE_PATH := $(CURDIR)/$(SCRIPT_NAME)
.PHONY: help install uninstall check lint test all clean
.DEFAULT_GOAL := help
##@ General
help: ## Show this help message @awk 'BEGIN {FS = ":.##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ Installation
install: ## Install symlink to ~/bin
@mkdir -p $(INSTALL_DIR)
@if [ -L "$(INSTALL_PATH)" ]; then
echo "Removing existing symlink...";
rm -f "$(INSTALL_PATH)";
elif [ -e "$(INSTALL_PATH)" ]; then
echo "Error: $(INSTALL_PATH) exists and is not a symlink";
exit 1;
fi
@ln -s "$(SOURCE_PATH)" "$(INSTALL_PATH)"
@echo "Installed: $(INSTALL_PATH) -> $(SOURCE_PATH)"
uninstall: ## Remove symlink from ~/bin
@if [ -L "$(INSTALL_PATH)" ]; then
rm -f "$(INSTALL_PATH)";
echo "Removed: $(INSTALL_PATH)";
elif [ -e "$(INSTALL_PATH)" ]; then
echo "Error: $(INSTALL_PATH) exists but is not a symlink (not removing)";
exit 1;
else
echo "Nothing to remove: $(INSTALL_PATH) does not exist";
fi
check: ## Check installation status
@echo "Source: $(SOURCE_PATH)"
@if [ -L "$(INSTALL_PATH)" ]; then
echo "Status: Installed (symlink)";
echo "Target: $$(readlink "$(INSTALL_PATH)")";
elif [ -e "$(INSTALL_PATH)" ]; then
echo "Status: Exists but NOT a symlink";
else
echo "Status: Not installed";
fi
##@ Development
lint: ## Run shellcheck on scripts shellcheck $(SCRIPT_NAME)
test: ## Run tests ./test.sh
Stubs to satisfy checkmake minphony rule
all: help clean: uninstall