A makefile tells the make utility how to compile and run programs. Instead of typing long commands every time you build your code, you write the instructions once and run a single command. Before starting, confirm that make is installed on your system.

How a Makefile Works: Rules, Targets, and Prerequisites

Create an empty directory called myproject. Inside it, create a file named Makefile with this content:

say_hello:
        echo "Hello World"

Run it by typing make inside that directory:

$ make
echo "Hello World"
Hello World

Every rule in a makefile has three parts.

PartDescription
TargetThe name of what you’re building, or a label for a recipe
PrerequisitesFiles or targets that the target depends on
RecipeThe commands that produce the target

The general syntax is:

target: prerequisites
<TAB> recipe

A target doesn’t have to be a file. When it’s just a label, it’s called a phony target. The full command prints before its output by default. To suppress that, add @ before the command:

say_hello:
        @echo "Hello World"

Now make shows only Hello World without printing the command itself.

Setting a Default Target in a Makefile

By default, make runs only the first target it finds. To change this, use .DEFAULT_GOAL:

.DEFAULT_GOAL := generate

This tells make to run generate instead of the first target. Since .DEFAULT_GOAL handles one target at a time, most projects use an all target to call multiple targets together. Here’s a more complete example:

.PHONY: all say_hello generate clean

all: say_hello generate

say_hello:
        @echo "Hello World"

generate:
        @echo "Creating empty text files..."
        touch file-{1..10}.txt

clean:
        @echo "Cleaning up..."
        rm *.txt

The .PHONY declaration tells make that these targets aren’t actual files. Running make now executes both say_hello and generate. The touch command creates the files — see how it handles shell brace expansion in this context. The clean target should be called manually with make clean rather than including it in all.

Using Variables in a Makefile to Avoid Hardcoded Values

Hardcoded values make makefiles fragile. Variables let you reuse values and change them in one place.

Use = to define a recursive expanded variable:

CC = gcc

hello: hello.c
    ${CC} hello.c -o hello

Both ${CC} and $(CC) are valid references. Assigning a variable to itself with =, though, causes an infinite loop. Use := instead — called a simply expanded variable — which evaluates the right side once at assignment:

CC := gcc
CC := ${CC}

This is the safer default. Reserve = for cases where you specifically need the value to re-evaluate every time it’s referenced.

Makefile Pattern Rules and Functions for Automating C Builds

This makefile compiles all .c files in a directory automatically:

.PHONY = all clean

CC = gcc
LINKERFLAG = -lm
SRCS := $(wildcard *.c)
BINS := $(SRCS:%.c=%)

all: ${BINS}

%: %.o
        @echo "Checking.."
        ${CC} ${LINKERFLAG} $< -o $@

%.o: %.c
        @echo "Creating object.."
        ${CC} -c $<

clean:
        @echo "Cleaning up..."
        rm -rvf *.o ${BINS}

Here’s what each part does.

LinePurpose
$(wildcard *.c)Collects all .c files into SRCS
$(SRCS:%.c=%)Substitution reference — strips .c extensions to get binary names
%: %.oPattern rule — builds each binary from its matching object file
%.o: %.cCompiles each .c file into an object file
$<Refers to the first prerequisite
$@Refers to the current target

For a single file foo.c, the makefile expands to build foo.o from foo.c, then link foo.o into the final foo binary. The clean target removes all generated files. Once you have compiled binaries, you’ll need to set the right executable permissions with chmod before they can run.

Automatic Variables in Makefile Recipes

The automatic variables $@ and $< are the two you’ll use most often, but make provides others that come up in larger projects.

VariableExpands to
$@The target name of the current rule
$<The first prerequisite
$^All prerequisites, deduplicated
$?Prerequisites newer than the target
$*The stem matched by a pattern rule

The $? variable is useful when you want a recipe to run only for changed files rather than all prerequisites. You can verify what make will run before executing it by passing -n, which prints the commands without running them. For anything involving recursive directories or environment variables, the grep command can help you quickly search through generated output or log files during debugging.

Common Makefile Mistakes to Avoid

The recipe line must start with a tab character, not spaces. This is the single most common source of confusing errors — make will report “missing separator” without pointing to the actual problem. Most editors let you configure this per-file type.

Leaving non-file targets out of .PHONY causes make to skip them when a file with that name exists. A clean target without .PHONY does nothing if a file named clean is present in the directory.

Using = when := is more appropriate can produce subtle bugs, especially when variables reference each other. If your build produces unexpected output, check variable assignments first.

FAQs

What is a makefile used for?

A makefile tells the make utility how to build a project. It defines which commands to run, in what order, and which files depend on which others — so only changed files get recompiled.

What is .PHONY in a makefile?

.PHONY marks targets that are not actual files. It tells make to run the recipe regardless of whether a file with the same name exists in the directory.

What is the difference between = and := in a makefile?

= defines a recursively expanded variable — evaluated each time it’s used. := evaluates once at assignment. Use := by default to avoid unexpected behavior.

How do you run a specific makefile target?

Run make target_name from the directory containing your Makefile. For example, make clean runs the clean recipe. Without a target name, make runs the default target.

Can a makefile compile multiple files at once?

Yes. Use $(wildcard *.c) to collect all source files automatically, then pattern rules like %.o: %.c to compile each one. The all target links the results.

Willie has over 15 years of experience in Linux system administration and DevOps. After managing infrastructure for startups and enterprises alike, he founded Command Linux to share the practical knowledge he wished he had when starting out. He oversees content strategy and contributes guides on server management, automation, and security.