Kahibaro
Discord Login Register

Introduction to Make

Basic idea of Make

Make is a build automation tool. It reads a text file called a Makefile that describes how to produce one set of files from another, usually how to compile and link programs. In HPC work you often need to build the same code many times with different compilers, optimization levels, or platforms. Make helps you avoid retyping long compile commands, keeps builds consistent, and only rebuilds what has actually changed.

At its core, Make works with three concepts: targets, prerequisites, and commands. A target is usually a file that you want to build, such as an executable or an object file. Prerequisites are the input files needed to build that target, such as source files or other generated files. Commands are the shell commands that transform prerequisites into the target.

Make decides whether it needs to rebuild a target by comparing file timestamps. If any prerequisite is newer than its target, Make considers the target out of date and runs the commands for that target. This automatic tracking of dependencies is one of the main reasons Make is useful in HPC projects with many source files.

Key rule: A target is rebuilt if any of its prerequisites has a more recent timestamp than the target file.

Structure of a simple Makefile

A Makefile is just a plain text file, usually named Makefile or makefile, that lives in your project directory. Each build rule has a simple form:

target: prerequisites
<TAB>command

It is essential that the command line starts with a literal tab character, not spaces. Many beginners run into confusing errors because they used spaces.

Consider a minimal example for a C program:

myprog: main.o solver.o io.o
<TAB>gcc -O2 -o myprog main.o solver.o io.o
main.o: main.c
<TAB>gcc -O2 -c main.c
solver.o: solver.c
<TAB>gcc -O2 -c solver.c
io.o: io.c
<TAB>gcc -O2 -c io.c

When you type make in this directory, Make looks for the first target in the file (myprog) and tries to build it. It checks whether myprog exists and whether any of main.o, solver.o, or io.o are newer. If they are missing or newer, it runs the link command. For each of the object files, it follows the same logic with their .c prerequisites.

In HPC projects you will often see larger Makefiles, but they all follow this same pattern. The value of Make increases with the number of files and the complexity of dependencies.

Targets, prerequisites, and commands in practice

Targets are not limited to executables and object files. They can also be intermediate files, configuration files, or even symbolic targets that do not correspond to any file. These symbolic targets are called phony targets and are used for actions such as cleaning build products or running tests.

A prerequisite can appear in multiple rules, and this mirrors the idea that one source file can be used to build many different executables or libraries with different settings. This is common in HPC where you might build CPU and GPU versions of the same application, or debug and optimized versions.

Each rule’s commands are executed by the shell. In most environments that is /bin/sh. Make does not understand shell syntax itself, it only passes command lines to the shell. This means any shell constructs, such as environment variables, pipes, or redirection, can be used in Make commands, which is sometimes useful for more complex HPC build steps, for example auto-generating code or running pre-processing tools.

Important rule syntax:

  1. One rule per target.
  2. Prerequisites listed after :.
  3. Each build command line must start with a tab.
  4. Make runs commands using the system shell.

Using variables in Makefiles

Make provides its own variables, which help avoid duplication and make your Makefile easier to modify when you switch compilers or optimization levels on an HPC system. Variables are defined with = and referenced with $().

A simple example:

CC = gcc
CFLAGS = -O3 -march=native
myprog: main.o solver.o io.o
<TAB>$(CC) $(CFLAGS) -o myprog main.o solver.o io.o
main.o: main.c
<TAB>$(CC) $(CFLAGS) -c main.c
solver.o: solver.c
<TAB>$(CC) $(CFLAGS) -c solver.c
io.o: io.c
<TAB>$(CC) $(CFLAGS) -c io.c

Now if you want to change the compiler to icc or add flags like -g for a debug build, you only need to change the variable definitions. This is especially useful in HPC environments where you might load different compiler modules or need to adjust flags to use specific hardware features.

Variables also help keep separate configurations side by side. You can define different sets of flags for debug and optimized builds and refer to them in different rules or conditional sections of the Makefile.

Key practice: Use variables like CC, CXX, FC, CFLAGS, CXXFLAGS, and LDFLAGS to make your Makefile portable and easy to adapt to different HPC systems and compilers.

Pattern rules and automatic variables

When you have many similar files, it is inefficient to write a separate rule for each object file. Make supports pattern rules, which describe how to build any file that matches a pattern. For C source files you commonly see:

CC = gcc
CFLAGS = -O3
OBJS = main.o solver.o io.o
myprog: $(OBJS)
<TAB>$(CC) $(CFLAGS) -o $@ $(OBJS)
%.o: %.c
<TAB>$(CC) $(CFLAGS) -c $<

Here %.o: %.c is a pattern rule. It tells Make how to build any .o file from a .c file with the same base name. The $< is an automatic variable that means "the first prerequisite," which is the corresponding .c file. The $@ in the link command is another automatic variable that means "the target name," in this case myprog.

Automatic variables reduce repetition and make it easier to maintain the Makefile. If you add a new source file, you only need to add its object file to OBJS. The pattern rule covers how to build the object.

Common automatic variables include:

$@ for the target name.

$< for the first prerequisite.

$^ for the list of all prerequisites with duplicates removed.

Useful automatic variables:
$@ is the target.
$< is the first prerequisite.
$^ is the list of all prerequisites.
Use them to avoid hardcoding file names in commands.

Phony targets and cleaning builds

Phony targets are targets that do not correspond to real files. They are convenient shortcuts for actions you run often, such as cleaning up build outputs. Consider:

CC = gcc
CFLAGS = -O3
OBJS = main.o solver.o io.o
myprog: $(OBJS)
<TAB>$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
<TAB>$(CC) $(CFLAGS) -c $<
.PHONY: clean
clean:
<TAB>rm -f $(OBJS) myprog

By declaring clean as .PHONY, you tell Make that clean is not a file. When you type make clean, Make will always run the rm command, regardless of what files exist. This is important because if a file named clean appears in the directory, Make might otherwise think the target is up to date and skip the commands.

Cleaning is especially important in HPC environments where you may recompile the same code with different compilers, flags, or architectures. A clean build avoids mixing object files built with incompatible options.

Handling multiple build configurations

On HPC systems you might need several builds of the same program, for example a debug build and a highly optimized build, or different builds for CPUs and GPUs. Make can handle this using variables and multiple targets.

A simple pattern for debug and release builds is:

CC = gcc
DEBUG_FLAGS = -O0 -g
RELEASE_FLAGS = -O3
debug: CFLAGS = $(DEBUG_FLAGS)
debug: myprog_debug
release: CFLAGS = $(RELEASE_FLAGS)
release: myprog
myprog_debug: main.o solver.o io.o
<TAB>$(CC) $(CFLAGS) -o $@ $^
myprog: main.o solver.o io.o
<TAB>$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
<TAB>$(CC) $(CFLAGS) -c $<

Here debug and release are separate top-level targets. When you run make debug, Make rebuilds with CFLAGS set to debug settings. When you run make release, it uses the optimized settings. This pattern can be extended to handle GPU builds by adding additional variables like GPU_FLAGS and different programs as targets.

In real HPC projects, builds can become more complex, including different MPI libraries, math libraries, or accelerator flags. Make is usually the first tool used to define these combinations in a consistent and repeatable way before moving to more advanced build systems.

Environment integration on HPC clusters

Make works in the environment in which you invoke it. This is important on HPC systems where you use environment modules to select compilers and libraries. For example, after loading a compiler module, CC might refer to a wrapper compiler that automatically adds MPI or math library flags.

You can either rely on the environment to set variables such as CC, or you can override them in the Makefile. On many HPC clusters MPI wrappers like mpicc, mpicxx, or mpif90 are used. A Makefile might look like:

CC = mpicc
CFLAGS = -O3
OBJS = main.o mpi_utils.o
my_mpi_prog: $(OBJS)
<TAB>$(CC) $(CFLAGS) -o $@ $^

If the user loads a different MPI module, the mpicc in their path might change, but the Makefile can stay the same. This approach helps create portable build recipes that adapt automatically to the current module environment on the cluster.

When you need to override variables from the command line, Make allows that too. For example:

make CC=icc CFLAGS="-O3 -xHost"

This replaces the values defined in the Makefile for this invocation. On a shared HPC system, this flexibility can be useful when experimenting with different compilers or flags without editing the Makefile itself.

HPC tip: Combine Makefiles with environment modules. Let modules choose compilers and libraries, and let Make describe how to build. Override variables like CC or CFLAGS from the command line when you need to experiment.

Make and parallel builds

Make can compile independent files in parallel if you give it the -j option. For example, make -j 4 allows Make to run up to 4 compile commands at the same time. On a multicore node this can significantly reduce build time for large HPC codes.

Internally, Make examines the dependency graph and runs commands for targets that do not depend on each other simultaneously. Targets that depend on previous ones still respect that order. For simple C or C++ projects with one object file per source file, object file compilation can usually be parallelized widely because object files are largely independent.

On HPC login nodes you should be careful with parallel builds. Many systems discourage heavy parallel compilation on shared login nodes. It is often better to use a moderate -j value, or run builds on dedicated build nodes if available. The exact policy depends on the cluster, but Make itself supports parallelism as long as the underlying rules are correct.

When to move beyond simple Makefiles

For small to medium HPC projects, Make is often sufficient. It gives you basic dependency tracking, variables, pattern rules, and an easy way to integrate with the compiler and module environment. However, as projects grow in size and need to support many platforms, compilers, and optional features, you may find that maintaining a hand-written Makefile becomes difficult.

At that stage more advanced tools such as CMake are often introduced to generate platform-specific Makefiles automatically. Make remains useful even then, because many of these systems still generate Makefiles, and you will run make as the final build step. Understanding Make helps you debug and adapt builds even when higher-level tools are used.

For an introduction to HPC development, however, knowing how to write and read basic Makefiles is enough to control your own builds, integrate with the cluster environment, and reproduce builds in a reliable way.

Views: 1

Comments

Please login to add a comment.

Don't have an account? Register now!