
Makefile Validator
- 392 installs
- 286 repo stars
- Updated July 26, 2026
- akin-ozer/cc-devops-skills
makefile-validator is a Claude Code skill that validates Makefile syntax, targets, conventions, and security anti-patterns for developers who need deterministic build-config checks before merge or in CI.
About
makefile-validator is a Claude Code skill from akin-ozer/cc-devops-skills that audits Makefile, makefile, and *.mk files with a local-first, deterministic validation pipeline. The skill runs GNU make dry-run syntax checks, mbake validate and format --check stages inside a temporary Python venv, and custom security and best-practice pattern scans with optional checkmake and unmake coverage when installed. It reports line numbers, fix suggestions, and color-coded exit codes where 0 means clean, 1 signals warnings, and 2 blocks on errors. Developers reach for makefile-validator before merging Makefile changes, during code review of build configs, or when wiring the bundled GitHub Action wrapper into CI/CD. The validator enforces .PHONY declarations, tab indentation, circular dependency detection, parallelism safety, and credential-leak grep checks so broken or unsafe recipes fail early.
- Makefile syntax and target validation
- Catches broken recipes before CI
- Enforces naming and dependency conventions
- Reduces local vs CI build drift
- Fast feedback on DevOps scripts
Makefile Validator by the numbers
- 392 all-time installs (skills.sh)
- Ranked #304 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/akin-ozer/cc-devops-skills --skill makefile-validatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 392 |
|---|---|
| repo stars | ★ 286 |
| Last updated | July 26, 2026 |
| Repository | akin-ozer/cc-devops-skills ↗ |
How do you validate Makefile syntax before CI merge?
Validate Makefile syntax, targets, and conventions before merge to prevent broken builds and inconsistent developer workflows.
Who is it for?
DevOps engineers and maintainers who own repo Makefiles and need mbake-backed linting plus GNU make syntax checks in local runs or GitHub Actions.
Skip if: Projects using only npm scripts or Bazel with no Makefiles, or teams wanting a one-line syntax check without layered mbake and security auditing.
When should I use this skill?
A developer edits a Makefile, sees mysterious build failures, or wants CI to block merge on Makefile syntax and best-practice violations.
What you get
Validation report with line numbers, mbake format findings, exit code 0/1/2 summary, and fix suggestions for Makefile targets.
- Color-coded validation report
- CI-ready exit code
- Line-number fix suggestions
By the numbers
- Uses exit codes 0, 1, and 2 for clean, warning, and error CI gating
- Runs a 6-stage pipeline: preflight, GNU make dry-run, mbake validate, mbake format, custom checks, optional checkmake/un
Files
Makefile Validator
Overview
Use this skill to validate Makefiles with a local-first, deterministic flow.
Default validator entrypoint:
bash scripts/validate_makefile.sh <makefile-path>Validation layers:
1. Dependency preflight (python3, pip3, make) 2. GNU make syntax check (make -n --dry-run) when make is available 3. mbake validate 4. mbake format --check 5. Custom security, best-practice, and optimization checks 6. Optional checkmake and unmake checks when installed
Trigger Guidance
Use this skill when the request includes Makefile quality, linting, validation, hardening, or troubleshooting.
Trigger Phrases
- "Validate this Makefile"
- "Lint my
.mkfile" - "Find issues in this build Makefile"
- "Check Makefile security problems"
- "Review this Makefile before CI"
Non-Trigger Examples
- Creating a brand new Makefile from scratch (use
makefile-generator) - Running build targets as part of delivery
- General shell scripting work unrelated to Makefiles
Deterministic Execution Model
Run from this skill directory for shortest commands:
cd devops-skills-plugin/skills/makefile-validatorStep 1: Preflight
1. Confirm target file exists and is readable. 2. Confirm whether file edits are allowed (writable) or only suggestions can be returned (read-only). 3. Prefer the default validator path first; use fallbacks only when blocked by environment constraints.
Step 2: Baseline Validation (Default Path)
# From skill directory
bash scripts/validate_makefile.sh <makefile-path>
# From repository root
bash devops-skills-plugin/skills/makefile-validator/scripts/validate_makefile.sh <makefile-path>Always record:
- command executed
- exit code
- summary counts (
Errors,Warnings,Info) - issue locations reported by tools
Step 3: Interpret Output and Exit Code
| Exit code | Meaning | Expected summary line | Action |
|---|---|---|---|
0 | no blocking findings | Validation PASSED | optional improvements only |
1 | warning-only result | Validation PASSED with warnings | fix recommended; merge policy dependent |
2 | error result | Validation FAILED - errors must be fixed | fix required, then rerun |
Step 4: Progressive Reference Loading
Open only the docs required by the current findings.
| Finding type | Reference doc |
|---|---|
.PHONY, .DELETE_ON_ERROR, .ONESHELL, variable usage, performance structure | docs/best-practices.md |
| tabs vs spaces, dependency mistakes, credential patterns, anti-patterns | docs/common-mistakes.md |
mbake behavior, formatter flags, known mbake caveats | docs/bake-tool.md |
Step 5: Fix + Rerun Loop
After applying fixes, rerun:
bash scripts/validate_makefile.sh <makefile-path>Loop rules:
1. Stop only when no new errors are introduced. 2. If warnings remain intentionally, document why they are accepted. 3. Always report the latest rerun exit code.
How to Open Docs
Use explicit file-open commands so paths are unambiguous.
From repository root:
sed -n '1,220p' devops-skills-plugin/skills/makefile-validator/docs/best-practices.md
sed -n '1,220p' devops-skills-plugin/skills/makefile-validator/docs/common-mistakes.md
sed -n '1,220p' devops-skills-plugin/skills/makefile-validator/docs/bake-tool.md
rg -n "PHONY|DELETE_ON_ERROR|ONESHELL|tab|credential|mbake" devops-skills-plugin/skills/makefile-validator/docs/*.mdFrom devops-skills-plugin/skills/makefile-validator:
sed -n '1,220p' docs/best-practices.md
sed -n '1,220p' docs/common-mistakes.md
sed -n '1,220p' docs/bake-tool.md
rg -n "PHONY|DELETE_ON_ERROR|ONESHELL|tab|credential|mbake" docs/*.mdIf shell commands are unavailable, use the environment's file-open/read actions on the same paths.
Fallback Behavior
Use these only when the default validator path cannot run fully.
| Constraint | Fallback action | Reporting requirement |
|---|---|---|
python3 or pip3 unavailable | Run limited checks (make -f <file> -n --dry-run if make exists, plus focused grep checks) | State that mbake stages were skipped and coverage is reduced |
pip3 install mbake fails (offline/proxy/index issue) | Keep syntax/custom checks that still work; defer formatter/linter stages | Report install failure and request rerun in a network-enabled environment |
make unavailable | Continue with non-syntax stages; script already downgrades syntax stage | Explicitly note syntax coverage was skipped |
checkmake or unmake unavailable | Continue; these are optional stages | Note optional lint/portability coverage not executed |
| target file is read-only | Provide patch suggestions only | Mark response as advisory only |
| command execution unavailable | Provide static review from file contents and docs | Mark result as non-executed analysis |
Minimal fallback commands:
# Syntax only (when make exists)
make -f <makefile-path> -n --dry-run
# Focused quick checks
grep -n "^\\.DELETE_ON_ERROR:" <makefile-path>
grep -n "^\\.PHONY:" <makefile-path>
grep -nE "^( | | )[a-zA-Z@\\$\\(]" <makefile-path>Example Outcomes Mapped to Exit Codes
Clean Result (exit 0)
Errors: 0
Warnings: 0
Info: 2
✓ Validation PASSEDWarning-Only Result (exit 1)
Errors: 0
Warnings: 3
Info: 1
⚠ Validation PASSED with warningsError Result (exit 2)
Errors: 2
Warnings: 1
Info: 0
⚠ Validation FAILED - errors must be fixedTroubleshooting Quick Start
1. Verify required tools:
command -v python3 pip3 make2. Isolate GNU make syntax failures:
make -f <makefile-path> -n --dry-run3. Check likely tab-indentation violations:
grep -nE "^( | | )[a-zA-Z@\\$\\(]" <makefile-path>4. Rerun validator with plain output and capture log:
NO_COLOR=1 bash scripts/validate_makefile.sh <makefile-path> > /tmp/makefile-validator.log 2>&1
echo "exit=$? log=/tmp/makefile-validator.log"5. If mbake install keeps failing, validate in a network-enabled shell or with an internal PyPI mirror, then rerun the full validator.
Skill Paths
makefile-validator/
├── SKILL.md
├── scripts/
│ └── validate_makefile.sh
├── docs/
│ ├── best-practices.md
│ ├── common-mistakes.md
│ └── bake-tool.md
└── examples/
├── good-makefile.mk
└── bad-makefile.mkDone Criteria
This skill update is complete when all are true:
1. Trigger guidance is explicit and easy to identify. 2. Execution flow is deterministic (preflight -> run -> interpret -> docs -> rerun). 3. "How to open docs" instructions include exact commands and paths. 4. Example outcomes are explicitly tied to exit codes (0, 1, 2). 5. Fallback behavior is documented for missing tools and constrained environments. 6. Troubleshooting quick-start is concise and runnable. 7. Frontmatter name and description remain unchanged.
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
1. Installation 2. Quick Start 3. Commands 4. Configuration 5. Features 6. CI/CD Integration 7. Editor Integration 8. Advanced Usage 9. Troubleshooting
Installation
PyPI Installation (Recommended)
# Install mbake
pip install mbake
# Upgrade to latest version
pip install --upgrade mbake
# Verify installation
mbake --versionSystem 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
deactivateNote: The makefile-validator skill automatically handles venv creation and cleanup.
VS Code Extension
Install the "mbake Makefile Formatter" extension from the VS Code marketplace:
1. Open VS Code 2. Go to Extensions (Ctrl+Shift+X) 3. Search for "mbake Makefile Formatter" 4. 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 MakefileFirst-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 MakefileCommands
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/*.mkOptions:
--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/*.mkWhat 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 behaviormbake config
Display current configuration settings.
# Show active configuration
mbake config
# Output includes:
# - Configuration file location
# - All active settings
# - Default values for unset optionsmbake update
Update mbake to the latest version.
# Update via pip
mbake update
# Equivalent to: pip install --upgrade mbakeConfiguration
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 = falsePer-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 = truePriority: 1. .bake.toml in current directory (highest) 2. ~/.bake.toml in home directory 3. 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 app2. 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 = value3. Target Colon Spacing
Standardizes spacing after target colons.
# Before
target1:prerequisites
target2 :prerequisites
target3: prerequisites
# After
target1: prerequisites
target2: prerequisites
target3: prerequisites4. 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.c6. 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 MakefileValidation Process: 1. Validates original file with make --dry-run 2. Applies formatting changes 3. Validates formatted file 4. 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:prerequisitesUse 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"
doneGitLab 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
- mainPre-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-filesMake 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-makefileEditor 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=8Emacs
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"
doneSelective 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 formatDiff 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 MakefileIntegration 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
doneAutomated 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 Makefile2. 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 Makefile3. 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 Makefile4. Permission Denied
# Problem: Cannot write to file
$ mbake format Makefile
Error: Permission denied
# Solution: Check file permissions
ls -l Makefile
chmod u+w Makefile5. 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 mbakeDebug 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-runExit 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
1. Always use --check in CI/CD to prevent automatic modifications 2. Review diffs with --diff before applying formatting 3. Create backups with --backup for important files 4. Use configuration files for consistent team formatting 5. Combine with validation using --validate flag 6. Document exceptions with # bake-format off comments 7. Run in pre-commit hooks to catch issues early 8. Format incrementally during refactoring, not all at once 9. Test after formatting to ensure builds still work 10. 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 MakefileUsage
# Basic linting
checkmake Makefile
# List available rules
checkmake list-rules
# JSON output
checkmake --output json Makefile
# With config file
checkmake --config checkmake.ini MakefileWhat 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 MakefileResources
- 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
1. Essential Special Targets 2. File Organization 3. Target Declarations 4. Variable Management 5. Recipe Best Practices 6. Dependency Management 7. Performance Optimization 8. Portability 9. Documentation 10. Security 11. 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-rulesExplanation:
| 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.shWithout `.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 .oWhy: 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 targetsModular 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 testNamespace 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-cleanTarget 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 uninstallDefault 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 -O2Use := 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 environmentVariable 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 := -WallRecipe 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 appNote: 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; \
fiMulti-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 # VerboseDependency 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 changeVPATH 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 automaticallyPerformance 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 buildIntermediate 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 buildAvoid 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 valuePortability
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 portabilityCross-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.shHelp 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/deployValidate 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)/* || trueAdvanced 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
# $* = mainFunctions
# 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
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 rebuildImpact:
- 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 # TABImpact: 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 valueSolution: 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 behaviorSolution: 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 appEditor 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 dateSolution: 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.shVariable 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 valueImpact: 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 failsSolution: 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 makeSolution: 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
exportImpact: 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)/*; \
fi17. 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 runSolution: 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; doneImpact: 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 filesImpact: 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 implementationsSolution: 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.c23. 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 WindowsSolution: 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.goBuild 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 simultaneouslySolution: 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: install27. 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 → deployQuick 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
# Bad Makefile Example
# Demonstrates common mistakes and anti-patterns
# DO NOT USE THIS IN PRODUCTION!
# Variables with recursive expansion (inefficient)
BUILD_TIME = $(shell date +%Y%m%d-%H%M%S)
SOURCES = $(wildcard src/*.c)
# Hardcoded credentials (SECURITY ISSUE!)
API_KEY = sk-1234567890abcdef
DB_PASSWORD = super_secret_password
# No .PHONY declarations (major issue!)
all: build test
build:
echo "Building..."
gcc -o app $(SOURCES)
# Missing .PHONY - won't run if file 'test' exists
test:
go test ./...
# Missing .PHONY - dangerous!
clean:
rm -rf $(BUILD_DIR)/*
# Unsafe variable expansion
deploy:
ssh user@$(SERVER) "cd /app && git pull origin $(BRANCH)"
# Missing error handling
install:
cp app /usr/local/bin/
cp docs/app.1 /usr/share/man/man1/
# Spaces instead of tabs in some recipes (syntax error!)
broken-target:
echo "This uses spaces!"
echo "This will fail!"
# Mixed tabs and spaces (syntax error!)
mixed:
echo "This uses a tab"
echo "This uses spaces - ERROR!"
# No dependency specification
app.o:
gcc -c app.c
# Hardcoded paths (not portable)
backup:
cp app $(HOME)/backups/
# No validation of dangerous operations
dangerous-clean:
rm -rf $(USER_INPUT)/*
# Incorrect variable expansion in loop
loop-bug:
for file in *.c; do \
echo "Compiling $file"; \
gcc -c $file; \
done
# Missing := causing repeated shell calls
debug:
echo $(BUILD_TIME)
echo $(BUILD_TIME)
# Unquoted variable in dangerous context
unsafe-rm:
rm -rf $(BUILD_DIR)
# No default values for critical variables
install-unsafe:
cp app $(PREFIX)/bin/
# Good Makefile Example
# Demonstrates best practices for Makefile creation
# Project: Example Application
# Version: 1.0.0
# Modern Makefile Header (GNU Make 4.0+)
# See: https://tech.davis-hansson.com/p/make/
SHELL := bash
# Delete target on recipe failure (GNU Make best practice)
.DELETE_ON_ERROR:
# Disable built-in suffix rules for faster builds
.SUFFIXES:
# Variables with immediate expansion (recommended)
PROJECT := example-app
VERSION := 1.0.0
BUILD_DIR := build
SRC_DIR := src
TEST_DIR := tests
# Compiler and flags
CC ?= gcc
CFLAGS := -Wall -Wextra -O2
LDFLAGS :=
# Find source files
SOURCES := $(wildcard $(SRC_DIR)/*.c)
OBJECTS := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SOURCES))
# Phony targets properly declared
.PHONY: all build clean test install uninstall help
# Default target (first in file)
all: build test
# Help target for user guidance
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 (default: /usr/local)"
@echo " make uninstall - Remove installed files"
@echo " make help - Show this help message"
# Build the main application
build: $(BUILD_DIR)/$(PROJECT)
@echo "Build complete: $(BUILD_DIR)/$(PROJECT)"
# Link object files into executable
$(BUILD_DIR)/$(PROJECT): $(OBJECTS) | $(BUILD_DIR)
@echo "Linking $@..."
$(CC) $(OBJECTS) $(LDFLAGS) -o $@
# Compile source files to object files
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
@echo "Compiling $<..."
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c $< -o $@
# Create build directory (order-only prerequisite)
$(BUILD_DIR):
@mkdir -p $(BUILD_DIR)
# Run tests
test: build
@echo "Running tests..."
@set -e; \
for test in $(TEST_DIR)/*.sh; do \
echo " Running $$test..."; \
bash $$test; \
done
@echo "All tests passed!"
# Clean build artifacts
clean:
@echo "Cleaning build artifacts..."
@if [ -d "$(BUILD_DIR)" ]; then \
rm -rf $(BUILD_DIR); \
echo " Removed $(BUILD_DIR)"; \
fi
@echo "Clean complete"
# Installation with proper defaults
PREFIX ?= /usr/local
BINDIR ?= $(PREFIX)/bin
install: build
@echo "Installing $(PROJECT) to $(DESTDIR)$(BINDIR)..."
@install -d $(DESTDIR)$(BINDIR)
@install -m 755 $(BUILD_DIR)/$(PROJECT) $(DESTDIR)$(BINDIR)/
@echo "Installation complete"
# Uninstall
uninstall:
@echo "Uninstalling $(PROJECT) from $(DESTDIR)$(BINDIR)..."
@if [ -f "$(DESTDIR)$(BINDIR)/$(PROJECT)" ]; then \
rm -f $(DESTDIR)$(BINDIR)/$(PROJECT); \
echo " Removed $(DESTDIR)$(BINDIR)/$(PROJECT)"; \
fi
@echo "Uninstall complete"#!/usr/bin/env bash
#
# Regression test suite for validate_makefile.sh
#
# Runs the validator against fixture files and asserts expected exit codes and
# output patterns. Each fixture file is validated once; all assertions on it
# share that single run so mbake is only installed once per fixture.
#
# Exit 0 when all assertions pass; exit 1 on the first failure.
#
# Usage:
# bash scripts/test_validate.sh # from makefile-validator/ directory
# bash devops-skills-plugin/skills/makefile-validator/scripts/test_validate.sh
#
# Suites:
# MAKEFILE_VALIDATOR_TEST_SUITE=offline # deterministic default (MBAKE_SKIP_INSTALL=1)
# MAKEFILE_VALIDATOR_TEST_SUITE=integration # mbake install path checks
# MAKEFILE_VALIDATOR_TEST_SUITE=all # run both suites
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
VALIDATOR="$SCRIPT_DIR/validate_makefile.sh"
EXAMPLES_DIR="$SCRIPT_DIR/../examples"
TEST_SUITE="${MAKEFILE_VALIDATOR_TEST_SUITE:-${TEST_SUITE:-offline}}"
PASS=0
FAIL=0
TMPFILES=()
RUN_OFFLINE=0
RUN_INTEGRATION=0
case "$TEST_SUITE" in
offline)
RUN_OFFLINE=1
;;
integration)
RUN_INTEGRATION=1
;;
all)
RUN_OFFLINE=1
RUN_INTEGRATION=1
;;
*)
echo "[ERROR] Unsupported test suite: $TEST_SUITE" >&2
echo " Use one of: offline, integration, all" >&2
exit 2
;;
esac
# ─── cleanup ────────────────────────────────────────────────────────────────
cleanup() {
local f
for f in "${TMPFILES[@]:-}"; do
if [[ -d "$f" ]]; then
rm -rf "$f" 2>/dev/null || true
else
rm -f "$f" 2>/dev/null || true
fi
done
TMPFILES=()
}
trap cleanup EXIT INT TERM
# ─── helpers ────────────────────────────────────────────────────────────────
pass() { printf " PASS %s\n" "$1"; PASS=$((PASS + 1)); }
fail() { printf " FAIL %s\n" "$1"; FAIL=$((FAIL + 1)); }
# Run the validator once; populate OUTPUT and EXIT_CODE globals.
# The || true prevents set -e from aborting on a non-zero validator exit.
_run() {
local file="$1"
local mbake_skip_install="${2:-1}"
EXIT_CODE=0
OUTPUT=$(NO_COLOR=1 MBAKE_SKIP_INSTALL="$mbake_skip_install" bash "$VALIDATOR" "$file" 2>&1) || EXIT_CODE=$?
}
# Assert the exit code stored in EXIT_CODE equals $1.
assert_exit() {
local label="$1" expected="$2"
if [[ "$EXIT_CODE" -eq "$expected" ]]; then
pass "$label (exit $EXIT_CODE)"
else
fail "$label — expected exit $expected, got $EXIT_CODE"
printf '%s\n' "$OUTPUT" | sed 's/^/ /'
fi
}
# Assert that OUTPUT matches an ERE pattern.
assert_contains() {
local label="$1" pattern="$2"
if printf '%s\n' "$OUTPUT" | grep -qE "$pattern"; then
pass "$label"
else
fail "$label — pattern not found: $pattern"
printf '%s\n' "$OUTPUT" | sed 's/^/ /'
fi
}
# Assert that OUTPUT does NOT match an ERE pattern.
assert_not_contains() {
local label="$1" pattern="$2"
if printf '%s\n' "$OUTPUT" | grep -qE "$pattern"; then
fail "$label — unexpected pattern found: $pattern"
printf '%s\n' "$OUTPUT" | grep -E "$pattern" | sed 's/^/ /'
else
pass "$label"
fi
}
# Assert a path does not exist.
assert_path_absent() {
local label="$1" path="$2"
if [[ ! -e "$path" ]]; then
pass "$label"
else
fail "$label — path still exists: $path"
fi
}
# Create a named temp fixture (.mk extension) and register it for cleanup.
# macOS mktemp requires Xs to be at the very end of the template, so we create
# a plain temp file and rename it with a .mk suffix.
mktemp_mk() {
local base f
base=$(mktemp "${TMPDIR:-/tmp}/test-mk-XXXXXX")
f="${base}.mk"
mv "$base" "$f"
TMPFILES+=("$f")
echo "$f"
}
# ─── Test Groups ────────────────────────────────────────────────────────────
echo ""
echo "Running validate_makefile.sh regression tests (suite: $TEST_SUITE)..."
if [[ "$RUN_OFFLINE" -eq 1 ]]; then
# ── Harness: cleanup removes both files and directories ───────────────────
echo ""
echo "── harness: cleanup removes files and directories ───────────────────"
cleanup_file=$(mktemp "${TMPDIR:-/tmp}/test-clean-file-XXXXXX")
cleanup_dir=$(mktemp -d "${TMPDIR:-/tmp}/test-clean-dir-XXXXXX")
TMPFILES+=("$cleanup_file" "$cleanup_dir")
cleanup
assert_path_absent "cleanup removes temp file" "$cleanup_file"
assert_path_absent "cleanup removes temp directory" "$cleanup_dir"
# ── Offline mode: deterministic mbake skip ────────────────────────────────
echo ""
echo "── offline: deterministic mbake skip mode ───────────────────────────"
FAKEOFFLINEBIN=$(mktemp -d "${TMPDIR:-/tmp}/test-offlinebin-XXXXXX")
TMPFILES+=("$FAKEOFFLINEBIN")
printf '#!/usr/bin/env bash\necho "python3: blocked" >&2; exit 127\n' > "$FAKEOFFLINEBIN/python3"
printf '#!/usr/bin/env bash\necho "pip3: blocked" >&2; exit 127\n' > "$FAKEOFFLINEBIN/pip3"
chmod +x "$FAKEOFFLINEBIN/python3" "$FAKEOFFLINEBIN/pip3"
PATH="$FAKEOFFLINEBIN:$PATH" _run "$EXAMPLES_DIR/good-makefile.mk" 1
assert_exit "offline mode exits cleanly" 0
assert_contains "offline mode banner printed" "MBAKE_SKIP_INSTALL=1 enabled"
assert_contains "mbake stages skipped by deterministic mode" "Skipped.*MBAKE_SKIP_INSTALL=1"
assert_not_contains "python3 warning suppressed in offline mode" "python3 not found"
assert_not_contains "pip3 warning suppressed in offline mode" "pip3 not found"
assert_not_contains "venv setup skipped in offline mode" "Creating temporary venv|Installing mbake"
# ── good-makefile.mk ─────────────────────────────────────────────────────
echo ""
echo "── good-makefile.mk ─────────────────────────────────────────────────"
_run "$EXAMPLES_DIR/good-makefile.mk"
assert_exit "exits cleanly" 0
assert_not_contains "no credential false positive" "hardcoded credentials"
assert_not_contains "no tab false positive" "spaces instead of tabs"
assert_not_contains "no validation-failed summary" "Validation FAILED"
assert_contains "passes summary printed" "Validation PASSED"
# ── bad-makefile.mk ──────────────────────────────────────────────────────
echo ""
echo "── bad-makefile.mk ──────────────────────────────────────────────────"
_run "$EXAMPLES_DIR/bad-makefile.mk"
assert_exit "exits with error code" 2
assert_contains "catches spaces-as-tabs" "spaces instead of tabs"
assert_contains "catches hardcoded credentials" "hardcoded credentials"
assert_contains "catches missing .DELETE_ON_ERROR" "Missing .DELETE_ON_ERROR"
assert_contains "catches missing .PHONY" "No .PHONY declarations"
assert_contains "catches unsafe variable expansion" "without defaults"
assert_contains "catches recursive = with shell" "Shell commands with recursive expansion"
assert_contains "failed summary printed" "Validation FAILED"
# ── Edge: commented-out credentials must NOT be flagged ──────────────────
echo ""
echo "── edge: commented credentials ──────────────────────────────────────"
F=$(mktemp_mk)
cat > "$F" << 'MKEOF'
SHELL := bash
.DELETE_ON_ERROR:
.PHONY: deploy
# Documentation example (not a real secret):
# API_KEY = sk-example-1234567890abcdef
# DB_PASSWORD = super_secret_password
# github_token = ghp_example_token
deploy:
@echo "Deploying..."
MKEOF
_run "$F"
assert_not_contains "commented creds not flagged" "hardcoded credentials"
# ── Edge: actual credentials after comment lines must still be caught ────
echo ""
echo "── edge: real credentials are still caught ──────────────────────────"
F=$(mktemp_mk)
cat > "$F" << 'MKEOF'
SHELL := bash
.DELETE_ON_ERROR:
.PHONY: deploy
# This is a comment about the key below
API_KEY = sk-1234567890abcdef
deploy:
@echo "Deploying..."
MKEOF
_run "$F"
assert_contains "real creds after comments still caught" "hardcoded credentials"
# ── Edge: lowercase variable in dangerous command ─────────────────────────
echo ""
echo "── edge: lowercase variable in rm -rf ───────────────────────────────"
F=$(mktemp_mk)
cat > "$F" << 'MKEOF'
SHELL := bash
.DELETE_ON_ERROR:
.PHONY: clean
clean:
rm -rf $(build_dir)
MKEOF
_run "$F"
assert_contains "lowercase unsafe var detected" "without defaults"
# ── Edge: bare make call at end of line (no args) ─────────────────────────
echo ""
echo "── edge: bare make call (no args at end of line) ────────────────────"
F=$(mktemp_mk)
cat > "$F" << 'MKEOF'
SHELL := bash
.DELETE_ON_ERROR:
.PHONY: recurse
recurse:
make
MKEOF
_run "$F"
assert_contains "bare make call detected" "Direct.*call"
# ── Edge: make with args is also caught ───────────────────────────────────
echo ""
echo "── edge: make with args ─────────────────────────────────────────────"
F=$(mktemp_mk)
cat > "$F" << 'MKEOF'
SHELL := bash
.DELETE_ON_ERROR:
.PHONY: recurse
recurse:
make build
MKEOF
_run "$F"
assert_contains "make-with-args detected" "Direct.*call"
# ── Edge: $(MAKE) must NOT be flagged as a bare make call ─────────────────
echo ""
echo "── edge: \$(MAKE) is not flagged ────────────────────────────────────"
F=$(mktemp_mk)
cat > "$F" << 'MKEOF'
SHELL := bash
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
.PHONY: recurse
## Default target
recurse:
$(MAKE) build
MKEOF
_run "$F"
assert_not_contains "MAKE macro not flagged" "Direct.*call"
# ── Edge: syntax_check single make invocation (output present on error) ───
echo ""
echo "── edge: syntax error output is shown ───────────────────────────────"
F=$(mktemp_mk)
# Write a Makefile with a deliberate syntax error (spaces instead of tabs)
printf 'build:\n echo broken\n' > "$F"
_run "$F"
assert_contains "syntax error output shown" "missing separator|Syntax errors detected"
# ── Edge: graceful degradation when python3 is absent ─────────────────────
# Shadow python3 with a stub that exits 127 so the validator cannot set up mbake.
# The script should warn, skip mbake stages, still run custom/syntax checks,
# and exit with warnings (1) rather than a hard error.
echo ""
echo "── edge: graceful degradation (no python3) ───────────────────────────"
FAKEBINDIR=$(mktemp -d "${TMPDIR:-/tmp}/test-fakebin-XXXXXX")
TMPFILES+=("$FAKEBINDIR")
printf '#!/usr/bin/env bash\necho "python3: not found" >&2; exit 127\n' > "$FAKEBINDIR/python3"
chmod +x "$FAKEBINDIR/python3"
F=$(mktemp_mk)
cat > "$F" << 'MKEOF'
SHELL := bash
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
.PHONY: build
## Default target
build:
@echo "ok"
MKEOF
PATH="$FAKEBINDIR:$PATH" _run "$F" 0
assert_exit "degraded run exits with warnings" 1
assert_not_contains "no hard exit on missing python3" "ERROR.*python3"
assert_contains "warns about missing python3" "python3 not found|mbake.*skipped"
assert_contains "mbake stages shown as skipped" "Skipped.*mbake"
assert_contains "custom checks section printed" "\\[CUSTOM CHECKS\\]"
assert_contains "custom checks executed" "No additional issues found|No \\.PHONY declarations found|Missing \\.DELETE_ON_ERROR declaration"
assert_contains "degraded summary is warning mode" "Validation PASSED with warnings|Validation PASSED"
else
echo ""
echo "── offline suite skipped (set MAKEFILE_VALIDATOR_TEST_SUITE=offline|all) ──"
fi
if [[ "$RUN_INTEGRATION" -eq 1 ]]; then
# ── Integration: ensure mbake path executes when skip mode is disabled ────
echo ""
echo "── integration: mbake path is exercised ──────────────────────────────"
_run "$EXAMPLES_DIR/good-makefile.mk" 0
assert_not_contains "integration run does not use skip mode" "MBAKE_SKIP_INSTALL=1"
assert_not_contains "mbake stages are not skipped" "Skipped.*mbake not available|Skipped.*MBAKE_SKIP_INSTALL=1"
assert_contains "venv setup is attempted" "ENVIRONMENT SETUP|Creating temporary venv|Installing mbake"
assert_contains "mbake validation stage is present" "Running mbake validate|mbake validation passed|mbake validation failed"
# ── Edge: .PRECIOUS (non-list special target) not flagged by format check ─
# When mbake reports "Unknown special target '.PRECIOUS'", it should be
# treated as a known mbake false-positive (not a real error or warning).
echo ""
echo "── integration: .PRECIOUS not flagged as mbake error ─────────────────"
F=$(mktemp_mk)
cat > "$F" << 'MKEOF'
SHELL := bash
.DELETE_ON_ERROR:
.PRECIOUS: %.tar.gz
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
.PHONY: build
## Default target
build:
@echo "ok"
MKEOF
_run "$F" 0
# .PRECIOUS is valid GNU Make but mbake may not know it; should be info, not warning/error
assert_not_contains ".PRECIOUS not counted as error" "Errors:[[:space:]]+[1-9]"
else
echo ""
echo "── integration suite skipped (set MAKEFILE_VALIDATOR_TEST_SUITE=integration|all) ──"
fi
# ─── Summary ────────────────────────────────────────────────────────────────
echo ""
echo "══════════════════════════════════════════════════════════════════════"
printf " Results: %d passed, %d failed\n" "$PASS" "$FAIL"
echo "══════════════════════════════════════════════════════════════════════"
echo ""
[[ "$FAIL" -eq 0 ]]
#!/usr/bin/env bash
# Makefile Validator Script
# Validates Makefile syntax, best practices, security, and optimization using mbake tool
# Features: venv isolation, automatic cleanup via trap, comprehensive validation
set -euo pipefail
# Colors for output (supports NO_COLOR standard: https://no-color.org/)
if [ -n "${NO_COLOR:-}" ]; then
RED=''
YELLOW=''
GREEN=''
BLUE=''
NC=''
else
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
fi
# Counters
ERRORS=0
WARNINGS=0
INFO=0
# mbake availability flag — set to 1 by setup_venv when the venv+mbake are ready.
# Stays 0 when python3/pip3 are absent or the venv/install step fails, allowing
# the validator to fall back to GNU make + custom checks only.
MBAKE_AVAILABLE=0
MBAKE_SKIP_INSTALL_MODE=0
case "${MBAKE_SKIP_INSTALL:-0}" in
1|true|TRUE|yes|YES|on|ON)
MBAKE_SKIP_INSTALL_MODE=1
;;
0|false|FALSE|no|NO|off|OFF|"")
MBAKE_SKIP_INSTALL_MODE=0
;;
*)
echo -e "${YELLOW}[WARNING]${NC} Unsupported MBAKE_SKIP_INSTALL='${MBAKE_SKIP_INSTALL}', treating as disabled"
((WARNINGS+=1))
;;
esac
# Temporary venv directory (unique per invocation, respects TMPDIR)
VENV_DIR="${TMPDIR:-/tmp}/makefile-validator-venv-$$"
CLEANUP_DONE=0
# Cleanup function - always runs on exit
cleanup() {
if [ "$CLEANUP_DONE" -eq 0 ]; then
CLEANUP_DONE=1
# Safety check: only remove if it's our temp venv (works with custom TMPDIR)
if [ -d "$VENV_DIR" ] && [[ "$VENV_DIR" == */makefile-validator-venv-* ]]; then
echo -e "${BLUE}[CLEANUP]${NC} Removing temporary venv..."
rm -rf "$VENV_DIR"
fi
fi
}
# Register cleanup trap for all exit scenarios
trap cleanup EXIT INT TERM
# Print error and exit
error_exit() {
echo -e "${RED}[ERROR]${NC} $1" >&2
exit 1
}
# Print section header
print_header() {
echo ""
echo "========================================"
echo "$1"
echo "========================================"
}
# Print sub-header
print_subheader() {
echo -e "\n${BLUE}[$1]${NC}"
}
# Check dependencies.
# python3 and pip3 are needed only for the mbake stages; their absence degrades
# coverage but does not abort — GNU make syntax and all custom checks still run.
check_dependencies() {
local mbake_prereqs_ok=1
if [ "$MBAKE_SKIP_INSTALL_MODE" -eq 1 ]; then
echo -e "${BLUE}ℹ${NC} MBAKE_SKIP_INSTALL=1 enabled — deterministic mode will skip mbake stages"
((INFO+=1))
else
if ! command -v python3 &> /dev/null; then
echo -e "${YELLOW}[WARNING]${NC} python3 not found — mbake stages will be skipped"
echo " Install python3 to enable mbake validation and format-check coverage"
((WARNINGS+=1))
mbake_prereqs_ok=0
fi
if ! command -v pip3 &> /dev/null; then
echo -e "${YELLOW}[WARNING]${NC} pip3 not found — mbake stages will be skipped"
echo " Install pip3 to enable mbake validation and format-check coverage"
((WARNINGS+=1))
mbake_prereqs_ok=0
fi
fi
if ! command -v make &> /dev/null; then
echo -e "${YELLOW}[WARNING]${NC} GNU make not found — syntax validation will be limited"
((WARNINGS+=1))
fi
# Signal to setup_venv (and main) that it is worth attempting venv setup
if [ "$MBAKE_SKIP_INSTALL_MODE" -eq 0 ] && [ "$mbake_prereqs_ok" -eq 1 ]; then
MBAKE_AVAILABLE=1
fi
}
# Setup virtual environment and install mbake.
# On failure (offline, proxy, bad python env) the function warns and clears
# MBAKE_AVAILABLE so the caller skips all mbake-dependent stages.
setup_venv() {
print_subheader "ENVIRONMENT SETUP"
echo "Creating temporary venv at: $VENV_DIR"
if ! python3 -m venv "$VENV_DIR" 2>&1; then
echo -e "${YELLOW}⚠${NC} Failed to create virtual environment — mbake stages will be skipped"
((WARNINGS+=1))
MBAKE_AVAILABLE=0
return 0
fi
# Activate venv
# shellcheck source=/dev/null
source "$VENV_DIR/bin/activate"
echo "Installing mbake..."
if ! pip3 install --quiet mbake 2>&1; then
echo -e "${YELLOW}⚠${NC} Failed to install mbake — mbake stages will be skipped"
echo " Ensure network access or an internal PyPI mirror is available, then rerun"
((WARNINGS+=1))
MBAKE_AVAILABLE=0
return 0
fi
MBAKE_AVAILABLE=1
echo -e "${GREEN}✓${NC} Environment ready"
}
# Validate Makefile exists and is readable
validate_file() {
local file=$1
if [ ! -f "$file" ]; then
error_exit "File not found: $file"
fi
if [ ! -r "$file" ]; then
error_exit "File not readable: $file"
fi
}
# Basic syntax check using GNU make
syntax_check() {
local file=$1
print_subheader "SYNTAX CHECK (GNU make)"
if ! command -v make &> /dev/null; then
echo -e "${YELLOW}⚠${NC} Skipped - GNU make not installed"
return 0
fi
# Get absolute path to Makefile
local abs_file
abs_file=$(cd "$(dirname "$file")" && pwd)/$(basename "$file")
local makefile_dir
makefile_dir=$(dirname "$abs_file")
local makefile_name
makefile_name=$(basename "$abs_file")
# Run make from the Makefile's directory to resolve relative paths correctly
# Capture output and exit code in a single invocation (avoids running make twice)
local make_output make_exit=0
make_output=$(cd "$makefile_dir" && make -f "$makefile_name" -n 2>&1) || make_exit=$?
if [ "$make_exit" -eq 0 ]; then
echo -e "${GREEN}✓${NC} No syntax errors found"
else
echo -e "${RED}✗${NC} Syntax errors detected:"
echo "$make_output"
((ERRORS+=1))
return 1
fi
}
# Run mbake validation
mbake_validation() {
local file=$1
print_subheader "MBAKE VALIDATION"
echo "Running mbake validate..."
if mbake validate "$file" 2>&1; then
echo -e "${GREEN}✓${NC} mbake validation passed"
else
echo -e "${RED}✗${NC} mbake validation failed"
((ERRORS+=1))
fi
}
# Run mbake format check
mbake_format_check() {
local file=$1
print_subheader "MBAKE FORMAT CHECK"
echo "Checking formatting consistency..."
# Capture output - mbake may wrap long lines
local format_output
format_output=$(mbake format --check "$file" 2>&1)
local format_exit=$?
# Join multi-line output for easier pattern matching (mbake wraps at ~80 chars)
# Also normalize whitespace (collapse multiple spaces to single space)
local format_oneline
format_oneline=$(echo "$format_output" | tr '\n' ' ' | tr -s ' ')
# Check for known false positives (mbake limitation with GNU Make special targets).
# mbake does not recognise many valid GNU Make special targets (.DELETE_ON_ERROR,
# .SUFFIXES, .ONESHELL, .POSIX, .PRECIOUS, .NOTPARALLEL, .INTERMEDIATE, etc.).
# The sed cleaning below already strips all of them; this flag just controls
# whether the "known mbake limitation" info note is printed.
local has_unknown_special_target=0
if echo "$format_oneline" | grep -qE "Unknown special target '\.[A-Z_]+'"; then
has_unknown_special_target=1
fi
# Check if there are real formatting issues ("Would reformat:" indicates changes needed)
local has_reformat=0
if echo "$format_oneline" | grep -q "Would reformat:"; then
has_reformat=1
fi
# Check for other errors (not related to unknown special targets)
local has_other_errors=0
# Remove known false positive patterns completely (including "Error:" before them)
# Pattern matches: "some_file.mk:0: Error: Unknown special target '.DELETE_ON_ERROR'"
local cleaned_output
cleaned_output=$(echo "$format_oneline" | sed -E "s/[^ ]*:[0-9]+: Error: Unknown special target '\.[A-Z_]+'//g")
if echo "$cleaned_output" | grep -qE "Error:|Fatal error"; then
has_other_errors=1
fi
# Decision logic
if [ $format_exit -eq 0 ]; then
echo -e "${GREEN}✓${NC} Formatting is consistent"
elif [ $has_unknown_special_target -eq 1 ] && [ $has_reformat -eq 0 ] && [ $has_other_errors -eq 0 ]; then
# Only false positive about unknown special targets - treat as success
echo -e "${GREEN}✓${NC} Formatting is consistent"
echo -e "${BLUE}ℹ${NC} mbake reported unknown special targets (this is a known mbake limitation)"
elif [ $has_reformat -eq 1 ] || [ $has_other_errors -eq 1 ]; then
# Real formatting issues or other errors exist
echo "$format_output" | grep -v "Unknown special target" | grep -v "^$" || true
echo -e "${YELLOW}⚠${NC} Formatting issues found"
echo ""
echo "Run 'mbake format $file' to fix formatting issues"
echo "Or run 'mbake format --diff $file' to preview changes"
((WARNINGS+=1))
else
# Unknown case - show as info, don't warn
echo -e "${GREEN}✓${NC} Formatting is consistent"
if [ $has_unknown_special_target -eq 1 ]; then
echo -e "${BLUE}ℹ${NC} mbake reported unknown special targets (this is a known mbake limitation)"
fi
fi
}
# Custom security and best practice checks
custom_checks() {
local file=$1
print_subheader "CUSTOM CHECKS"
local found_issues=0
# ============================================================
# CRITICAL: Check for .DELETE_ON_ERROR (GNU Make best practice)
# ============================================================
if ! grep -q "^\.DELETE_ON_ERROR:" "$file"; then
echo -e "${YELLOW}⚠${NC} Missing .DELETE_ON_ERROR declaration"
echo " GNU Make recommends this to delete targets on recipe failure"
echo " Add '.DELETE_ON_ERROR:' at the top of your Makefile"
echo " See: https://www.gnu.org/software/make/manual/html_node/Special-Targets.html"
((WARNINGS+=1))
found_issues=1
fi
# ============================================================
# Check for explicit SHELL setting (modern best practice)
# ============================================================
if ! grep -qE "^SHELL\s*:?=\s*(bash|/bin/bash|/usr/bin/bash)" "$file"; then
echo -e "${BLUE}ℹ${NC} No explicit SHELL setting"
echo " Consider 'SHELL := bash' for predictable behavior"
echo " See: https://tech.davis-hansson.com/p/make/"
((INFO+=1))
found_issues=1
fi
# ============================================================
# Check for recommended MAKEFLAGS settings
# ============================================================
if ! grep -q "MAKEFLAGS.*--warn-undefined-variables" "$file"; then
echo -e "${BLUE}ℹ${NC} Consider 'MAKEFLAGS += --warn-undefined-variables'"
echo " This alerts you to undefined Make variable references"
((INFO+=1))
found_issues=1
fi
if ! grep -q "MAKEFLAGS.*--no-builtin-rules" "$file"; then
echo -e "${BLUE}ℹ${NC} Consider 'MAKEFLAGS += --no-builtin-rules'"
echo " This disables built-in implicit rules for faster builds"
((INFO+=1))
found_issues=1
fi
# Check for .PHONY declarations
if ! grep -q "^\.PHONY:" "$file"; then
echo -e "${YELLOW}⚠${NC} No .PHONY declarations found"
echo " Consider adding .PHONY for targets that don't create files"
echo " Example: .PHONY: clean test install"
((WARNINGS+=1))
found_issues=1
fi
# Check for tabs vs spaces in recipes (improved regex)
# Catches 2, 4, or 8 space indentation that should be tabs
local space_lines
space_lines=$(grep -nE "^( | | )[a-zA-Z@\$\(]" "$file" 2>/dev/null | head -5) || true
if [ -n "$space_lines" ]; then
echo -e "${RED}✗${NC} Potential spaces instead of tabs in recipes detected:"
echo "$space_lines"
echo " Makefiles require TAB characters for recipe indentation"
((ERRORS+=1))
found_issues=1
fi
# Check for hardcoded credentials (expanded pattern for common credential names)
# Note: grep -n adds "N:" prefix, so filter must match "N:# comment" format, not "# comment"
local cred_lines
cred_lines=$(grep -niE '(password|secret|api[_-]?key|apikey|token|private[_-]?key|aws_access_key|aws_secret_access_key|github_token|auth_token|credentials|azure_client_secret|database_url|db_password|ssh_key|ssl_key|encryption_key)\s*[:?]?=' "$file" 2>/dev/null | grep -vE "^[0-9]+:[[:space:]]*#" | head -3) || true
if [ -n "$cred_lines" ]; then
echo -e "${RED}✗${NC} Potential hardcoded credentials detected:"
echo "$cred_lines"
echo " Use environment variables or secret management instead"
((ERRORS+=1))
found_issues=1
fi
# Check for TRULY unsafe variable expansion patterns
# Only flag variables that are NOT defined with defaults in the same file
# Look for rm/sudo/curl/wget with variables that could be empty or user-controlled
# Handles both UPPERCASE and lowercase variable names
local unsafe_vars=""
while IFS= read -r line; do
# Extract variable name from $(VAR) or $(var) pattern (any case)
var_name=$(echo "$line" | grep -oE '\$\([a-zA-Z_][a-zA-Z0-9_]*\)' | head -1 | tr -d '$()')
if [ -n "$var_name" ]; then
# Check if variable has a default value with := or ?=
if ! grep -qE "^${var_name}\s*[:?]=" "$file"; then
# Variable has no default, this is potentially unsafe
unsafe_vars="${unsafe_vars}${line}\n"
fi
fi
done < <(grep -E '\$\([a-zA-Z_][a-zA-Z0-9_]*\)' "$file" | grep -E '(rm -rf|sudo|curl|wget)')
if [ -n "$unsafe_vars" ]; then
echo -e "${YELLOW}⚠${NC} Variables without defaults used in dangerous commands:"
echo -e "$unsafe_vars" | head -3
echo " Consider adding default values (VAR := value) or validation"
((WARNINGS+=1))
found_issues=1
fi
# Check for missing error handling in recipes
if grep -E "^\t[^@#-]" "$file" | grep -vE "set -e|pipefail|\|\||&&" | grep -q .; then
echo -e "${BLUE}ℹ${NC} Some recipe commands may lack error handling"
echo " Consider using 'set -e', '||', '&&' or '-' prefix for error control"
((INFO+=1))
found_issues=1
fi
# Check for missing .INTERMEDIATE or .SECONDARY for temporary files
if grep -E "\.o|\.tmp|\.temp" "$file" | grep -q ":"; then
if ! grep -qE "^\.(INTERMEDIATE|SECONDARY):" "$file"; then
echo -e "${BLUE}ℹ${NC} Intermediate files detected (.o, .tmp, .temp)"
echo " Consider using .INTERMEDIATE or .SECONDARY for automatic cleanup"
((INFO+=1))
found_issues=1
fi
fi
# Check for missing default target documentation
# Look for various patterns: "Default target", "(default)", "Main target", "default:" before all:
if ! grep -qE "^##?.*(([Dd]efault|[Mm]ain|[Ff]irst).*target|target.*(default|main)|^\s*all:.*#.*default|\(default\))" "$file"; then
# Also check if there's a ## comment right before "all:" target
if ! grep -B1 "^all:" "$file" | grep -qE "^##"; then
echo -e "${BLUE}ℹ${NC} No documentation for default target"
echo " Consider adding a comment explaining the default target"
((INFO+=1))
found_issues=1
fi
fi
# Check for recursive variable expansion with shell commands (performance issue)
# Pattern uses single quotes so \$ reaches grep as a literal-dollar matcher,
# not as an end-of-line anchor (which double-quote expansion would produce).
local shell_lines
shell_lines=$(grep -nE '^\s*[A-Z_]+\s*=\s*\$\(shell' "$file" 2>/dev/null | head -3) || true
if [ -n "$shell_lines" ]; then
echo -e "${YELLOW}⚠${NC} Shell commands with recursive expansion '=' found:"
echo "$shell_lines"
echo " Use ':=' for immediate expansion to avoid repeated shell calls"
((WARNINGS+=1))
found_issues=1
fi
# Check for .SUFFIXES (optimization for disabling built-in rules)
# This is informational - not all Makefiles need this
if grep -qE "^%\." "$file" || grep -qE "^\.[a-z]+\.[a-z]+:" "$file"; then
if ! grep -q "^\.SUFFIXES:" "$file"; then
echo -e "${BLUE}ℹ${NC} Pattern/suffix rules found but no .SUFFIXES declaration"
echo " Consider adding '.SUFFIXES:' to disable built-in rules for faster builds"
((INFO+=1))
found_issues=1
fi
fi
# Check for using 'make' instead of '$(MAKE)' in recursive calls
# Exclude: echo statements, comments, and string literals
# Match 'make' followed by whitespace OR at end of line (bare 'make' with no args)
local make_lines
make_lines=$(grep -nE "^\t[^#@]*\bmake(\s|$)" "$file" 2>/dev/null | grep -vE '(echo|printf|MAKE\)|".*make.*"|'"'"'.*make.*'"'"')' | head -3) || true
if [ -n "$make_lines" ]; then
echo -e "${YELLOW}⚠${NC} Direct 'make' call in recipe (should use \$(MAKE)):"
echo "$make_lines"
echo " Use '\$(MAKE)' for recursive make calls to preserve flags and options"
((WARNINGS+=1))
found_issues=1
fi
# Check for .SUFFIXES recommendation for large Makefiles
local target_count
target_count=$(grep -cE "^[a-zA-Z_][a-zA-Z0-9_-]*:" "$file" 2>/dev/null) || target_count=0
if [ "$target_count" -gt 10 ] && ! grep -q "^\.SUFFIXES:" "$file"; then
echo -e "${BLUE}ℹ${NC} Large Makefile ($target_count targets) without .SUFFIXES"
echo " Consider adding '.SUFFIXES:' to disable built-in rules for faster builds"
((INFO+=1))
found_issues=1
fi
# ============================================================
# Check for .ONESHELL without proper error handling
# ============================================================
if grep -q "^\.ONESHELL:" "$file"; then
# .ONESHELL is used - check if .SHELLFLAGS includes recommended flags
local has_shellflags=0
local has_e_flag=0
local has_u_flag=0
local has_pipefail=0
if grep -qE "^\.?SHELLFLAGS\s*:?=" "$file"; then
has_shellflags=1
local shellflags_line
shellflags_line=$(grep -E "^\.?SHELLFLAGS\s*:?=" "$file" | head -1)
# Check for -e flag (can be standalone -e or combined like -eu, -euo, etc.)
# Match: -e, -eu, -euo, -eux, etc. (e after dash, possibly with other letters)
if [[ "$shellflags_line" =~ -[a-zA-Z]*e[a-zA-Z]* ]] || [[ "$shellflags_line" == *"-e "* ]] || [[ "$shellflags_line" == *"-e\""* ]]; then
has_e_flag=1
fi
# Check for -u flag (can be standalone -u or combined like -eu, -euo, etc.)
if [[ "$shellflags_line" =~ -[a-zA-Z]*u[a-zA-Z]* ]] || [[ "$shellflags_line" == *"-u "* ]] || [[ "$shellflags_line" == *"-u\""* ]]; then
has_u_flag=1
fi
# Check for pipefail (always as -o pipefail)
[[ "$shellflags_line" == *"pipefail"* ]] && has_pipefail=1
fi
if [ "$has_shellflags" -eq 0 ]; then
# No SHELLFLAGS at all, check if recipes commonly use set -e
local oneshell_recipes
oneshell_recipes=$(grep -cE "^\t" "$file" 2>/dev/null) || oneshell_recipes=0
local set_e_count
set_e_count=$(grep -cE "^\t.*set -e" "$file" 2>/dev/null) || set_e_count=0
# If less than 33% of recipe blocks have set -e, warn
if [ "$oneshell_recipes" -gt 0 ] && [ "$set_e_count" -lt $((oneshell_recipes / 3)) ]; then
echo -e "${YELLOW}⚠${NC} .ONESHELL used without .SHELLFLAGS"
echo " With .ONESHELL, recipe errors (except the last line) are silently ignored"
echo " Fix: Add '.SHELLFLAGS := -eu -o pipefail -c'"
echo " See: https://www.gnu.org/software/make/manual/html_node/One-Shell.html"
((WARNINGS+=1))
found_issues=1
fi
elif [ "$has_e_flag" -eq 0 ]; then
echo -e "${YELLOW}⚠${NC} .ONESHELL with SHELLFLAGS missing -e flag"
echo " Without -e, errors in recipe commands are ignored"
echo " Consider: .SHELLFLAGS := -eu -o pipefail -c"
((WARNINGS+=1))
found_issues=1
else
# Has -e, but recommend full flags as info
if [ "$has_u_flag" -eq 0 ] || [ "$has_pipefail" -eq 0 ]; then
echo -e "${BLUE}ℹ${NC} .SHELLFLAGS could include additional safety flags"
echo " Recommended: .SHELLFLAGS := -eu -o pipefail -c"
echo " -u: error on undefined variables, -o pipefail: catch pipe failures"
((INFO+=1))
found_issues=1
fi
fi
fi
# ============================================================
# Check for .EXPORT_ALL_VARIABLES (security concern)
# ============================================================
if grep -q "^\.EXPORT_ALL_VARIABLES:" "$file"; then
echo -e "${YELLOW}⚠${NC} .EXPORT_ALL_VARIABLES used - security consideration"
echo " This exports ALL Make variables to subprocesses, potentially leaking sensitive data"
echo " Consider using 'export VAR' for specific variables instead"
((WARNINGS+=1))
found_issues=1
fi
# ============================================================
# Check for order-only prerequisites (directory best practice)
# ============================================================
# If mkdir -p is used in recipes and order-only | syntax is not used
if grep -qE "^\t.*mkdir.*-p" "$file"; then
if ! grep -qE '\| \$\(' "$file"; then
echo -e "${BLUE}ℹ${NC} mkdir in recipes without order-only prerequisites"
echo " Consider using order-only prerequisites for directories:"
echo " Example: \$(BUILD_DIR)/app: \$(SOURCES) | \$(BUILD_DIR)"
echo " This prevents unnecessary rebuilds when only timestamps change"
((INFO+=1))
found_issues=1
fi
fi
# ============================================================
# Check for parallel-unsafe patterns without .NOTPARALLEL
# ============================================================
if grep -qE "^\t.*(docker build|npm install|pip install|yarn install|bundle install)" "$file"; then
if ! grep -q "^\.NOTPARALLEL:" "$file"; then
echo -e "${BLUE}ℹ${NC} Parallel-sensitive commands detected (npm/docker/pip install)"
echo " Consider using .NOTPARALLEL for targets with these commands"
echo " Or add proper dependencies to prevent race conditions"
((INFO+=1))
found_issues=1
fi
fi
if [ "$found_issues" -eq 0 ]; then
echo -e "${GREEN}✓${NC} No additional issues found"
fi
}
# Run checkmake if available
checkmake_validation() {
local file=$1
print_subheader "CHECKMAKE VALIDATION (optional)"
if ! command -v checkmake &> /dev/null; then
echo -e "${BLUE}ℹ${NC} checkmake not installed - skipping additional linting"
echo " Install with: go install github.com/checkmake/checkmake/cmd/checkmake@latest"
return 0
fi
echo "Running checkmake..."
local checkmake_output
checkmake_output=$(checkmake "$file" 2>&1) || true
if [ -n "$checkmake_output" ]; then
echo "$checkmake_output"
# Count warnings from checkmake
local cm_warnings
cm_warnings=$(echo "$checkmake_output" | grep -c "WARN" 2>/dev/null) || cm_warnings=0
if [ "$cm_warnings" -gt 0 ]; then
((WARNINGS+=cm_warnings))
fi
else
echo -e "${GREEN}✓${NC} checkmake validation passed"
fi
}
# Run unmake if available (for POSIX portability checks)
unmake_validation() {
local file=$1
print_subheader "UNMAKE VALIDATION (optional)"
if ! command -v unmake &> /dev/null; then
echo -e "${BLUE}ℹ${NC} unmake not installed - skipping POSIX portability checks"
echo " See: https://github.com/mcandre/unmake"
return 0
fi
echo "Running unmake for POSIX portability..."
local unmake_output
unmake_output=$(unmake "$file" 2>&1) || true
if [ -n "$unmake_output" ]; then
echo "$unmake_output"
# Count warnings from unmake
local um_warnings
um_warnings=$(echo "$unmake_output" | grep -cE "(warning|Warning)" 2>/dev/null) || um_warnings=0
if [ "$um_warnings" -gt 0 ]; then
((WARNINGS+=um_warnings))
fi
else
echo -e "${GREEN}✓${NC} unmake validation passed (POSIX compatible)"
fi
}
# Print summary
print_summary() {
local file=$1
print_header "VALIDATION SUMMARY"
echo "File: $file"
echo ""
echo -e "${RED}Errors: ${NC} $ERRORS"
echo -e "${YELLOW}Warnings:${NC} $WARNINGS"
echo -e "${BLUE}Info: ${NC} $INFO"
echo ""
if [ "$ERRORS" -gt 0 ]; then
echo -e "${RED}⚠ Validation FAILED - errors must be fixed${NC}"
return 2
elif [ "$WARNINGS" -gt 0 ]; then
echo -e "${YELLOW}⚠ Validation PASSED with warnings${NC}"
return 1
else
echo -e "${GREEN}✓ Validation PASSED${NC}"
return 0
fi
}
# Main execution
main() {
if [ $# -eq 0 ]; then
echo "Usage: $0 <Makefile>"
echo ""
echo "Validates Makefile for syntax errors, best practices, security issues,"
echo "and optimization opportunities using mbake and custom checks."
echo ""
echo "Examples:"
echo " $0 Makefile"
echo " $0 path/to/Makefile"
echo " $0 project.mk"
exit 1
fi
local makefile=$1
print_header "MAKEFILE VALIDATOR"
echo "File: $makefile"
# Validation pipeline
check_dependencies
validate_file "$makefile"
# Set up mbake venv only when python3+pip3 are present and skip mode is disabled.
if [ "$MBAKE_AVAILABLE" -eq 1 ]; then
setup_venv || true
fi
# Run syntax check — always runs (uses GNU make, not mbake)
syntax_check "$makefile" || true
# Run mbake stages only when the venv is ready
if [ "$MBAKE_AVAILABLE" -eq 1 ]; then
mbake_validation "$makefile" || true
mbake_format_check "$makefile" || true
else
print_subheader "MBAKE VALIDATION"
if [ "$MBAKE_SKIP_INSTALL_MODE" -eq 1 ]; then
echo -e "${BLUE}ℹ${NC} Skipped — MBAKE_SKIP_INSTALL=1 (deterministic mode)"
else
echo -e "${BLUE}ℹ${NC} Skipped — mbake not available (python3, pip3, and network required)"
fi
print_subheader "MBAKE FORMAT CHECK"
if [ "$MBAKE_SKIP_INSTALL_MODE" -eq 1 ]; then
echo -e "${BLUE}ℹ${NC} Skipped — MBAKE_SKIP_INSTALL=1 (deterministic mode)"
else
echo -e "${BLUE}ℹ${NC} Skipped — mbake not available (python3, pip3, and network required)"
fi
((INFO+=2))
fi
# Run custom checks — always runs (pure bash + grep, no external tools)
custom_checks "$makefile" || true
# Run checkmake if available (optional, continue even if it fails)
checkmake_validation "$makefile" || true
# Run unmake if available (optional, continue even if it fails)
unmake_validation "$makefile" || true
# Print summary and return appropriate exit code
print_summary "$makefile"
local exit_code=$?
return $exit_code
}
# Run main with all arguments and exit with its return code
main "$@"
exit $?
Related skills
How it compares
Pick makefile-validator over generic shell linters when you need mbake-backed Makefile formatting plus GNU make syntax and security pattern checks in one deterministic script.
FAQ
What does makefile-validator check?
makefile-validator checks GNU make syntax via dry-run, mbake formatting and lint rules, .PHONY and tab indentation, circular dependencies, parallelism safety, and custom security patterns. Optional checkmake and unmake stages add portability coverage when installed.
What exit codes does makefile-validator return?
makefile-validator returns exit code 0 when clean, 1 for warnings, and 2 for blocking errors. CI pipelines can gate merges on code 2 and optionally warn on code 1 using the bundled GitHub Action wrapper.