Table of Contents
What Make Is and Why It Matters in HPC
make is a classic build automation tool. In HPC, it is still one of the most common ways to:
- Compile multi-file projects (C, C++, Fortran, etc.)
- Manage different build variants (debug, optimized, MPI, GPU, …)
- Rebuild only what changed, saving time on large codes
At its core, make:
- Reads a file called
Makefile(ormakefile). - Understands relationships between files (what depends on what).
- Decides what must be rebuilt when a source file changes.
- Runs shell commands (typically compiler invocations) to update targets.
This chapter focuses on basic, practical usage of make for small to medium scientific codes.
Basic Concepts: Targets, Dependencies, and Rules
A Makefile consists of rules. Each rule usually has:
- A target: the file you want to create (e.g.,
myprog,main.o) - Dependencies (or prerequisites): files the target depends on
- A recipe: commands to build the target from its dependencies
General form:
target: dependencies
<TAB>recipeImportant details:
- The recipe line must start with a tab character, not spaces.
- If any dependency is newer than the target (by timestamp),
makewill run the recipe.
A minimal example:
myprog: main.o helper.o
gcc -O2 -o myprog main.o helper.o
main.o: main.c
gcc -O2 -c main.c
helper.o: helper.c
gcc -O2 -c helper.cBehavior:
make myprogwill:- Check if
myprogexists and if its dependencies are newer. - If
main.oorhelper.oare missing or out of date, it will rebuild them. - Then link them into
myprog.
This automatic decision-making is what saves time in HPC development: large codes often have many source files, and you only want to rebuild what actually changed.
A First Simple Makefile for an HPC-Style Program
Consider a simple C program split into multiple files:
main.csolver.cio.c
A simple Makefile:
CC = gcc
CFLAGS = -O2 -Wall
myprog: main.o solver.o io.o
$(CC) $(CFLAGS) -o myprog main.o solver.o io.o
main.o: main.c
$(CC) $(CFLAGS) -c main.c
solver.o: solver.c
$(CC) $(CFLAGS) -c solver.c
io.o: io.c
$(CC) $(CFLAGS) -c io.cUsage:
- Run
make(default target is the first one,myprog). - Run
make myprogexplicitly (same effect). - Modify
solver.conly;makewill just recompilesolver.cand relink, not recompilemain.corio.c.
Variables in Makefiles
Variables make Makefiles flexible and easier to maintain.
Defining variables:
CC = mpicc
CFLAGS = -O3 -march=native
LDFLAGS =
LIBS = -lmUsing variables:
myprog: main.o solver.o io.o
$(CC) $(LDFLAGS) -o myprog main.o solver.o io.o $(LIBS)
%.o: %.c
$(CC) $(CFLAGS) -c $<Notes:
$(CC)expands to whatever theCCvariable is set to.- In HPC, you often change
CC,CFLAGS,LDFLAGS,LIBSdepending on: - Compiler (GCC, Intel, Cray, etc.)
- MPI or non-MPI builds
- GPU vs CPU targets
You can override variables on the command line:
make CC=icc CFLAGS="-O3 -xHost"Pattern Rules and Automatic Variables
Typing a separate rule for every .c file is repetitive. Pattern rules solve this.
Pattern Rules
A pattern rule with % can apply to many files:
%.o: %.c
$(CC) $(CFLAGS) -c $<This means:
- To build any
something.o, compilesomething.cwith the given recipe.
Then you only need per-program link rules:
myprog: main.o solver.o io.o
$(CC) $(CFLAGS) -o myprog main.o solver.o io.oUseful Automatic Variables
Within a rule, make provides automatic variables:
$@— the target name$<— the first dependency$^— all dependencies, with duplicates removed
Examples:
%.o: %.c
$(CC) $(CFLAGS) -c $<
myprog: main.o solver.o io.o
$(CC) $(CFLAGS) -o $@ $^Here:
- In the link rule,
$@becomesmyprog,$^becomesmain.o solver.o io.o. - In the compile rule,
$<becomes the corresponding.cfile.
Multiple Targets and Phony Targets
Not all targets have to be real files. Some are just commands you want to run.
Phony Targets
Use phony targets for actions like cleaning build files:
.PHONY: clean
clean:
rm -f myprog *.o
Why .PHONY?
- If a file named
cleanever appears,make cleanwould otherwise do nothing (sincecleanwould look “up to date”). - Declaring
.PHONY: cleanforcesmaketo always run the recipe.
Common phony targets in HPC development:
clean— remove object files and binaries.distclean— more thorough cleaning (e.g., remove generated configs).test— run a test suite or quick checks.run— run a small test job locally (not on the cluster scheduler).
Typical HPC-Conscious Makefile Variants
HPC work often needs different build types:
- Debug build: more checks, less optimization.
- Optimized build: maximum performance.
- MPI build vs. serial build.
- Possibly GPU-accelerated vs. CPU-only builds.
You can add separate targets that reuse most of the same rules.
Debug vs Optimized
CC = gcc
CFLAGS_DEBUG = -O0 -g -Wall
CFLAGS_RELEASE = -O3 -march=native -DNDEBUG
.PHONY: debug release
debug: CFLAGS = $(CFLAGS_DEBUG)
debug: myprog
release: CFLAGS = $(CFLAGS_RELEASE)
release: myprog
myprog: main.o solver.o io.o
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $<Usage:
make debugbuildsmyprogwith debug flags.make releasebuildsmyprogwith optimized flags.
This pattern is very common for HPC codes shared by multiple users.
MPI vs Serial
CC_SERIAL = gcc
CC_MPI = mpicc
CFLAGS_COMMON = -O2 -Wall
CFLAGS_SERIAL = $(CFLAGS_COMMON)
CFLAGS_MPI = $(CFLAGS_COMMON) -DMPI_ENABLED
.PHONY: serial mpi
serial: CC = $(CC_SERIAL)
serial: CFLAGS = $(CFLAGS_SERIAL)
serial: myprog
mpi: CC = $(CC_MPI)
mpi: CFLAGS = $(CFLAGS_MPI)
mpi: myprogHere:
make serialbuilds with a normal compiler.make mpibuilds with the MPI wrapper compiler and sets a preprocessor macro.
Dependency Management Basics
Large HPC codes often depend on header files (.h), which can trigger many recompilations.
Minimal explicit dependencies:
main.o: main.c solver.h io.h
solver.o: solver.c solver.h
io.o: io.c io.h
This ensures that changing solver.h triggers recompilation of main.o and solver.o.
More advanced automatic dependency generation uses compiler options (e.g., -MMD -MP with GCC/Clang), but the details can be postponed to more advanced build-system material.
Running Make and Common Options
Typical usage patterns:
- Build default target (first in Makefile):
make- Build a specific target:
make myprog
make debug
make cleanUseful options:
make -j N— run up toNjobs in parallel.- Example:
make -j 8compiles multiple files at once, useful on multi-core nodes. make -n— dry run; show what would be executed without running the commands.make VERBOSE=1or adding@before commands to hide/print them (this depends on how you write your Makefile).
Note: On shared login nodes of HPC clusters, be careful with make -j if it spawns heavy compilation jobs; consider compiling on dedicated build nodes if available.
Good Practices for Make in HPC Contexts
Some practical tips:
- Centralize compiler flags in variables (
CFLAGS,FFLAGS,LDFLAGS) so you can adapt to different clusters and compilers easily. - Use pattern rules to avoid repeating compile rules for every file.
- Use phony targets for cleaning and special actions.
- Avoid hard-coding paths to libraries and include directories; use variables and allow overriding from the command line.
- Keep Makefiles simple for small projects; only add complexity (conditionals, auto-deps, etc.) when needed.
- For very large or complex projects, you may transition to more advanced systems (e.g., CMake), but understanding
makeremains valuable because many HPC codes still rely on it.
Minimal Complete Example
A compact Makefile for a small HPC-style C project:
# Compiler and flags
CC = mpicc
CFLAGS = -O3 -Wall
LDFLAGS =
LIBS = -lm
# Program and sources
PROG = myprog
SRCS = main.c solver.c io.c
OBJS = $(SRCS:.c=.o)
# Default target
$(PROG): $(OBJS)
$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
# Pattern rule for object files
%.o: %.c
$(CC) $(CFLAGS) -c $<
# Utility targets
.PHONY: clean debug
debug: CFLAGS = -O0 -g -Wall
debug: clean $(PROG)
clean:
rm -f $(PROG) $(OBJS)Usage:
make— build optimized MPI-enabledmyprog.make debug— rebuild with debug information.make clean— remove compiled objects and executable.
This pattern is a solid starting point for many small HPC exercises and prototype codes.