All skills
akin-ozer avatar

/makefile-generator

@1b2342a

Create, generate, or scaffold Makefiles with .PHONY targets and build automation.

Use this Skill: https://skilld.dev/gh/akin-ozer/cc-devops-skills/makefile-generator

This session only. Nothing lands on disk.

docsoptimization-guide.md

≈4.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Makefile Optimization Guide

Overview

This guide covers techniques for optimizing Makefile performance, including parallel builds, dependency tracking, incremental builds, caching strategies, and performance profiling.

Parallel Builds

Enabling Parallel Execution

# Run with 4 parallel jobs
make -j4

# Use all CPU cores
make -j$(nproc)

# Unlimited parallel jobs (careful!)
make -j

Making Makefiles Parallel-Safe

Problem: Shared resources

# WRONG: Multiple rules write to same file
target1:
	echo "data1" >> shared.log

target2:
	echo "data2" >> shared.log

# With -j2, file corruption likely!

Solution: Proper dependencies

# RIGHT: Serialize access with dependencies
target2: target1

# Or use separate files
target1:
	echo "data1" > target1.log

target2:
	echo "data2" > target2.log

Controlling Parallelism

# Disable parallel builds for this Makefile
.NOTPARALLEL:

# Disable parallelism for specific targets
.NOTPARALLEL: install clean

# Serialize specific targets
install: build
	# Install runs after build completes

GNU Make 4.4+ Parallel Control Features

GNU Make 4.4 (released October 2022) introduced new features for fine-grained parallel control. These are becoming part of the upcoming POSIX standard.

.WAIT Special Target

The .WAIT target provides explicit ordering without creating artificial dependencies:

# .WAIT ensures prerequisites to its left complete
# before starting prerequisites to its right
all: compile .WAIT link .WAIT package

# Equivalent behavior without .WAIT would require:
# link: compile
# package: link
# But .WAIT is cleaner when targets are independent conceptually

Use Cases for .WAIT:

# Build phases with explicit ordering
build: setup .WAIT compile .WAIT test .WAIT package
	@echo "Build complete"

# Parallel within phases, serial between phases
ci: lint fmt .WAIT test.unit test.integration .WAIT build
# lint and fmt run in parallel
# then unit and integration tests run in parallel
# finally build runs

# Database migrations before tests
test: migrate .WAIT run-tests
	@echo "Tests complete"

Important Notes:

  • .WAIT only affects parallel builds (make -j)
  • With sequential execution, .WAIT has no effect
  • .WAIT doesn't create actual dependencies, just ordering
  • Available in GNU Make 4.4+ (check with make --version)
.NOTPARALLEL with Prerequisites (Enhanced)

In Make 4.4+, .NOTPARALLEL can take specific targets as prerequisites:

# Traditional: Disable ALL parallel execution
.NOTPARALLEL:

# NEW in 4.4: Serialize only specific targets
.NOTPARALLEL: install deploy cleanup

# This implicitly adds .WAIT between each prerequisite
# of the listed targets

When to Use .NOTPARALLEL with Prerequisites:

# Deployment must be serial (avoid race conditions)
.NOTPARALLEL: deploy

deploy: deploy-database deploy-backend deploy-frontend
	@echo "Deployment complete"
# deploy-database -> deploy-backend -> deploy-frontend (serial)

# But compilation can still be parallel
build: $(OBJECTS)
	$(CC) $^ -o $(TARGET)
# Object files compile in parallel (unaffected by .NOTPARALLEL: deploy)
Version Checking for Make 4.4+ Features

Check Make version before using 4.4+ features:

# Check Make version (4.4 = 4.4, need >= 4.4)
MAKE_VERSION_MAJOR := $(word 1,$(subst ., ,$(MAKE_VERSION)))
MAKE_VERSION_MINOR := $(word 2,$(subst ., ,$(MAKE_VERSION)))

# Simple version check
ifeq ($(shell expr $(MAKE_VERSION_MAJOR) \>= 4),1)
ifeq ($(shell expr $(MAKE_VERSION_MINOR) \>= 4),1)
    HAVE_WAIT := 1
endif
endif

# Alternative: Graceful degradation
ifdef HAVE_WAIT
    # Use .WAIT for modern Make
    all: compile .WAIT link
else
    # Fall back to dependencies for older Make
    link: compile
    all: link
endif

Practical Version Check Pattern:

# At the top of your Makefile
MIN_MAKE_VERSION := 4.4
CURRENT_MAKE_VERSION := $(MAKE_VERSION)

# Check and warn if using older Make
ifeq ($(shell printf '%s\n' "$(MIN_MAKE_VERSION)" "$(CURRENT_MAKE_VERSION)" | sort -V | head -n1),$(MIN_MAKE_VERSION))
    # Make version is sufficient
else
    $(warning GNU Make $(MIN_MAKE_VERSION)+ recommended. You have $(CURRENT_MAKE_VERSION))
    $(warning Some parallel control features may not work)
endif
Comparison: .WAIT vs Dependencies vs .NOTPARALLEL
Feature Use Case Make Version
Dependencies (b: a) Actual dependency relationship All
.WAIT Ordering without dependency 4.4+
.NOTPARALLEL: (global) Disable all parallel All
.NOTPARALLEL: target Serialize specific target's prereqs 4.4+

Example Comparison:

# Using dependencies (works in all Make versions)
# Problem: Creates false dependency relationship
link: compile
package: link
all: package

# Using .WAIT (Make 4.4+)
# Cleaner: Explicit ordering, no false dependencies
all: compile .WAIT link .WAIT package

# Using .NOTPARALLEL with targets (Make 4.4+)
# Best for: Targets that must never run in parallel
.NOTPARALLEL: deploy
deploy: step1 step2 step3

Optimal Parallel Structure

# Good parallel structure
SOURCES := src1.c src2.c src3.c src4.c
OBJECTS := $(SOURCES:.c=.o)

# All .o files can build in parallel
program: $(OBJECTS)
	$(CC) $^ -o $@

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

Parallel execution:

make -j4
# Compiles 4 .c files simultaneously
# Then links when all are done

Dependency Tracking

Accurate Dependencies

Problem: Incorrect dependencies

# WRONG: Missing header dependencies
main.o: main.c
	$(CC) -c $< -o $@

# If common.h changes, main.o won't rebuild!

Solution: Automatic dependency generation

# Generate dependencies during compilation
%.o: %.c
	$(CC) $(CFLAGS) -MMD -MP -c $< -o $@

# Include generated .d files
-include $(OBJECTS:.o=.d)

Generated dependency file (main.d):

main.o: main.c common.h utils.h
common.h:
utils.h:

Dependency Flags

# -MMD: Generate dependency file (.d)
# -MP: Add phony targets for headers
# -MF file: Specify dependency file name

DEPFLAGS = -MMD -MP -MF $(@:.o=.d)

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

Why -MP is Important

Without -MP:

# Generated main.d:
main.o: main.c utils.h

# If utils.h is deleted:
make: *** No rule to make target 'utils.h'. Stop.

With -MP:

# Generated main.d:
main.o: main.c utils.h
utils.h:

# If utils.h is deleted, make continues
# (assumes you also removed #include "utils.h")

Incremental Builds

Timestamp-Based Builds

Make rebuilds targets when prerequisites are newer:

# program rebuilt if any .o is newer
program: $(OBJECTS)
	$(CC) $^ -o $@

# main.o rebuilt if main.c or headers are newer
main.o: main.c common.h
	$(CC) -c main.c -o main.o

Optimizing Dependency Chains

Inefficient:

# Every source depends on config.h
# Changing config.h rebuilds EVERYTHING
main.o: main.c config.h
utils.o: utils.c config.h
helper.o: helper.c config.h

Better: Only include where needed

# Only main.c actually uses config.h
main.o: main.c config.h
utils.o: utils.c
helper.o: helper.c

Best: Use automatic dependencies

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

-include $(DEPENDS)
# Automatically tracks which headers each file uses

Intermediate File Management

# Mark intermediate files
.INTERMEDIATE: $(OBJECTS)
# Deleted after use

# Keep important intermediate files
.SECONDARY: important.o
# Not deleted

# Never delete these files
.PRECIOUS: %.o %.d
# Protected from deletion

Avoiding Unnecessary Rebuilds

Problem: Timestamp updates without changes

# WRONG: Always updates config.h
config.h: config.h.in
	sed 's/@VERSION@/$(VERSION)/g' $< > $@
	# Updates timestamp even if content unchanged!

Solution: Conditional update

# RIGHT: Only update if different
config.h: config.h.in
	sed 's/@VERSION@/$(VERSION)/g' $< > $@.tmp
	cmp -s $@.tmp $@ || mv $@.tmp $@
	rm -f $@.tmp

Build Caching

Compiler Cache (ccache)

# Use ccache for faster recompilation
CC := ccache gcc
CXX := ccache g++

# Or conditionally:
ifeq ($(shell command -v ccache 2>/dev/null),)
CC ?= gcc
else
CC ?= ccache gcc
endif

Benefits:

  • Caches compilation results
  • Speeds up clean rebuilds
  • Useful for CI/CD and switching branches

Distcc for Distributed Compilation

# Distributed compilation across network
CC := distcc gcc
CXX := distcc g++

# Set number of jobs based on available hosts
DISTCC_HOSTS := localhost/2 build1/4 build2/4
JOBS := 10

Build Directory Caching

# Keep build artifacts between clean builds
.PHONY: clean distclean

clean:
	$(RM) $(TARGET)
	# Keep .o and .d files for faster rebuild

distclean: clean
	$(RM) -r $(BUILDDIR)
	# Complete clean

Performance Optimization Techniques

1. Use := Instead of =

# SLOW: Recursive expansion (evaluated every use)
SOURCES = $(wildcard src/*.c)
OBJECTS = $(SOURCES:.c=.o)
# $(OBJECTS) re-runs wildcard every time!

# FAST: Simple expansion (evaluated once)
SOURCES := $(wildcard src/*.c)
OBJECTS := $(SOURCES:.c=.o)
# Evaluated once when defined

2. Minimize Shell Invocations

# SLOW: Multiple shell calls
FILES = $(shell ls *.c)
COUNT = $(shell ls *.c | wc -l)

# FAST: Single shell call
FILES := $(wildcard *.c)
COUNT := $(words $(FILES))

3. Use Static Pattern Rules

OBJECTS := main.o utils.o helper.o

# FASTER: Static pattern rule (make knows exact files)
$(OBJECTS): %.o: %.c
	$(CC) -c $< -o $@

# SLOWER: Pattern rule (make searches for matches)
%.o: %.c
	$(CC) -c $< -o $@

4. Reduce Makefile Parsing Time

# SLOW: Complex shell commands in variable assignment
VERSION = $(shell git describe --tags --always --dirty)

# FAST: Use := to evaluate once
VERSION := $(shell git describe --tags --always --dirty)

# FASTER: Cache in file
VERSION := $(file < VERSION.txt)

5. Avoid Recursive Make

Inefficient: Recursive Make

# Top-level Makefile
SUBDIRS := lib1 lib2 app

all:
	for dir in $(SUBDIRS); do $(MAKE) -C $$dir; done

Problems:

  • Multiple make invocations (slow)
  • Incorrect dependency tracking
  • Parallel builds broken

Efficient: Non-Recursive Make

# Single Makefile
LIB1_SRC := $(wildcard lib1/*.c)
LIB2_SRC := $(wildcard lib2/*.c)
APP_SRC := $(wildcard app/*.c)

ALL_SRC := $(LIB1_SRC) $(LIB2_SRC) $(APP_SRC)
OBJECTS := $(ALL_SRC:.c=.o)

# Single dependency tree
# Accurate parallel builds

Reference: "Recursive Make Considered Harmful" by Peter Miller

Performance Profiling

Timing Individual Targets

# Time recipe execution
%.o: %.c
	@echo "Compiling $<..."
	@time $(CC) $(CFLAGS) -c $< -o $@

Build Time Measurement

# Time entire build
time make -j4

# Output:
# real    0m12.345s
# user    0m45.678s
# sys     0m3.456s

Debug Output for Performance Analysis

# Show what make is doing
make -d

# Show only remake decisions
make -d --debug=basic

# Show implicit rule search
make -d --debug=implicit

# Profile make itself
make --profile=profile.log

Finding Bottlenecks

# Add timing to critical paths
$(TARGET): $(OBJECTS)
	@echo "==> Linking $(TARGET)"
	@time $(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@

%.o: %.c
	@echo "==> Compiling $<"
	@time $(CC) $(CFLAGS) -c $< -o $@

Optimization Best Practices

1. Structure for Parallelism

# Good: Independent compilation
OBJECTS := a.o b.o c.o d.o

program: $(OBJECTS)
	$(CC) $^ -o $@

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

# make -j4 compiles 4 files at once

2. Accurate Dependencies

# Use automatic dependency generation
CFLAGS += -MMD -MP
-include $(OBJECTS:.o=.d)

# Not manual maintenance

3. Minimal Clean

# Keep intermediate files by default
clean:
	$(RM) $(TARGET)

# Full clean only when needed
distclean: clean
	$(RM) $(OBJECTS) $(DEPENDS)

4. Efficient Variable Usage

# Use := for computed values
SOURCES := $(wildcard src/*.c)
OBJECTS := $(SOURCES:.c=.o)

# Use ?= for user overrides
CC ?= gcc
CFLAGS ?= -O2

5. Avoid Unnecessary Work

# Don't rebuild if nothing changed
config.h: config.h.in Makefile
	@sed 's/@VERSION@/$(VERSION)/g' $< > $@.tmp
	@if ! cmp -s $@ $@.tmp; then \
		echo "  GEN     $@"; \
		mv $@.tmp $@; \
	else \
		rm -f $@.tmp; \
	fi

Complete Optimized Example

# Optimized Makefile for C project
.DELETE_ON_ERROR:
.SUFFIXES:

PROJECT := optimized
VERSION := 1.0.0

# User-overridable (use ?=)
CC ?= gcc
CFLAGS ?= -Wall -Wextra -O2
PREFIX ?= /usr/local

# Computed once (use :=)
SRCDIR := src
BUILDDIR := build
OBJDIR := $(BUILDDIR)/obj

SOURCES := $(wildcard $(SRCDIR)/*.c)
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
DEPENDS := $(OBJECTS:.o=.d)
TARGET := $(BUILDDIR)/$(PROJECT)

# Check for ccache
ifneq ($(shell command -v ccache 2>/dev/null),)
    CC := ccache $(CC)
endif

# Optimization flags for dependencies
DEPFLAGS = -MMD -MP

.PHONY: all clean distclean profile

all: $(TARGET)

# Link (serial)
$(TARGET): $(OBJECTS)
	@mkdir -p $(@D)
	@echo "  LD      $@"
	$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@

# Compile (parallel-safe)
$(OBJDIR)/%.o: $(SRCDIR)/%.c
	@mkdir -p $(@D)
	@echo "  CC      $<"
	$(CC) $(CPPFLAGS) $(CFLAGS) $(DEPFLAGS) -c $< -o $@

# Include auto-generated dependencies
-include $(DEPENDS)

# Minimal clean (keeps .o for faster rebuild)
clean:
	$(RM) $(TARGET)

# Full clean
distclean:
	$(RM) -r $(BUILDDIR)

# Profile build
profile:
	time $(MAKE) clean
	time $(MAKE) -j$(shell nproc) all

Benchmarking Results

Example project: 100 C files

Configuration Build Time Rebuild Time
Sequential (make) 45s 12s
Parallel -j2 25s 7s
Parallel -j4 15s 4s
Parallel -j8 12s 3s
Parallel + ccache (cold) 14s 3s
Parallel + ccache (warm) 3s 1s

Key takeaways:

  • Parallel builds: 3-4x speedup
  • ccache (warm): 10x speedup on clean builds
  • Accurate dependencies: Only rebuild what changed

Advanced Optimization

Precompiled Headers

# Generate precompiled header
$(OBJDIR)/common.h.gch: $(SRCDIR)/common.h
	@mkdir -p $(@D)
	$(CC) $(CPPFLAGS) $(CFLAGS) -x c-header $< -o $@

# Use precompiled header
$(OBJDIR)/%.o: $(SRCDIR)/%.c $(OBJDIR)/common.h.gch
	$(CC) $(CPPFLAGS) $(CFLAGS) -include $(OBJDIR)/common.h -c $< -o $@

Link-Time Optimization (LTO)

# Enable LTO for release builds
release: CFLAGS += -flto -O3
release: LDFLAGS += -flto -O3
release: $(TARGET)

Unity Builds

# Combine all sources into one compilation unit
unity.c: $(SOURCES)
	@echo "Generating unity build..."
	@for src in $(SOURCES); do \
		echo "#include \"$$src\"" >> $@; \
	done

unity.o: unity.c
	$(CC) $(CFLAGS) -c $< -o $@

# Fast single-file compilation
# Trade-off: No parallel compilation

Profiling Tools

# Make's built-in profiling
make --profile=profile.log
# Analyze profile.log

# Time individual targets
make -d 2>&1 | grep -E "Considering|Must remake"

# strace for system call analysis
strace -c make 2>&1 | tail -20

# Remake (make debugger)
remake --debug

References

Source: SKILL.md on GitHub

No alerts16d4 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The Makefile Generator skill is a robust and well-documented utility for scaffolding build systems across various programming languages. It adheres to security best practices, providing clear guidelines for secrets management and input validation within generated Makefiles. No malicious patterns, obfuscation, or unauthorized network activities were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 1b2342a. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Steadyupdated 7 months ago

README badge

README badge for akin-ozer/cc-devops-skills/makefile-generator