CtrlK
BlogDocsLog inGet started
Tessl Logo

pantheon-ai/makefile-validator

Comprehensive toolkit for validating, linting, and optimizing Makefiles. Use this skill when working with Makefiles (Makefile, makefile, *.mk files), validating build configurations, checking for best practices, identifying security issues, or debugging Makefile problems.

Overall
score

100%

Does it follow best practices?

Validation for skill structure

Overview
Skills
Evals
Files

common-mistakes.mdreferences/

Common Makefile Mistakes

A comprehensive guide to common mistakes in Makefiles, their consequences, and how to fix them.

Table of Contents

  1. Critical Missing Declarations
  2. Syntax Errors
  3. Indentation Issues
  4. Target and Dependency Problems
  5. Variable Issues
  6. Security Vulnerabilities
  7. Performance Problems
  8. Portability Issues
  9. Build Logic Errors

Critical Missing Declarations

0. Missing .DELETE_ON_ERROR

Problem: Not declaring .DELETE_ON_ERROR (most common critical mistake)

# WRONG: Missing .DELETE_ON_ERROR
.PHONY: all clean

all: app.bin

app.bin: app.c
 $(CC) -o $@ $<

# If compilation fails partway through, a partial/corrupt app.bin may exist
# Next "make" sees the file and thinks target is up-to-date!

Solution: Always add .DELETE_ON_ERROR: at the top

# CORRECT: Always include .DELETE_ON_ERROR
.DELETE_ON_ERROR:

.PHONY: all clean

all: app.bin

app.bin: app.c
 $(CC) -o $@ $<

# Now if build fails, the partial file is deleted
# Next "make" will properly rebuild

Impact:

  • Corrupt/partial files left behind after failed builds
  • Subsequent builds silently use corrupt files
  • Very difficult to debug ("it worked yesterday!")

GNU Make Manual Quote: "This is almost always what you want make to do, but it is not historical practice; so for compatibility, you must explicitly request it."

0b. Not Clearing .SUFFIXES

Problem: Built-in suffix rules slow down large projects

# Slow: Make checks ~90 built-in suffix rules
%.o: %.c
 $(CC) -c $< -o $@

Solution: Clear .SUFFIXES for faster builds

# Fast: Disable built-in suffix rules
.SUFFIXES:

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

Impact: Up to 40% faster rule resolution on large projects

Syntax Errors

1. Spaces Instead of Tabs

Problem: Using spaces for recipe indentation

# WRONG: Spaces (will fail!)
build:
    echo "Building..."  # 4 spaces
    go build -o app     # 4 spaces

# Error: Makefile:2: *** missing separator. Stop.

Solution: Use TAB characters

# CORRECT: Tab characters
build:
 echo "Building..."  # TAB
 go build -o app     # TAB

Impact: Build fails immediately with confusing error message

Detection: mbake automatically detects and fixes this issue

2. Missing Colon After Target

Problem: Forgetting colon in target definition

# WRONG
build $(SOURCES)
 $(CC) -o app $^

# Error: Makefile:1: *** missing separator. Stop.

Solution: Always include colon

# CORRECT
build: $(SOURCES)
 $(CC) -o app $^

3. Incorrect Line Continuation

Problem: Missing backslash or space after backslash

# WRONG: Missing backslash
SOURCES = main.c
          utils.c
          config.c

# WRONG: Space after backslash
SOURCES = main.c \
          utils.c \
          config.c

# Error: Unexpected token or incorrect variable value

Solution: Proper line continuation

# CORRECT
SOURCES = main.c \
          utils.c \
          config.c

# Or use wildcards
SOURCES := $(wildcard src/*.c)

4. Mismatched Quotes

Problem: Unmatched or incorrect quotes

# WRONG
message:
 echo "Building project $(PROJECT)'

# Error: Syntax error or unexpected behavior

Solution: Match quotes properly

# CORRECT
message:
 echo "Building project $(PROJECT)"

# Or use single quotes
message:
 echo 'Building project $(PROJECT)'

Indentation Issues

5. Mixed Tabs and Spaces

Problem: Mixing tabs and spaces in recipes

# WRONG: First line has tab, second has spaces
build:
 @echo "Starting..."
    go build -o app  # Spaces!

# Error: Makefile:3: *** missing separator. Stop.

Solution: Use tabs consistently

# CORRECT: All tabs
build:
 @echo "Starting..."
 go build -o app

Editor Configuration:

" Vim: .vimrc
autocmd FileType make setlocal noexpandtab

# VS Code: settings.json
"[makefile]": {
    "editor.insertSpaces": false,
    "editor.detectIndentation": false
}

6. Tab Width Confusion

Problem: Assuming tab width instead of using actual tabs

# WRONG: Looks like tab but is 8 spaces
build:
        echo "Building..."  # 8 spaces, not a tab!

Solution: Configure editor to show whitespace and use real tabs

# CORRECT: Actual tab character
build:
 echo "Building..."  # TAB (shows as single character)

Target and Dependency Problems

7. Missing .PHONY Declarations

Problem: Not declaring non-file targets as phony

# WRONG: Missing .PHONY
clean:
 rm -rf build

test:
 go test ./...

# If files named 'clean' or 'test' exist, targets won't run!
# $ touch clean  # Create a file named 'clean'
# $ make clean
# make: 'clean' is up to date.

Solution: Always declare non-file targets

# CORRECT: Declare .PHONY targets
.PHONY: clean test all install

clean:
 rm -rf build

test:
 go test ./...

Impact:

  • 35%+ of developers face issues due to missing .PHONY
  • Targets may not run if files with same names exist
  • Performance degradation (implicit rule search)

8. Incorrect Dependency Specification

Problem: Missing or incomplete dependencies

# WRONG: Missing header dependencies
app: main.o utils.o
 $(CC) -o $@ $^

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

# If headers change, .o files won't rebuild!

Solution: Include all dependencies

# CORRECT: Include header dependencies
app: main.o utils.o
 $(CC) -o $@ $^

main.o: main.c main.h common.h
 $(CC) $(CFLAGS) -c main.c

utils.o: utils.c utils.h common.h
 $(CC) $(CFLAGS) -c utils.c

# BETTER: Auto-generate dependencies
DEPFLAGS = -MT $@ -MMD -MP -MF $(DEPDIR)/$*.d
%.o: %.c
 $(CC) $(DEPFLAGS) $(CFLAGS) -c $<

-include $(DEPS)

Impact: Over 60% reduction in unnecessary recompilation with proper dependencies

9. Circular Dependencies

Problem: Targets depending on each other

# WRONG: Circular dependency
A: B
 @echo "Target A"

B: A
 @echo "Target B"

# Error: Makefile:1: *** Circular A <- B dependency dropped.

Solution: Break the cycle

# CORRECT: Proper dependency chain
A: B
 @echo "Target A depends on B"

B: C
 @echo "Target B depends on C"

C:
 @echo "Target C has no dependencies"

10. Phony Target as Prerequisite of Real Target

Problem: Using phony target as dependency of file target

# WRONG: Phony prerequisite causes always-rebuild
.PHONY: generate

app.o: app.c generate
 $(CC) -c app.c -o app.o

generate:
 ./gen-config.sh

# app.o rebuilds EVERY time because 'generate' is always out of date

Solution: Use real file dependencies

# CORRECT: Depend on actual generated file
app.o: app.c config.h
 $(CC) -c app.c -o app.o

config.h:
 ./gen-config.sh

Variable Issues

11. Using = Instead of :=

Problem: Recursive expansion causing performance issues

# WRONG: Recursive expansion (re-evaluated every time)
BUILD_TIME = $(shell date +%Y%m%d-%H%M%S)
GIT_HASH = $(shell git rev-parse HEAD)

target1:
 echo $(BUILD_TIME)  # Shell called here

target2:
 echo $(BUILD_TIME)  # Shell called AGAIN with different time!
 echo $(GIT_HASH)    # Shell called here

target3:
 echo $(GIT_HASH)    # Shell called AGAIN!

Solution: Use := for immediate expansion

# CORRECT: Immediate expansion (evaluated once)
BUILD_TIME := $(shell date +%Y%m%d-%H%M%S)
GIT_HASH := $(shell git rev-parse HEAD)

target1:
 echo $(BUILD_TIME)  # Uses cached value

target2:
 echo $(BUILD_TIME)  # Same cached value
 echo $(GIT_HASH)    # Cached value

target3:
 echo $(GIT_HASH)    # Same cached value

Impact: Can cause significant slowdown and inconsistent builds

12. Undefined Variables

Problem: Using variables without defaults

# WRONG: No default value
install:
 cp app $(PREFIX)/bin/

# If PREFIX is not set, installs to /bin/ (wrong!) or fails

Solution: Always provide defaults

# CORRECT: Provide sensible defaults
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin

install:
 mkdir -p $(DESTDIR)$(BINDIR)
 cp app $(DESTDIR)$(BINDIR)/

13. Incorrect Variable Expansion

Problem: Using wrong expansion syntax

# WRONG: Shell variable vs Make variable confusion
build:
 for file in *.c; do \
  echo "Compiling $file"; \
  $(CC) -c $file; \
 done

# $file expands as Make variable (empty!), not shell variable
# Output: Compiling (nothing)

Solution: Escape shell variables

# CORRECT: Escape $ for shell variables
build:
 for file in *.c; do \
  echo "Compiling $$file"; \
  $(CC) -c $$file; \
 done

# Output: Compiling main.c, Compiling utils.c, etc.

14. Variable Naming Conflicts

Problem: Overriding special Make variables

# WRONG: Overriding built-in variable
MAKEFLAGS = -j4  # This overrides Make's internal flags!

# AVOID: Using reserved names
MAKE = my-build-tool  # Breaks recursive make

Solution: Use unique names

# CORRECT: Use custom names for your variables
BUILD_FLAGS := -j4
MY_BUILD_TOOL := custom-builder

build:
 $(MAKE) -f sub.mk $(BUILD_FLAGS)

Security Vulnerabilities

15. Hardcoded Credentials

Problem: Secrets in Makefile

# WRONG: Hardcoded secrets
API_KEY = sk-1234567890abcdef
DB_PASSWORD = super_secret_123

deploy:
 curl -H "Authorization: Bearer $(API_KEY)" https://api.example.com/
 psql -U admin -p $(DB_PASSWORD) -c "SELECT version();"

Solution: Use environment variables

# CORRECT: Load from environment
deploy:
 @if [ -z "$$API_KEY" ]; then \
  echo "Error: API_KEY not set"; \
  exit 1; \
 fi
 curl -H "Authorization: Bearer $$API_KEY" https://api.example.com/

# Or use a .env file (not committed)
include .env
export

Impact: Credentials exposed in version control, logs, and process listings

16. Unsafe Variable Expansion

Problem: Unvalidated variables in dangerous commands

# WRONG: Unsafe rm command
BUILD_DIR = $(USER_INPUT)

clean:
 rm -rf $(BUILD_DIR)/*

# If BUILD_DIR is empty or "/", this is catastrophic!
# $ make clean BUILD_DIR=/
# rm -rf /*  # Disaster!

Solution: Validate before dangerous operations

# CORRECT: Validate variables
BUILD_DIR := build  # Default value

clean:
 @if [ -z "$(BUILD_DIR)" ] || [ "$(BUILD_DIR)" = "/" ]; then \
  echo "Error: Invalid BUILD_DIR=$(BUILD_DIR)"; \
  exit 1; \
 fi
 @if [ -d "$(BUILD_DIR)" ]; then \
  rm -rf $(BUILD_DIR)/*; \
 fi

17. Command Injection

Problem: Unsanitized input in shell commands

# WRONG: User input directly in command
deploy:
 ssh user@$(SERVER) "cd /app && git pull origin $(BRANCH)"

# Malicious input: BRANCH="; rm -rf /"
# Executes: git pull origin ; rm -rf /

Solution: Validate and quote input

# CORRECT: Validate input
ALLOWED_BRANCHES := main develop staging
BRANCH ?= main

deploy:
 @if ! echo "$(ALLOWED_BRANCHES)" | grep -wq "$(BRANCH)"; then \
  echo "Error: Invalid branch $(BRANCH)"; \
  exit 1; \
 fi
 ssh user@$(SERVER) "cd /app && git pull origin '$(BRANCH)'"

18. Logging Sensitive Information

Problem: Echoing secrets in build output

# WRONG: Secrets visible in logs
deploy:
 echo "Deploying with token: $(API_TOKEN)"
 curl -H "Authorization: Bearer $(API_TOKEN)" https://api.example.com/

Solution: Suppress sensitive output

# CORRECT: Hide sensitive information
deploy:
 @echo "Deploying to production..."
 @curl -s -H "Authorization: Bearer $$API_TOKEN" https://api.example.com/
 @echo "Deployment complete"

# Or mask partial value
 @echo "Using token: $${API_TOKEN:0:8}..."

Performance Problems

19. Inefficient Wildcards

Problem: Repeated wildcard evaluation

# WRONG: wildcard called every time
build:
 $(CC) -o app $(wildcard src/*.c)

test:
 for file in $(wildcard tests/*.sh); do bash $$file; done

# wildcard searches filesystem every time these targets run

Solution: Evaluate once with :=

# CORRECT: Evaluate wildcard once
SOURCES := $(wildcard src/*.c)
TESTS := $(wildcard tests/*.sh)

build:
 $(CC) -o app $(SOURCES)

test:
 for file in $(TESTS); do bash $$file; done

Impact: Significant speedup for large projects (40%+ in some cases)

20. Missing Incremental Build Support

Problem: Always rebuilding everything

# WRONG: No incremental build
build:
 rm -rf build
 mkdir -p build
 $(CC) -o build/app $(SOURCES)

# Rebuilds from scratch every time!

Solution: Proper dependency tracking

# CORRECT: Incremental build
OBJECTS := $(patsubst src/%.c,build/%.o,$(SOURCES))

build: build/app

build/app: $(OBJECTS)
 $(CC) -o $@ $^

build/%.o: src/%.c
 @mkdir -p $(dir $@)
 $(CC) $(CFLAGS) -c $< -o $@

# Only rebuilds changed files

Impact: Can reduce build times by up to 60% with proper dependencies

21. Not Using Pattern Rules

Problem: Duplicated rules for similar targets

# WRONG: Repetitive rules
main.o: main.c
 $(CC) $(CFLAGS) -c main.c -o main.o

utils.o: utils.c
 $(CC) $(CFLAGS) -c utils.c -o utils.o

config.o: config.c
 $(CC) $(CFLAGS) -c config.c -o config.o

# Lots of duplication!

Solution: Use pattern rules

# CORRECT: Single pattern rule
%.o: %.c
 $(CC) $(CFLAGS) -c $< -o $@

# Or with directories
build/%.o: src/%.c
 @mkdir -p $(dir $@)
 $(CC) $(CFLAGS) -c $< -o $@

Portability Issues

22. Assuming GNU Make

Problem: Using GNU Make-specific features

# WRONG: GNU Make specific
SOURCES := $(shell find src -name '*.c')

build: $(SOURCES:.c=.o)
 $(CC) -o app $^

# Fails with BSD make or other Make implementations

Solution: Use portable constructs

# CORRECT: More portable (though still uses shell)
SOURCES != find src -name '*.c' || find src -name '*.c'

# Or manually list sources for maximum portability
SOURCES = src/main.c src/utils.c src/config.c

23. Hard-Coded Tools

Problem: Assuming specific tool paths

# WRONG: Hard-coded tool paths
CC = /usr/bin/gcc
PYTHON = /usr/bin/python3

build:
 $(CC) -o app $(SOURCES)

Solution: Use which or allow override

# CORRECT: Allow override with defaults
CC ?= gcc
PYTHON ?= python3
INSTALL ?= install

# Or detect at runtime
CC := $(shell command -v gcc || command -v clang)

24. Platform-Specific Commands

Problem: Using OS-specific commands

# WRONG: Linux-specific
clean:
 rm -rf build

copy:
 cp -r src/* dest/

# Fails on Windows

Solution: Detect platform or use portable commands

# CORRECT: Platform detection
UNAME_S := $(shell uname -s 2>/dev/null || echo Windows)

ifeq ($(UNAME_S),Windows)
 RM := del /Q /S
 MKDIR := mkdir
else
 RM := rm -rf
 MKDIR := mkdir -p
endif

clean:
 $(RM) build

# Or use Go/Python for cross-platform scripts
clean:
 @go run scripts/clean.go

Build Logic Errors

25. Silent Failures

Problem: Not checking command exit codes

# WRONG: Ignoring failures
test:
 go test ./pkg1
 go test ./pkg2
 go test ./pkg3
 @echo "All tests passed!"

# If pkg1 fails, Make continues to pkg2, pkg3, and prints "passed"

Solution: Use set -e or check exit codes

# CORRECT: Stop on first failure
test:
 @set -e; \
 go test ./pkg1; \
 go test ./pkg2; \
 go test ./pkg3; \
 echo "All tests passed!"

# Or check explicitly
test:
 @go test ./pkg1 || exit 1
 @go test ./pkg2 || exit 1
 @go test ./pkg3 || exit 1
 @echo "All tests passed!"

26. Race Conditions in Parallel Builds

Problem: Unsafe parallel execution

# WRONG: Race condition with parallel builds
all: build-frontend build-backend

build-frontend:
 npm install  # Both may write to node_modules!
 npm run build

build-backend:
 npm install  # Race condition!
 go build

# With make -j2, both run npm install simultaneously

Solution: Use order dependencies or .NOTPARALLEL

# CORRECT: Sequential dependencies
all: build-frontend build-backend

build-frontend: node_modules
 npm run build

build-backend: node_modules
 go build

node_modules: package.json
 npm install
 @touch node_modules  # Update timestamp

# Or use .NOTPARALLEL for specific target
.NOTPARALLEL: install

27. Assuming Build Order

Problem: Relying on target order without dependencies

# WRONG: Assuming build is run before test
all: build test deploy

build:
 go build -o app

test:
 ./scripts/test.sh  # Assumes app exists!

deploy:
 ./scripts/deploy.sh  # Assumes tests passed!

# Direct "make test" or "make deploy" fails!

Solution: Explicit dependencies

# CORRECT: Explicit dependencies
all: deploy

build:
 go build -o app

test: build
 ./scripts/test.sh

deploy: test
 ./scripts/deploy.sh

# Now "make deploy" automatically runs build → test → deploy

Quick Fix Checklist

When you encounter Makefile issues, check:

  • Is .DELETE_ON_ERROR: declared at top? (Critical!)
  • Are you using TAB characters (not spaces) for recipes?
  • Are all non-file targets declared as .PHONY?
  • Is .SUFFIXES: declared to disable built-in rules?
  • Are dependencies complete and correct?
  • Are variables using := instead of = for expensive operations?
  • Are shell variables escaped with $$?
  • Are dangerous operations (rm, sudo) validated?
  • Are secrets loaded from environment, not hardcoded?
  • Are wildcard results cached with :=?
  • Is error handling present in critical recipes?
  • Are tools and paths configurable (CC ?= gcc)?
  • Is parallel build safety considered?
  • Are pattern rules used instead of duplicate rules?

Impact Statistics

According to research from build system studies (2024-2025):

  • 35% of developers face issues with outdated targets due to improper dependencies
  • 40% experience inaccurate profiling due to incorrect compiler flag usage
  • 60% reduction in unnecessary recompilation possible with proper dependency tracking
  • 40% faster incremental builds achievable with optimized Makefile patterns

Additional Resources

Sources

Install with Tessl CLI

npx tessl i pantheon-ai/makefile-validator@0.1.0

references

bake-tool.md

best-practices.md

common-mistakes.md

SKILL.md

tile.json