Awesome-omni-skill makefile
Best practices for authoring GNU Make Makefiles Triggers on: **/Makefile, **/makefile, **/*.mk, **/GNUmakefile
install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/cli-automation/makefile" ~/.claude/skills/diegosouzapw-awesome-omni-skill-makefile && rm -rf "$T"
manifest:
skills/cli-automation/makefile/SKILL.mdsource content
Makefile Development Instructions
Instructions for writing clean, maintainable, and portable GNU Make Makefiles. These instructions are based on the GNU Make manual.
General Principles
- Write clear and maintainable makefiles that follow GNU Make conventions
- Use descriptive target names that clearly indicate their purpose
- Keep the default goal (first target) as the most common build operation
- Prioritize readability over brevity when writing rules and recipes
- Add comments to explain complex rules, variables, or non-obvious behavior
Naming Conventions
- Name your makefile
(recommended for visibility) orMakefilemakefile - Use
only for GNU Make-specific features incompatible with other make implementationsGNUmakefile - Use standard variable names:
,objects
,OBJECTS
,objs
,OBJS
, orobj
for object file listsOBJ - Use uppercase for built-in variable names (e.g.,
,CC
,CFLAGS
)LDFLAGS - Use descriptive target names that reflect their action (e.g.,
,clean
,install
)test
File Structure
- Place the default goal (primary build target) as the first rule in the makefile
- Group related targets together logically
- Define variables at the top of the makefile before rules
- Use
to declare targets that don't represent files.PHONY - Structure makefiles with: variables, then rules, then phony targets
# Variables CC = gcc CFLAGS = -Wall -g objects = main.o utils.o # Default goal all: program # Rules program: $(objects) $(CC) -o program $(objects) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ # Phony targets .PHONY: clean all clean: rm -f program $(objects)
Variables and Substitution
- Use variables to avoid duplication and improve maintainability
- Define variables with
(simple expansion) for immediate evaluation,:=
for recursive expansion= - Use
to set default values that can be overridden?= - Use
to append to existing variables+= - Reference variables with
not$(VARIABLE)
(unless single character)$VARIABLE - Use automatic variables (
,$@
,$<
,$^
,$?
) in recipes to make rules more generic$*
# Simple expansion (evaluates immediately) CC := gcc # Recursive expansion (evaluates when used) CFLAGS = -Wall $(EXTRA_FLAGS) # Conditional assignment PREFIX ?= /usr/local # Append to variable CFLAGS += -g
Rules and Prerequisites
- Separate targets, prerequisites, and recipes clearly
- Use implicit rules for standard compilations (e.g.,
to.c
).o - List prerequisites in logical order (normal prerequisites before order-only)
- Use order-only prerequisites (after
) for directories and dependencies that shouldn't trigger rebuilds| - Include all actual dependencies to ensure correct rebuilds
- Avoid circular dependencies between targets
- Remember that order-only prerequisites are omitted from automatic variables like
, so reference them explicitly if needed$^
The example below shows a pattern rule that compiles objects into an
obj/ directory. The directory itself is listed as an order-only prerequisite so it is created before compiling but does not force recompilation when its timestamp changes.
# Normal prerequisites program: main.o utils.o $(CC) -o $@ $^ # Order-only prerequisites (directory creation) obj/%.o: %.c | obj $(CC) $(CFLAGS) -c $< -o $@ obj: mkdir -p obj
Recipes and Commands
- Start every recipe line with a tab character (not spaces) unless
is changed.RECIPEPREFIX - Use
prefix to suppress command echoing when appropriate@ - Use
prefix to ignore errors for specific commands (use sparingly)- - Combine related commands with
or&&
on the same line when they must execute together; - Keep recipes readable; break long commands across multiple lines with backslash continuation
- Use shell conditionals and loops within recipes when needed
# Silent command clean: @echo "Cleaning up..." @rm -f $(objects) # Ignore errors .PHONY: clean-all clean-all: -rm -rf build/ -rm -rf dist/ # Multi-line recipe with proper continuation install: program install -d $(PREFIX)/bin && \ install -m 755 program $(PREFIX)/bin
Phony Targets
- Always declare phony targets with
to avoid conflicts with files of the same name.PHONY - Use phony targets for actions like
,clean
,install
,testall - Place phony target declarations near their rule definitions or at the end of the makefile
.PHONY: all clean test install all: program clean: rm -f program $(objects) test: program ./run-tests.sh install: program install -m 755 program $(PREFIX)/bin
Pattern Rules and Implicit Rules
- Use pattern rules (
) for generic transformations%.o: %.c - Leverage built-in implicit rules when appropriate (GNU Make knows how to compile
to.c
).o - Override implicit rule variables (like
,CC
) rather than rewriting the rulesCFLAGS - Define custom pattern rules only when built-in rules are insufficient
# Use built-in implicit rules by setting variables CC = gcc CFLAGS = -Wall -O2 # Custom pattern rule for special cases %.pdf: %.md pandoc $< -o $@
Splitting Long Lines
- Use backslash-newline (
) to split long lines for readability\ - Be aware that backslash-newline is converted to a single space in non-recipe contexts
- In recipes, backslash-newline preserves the line continuation for the shell
- Avoid trailing whitespace after backslashes
Splitting Without Adding Whitespace
If you need to split a line without adding whitespace, you can use a special technique: insert
$ (dollar-space) followed by a backslash-newline. The $ refers to a variable with a single-space name, which doesn't exist and expands to nothing, effectively joining the lines without inserting a space.
# Concatenate strings without adding whitespace # The following creates the value "oneword" var := one$ \ word # This is equivalent to: # var := oneword
# Variable definition split across lines sources = main.c \ utils.c \ parser.c \ handler.c # Recipe with long command build: $(objects) $(CC) -o program $(objects) \ $(LDFLAGS) \ -lm -lpthread
Including Other Makefiles
- Use
directive to share common definitions across makefilesinclude - Use
(or-include
) to include optional makefiles without errorssinclude - Place
directives after variable definitions that may affect included filesinclude - Use
for shared variables, pattern rules, or common targetsinclude
# Include common settings include config.mk # Include optional local configuration -include local.mk
Conditional Directives
- Use conditional directives (
,ifeq
,ifneq
,ifdef
) for platform or configuration-specific rulesifndef - Place conditionals at the makefile level, not within recipes (use shell conditionals in recipes)
- Keep conditionals simple and well-documented
# Platform-specific settings ifeq ($(OS),Windows_NT) EXE_EXT = .exe else EXE_EXT = endif program: main.o $(CC) -o program$(EXE_EXT) main.o
Automatic Prerequisites
- Generate header dependencies automatically rather than maintaining them manually
- Use compiler flags like
and-MMD
to generate-MP
files with dependencies.d - Include generated dependency files with
to avoid errors if they don't exist-include $(deps)
objects = main.o utils.o deps = $(objects:.o=.d) # Include dependency files -include $(deps) # Compile with automatic dependency generation %.o: %.c $(CC) $(CFLAGS) -MMD -MP -c $< -o $@
Error Handling and Debugging
- Use
or$(error text)
functions for build-time diagnostics$(warning text) - Test makefiles with
(dry run) to see commands without executingmake -n - Use
to print the database of rules and variables for debuggingmake -p - Validate required variables and tools at the beginning of the makefile
# Check for required tools ifeq ($(shell which gcc),) $(error "gcc is not installed or not in PATH") endif # Validate required variables ifndef VERSION $(error VERSION is not defined) endif
Clean Targets
- Always provide a
target to remove generated filesclean - Declare
as phony to avoid conflicts with a file named "clean"clean - Use
prefix with-
commands to ignore errors if files don't existrm - Consider separate
(removes objects) andclean
(removes all generated files) targetsdistclean
.PHONY: clean distclean clean: -rm -f $(objects) -rm -f $(deps) distclean: clean -rm -f program config.mk
Portability Considerations
- Avoid GNU Make-specific features if portability to other make implementations is required
- Use standard shell commands (prefer POSIX shell constructs)
- Test with
to force rebuild all targetsmake -B - Document any platform-specific requirements or GNU Make extensions used
Performance Optimization
- Use
for variables that don't need recursive expansion (faster):= - Avoid unnecessary use of
which creates subprocesses$(shell ...) - Order prerequisites efficiently (most frequently changing files last)
- Use parallel builds (
) safely by ensuring targets don't conflictmake -j
Documentation and Comments
- Add a header comment explaining the makefile's purpose
- Document non-obvious variable settings and their effects
- Include usage examples or targets in comments
- Add inline comments for complex rules or platform-specific workarounds
# Makefile for building the example application # # Usage: # make - Build the program # make clean - Remove generated files # make install - Install to $(PREFIX) # # Variables: # CC - C compiler (default: gcc) # PREFIX - Installation prefix (default: /usr/local) # Compiler and flags CC ?= gcc CFLAGS = -Wall -Wextra -O2 # Installation directory PREFIX ?= /usr/local
Special Targets
- Use
for non-file targets.PHONY - Use
to preserve intermediate files.PRECIOUS - Use
to mark files as intermediate (automatically deleted).INTERMEDIATE - Use
to prevent deletion of intermediate files.SECONDARY - Use
to remove targets if recipe fails.DELETE_ON_ERROR - Use
to suppress echoing for all recipes (use sparingly).SILENT
# Don't delete intermediate files .SECONDARY: # Delete targets if recipe fails .DELETE_ON_ERROR: # Preserve specific files .PRECIOUS: %.o
Common Patterns
Standard Project Structure
CC = gcc CFLAGS = -Wall -O2 objects = main.o utils.o parser.o .PHONY: all clean install all: program program: $(objects) $(CC) -o $@ $^ %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: -rm -f program $(objects) install: program install -d $(PREFIX)/bin install -m 755 program $(PREFIX)/bin
Managing Multiple Programs
programs = prog1 prog2 prog3 .PHONY: all clean all: $(programs) prog1: prog1.o common.o $(CC) -o $@ $^ prog2: prog2.o common.o $(CC) -o $@ $^ prog3: prog3.o $(CC) -o $@ $^ clean: -rm -f $(programs) *.o
Anti-Patterns to Avoid
- Don't start recipe lines with spaces instead of tabs
- Avoid hardcoding file lists when they can be generated with wildcards or functions
- Don't use
to get file lists (use$(shell ls ...)
instead)$(wildcard ...) - Avoid complex shell scripts in recipes (move to separate script files)
- Don't forget to declare phony targets as
.PHONY - Avoid circular dependencies between targets
- Don't use recursive make (
) unless absolutely necessary$(MAKE) -C subdir