---
title: "Common Makefile Mistakes"
description: "A comprehensive guide to common mistakes in Makefiles, their consequences, and how to fix them."
type: skill
canonical_url: https://claudary.paisolsolutions.com/skills/common-mistakes-1
source: "Claudary"
difficulty: intermediate
author: "Claude Code Knowledge Pack"
date: 2026-07-10T11:18:31.733Z
license: CC-BY-4.0
attribution: "Common Makefile Mistakes — Claudary (https://claudary.paisolsolutions.com/skills/common-mistakes-1)"
---

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

## Overview

# 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](#critical-missing-declarations)
2. [Syntax Errors](#syntax-errors)
3. [Indentation Issues](#indentation-issues)
4. [Target and Dependency Problems](#target-and-dependency-problems)
5. [Variable Issues](#variable-issues)
6. [Security Vulnerabilities](#security-vulnerabilities)
7. [Performance Problems](#performance-problems)
8. [Portability Issues](#portability-issues)
9. [Build Logic Errors](#build-logic-errors)

## Critical Missing Declarations

### 0. Missing .DELETE_ON_ERROR

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

```makefile
# 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

```makefile
# 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

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

**Solution**: Clear .SUFFIXES for faster builds

```makefile
# 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

```makefile
# 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

```makefile
# 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

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

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

**Solution**: Always include colon

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

### 3. Incorrect Line Continuation

**Problem**: Missing backslash or space after backslash

```makefile
# 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

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

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

### 4. Mismatched Quotes

**Problem**: Unmatched or incorrect quotes

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

# Error: Syntax error or unexpected behavior
```

**Solution**: Match quotes properly

```makefile
# 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

```makefile
# 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

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

**Editor Configuration**:
```vim
" 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

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

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

**Solution**: Always provide defaults

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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 :=

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

```makefile
# 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

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

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

**Solution**: Use which or allow override

```makefile
# 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

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

copy:
	cp -r src/* dest/

# Fails on Windows
```

**Solution**: Detect platform or use portable commands

`

---

Source: [Claudary](https://claudary.paisolsolutions.com/skills/common-mistakes-1) · https://claudary.paisolsolutions.com
