Makefile Validator
Comprehensive toolkit for validating, linting, and optimizing Makefiles. Use when working with Makefiles (Makefile, makefile, *.mk files), validating build configurations, checking for best practices, identifying security issues, or debugging Makefile problems. Concrete capabilities include detecting missing .PHONY declarations, validating tab indentation in recipes, checking variable expansion safety, identifying hardcoded credentials, and flagging missing prerequisites or syntax errors.
npx skills add github:pantheon-org/tekhne --skill development--scripting--makefile--validatorMindset
Treat a Makefile as a dependency graph that happens to be readable, not a list of shell commands with headings. Always check what a target actually depends on before trusting its recipe, because a missing prerequisite makes Make skip work it should have done, and a stale .PHONY list makes Make skip a target entirely the moment a same-named file appears on disk. Never accept “the recipe looks right” as validation; run make -n to confirm the dependency graph itself resolves the way the author intended, since a Makefile can have flawless shell inside every recipe and still be broken by an incorrect dependency edge. Verify variable expansion the same way you would in a shell script: unquoted $(VAR) inside rm, sudo, or curl is exactly as dangerous as unquoted $VAR in bash, because Make substitutes the text and then hands it to the shell verbatim. Ensure every finding is reported as a concrete line, the risk it creates, and the fix — a validator that only says “found an issue” without location and remedy is not more useful than reading the Makefile by hand.
When to Use
Use this skill when validating an existing Makefile for correctness, running it through syntax and best-practice checks before a commit, auditing a Makefile for security issues (unquoted variable expansion, hardcoded credentials, .EXPORT_ALL_VARIABLES leakage), diagnosing why a target is skipped or rebuilt unexpectedly, or converting a legacy Makefile to modern conventions (.PHONY, $(MAKE), formatting).
When NOT to Use
Do not use this skill to generate a new Makefile from scratch — that is the makefile-generator skill’s job; this one only validates and explains what is already there. Do not use it to debug a build failure that is purely a compiler or toolchain error unrelated to the Makefile’s own structure (a missing header, a linker error) — those need the language’s own build diagnostics, not Makefile validation.
Validation Capabilities
- Syntax: GNU make
-n --dry-runvalidation; catches errors with line numbers - Formatting: mbake tab indentation, variable assignment consistency, trailing whitespace, line continuations
- Best practices:
.DELETE_ON_ERROR,.PHONYdeclarations,$(MAKE)for recursive calls,.ONESHELLerror handling - Security: hardcoded credentials, unsafe variable expansion in
rm/sudo/curl/wget, command injection,.EXPORT_ALL_VARIABLESleakage - Optimization:
.NOTPARALLEL,.INTERMEDIATE/.SECONDARY, VPATH usage, incremental build efficiency
See references/best-practices.md, references/common-mistakes.md, and references/bake-tool.md for detailed explanations.
Quick Start
Basic Validation
# Validate a Makefile
bash scripts/validate_makefile.sh Makefile
Example Output
========================================
MAKEFILE VALIDATOR
========================================
File: Makefile
[SYNTAX CHECK (GNU make)]
✓ No syntax errors found
[MBAKE VALIDATION]
✓ mbake validation passed
[MBAKE FORMAT CHECK]
⚠ Formatting issues found
Run 'mbake format Makefile' to fix formatting issues
Or run 'mbake format --diff Makefile' to preview changes
[CUSTOM CHECKS]
⚠ No .PHONY declarations found
✗ Potential spaces instead of tabs in recipes detected
ℹ No VPATH/vpath declarations found
========================================
VALIDATION SUMMARY
========================================
Errors: 1
Warnings: 2
Info: 1
⚠ Validation PASSED with warnings
Exit Codes
- 0: No issues found
- 1: Warnings found (passed with warnings)
- 2: Errors found (failed validation)
Common Validation Scenarios
Scenario 1: Pre-commit Validation
bash scripts/validate_makefile.sh Makefile
Scenario 2: Formatting Consistency
# Preview changes
mbake format --diff Makefile
# Apply formatting
mbake format Makefile
# Re-validate
bash scripts/validate_makefile.sh Makefile
Scenario 3: Converting Legacy Makefiles
# 1. Validate current state
bash scripts/validate_makefile.sh legacy.mk
# 2. Fix critical errors (tabs, syntax), then apply formatting
mbake format legacy.mk
# 3. Add .PHONY declarations
mbake format --auto-insert-phony-declarations legacy.mk
# 4. Re-validate
bash scripts/validate_makefile.sh legacy.mk
# 5. Reference best-practices.md for modernization guidance
Scenario 4: Security Audit
The validator automatically checks for hardcoded credentials, unsafe variable expansion in dangerous commands, and command injection vulnerabilities. Reference references/common-mistakes.md for detailed explanations and fixes.
Integration with Development Workflow
For pre-commit hooks, CI/CD pipelines (e.g. GitHub Actions), and self-validating Makefile targets, use the validation script at scripts/validate_makefile.sh and match on files named Makefile, makefile, or *.mk. The script’s exit codes (0/1/2) map cleanly to pass/warn/fail states for any automation context. See references/bake-tool.md for CI/CD configuration details.
Installation Requirements
Required
- python3, pip3: For venv and mbake installation
- bash: For running validation script
- GNU make: For syntax validation (
make -n)
Optional (Recommended)
- checkmake: Additional linting (
minphony,phonydeclaredrules)go install github.com/checkmake/checkmake/cmd/checkmake@latest - unmake: POSIX portability checks — see github.com/mcandre/unmake
Automatic Installation
mbake is automatically installed in an isolated venv per invocation and cleaned up on exit — no manual installation required.
Directory Structure
makefile-validator/
├── skill.md # This file
├── scripts/
│ └── validate_makefile.sh # Main validation script
├── references/
│ ├── best-practices.md # Makefile best practices
│ ├── common-mistakes.md # Common Makefile mistakes
│ └── bake-tool.md # mbake tool reference (config, CI/CD, advanced features)
└── assets/
├── good-makefile.mk # Well-written example
└── bad-makefile.mk # Anti-patterns example
Anti-Patterns
NEVER use spaces instead of tabs for recipe indentation
- WHY: GNU Make requires a hard tab character to start recipe lines. Spaces silently produce
missing separatorerrors that are notoriously confusing. - BAD: two spaces then
echo "building"(spaces instead of tab) - GOOD:
\techo "building"(hard tab —\t) - DETECTION:
scripts/validate_makefile.shcatches this; runmake -nto surface the error.
NEVER omit .PHONY for non-file targets
- WHY: Without
.PHONY, if a file namedcleanortestexists, Make will silently skip the target because it considers it up-to-date. - BAD:
clean:with no.PHONYdeclaration - GOOD:
.PHONY: clean test build alldeclared at the top
NEVER use bare $(shell ...) calls in recipe variables without quoting
- WHY: Unquoted shell variable expansion in
rm,sudo, orcurlcommands enables command injection when variable values contain spaces or special characters. - BAD:
rm -rf $(DIR)whenDIRcan be user-controlled - GOOD:
rm -rf "$(DIR)"or validate thatDIRdoes not contain path separators
NEVER use recursive make with a bare make command
- WHY: Bare
makein a recipe does not inherit the jobserver flags passed by the parent make, breaking parallel builds and potentially starting a separate build chain with different settings. - BAD:
make -C subdir - GOOD:
$(MAKE) -C subdir
NEVER export all variables globally with .EXPORT_ALL_VARIABLES
- WHY: Every variable in scope (including secrets loaded from
.envfiles) gets exported to every sub-process, creating credential leakage risk. - BAD:
.EXPORT_ALL_VARIABLES:at top of Makefile - GOOD: Use explicit
export VARonly for variables that need sub-process visibility
Known Limitations
mbake doesn’t recognize some valid GNU Make special targets (.DELETE_ON_ERROR, .SUFFIXES, .ONESHELL, .POSIX) — the validator filters these false positives and surfaces them as informational messages. The mbake format --check vs mbake format output may also differ; this is a known upstream issue. See references/bake-tool.md for full details including mbake configuration (~/.bake.toml) and format-disable comments.
References
Internal:
- Makefile Best Practices — idiomatic target patterns, variable scoping, and build organization guidelines
- Common Makefile Mistakes — detailed explanations of errors caught by the validator with fixes
- mbake Tool Reference — mbake configuration, CI/CD integration, and format-disable comments
External:
- GNU Make Manual — canonical reference for GNU make syntax, special targets, and built-in variables
- mbake GitHub — source and issue tracker for the mbake formatter
- checkmake GitHub — optional linter for
.PHONYand rule declaration checks
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
No eval scenarios available.
mbake Tool Reference
Comprehensive guide to using mbake (Makefile formatter and linter) for Makefile validation and formatting.
Overview
mbake is a modern Python-based tool designed to format and validate Makefiles with intelligent features. It’s the first comprehensive Makefile formatter and linter, filling a 50-year gap in build tooling.
Current Version: See PyPI for latest
Known Limitations
While mbake is excellent for GNU Make formatting, be aware of these limitations:
- POSIX Make: mbake is designed for GNU Make; it may not recognize all POSIX make syntax
- .SUFFIXES: mbake doesn’t understand
.SUFFIXESspecial target - Format vs Check: Some users report
mbake format --checkwarns about different things thanmbake formatfixes
For additional linting coverage, consider using checkmake alongside mbake.
Table of Contents
- Installation
- Quick Start
- Commands
- Configuration
- Features
- CI/CD Integration
- Editor Integration
- Advanced Usage
- Troubleshooting
Installation
PyPI Installation (Recommended)
# Install mbake
pip install mbake
# Upgrade to latest version
pip install --upgrade mbake
# Verify installation
mbake --version
System Requirements
- Python: 3.9 or higher
- GNU Make: Required for validation (syntax checking)
- pip: For package management
Virtual Environment (Isolated Installation)
# Create venv
python3 -m venv mbake-env
# Activate venv
source mbake-env/bin/activate # Linux/macOS
# or
mbake-env\Scripts\activate # Windows
# Install mbake
pip install mbake
# Use mbake
mbake format Makefile
# Deactivate when done
deactivate
Note: The makefile-validator skill automatically handles venv creation and cleanup.
VS Code Extension
Install the “mbake Makefile Formatter” extension from the VS Code marketplace:
- Open VS Code
- Go to Extensions (Ctrl+Shift+X)
- Search for “mbake Makefile Formatter”
- Click Install
Quick Start
Basic Workflow
# 1. Check current formatting status
mbake format --check Makefile
# 2. Preview changes before applying
mbake format --diff Makefile
# 3. Apply formatting
mbake format Makefile
# 4. Validate syntax
mbake validate Makefile
First-Time Usage
# Initialize configuration file
mbake init
# This creates ~/.bake.toml with default settings
# Edit the file to customize mbake behavior
# View current configuration
mbake config
# Format with current settings
mbake format Makefile
Commands
mbake format
Format and standardize Makefile structure.
# Basic formatting
mbake format Makefile
# Check formatting without modifying (CI/CD)
mbake format --check Makefile
# Exit code: 0 (properly formatted), 1 (needs formatting)
# Show diff of changes
mbake format --diff Makefile
# Backup before formatting
mbake format --backup Makefile
# Creates Makefile.bak
# Validate after formatting
mbake format --validate Makefile
# Specify custom config file
mbake format --config /path/to/.bake.toml Makefile
# Format multiple files
mbake format Makefile tests/*.mk build/*.mk
Options:
--check: Check formatting without modifying (exit 0 if formatted, 1 if not)--diff: Display potential changes without applying--backup: Create .bak backup before modifying--validate: Run syntax validation after formatting--config PATH: Use custom configuration file
mbake validate
Validate Makefile syntax using GNU Make.
# Validate syntax
mbake validate Makefile
# Validates with: make -f Makefile --dry-run
# Exit code: 0 (valid), 1 (invalid)
# Validate multiple files
mbake validate Makefile src/*.mk
What it checks:
- Syntax errors (missing colons, invalid characters)
- Target definition correctness
- Variable expansion syntax
- Recipe format
- Dependency chain validity
mbake init
Create initial configuration file.
# Create ~/.bake.toml with defaults
mbake init
# The configuration file includes all formatting options
# Edit it to customize mbake behavior
mbake config
Display current configuration settings.
# Show active configuration
mbake config
# Output includes:
# - Configuration file location
# - All active settings
# - Default values for unset options
mbake update
Update mbake to the latest version.
# Update via pip
mbake update
# Equivalent to: pip install --upgrade mbake
Configuration
Configuration File: ~/.bake.toml
Create and edit ~/.bake.toml to customize mbake behavior:
# ~/.bake.toml - mbake configuration
# Add spaces around = in variable assignments
# Example: VAR = value (instead of VAR=value)
space_around_assignment = true
# Add space after : in target definitions
# Example: target : prerequisites (instead of target: prerequisites)
space_after_colon = true
# Normalize line continuation characters (backslashes)
# Removes trailing spaces before \ and ensures proper continuation
normalize_line_continuations = true
# Remove trailing whitespace from all lines
remove_trailing_whitespace = true
# Fix missing tabs in recipes (convert spaces to tabs)
# This is critical - Makefiles MUST use tabs for recipes
fix_missing_recipe_tabs = true
# Automatically detect and insert .PHONY declarations
# Analyzes recipes to identify phony targets (clean, test, etc.)
auto_insert_phony_declarations = true
# Group multiple .PHONY declarations into single declaration
# .PHONY: clean test (instead of two separate lines)
group_phony_declarations = true
# Place .PHONY declarations at the top of the file
# If false, keeps them near their target definitions
phony_at_top = false
Per-Project Configuration
Create .bake.toml in your project root:
# Project-specific mbake settings
# These override ~/.bake.toml for this project
space_around_assignment = false # Compact style for this project
auto_insert_phony_declarations = true
phony_at_top = true
Priority:
.bake.tomlin current directory (highest)~/.bake.tomlin home directory- Built-in defaults (lowest)
Configuration Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
space_around_assignment |
bool | true |
Add spaces around = |
space_after_colon |
bool | true |
Add space after : |
normalize_line_continuations |
bool | true |
Clean backslash continuations |
remove_trailing_whitespace |
bool | true |
Remove end-of-line spaces |
fix_missing_recipe_tabs |
bool | true |
Convert spaces to tabs in recipes |
auto_insert_phony_declarations |
bool | true |
Auto-detect and add .PHONY |
group_phony_declarations |
bool | true |
Combine .PHONY lines |
phony_at_top |
bool | false |
Place .PHONY at file start |
Features
1. Tab Indentation Enforcement
Automatically converts spaces to tabs in recipe sections.
# Before (spaces - invalid!)
build:
echo "Building..."
go build -o app
# After (tabs - correct!)
build:
echo "Building..."
go build -o app
2. Variable Assignment Formatting
Consistent spacing around assignments.
# Before (inconsistent)
VAR1=value
VAR2 =value
VAR3= value
VAR4 = value
# After (consistent)
VAR1 = value
VAR2 = value
VAR3 = value
VAR4 = value
3. Target Colon Spacing
Standardizes spacing after target colons.
# Before
target1:prerequisites
target2 :prerequisites
target3: prerequisites
# After
target1: prerequisites
target2: prerequisites
target3: prerequisites
4. Intelligent .PHONY Detection
Automatically identifies phony targets by analyzing recipes.
# Before
clean:
rm -rf build
test:
go test ./...
install:
cp app /usr/local/bin/
# After
.PHONY: clean test install
clean:
rm -rf build
test:
go test ./...
install:
cp app /usr/local/bin/
Detection Logic:
- Targets with
rm,mkdir,echocommands → Phony - Targets with
npm,go test,dockercommands → Phony - Targets with
curl,ssh,scpcommands → Phony - Targets producing actual files (.o,.a, binaries) → Not phony
5. Line Continuation Normalization
Cleans up line continuation characters.
# Before (trailing space after \, inconsistent)
SOURCES = main.c \
utils.c\
config.c \
# After (consistent, no trailing spaces)
SOURCES = main.c \
utils.c \
config.c
6. Trailing Whitespace Removal
Removes all trailing spaces and tabs.
# Before (invisible trailing spaces marked with ·)
VAR = value···
build:···
echo "test"··
# After (clean)
VAR = value
build:
echo "test"
7. Syntax Validation
Validates Makefile syntax before and after formatting.
mbake format --validate Makefile
Validation Process:
- Validates original file with
make --dry-run - Applies formatting changes
- Validates formatted file
- Only saves if both validations pass
8. Format Disable Comments
Selectively disable formatting for specific sections.
# Standard formatting applies here
VAR1=value
target1:prerequisites
# bake-format off
# Preserve legacy formatting in this section
VAR2 = value
target2 : prerequisites
echo "custom spacing"
# bake-format on
# Standard formatting resumes
VAR3=value
target3:prerequisites
Use cases:
- Legacy Makefiles with specific formatting
- Auto-generated sections
- Intentional custom spacing
- Compatibility with other tools
CI/CD Integration
GitHub Actions
name: Validate Makefiles
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install mbake
run: pip install mbake
- name: Check Makefile formatting
run: mbake format --check Makefile
- name: Validate Makefile syntax
run: mbake validate Makefile
- name: Check all .mk files
run: |
for file in $(find . -name "*.mk" -o -name "Makefile"); do
echo "Checking $file..."
mbake format --check "$file"
mbake validate "$file"
done
GitLab CI
# .gitlab-ci.yml
validate-makefiles:
image: python:3.11
stage: test
before_script:
- pip install mbake
script:
- find . -name "Makefile" -o -name "*.mk" | while read file; do
echo "Validating $file";
mbake format --check "$file";
mbake validate "$file";
done
only:
- merge_requests
- main
Pre-commit Hook
Install as a pre-commit hook:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: mbake-format
name: mbake format
entry: mbake format --check
language: system
files: (Makefile|.*\.mk)$
- id: mbake-validate
name: mbake validate
entry: mbake validate
language: system
files: (Makefile|.*\.mk)$
Install and use:
# Install pre-commit
pip install pre-commit
# Install hooks
pre-commit install
# Run manually
pre-commit run --all-files
Make Target for Self-Validation
Add to your Makefile:
# Self-validation targets
.PHONY: format-check format-fix validate-makefile
format-check:
@echo "Checking Makefile formatting..."
@mbake format --check $(MAKEFILE_LIST)
format-fix:
@echo "Applying formatting to Makefile..."
@mbake format $(MAKEFILE_LIST)
validate-makefile:
@echo "Validating Makefile syntax..."
@mbake validate $(MAKEFILE_LIST)
# Run all checks
.PHONY: check
check: format-check validate-makefile
@echo "All checks passed!"
Usage:
# Check formatting and syntax
make check
# Auto-fix formatting
make format-fix
# Validate only
make validate-makefile
Editor Integration
VS Code
Extension
Install “mbake Makefile Formatter” from marketplace.
Features:
- Format on save
- Format on demand (Shift+Alt+F)
- Real-time validation
- Error highlighting
Manual Setup
Add to .vscode/settings.json:
{
"[makefile]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "mbake.mbake-formatter",
"editor.insertSpaces": false,
"editor.detectIndentation": false,
"editor.tabSize": 8
},
"mbake.validateOnSave": true,
"mbake.autoFixOnSave": false
}
Tasks
Add to .vscode/tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"label": "mbake: Format Makefile",
"type": "shell",
"command": "mbake",
"args": ["format", "${file}"],
"problemMatcher": []
},
{
"label": "mbake: Validate Makefile",
"type": "shell",
"command": "mbake",
"args": ["validate", "${file}"],
"problemMatcher": []
},
{
"label": "mbake: Check Format",
"type": "shell",
"command": "mbake",
"args": ["format", "--check", "${file}"],
"problemMatcher": []
}
]
}
Vim/Neovim
Add to .vimrc or init.vim:
" Format Makefile with mbake
autocmd FileType make nnoremap <buffer> <leader>f :!mbake format %<CR>:e<CR>
" Validate Makefile
autocmd FileType make nnoremap <buffer> <leader>v :!mbake validate %<CR>
" Check format
autocmd FileType make nnoremap <buffer> <leader>c :!mbake format --check %<CR>
" Ensure tabs in Makefiles
autocmd FileType make setlocal noexpandtab tabstop=8 shiftwidth=8
Emacs
Add to .emacs or init.el:
;; mbake formatting for Makefiles
(defun mbake-format-buffer ()
"Format current Makefile with mbake."
(interactive)
(shell-command (format "mbake format %s" (buffer-file-name)))
(revert-buffer t t t))
(defun mbake-validate-buffer ()
"Validate current Makefile with mbake."
(interactive)
(compile (format "mbake validate %s" (buffer-file-name))))
;; Key bindings
(add-hook 'makefile-mode-hook
(lambda ()
(local-set-key (kbd "C-c f") 'mbake-format-buffer)
(local-set-key (kbd "C-c v") 'mbake-validate-buffer)))
Advanced Usage
Batch Processing
# Format all Makefiles in project
find . -name "Makefile" -o -name "*.mk" | xargs mbake format
# Check all files without modifying
find . -name "Makefile" -o -name "*.mk" | xargs mbake format --check
# Create backups of all files
find . -name "Makefile" -o -name "*.mk" | while read file; do
mbake format --backup "$file"
done
Selective Formatting
# Format only specific files
mbake format Makefile build.mk test.mk
# Format with pattern
mbake format **/*.mk
# Exclude certain files
find . -name "*.mk" ! -name "legacy.mk" | xargs mbake format
Diff Review Workflow
# 1. Review changes before applying
mbake format --diff Makefile > changes.diff
# 2. Review the diff
less changes.diff
# 3. If satisfied, apply
mbake format Makefile
# 4. Validate result
mbake validate Makefile
Integration with Git
# Check if formatting is needed before commit
git diff --cached --name-only | grep -E '(Makefile|.*\.mk)$' | while read file; do
if ! mbake format --check "$file"; then
echo "Error: $file needs formatting"
echo "Run: mbake format $file"
exit 1
fi
done
Automated Refactoring
# Refactor entire codebase
#!/bin/bash
echo "Refactoring all Makefiles..."
find . -type f \( -name "Makefile" -o -name "*.mk" \) | while read file; do
echo "Processing: $file"
# Backup
cp "$file" "$file.backup"
# Format
if mbake format "$file"; then
echo " ✓ Formatted"
else
echo " ✗ Format failed"
mv "$file.backup" "$file"
continue
fi
# Validate
if mbake validate "$file"; then
echo " ✓ Validated"
rm "$file.backup"
else
echo " ✗ Validation failed - reverting"
mv "$file.backup" "$file"
fi
done
echo "Refactoring complete!"
Troubleshooting
Common Issues
1. mbake Command Not Found
# Problem: mbake not in PATH
$ mbake format Makefile
bash: mbake: command not found
# Solution: Ensure pip install directory is in PATH
export PATH="$HOME/.local/bin:$PATH"
# Or use python -m
python3 -m mbake format Makefile
2. Syntax Errors After Formatting
# Problem: Validation fails after formatting
$ mbake format --validate Makefile
Error: Syntax validation failed
# Solution: Check format disable comments
# Look for unclosed # bake-format off sections
grep -n "bake-format" Makefile
# Or restore from backup
cp Makefile.bak Makefile
3. Configuration Not Applied
# Problem: Settings in .bake.toml ignored
$ mbake format Makefile
# Formatting doesn't match config
# Solution: Verify config file location
mbake config
# Or specify config explicitly
mbake format --config .bake.toml Makefile
4. Permission Denied
# Problem: Cannot write to file
$ mbake format Makefile
Error: Permission denied
# Solution: Check file permissions
ls -l Makefile
chmod u+w Makefile
5. Python Version Incompatibility
# Problem: Wrong Python version
$ pip install mbake
ERROR: mbake requires Python '>=3.9'
# Solution: Use correct Python version
python3.11 -m pip install mbake
# Or use pyenv
pyenv install 3.11
pyenv local 3.11
pip install mbake
Debug Mode
# Enable verbose output (if supported in future versions)
MBAKE_DEBUG=1 mbake format Makefile
# Check Python environment
python3 -c "import mbake; print(mbake.__version__)"
# Validate manually
make -f Makefile --dry-run
Exit Codes
mbake uses standard exit codes:
| Code | Meaning | Commands |
|---|---|---|
| 0 | Success / No changes needed | All commands |
| 1 | Formatting needed / Validation failed | format --check, validate |
| 2 | Error occurred | All commands |
Usage in Scripts:
# Check formatting
if mbake format --check Makefile; then
echo "Formatting OK"
else
echo "Needs formatting"
exit 1
fi
# Validate
mbake validate Makefile || {
echo "Validation failed!"
exit 1
}
Best Practices
- Always use –check in CI/CD to prevent automatic modifications
- Review diffs with
--diffbefore applying formatting - Create backups with
--backupfor important files - Use configuration files for consistent team formatting
- Combine with validation using
--validateflag - Document exceptions with
# bake-format offcomments - Run in pre-commit hooks to catch issues early
- Format incrementally during refactoring, not all at once
- Test after formatting to ensure builds still work
- Version control config by committing
.bake.toml
Alternative Tool: checkmake
checkmake is a complementary linter that can be used alongside mbake for additional coverage.
Installation
# With Go (1.16+)
go install github.com/checkmake/checkmake/cmd/checkmake@latest
# Docker
docker run --rm -v $(pwd):/data checkmake/checkmake Makefile
Usage
# Basic linting
checkmake Makefile
# List available rules
checkmake list-rules
# JSON output
checkmake --output json Makefile
# With config file
checkmake --config checkmake.ini Makefile
What checkmake Checks
- Missing required phony targets (all, test)
- Targets that should be declared PHONY
- Other configurable rules
Using Both Tools Together
# Makefile validation target
.PHONY: lint
lint:
@echo "Running mbake..."
mbake format --check Makefile
mbake validate Makefile
@echo "Running checkmake..."
checkmake Makefile || true
@echo "Lint complete!"
CI/CD with Both Tools
# GitHub Actions example
- name: Lint Makefile
run: |
pip install mbake
go install github.com/checkmake/checkmake/cmd/checkmake@latest
mbake format --check Makefile
mbake validate Makefile
checkmake Makefile
Resources
- mbake GitHub: https://github.com/EbodShojaei/bake
- mbake PyPI: https://pypi.org/project/mbake/
- mbake Issues: https://github.com/EbodShojaei/bake/issues
- mbake VS Code Extension: Search “mbake” in Extensions marketplace
- checkmake GitHub: https://github.com/checkmake/checkmake
Version Compatibility
- mbake: Latest stable version recommended
- Python: 3.9+ required
- GNU Make: Any version with
--dry-runsupport - OS: Linux, macOS, Windows (with GNU Make installed)
License
mbake is released under the MIT License.
Note: This documentation covers mbake as used by the makefile-validator skill. For the latest features and updates, visit the official GitHub repository.
Makefile Best Practices
Comprehensive guide to writing professional, maintainable, and efficient Makefiles.
Table of Contents
- Essential Special Targets
- File Organization
- Target Declarations
- Variable Management
- Recipe Best Practices
- Dependency Management
- Performance Optimization
- Portability
- Documentation
- Security
- Advanced Patterns
Modern Makefile Header (Recommended)
For modern, robust Makefiles, start with this recommended preamble from Jacob Davis-Hansson:
# Modern Makefile Header
SHELL := bash
.ONESHELL:
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
Explanation:
| Setting | Purpose |
|---|---|
SHELL := bash |
Use bash instead of /bin/sh for modern shell features |
.ONESHELL: |
Run entire recipe in single shell (enables multi-line scripts) |
.SHELLFLAGS := -eu -o pipefail -c |
Stop on errors (-e), undefined vars (-u), pipe failures |
.DELETE_ON_ERROR: |
Delete target on recipe failure (prevents corrupt builds) |
--warn-undefined-variables |
Alert on undefined Make variable references |
--no-builtin-rules |
Disable built-in implicit rules for faster builds |
Note: This preamble is for GNU Make 4.0+. For maximum portability, use a simpler header.
Essential Special Targets
GNU Make provides several special targets that should be used in professional Makefiles.
.DELETE_ON_ERROR (Critical)
Always include .DELETE_ON_ERROR: at the top of your Makefile. This ensures partially built targets are deleted when a recipe fails, preventing corrupt builds.
# CRITICAL: Delete target on recipe failure
.DELETE_ON_ERROR:
# Rest of Makefile follows...
Why it matters:
- Without this, a failed build leaves a partial/corrupt file
- Next
makerun sees the file exists and skips rebuilding - Results in broken builds that are hard to debug
From GNU Make Manual: “This is almost always what you want make to do, but it is not historical practice; so for compatibility, you must explicitly request it.”
Exception: Use .PRECIOUS to protect specific targets that should be preserved even on error:
.DELETE_ON_ERROR:
.PRECIOUS: expensive-to-rebuild.dat
.PHONY (Always Required)
Declare non-file targets as phony to avoid conflicts and improve performance:
.PHONY: all build clean test install
.ONESHELL (For Multi-line Recipes)
Run entire recipe in a single shell invocation:
.ONESHELL:
deploy:
set -e
echo "Deploying..."
cd /app
git pull
./restart.sh
Without .ONESHELL, each line runs in a separate shell, so cd has no effect on subsequent lines.
.SUFFIXES (For Performance)
Clear built-in suffix rules to speed up builds:
# Disable all built-in suffix rules
.SUFFIXES:
# Only keep rules you need (optional)
.SUFFIXES: .c .o
Why: GNU Make has ~90 built-in implicit rules. Clearing them speeds up rule resolution.
Complete Special Targets Header
# Modern Makefile Header
.DELETE_ON_ERROR:
.SUFFIXES:
.PHONY: all build clean test install deploy
# Your targets follow...
File Organization
Directory Structure
# Organized Makefile structure
.PHONY: all clean test install
# Variables section
PROJECT := myapp
VERSION := 1.0.0
BUILD_DIR := build
SRC_DIR := src
# Include external makefiles
include config.mk
include rules/*.mk
# Default target (should be first)
all: build test
# Build targets
build: $(BUILD_DIR)/$(PROJECT)
# ... more targets
Modular Organization
Use include for large projects:
# Main Makefile
include config/variables.mk
include rules/build.mk
include rules/test.mk
include rules/deploy.mk
.PHONY: all
all: build test
Namespace Targets
Use / as delimiter for namespaced targets:
# Good: Namespaced targets
.PHONY: docker/build docker/push docker/clean
docker/build:
docker build -t $(IMAGE) .
docker/push:
docker push $(IMAGE)
docker/clean:
docker rmi $(IMAGE)
# Avoid: Flat namespace
.PHONY: docker-build docker-push docker-clean
Target Declarations
Always Declare .PHONY
Declare targets that don’t create files as phony:
# GOOD: Proper .PHONY declarations
.PHONY: all clean test install build deploy
all: build test
clean:
rm -rf $(BUILD_DIR)
test:
go test ./...
# BAD: Missing .PHONY - causes issues if files named 'clean' or 'test' exist
clean:
rm -rf build
test:
go test ./...
Organize .PHONY Declarations
# Group related phony targets
.PHONY: all build clean
.PHONY: test test-unit test-integration
.PHONY: install uninstall
.PHONY: docker/build docker/push docker/clean
# Or use a single declaration (mbake can organize this)
.PHONY: all build clean test test-unit test-integration install uninstall
Default Target
First target is the default (or use .DEFAULT_GOAL):
# Method 1: First target is default
.PHONY: all
all: build test
# Method 2: Explicit default goal
.DEFAULT_GOAL := build
.PHONY: build test
build:
go build -o app
test:
go test ./...
Variable Management
Variable Assignment Operators
Choose the right operator for your use case:
# Simple assignment (=) - Recursive expansion (evaluated when used)
CFLAGS = -Wall $(OPTIMIZE)
OPTIMIZE = -O2
# CFLAGS will expand to: -Wall -O2 (recursive)
# Immediate assignment (:=) - Expanded immediately (RECOMMENDED for most cases)
BUILD_TIME := $(shell date +%Y%m%d-%H%M%S)
VERSION := 1.0.0
# Evaluated once, avoids repeated shell calls
# Conditional assignment (?=) - Set only if not already defined
CC ?= gcc
PREFIX ?= /usr/local
# Allows environment variable override
# Append (+=) - Add to existing value
CFLAGS := -Wall
CFLAGS += -Wextra
CFLAGS += -O2
# CFLAGS = -Wall -Wextra -O2
Use := for Most Variables
# GOOD: Immediate expansion (predictable, faster)
BUILD_DIR := build
SRC_FILES := $(wildcard src/*.c)
TIMESTAMP := $(shell date +%s)
# AVOID: Recursive expansion (unpredictable, slower)
BUILD_DIR = build
SRC_FILES = $(wildcard src/*.c) # Re-evaluated every time!
TIMESTAMP = $(shell date +%s) # Shell called multiple times!
Sane Defaults with ?=
# Allow user/environment override
CC ?= gcc
CXX ?= g++
PREFIX ?= /usr/local
DESTDIR ?=
VERBOSE ?= 0
# Usage:
# make # Uses defaults
# make CC=clang # Override CC
# PREFIX=/opt make # Override via environment
Variable Naming
# GOOD: Clear, consistent naming
PROJECT_NAME := myapp
BUILD_DIR := build
SOURCE_FILES := $(wildcard src/*.c)
COMPILER_FLAGS := -Wall -Wextra -O2
# AVOID: Unclear abbreviations
PROJ := myapp
BDIR := build
SRCS := $(wildcard src/*.c)
FLAGS := -Wall
Recipe Best Practices
Use Tabs, Not Spaces
# GOOD: Tab character (required)
build:
@echo "Building..."
go build -o app
# BAD: Spaces (will fail)
build:
@echo "Building..."
go build -o app
Note: Makefiles require TAB characters for recipes. Configure your editor to use tabs for Makefiles.
Error Handling
# Method 1: Prefix with @ to suppress echo, - to ignore errors
clean:
@echo "Cleaning build artifacts..."
-rm -rf $(BUILD_DIR)
@echo "Done!"
# Method 2: Use || for conditional error handling
build:
mkdir -p $(BUILD_DIR) || exit 1
go build -o $(BUILD_DIR)/app || exit 1
# Method 3: Use set -e for strict error handling
test:
@set -e; \
echo "Running tests..."; \
go test ./...; \
echo "All tests passed!"
# Method 4: Check exit codes explicitly
deploy:
@./scripts/deploy.sh
@if [ $$? -ne 0 ]; then \
echo "Deployment failed!"; \
exit 1; \
fi
Multi-line Recipes
# Use backslash for line continuation
build: $(SOURCES)
@echo "Building $(PROJECT)..."; \
mkdir -p $(BUILD_DIR); \
$(CC) $(CFLAGS) -o $(BUILD_DIR)/$(PROJECT) $(SOURCES); \
echo "Build complete!"
# Or use .ONESHELL for easier multi-line scripts
.ONESHELL:
test:
echo "Running tests..."
for file in tests/*.sh; do
bash $$file
done
echo "All tests passed!"
Silent vs Verbose Output
# Use @ to suppress command echo
.PHONY: build
build:
@echo "Building..."
@$(CC) $(CFLAGS) -o app $(SOURCES)
# Optional verbose mode
VERBOSE ?= 0
ifeq ($(VERBOSE),1)
Q :=
else
Q := @
endif
build:
$(Q)echo "Building..."
$(Q)$(CC) $(CFLAGS) -o app $(SOURCES)
# Usage:
# make build # Silent
# make build VERBOSE=1 # Verbose
Dependency Management
Specify Dependencies Correctly
# GOOD: Proper dependency chain
app: $(OBJECTS)
$(CC) -o $@ $^
%.o: %.c %.h
$(CC) $(CFLAGS) -c $< -o $@
# BAD: Missing dependencies - app won't rebuild when headers change
app: $(OBJECTS)
$(CC) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
Auto-generate Dependencies (C/C++)
# Automatic dependency generation
DEPDIR := .deps
DEPFLAGS = -MT $@ -MMD -MP -MF $(DEPDIR)/$*.d
%.o: %.c $(DEPDIR)/%.d | $(DEPDIR)
$(CC) $(DEPFLAGS) $(CFLAGS) -c $< -o $@
$(DEPDIR):
@mkdir -p $@
# Include generated dependency files
-include $(patsubst %,$(DEPDIR)/%.d,$(basename $(SOURCES)))
Order-Only Prerequisites
Use | for prerequisites that shouldn’t trigger rebuilds:
# Regular prerequisites trigger rebuild
$(BUILD_DIR)/app: $(SOURCES)
$(CC) -o $@ $^
# Order-only prerequisites (directories) don't trigger rebuild
$(BUILD_DIR)/app: $(SOURCES) | $(BUILD_DIR)
$(CC) -o $@ $^
$(BUILD_DIR):
mkdir -p $@
# Without |, updating BUILD_DIR timestamp would trigger app rebuild
# With |, app only rebuilds when SOURCES change
VPATH for Source Organization
# Search for prerequisites in multiple directories
VPATH = src:include:tests
# Or use vpath for specific patterns
vpath %.c src
vpath %.h include
vpath %.test tests
# Now Make will find files in these directories
app: main.o utils.o
$(CC) -o $@ $^
# Make will find src/main.c and src/utils.c automatically
Performance Optimization
Use .PHONY for Performance
# GOOD: Phony targets skip implicit rule search
.PHONY: clean test install
clean:
rm -rf $(BUILD_DIR)
# BAD: Without .PHONY, Make checks for file existence
clean:
rm -rf $(BUILD_DIR)
Parallel Builds
# Enable parallel builds (use -j flag)
# make -j8 build # 8 parallel jobs
# For sequential targets, use .NOTPARALLEL
.NOTPARALLEL: deploy
deploy: build test
./scripts/deploy.sh
# Or use order-only prerequisites for partial ordering
build-frontend: | build-backend
npm run build
Intermediate File Cleanup
# Mark intermediate files for auto-deletion
.INTERMEDIATE: $(OBJECTS)
# Or mark files to keep through one build
.SECONDARY: $(OBJECTS)
# Delete on error (recommended)
.DELETE_ON_ERROR:
# Example: .o files cleaned after linking
app: main.o utils.o
$(CC) -o $@ $^
# main.o and utils.o auto-deleted after successful build
Avoid Redundant Shell Calls
# BAD: Shell called every time variable is used
DATE = $(shell date +%Y%m%d)
VERSION = $(shell git describe --tags)
target1:
echo $(DATE) # Shell called here
target2:
echo $(DATE) # Shell called again!
# GOOD: Use := for one-time evaluation
DATE := $(shell date +%Y%m%d)
VERSION := $(shell git describe --tags)
target1:
echo $(DATE) # Expands to cached value
target2:
echo $(DATE) # Same cached value
Portability
POSIX Shell Compatibility
# GOOD: POSIX-compatible commands
.PHONY: install
install:
mkdir -p $(DESTDIR)$(PREFIX)/bin
cp -f app $(DESTDIR)$(PREFIX)/bin/
chmod 755 $(DESTDIR)$(PREFIX)/bin/app
# AVOID: Bashisms or GNU-specific features
install:
mkdir -p $(DESTDIR)$(PREFIX)/bin
cp app $(DESTDIR)$(PREFIX)/bin/ # Missing -f for portability
Cross-Platform Variables
# Detect operating system
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
PLATFORM := linux
EXE_EXT :=
endif
ifeq ($(UNAME_S),Darwin)
PLATFORM := macos
EXE_EXT :=
endif
ifeq ($(OS),Windows_NT)
PLATFORM := windows
EXE_EXT := .exe
endif
# Use platform-specific settings
APP := app$(EXE_EXT)
Avoid Hard-Coded Paths
# BAD: Hard-coded paths
install:
cp app /usr/local/bin/
cp docs/app.1 /usr/share/man/man1/
# GOOD: Use variables for paths
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin
MANDIR ?= $(PREFIX)/share/man
install:
install -d $(DESTDIR)$(BINDIR)
install -m 755 app $(DESTDIR)$(BINDIR)/
install -d $(DESTDIR)$(MANDIR)/man1
install -m 644 docs/app.1 $(DESTDIR)$(MANDIR)/man1/
Documentation
Comment Your Makefiles
# Project: MyApp
# Description: Build system for MyApp project
# Author: Your Name
# Version: 1.0.0
# Configuration variables
PROJECT := myapp
VERSION := $(shell git describe --tags 2>/dev/null || echo "dev")
# Build directories
BUILD_DIR := build
SRC_DIR := src
# Compiler settings
CC := gcc
CFLAGS := -Wall -Wextra -O2
# Default target: Build and test the application
.PHONY: all
all: build test
# Build the main application binary
.PHONY: build
build: $(BUILD_DIR)/$(PROJECT)
@echo "Build complete: $(BUILD_DIR)/$(PROJECT)"
# Run all test suites
.PHONY: test
test:
@echo "Running tests..."
@./scripts/run-tests.sh
Help Target
# Provide a help target
.PHONY: help
help:
@echo "Available targets:"
@echo " make build - Build the application"
@echo " make test - Run tests"
@echo " make clean - Remove build artifacts"
@echo " make install - Install to $(PREFIX)"
@echo ""
@echo "Variables:"
@echo " PREFIX=$(PREFIX)"
@echo " CC=$(CC)"
# Or auto-generate from comments
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}'
build: ## Build the application
@go build -o app
test: ## Run all tests
@go test ./...
clean: ## Remove build artifacts
@rm -rf $(BUILD_DIR)
Security
Avoid Hardcoded Credentials
# BAD: Hardcoded secrets
deploy:
curl -H "Authorization: Bearer sk-1234567890" https://api.example.com/deploy
# GOOD: Use environment variables
deploy:
@if [ -z "$$API_TOKEN" ]; then \
echo "Error: API_TOKEN not set"; \
exit 1; \
fi
curl -H "Authorization: Bearer $$API_TOKEN" https://api.example.com/deploy
Validate Input Variables
# Validate critical variables
.PHONY: deploy
deploy:
@if [ -z "$(ENV)" ]; then \
echo "Error: ENV not specified (prod|staging|dev)"; \
exit 1; \
fi
@if [ "$(ENV)" != "prod" ] && [ "$(ENV)" != "staging" ] && [ "$(ENV)" != "dev" ]; then \
echo "Error: Invalid ENV=$(ENV)"; \
exit 1; \
fi
@echo "Deploying to $(ENV)..."
./scripts/deploy.sh $(ENV)
Safe Variable Expansion
# BAD: Unsafe variable expansion
clean:
rm -rf $(BUILD_DIR)/* # Dangerous if BUILD_DIR is empty or /
# GOOD: Validate before dangerous operations
.PHONY: clean
clean:
@if [ -z "$(BUILD_DIR)" ] || [ "$(BUILD_DIR)" = "/" ]; then \
echo "Error: Invalid BUILD_DIR"; \
exit 1; \
fi
rm -rf $(BUILD_DIR)/*
# BETTER: Use safer patterns
BUILD_DIR := build # Never empty
clean:
@test -d $(BUILD_DIR) && rm -rf $(BUILD_DIR)/* || true
Advanced Patterns
Pattern Rules
# Pattern rule for object files
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# Multiple pattern rules
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
# Static pattern rules
$(OBJECTS): %.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
Automatic Variables
# $@ - Target name
# $< - First prerequisite
# $^ - All prerequisites
# $? - Prerequisites newer than target
# $* - Stem of pattern rule
build/%.o: src/%.c
@mkdir -p $(dir $@) # Directory of target
$(CC) -c $< -o $@ # First prereq to target
@echo "Built $@" # Target name
# Example:
# build/main.o: src/main.c
# $@ = build/main.o
# $< = src/main.c
# $* = main
Functions
# Built-in functions
SOURCES := $(wildcard src/*.c)
OBJECTS := $(patsubst src/%.c,build/%.o,$(SOURCES))
HEADERS := $(shell find include -name '*.h')
# String manipulation
UPPERCASE := $(shell echo $(PROJECT) | tr '[:lower:]' '[:upper:]')
VERSION_MAJOR := $(word 1,$(subst ., ,$(VERSION)))
# Custom functions
define compile_template
$(1): $(2)
$(CC) $(CFLAGS) -c $$< -o $$@
endef
$(foreach src,$(SOURCES),$(eval $(call compile_template,$(patsubst %.c,%.o,$(src)),$(src))))
Conditional Compilation
# Debug vs Release builds
DEBUG ?= 0
ifeq ($(DEBUG),1)
CFLAGS := -g -O0 -DDEBUG
BUILD_TYPE := debug
else
CFLAGS := -O2 -DNDEBUG
BUILD_TYPE := release
endif
build:
@echo "Building $(BUILD_TYPE) version..."
$(CC) $(CFLAGS) -o app $(SOURCES)
Summary Checklist
- .DELETE_ON_ERROR: declared at top (critical)
- All non-file targets declared as .PHONY
- Tabs used for recipe indentation (not spaces)
- Variables use := for immediate expansion
- Sane defaults with ?= for user override
- Dependencies properly specified
- Error handling in critical recipes
- Default target documented and listed first
- No hardcoded credentials or paths
- Help target provided
- Parallel build safety considered
- Intermediate files managed (.INTERMEDIATE/.SECONDARY)
- Comments explain complex logic
- Portable commands used (POSIX compatible)
- Variables validated before dangerous operations
- .SUFFIXES: considered for disabling built-in rules
Additional Resources
Common Makefile Mistakes
A comprehensive guide to common mistakes in Makefiles, their consequences, and how to fix them.
Table of Contents
- Critical Missing Declarations
- Syntax Errors
- Indentation Issues
- Target and Dependency Problems
- Variable Issues
- Security Vulnerabilities
- Performance Problems
- Portability Issues
- 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