Die Oberfläche ist lokalisiert; Skill-Beschreibungen und -Dokumente werden per KI automatisch in Ihre Sprache übersetzt. Code, Befehle, Pfade und Fachbegriffe bleiben im Original, maßgeblich ist das Dokument des Autors.
Skills entdecken
Entwicklung · SKILL.md

make-build-system

make-build-system

Implements GNU Make build automation including dependency graphs, phony targets, variable scoping, pattern rules, automatic variables, cross-compilation, and CI/CD integration for reproducible software builds.

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill-Dokumentation

GitHub

name: make-build-system description: Implements GNU Make build automation including dependency graphs, phony targets, variable scoping, pattern rules, automatic variables, cross-compilation, and CI/CD integration for reproducible software builds. license: MIT compatibility: opencode metadata: version: "2.0.0" domain: linux triggers: makefile, GNU make, build automation, incremental builds, phony targets, pattern rules, automatic variables, how do i automate builds with make archetypes: - tactical - generation anti_triggers: - brainstorming - vague ideation response_profile: verbosity: low directive_strength: high abstraction_level: operational role: implementation scope: implementation output-format: code content-types: [code, guidance, config, do-dont, examples] related-skills: coding-just-task-runner, coding-mage-build-tool, linux-systemd-services, coding-test-driven-development


GNU Make Build System

Implements reproducible, incremental software builds using GNU Make. Makefiles are not just lists of commands — they are declarative dependency graphs that encode build logic, variable scoping rules, platform detection, and automated workflows for compilation, testing, packaging, and deployment across diverse development environments.

TL;DR Checklist

  • Define all non-file targets as .PHONY
  • Use explicit pattern rules (%.o: %.c) instead of hardcoded recipes
  • Separate build variables by scope (global vs target-specific)
  • Validate dependency graphs with make -n before committing Makefiles
  • Use automatic variables ($@, lt;, $^, ${?}) to keep recipes DRY
  • Include .PHONY: all clean install test build at the top
  • Use $(shell ...) only when make itself cannot determine a value from file timestamps

When to Use

Use this skill when:

  • Writing or refactoring a Makefile for a C, C++, Go, Rust, or general-purpose project build pipeline
  • Automating repetitive development workflows (compile, test, lint, package, deploy) into a single make command
  • Setting up cross-compilation targets for different architectures or platforms
  • Integrating Make-based builds into CI/CD pipelines (GitHub Actions, GitLab CI, Jenkins)
  • Migrating from manual shell scripts to structured, dependency-aware build automation
  • Creating portable build systems that work across Linux, macOS, and WSL environments

When NOT to Use

Avoid GNU Make when:

  • Building complex software projects with deep dependency resolution — use cmake, ninja, or language-specific build tools (Maven, Gradle, cargo) instead
  • Working with pure Go projects — use Mage (coding-mage-build-tool) which provides type-safe targets in Go code
  • You need a cross-platform task runner without Make's tab-parsing quirks — use just (coding-just-task-runner) instead
  • The project is Python-based with no compiled components — use uv run, pyproject.toml scripts, or plain shell for simplicity
  • Build logic exceeds 500 lines in a single Makefile — split into multiple .mk include files or consider a dedicated build system

Core Workflow

  1. Analyze the project structure — Identify source directories, target binary names, test frameworks, and any existing build scripts. Determine whether the project needs cross-compilation or is single-platform only.
  2. Define core targets first — Start with all, clean, build, test as .PHONY targets. Each must have an explicit recipe or dependency chain. Checkpoint: Run make -n all to verify the dependency graph produces the expected build sequence without executing commands.
  3. Add pattern rules — Replace hardcoded compilation recipes with pattern rules (%.o: %.c). Use automatic variables so the rule works for every file without modification. Checkpoint: Verify that adding a new .c source file to SRCS automatically includes it in the build.
  4. Configure variable scoping — Separate global variables (compiler flags, install prefix) from target-specific variables (per-target CFLAGS). Use override only when an external environment variable must be superseded by Makefile logic. Checkpoint: Run make -p | grep "CFLAGS" to verify no unintended global override of target-specific flags.
  5. Add cross-compilation and platform detection — Detect the host OS via $(OS) or $(shell uname -s). Define architecture-specific toolchain variables (CC, CXX) that can be overridden from the command line. Checkpoint: Cross-compile for a different target using make CROSS_COMPILE=arm-linux-gnu- ARCH=arm64.
  6. Add CI/CD integration targets — Create dedicated targets like ci, lint, coverage, docker-build that mirror what the CI pipeline runs locally. This ensures local and CI builds stay in sync. Checkpoint: Run make ci on a clean checkout and confirm it matches CI output exactly.

Implementation Patterns

Pattern 1: Variable Scoping and Automatic Variables

GNU Make supports multiple variable types. Understanding scope prevents subtle bugs where flags leak between targets or environment variables override intended defaults.

# Global build configuration — defaults that can be overridden from command line
CC           ?= gcc
CXX          ?= g++
AR           ?= ar
CFLAGS       ?= -Wall -Wextra -Wpedantic -O2
CXXFLAGS     ?= $(CFLAGS)
LDFLAGS      ?= -lm
PREFIX       ?= /usr/local

# Directory layout (relative paths — portable across systems)
SRCDIR       := src
BUILDDIR     := build
INCDIR       := include
BINDIR       := bin

# Collect all sources and derive object file paths automatically
SRCS         := $(wildcard $(SRCDIR)/*.c)
OBJS         := $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRCS))
TARGET       := $(BINDIR)/myapp

# Automatic variables in pattern rules:
#   $@  — target filename (the thing on the left side of :)
#   lt;  — first prerequisite (leftmost dependency)
#   $^  — all prerequisites, deduplicated
#   $?  — only prerequisites newer than target
#   $*  — stem from pattern rule matching (%.c -> stem is the part before .c)

$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
	$(CC) $(CFLAGS) -I$(INCDIR) -c lt; -o $@

Why this works: The | $(BUILDDIR) order-only prerequisite ensures the build directory exists before compilation starts, but changes to the directory itself do not trigger recompilation. Pattern rules apply automatically to every .c file in SRCDIR, so adding new sources requires no Makefile changes.

Pattern 2: Phony Targets and Dependency Chains

Phony targets represent actions that do not produce output files. They must be declared explicitly or Make may confuse them with real files.

# Declare all phony targets at the top of the file — this is critical
.PHONY: all clean install uninstall test lint docker-build ci help

# Default target — builds everything
all: $(TARGET)
	@echo "Build complete: $(TARGET)"

# Binary depends on all object files
$(TARGET): $(OBJS)
	$(CC) $^ $(LDFLAGS) -o $@

# Build directory creation (order-only prerequisite for .o targets)
$(BUILDDIR):
	mkdir -p $@

$(BINDIR):
	mkdir -p $@

# Clean — remove all generated artifacts
clean:
	rm -rf $(BUILDDIR) $(BINDIR)

# Install — copy binary to PREFIX/bin
install: $(TARGET)
	install -d $(PREFIX)/bin
	install -m 755 $(TARGET) $(PREFIX)/bin/

# Uninstall — remove installed files
uninstall:
	rm -f $(PREFIX)/bin/$(notdir $(TARGET))

# Test — compile and run the test suite
test: $(TARGET)
	./run_tests.sh --verbose

# Help — display usage information
help:
	@echo "Usage: make [target]"
	@echo ""
	@echo "Targets:"
	@echo "  all          Build the project (default)"
	@echo "  clean        Remove all generated files"
	@echo "  install      Install to $(PREFIX)/bin/"
	@echo "  uninstall    Remove installed binary"
	@echo "  test         Run the test suite"
	@echo "  lint         Run static analysis (requires clang-tidy)"
	@echo "  docker-build Build Docker image"
	@echo "  ci           Full CI pipeline (lint + test + build)"
	@echo "  help         Show this help message"

Why this works: The help target uses @echo with no recipe dependencies, making it a pure documentation tool. Targets are ordered by dependency — all → $(TARGET) → objects — so running make alone produces a complete build. Each target is independent; running make test after make clean still rebuilds the binary first because of the dependency chain.

Pattern 3: Cross-Compilation with Platform Detection

Portable Makefiles detect the host platform and allow overrides for cross-compilation targets. This pattern works on Linux, macOS, and WSL without modification.

# Platform detection — works on Linux, macOS, and WSL
ifeq ($(OS),Windows_NT)
    HOST_OS := windows
    RM      := del /Q /F
else
    UNAME_S := $(shell uname -s)
    ifeq ($(UNAME_S),Linux)
        HOST_OS := linux
        RM      := rm -f
    endif
    ifeq ($(UNAME_S),Darwin)
        HOST_OS := darwin
        RM      := rm -f
    endif
endif

# Cross-compilation support — override these from command line:
#   make CROSS_COMPILE=arm-linux-gnu- ARCH=arm64
CROSS_COMPILE ?=
CC            := $(CROSS_COMPILE)gcc
CXX           := $(CROSS_COMPILE)g++
AR            := $(CROSS_COMPILE)ar

# Architecture-specific flags
ifeq ($(ARCH),arm64)
    CFLAGS += -march=armv8-a+fp+simd
else ifeq ($(ARCH),riscv64)
    CFLAGS += -march=rv64gc
endif

# Docker cross-compile target — builds inside a container for reproducible results
docker-build:
	docker build --build-arg ARCH=$(ARCH) -t $(TARGET):$(ARCH) .

# Native build with verbose output (for debugging build failures)
V ?= 0
ifeq ($(V),1)
    Q :=
    VERBOSE := | tee build.log
else
    Q := @
    VERBOSE :=
endif

quiet-build:
	$(Q)$(MAKE) all $(VERBOSE)

Why this works: The CROSS_COMPILE prefix pattern matches how the Linux kernel and most toolchains handle cross-compilation. Platform detection uses uname -s output, which is reliable across POSIX systems. The V=1 variable allows on-demand verbose builds without modifying the Makefile, following the convention used by the Linux kernel build system.

Pattern 4: CI/CD Integration and Advanced Patterns

Production Makefiles include linting, coverage tracking, Docker image building, and environment-aware behavior that mirrors CI pipeline execution locally.

# Coverage tracking with gcov/lcov (GCC projects)
COVERAGE_DIR := $(BUILDDIR)/coverage
COVERAGE_FLAGS := -fprofile-arcs -ftest-coverage

test-coverage: CFLAGS += $(COVERAGE_FLAGS)
test-coverage: $(TARGET)
	$(Q)mkdir -p $(COVERAGE_DIR)
	$(Q)cd $(BUILDDIR) && lcov --capture --directory . --output-file coverage.info
	$(Q)genhtml coverage.info --output-directory $(COVERAGE_DIR)/report

# Linting target — requires clang-tidy and shellcheck
lint:
	@command -v clang-tidy >/dev/null 2>&1 || { echo "Error: clang-tidy not found" >&2; exit 1; }
	$(Q)clang-tidy $(SRCS) -- -I$(INCDIR) $(CFLAGS) --quiet
	$(Q)shellcheck scripts/*.sh || true

# Docker image build with multi-stage support
docker-build:
	docker build --pull --target production -t $(TARGET):latest .
	docker tag $(TARGET):latest $(TARGET):$(shell git rev-parse --short HEAD)

# CI pipeline target — runs everything the CI server runs
ci: lint test-coverage all docker-build
	@echo "CI pipeline complete"

# Environment variable injection for build-time configuration
BUILD_VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "unknown")
BUILD_DATE    ?= $(shell date +%Y-%m-%dT%H:%M:%S%z)
CFLAGS        += -DBUILD_VERSION=\"$(BUILD_VERSION)\" \
                 -DBUILD_DATE=\"$(BUILD_DATE)\"

# Include external makefiles for modular build logic (project split pattern)
# Create separate .mk files for subsystems: src/core.mk, tests/unit.mk, etc.
-include $(wildcard mk/*.mk)

# Dry-run — show what make would do without executing
.PHONY: dryrun
dryrun:
	$(info === Makefile variables ===)
	$(info CC=$(CC), CFLAGS=$(CFLAGS))
	$(info SRCS=$(SRCS))
	$(info OBJS=$(OBJS))
	$(info TARGET=$(TARGET))
	$(info HOST_OS=$(HOST_OS))
	@echo ""
	@echo "=== Build plan (dry-run) ==="
	@make -n all

Why this works: The -include $(wildcard mk/*.mk) pattern allows splitting large Makefiles into modular subsystems without breaking compatibility — if the directory doesn't exist, the include is silently ignored (the - prefix suppresses errors). Build-time version injection via $(shell ...) captures git metadata at build time for reproducible traceability. The dryrun target uses Make's built-in $(info ...) function to print variable states alongside a dry-run of the actual build commands, which is invaluable for debugging complex dependency graphs.


Constraints

MUST DO

  • Declare every non-file-producing target as .PHONY — this prevents stale builds when a file with that name exists
  • Use $(wildcard ...) and $(patsubst ...) to auto-discover sources instead of maintaining hardcoded source lists
  • Prefer automatic variables ($@, lt;, $^) over hardcoded paths in recipes — they adapt to any file names
  • Separate build directories from source using the | order-only prerequisite syntax (e.g., $(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR))
  • Use ?= for variables that may be overridden from the environment or command line, and := for computed values set by the Makefile
  • Include a help target that documents all available targets — every production Makefile needs this
  • Run make -n (dry-run) after any Makefile change to verify dependency resolution before committing

MUST NOT DO

  • Do not use tab characters inconsistently — Makefile recipes MUST begin with a tab, not spaces. Mixing tabs and spaces is the #1 Makefile error source.
  • Do not hardcode absolute paths in recipes — use variables ($(BINDIR), $(PREFIX)) for portability across developer machines and CI environments
  • Do not place logic-heavy shell commands inline in recipes when a separate script file would be cleaner — move complex logic to .sh scripts and invoke them via Make targets
  • Do not forget .PHONY declarations — without them, Make treats target names as files and skips execution if a file with that name exists in the directory
  • Do not use recursive $(MAKE) calls ($(MAKE) clean) unless necessary — it obscures dependency tracking and makes make -j parallel builds unreliable
  • Do not define targets without recipe or dependency — an empty target with no prerequisites is silently skipped, creating silent failures

Output Template

When this skill is active, the model's output must contain:

  1. Complete Makefile content — a fully functional, copy-paste-ready Makefile with all targets declared as .PHONY
  2. Variable definitions — clear separation of compiler settings, directory layout, source discovery, and platform detection variables
  3. Pattern rules — at least one explicit pattern rule using automatic variables ($@, lt;, $^)
  4. Dependency chain explanation — a brief description of how all → build targets → sources connect
  5. CI/CD integration target — a combined target (e.g., ci) that runs lint, test, and build in sequence

Related Skills

SkillPurpose
coding-just-task-runnerCross-platform task runner for projects where Make's tab/space parsing is problematic; supports named arguments and runsets
coding-mage-build-toolType-safe build tool in Go — ideal when project is already Go-based and you want compile-time target validation
linux-systemd-servicesDefine systemd unit files for deploying applications built via Make as persistent services
coding-test-driven-developmentTDD workflow patterns that pair naturally with make test targets for automated quality gates

Live References

Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.

Gespeicherte Skills