Cloud Explainers

Linux Makefiles Will Finally Make Sense After This

Linux9 min videoPublished Nov 12, 2026

A Makefile looks cryptic until you see the one idea it repeats: a rule with a target, the files it's made from, and the command that makes it. We start from the classic first error, missing separator, then build a small C program with the real make and watch it rebuild only what is out of date. Along the way: why make clean can answer 'clean is up to date', the header change make can't see, and how to read the dollar-sign symbols.

Missing separator

You write your first Makefile, type make, and get this: missing separator. Stop. The message points at line two. You read it, twice, and it looks fine. It isn't. That line starts with four spaces, and make accepts only one thing there: a tab.

Ana's terminal: the ferry office's Linux box
cat Makefileferry: main.o times.o fares.o
    cc -o ferry main.o times.o fares.o

main.o: main.c
    cc -c main.c

times.o: times.c
    cc -c times.c

fares.o: fares.c
    cc -c fares.c
Ana's terminal: the ferry office's Linux box
makeMakefile:2: *** missing separator.  Stop.

One idea: the rule

Ana, who wrote that file, swaps the spaces for tabs. cat -T shows each tab as a caret and a capital I. That tab is the one odd part. Everything else in a Makefile is a single idea, repeated: a rule. First, the target: the file you want. Here, a program called ferry. After the colon, its prerequisites: the files it's made from. And on the next line, behind the tab, the recipe: the command that makes it. Those three also explain a strange day that's coming: you ask make to clean up, it answers that clean is up to date, and it deletes nothing.

Ana's terminal: the ferry office's Linux box
cat -T Makefileferry: main.o times.o fares.o
^Icc -o ferry main.o times.o fares.o

main.o: main.c
^Icc -c main.c

times.o: times.c
^Icc -c times.c

fares.o: fares.c
^Icc -c fares.c

Think of a recipe card: the dish, its ingredients, the cooking steps. And picture a cook who never wastes effort. If the dish is already on the table, and no ingredient is fresher than the dish, she cooks nothing. That cook is make.

A rule reads like a recipe card: diagram of The cook, Recipe card, The dish, Ingredients, Cooking steps

Ana's program prints the day's times and fares for a ferry ticket office. It's written in the C language, in three source files and two small header files they share, and C has to be compiled before it can run. Each source file is compiled into an object file, ending in.o: one finished piece. Then the pieces are joined into a single program, called ferry. So her Makefile holds four rules: one for each piece, and one for the program.

What Ana's program is made from: diagram of ferry, main.o, times.o, fares.o, main.c, times.c, fares.c

The first build

What happens when she types make? Four commands run, each starting with cc, the C compiler. With -c it compiles one piece; the last line joins the pieces, and -o names the finished program. Make chose that order by itself, because the program's rule asks for the pieces. The program works. Now she types make a second time. Ferry is up to date. It ran nothing at all.

Ana's terminal: the ferry office's Linux box
makecc -c main.c
cc -c times.c
cc -c fares.c
cc -o ferry main.o times.o fares.o
Ana's terminal: the ferry office's Linux box
./ferryNext ferry: 14:20
Last ferry: 21:30
Adult fare: 4.50
Ana's terminal: the ferry office's Linux box
makemake: 'ferry' is up to date.

How does make know?

How did it know? Not by reading her code. Every file on Linux carries the time it was last changed. For each rule, make asks one question: is the target missing, or older than any of its prerequisites? If yes, run the recipe. If no, skip it. Here, the piece was built two seconds after its source file was saved. It's the newer of the two, so: skip.

One question per rule: diagram of times.c, times.o, Target missing, or older?, Yes: run the recipe, No: skip it

Ana changes the next ferry's time in times.c, and saves. A long listing, with times to the second, shows it: the source file is now newer than its piece. She is about to type make. Guess first: of those four commands, how many will run? Two. The changed file is compiled again. That makes its piece newer than the program, so the program is joined again. The other two pieces are left alone.

Ana's terminal: the ferry office's Linux box
ls -l --time-style=+%T times.c times.o-rw-r--r-- 1 ana ana  130 14:04:13 times.c
-rw-r--r-- 1 ana ana 1672 14:04:11 times.o
Ana's terminal: the ferry office's Linux box
makecc -c times.c
cc -o ferry main.o times.o fares.o

Notice what make did not do: it never looked inside the file. The touch command gives a file a new time, and changes nothing in it. Make rebuilds anyway. Times are all it compares.

Ana's terminal: the ferry office's Linux box
touch fares.c
Ana's terminal: the ferry office's Linux box
makecc -c fares.c
cc -o ferry main.o times.o fares.o

The change make can't see

Which change can make not see? This one costs people hours. The last ferry's time is kept in times.h, a header: a small file that two of her source files pull in. Ana moves the last ferry to 22:15, and saves. She types make. Guess again: what happens? Nothing. Ferry is up to date. And the program still says 21:30.

Ana's terminal: the ferry office's Linux box
cat times.h#define LAST_FERRY "21:30"

const char *next_ferry(void);
const char *last_ferry(void);
Ana's terminal: the ferry office's Linux box
grep LAST times.h#define LAST_FERRY "22:15"
Ana's terminal: the ferry office's Linux box
makemake: 'ferry' is up to date.
Ana's terminal: the ferry office's Linux box
./ferryNext ferry: 14:50
Last ferry: 21:30
Adult fare: 4.50

Why? Look at the picture make was given. Source files, pieces, the program. The header sits in the folder, but no arrow touches it, because no rule lists it. Make knows only what the Makefile tells it. So Ana lists the header as a prerequisite of the two pieces that pull it in.

The picture make was given: diagram of ferry, main.o, times.o, fares.o, main.c, times.c, fares.c, times.h

Here are the new rules, each header beside the piece that uses it. make -n is a dry run: it prints what make would do, and runs none of it. Three commands: two pieces, then the program. Now for real. And the program is right: 22:15. You don't have to keep these lists by hand: the compiler can write them for you. The option to search for is -MMD.

Ana's terminal: the ferry office's Linux box
grep '\.h' Makefilemain.o: main.c times.h fares.h
times.o: times.c times.h
fares.o: fares.c fares.h
Ana's terminal: the ferry office's Linux box
make -ncc -c main.c
cc -c times.c
cc -o ferry main.o times.o fares.o
Ana's terminal: the ferry office's Linux box
makecc -c main.c
cc -c times.c
cc -o ferry main.o times.o fares.o
Ana's terminal: the ferry office's Linux box
./ferryNext ferry: 14:50
Last ferry: 22:15
Adult fare: 4.50

Targets that aren't files

Does a target have to be a file? No. Ana adds a rule named clean. It has no prerequisites, and its recipe deletes everything make has built. Plain make always builds the first rule in the file. make clean asks for this one by name. No file called clean is ever created, so its recipe runs every time.

Ana's terminal: the ferry office's Linux box
tail -n 2 Makefileclean:
	rm -f ferry *.o
Ana's terminal: the ferry office's Linux box
make cleanrm -f ferry *.o
Ana's terminal: the ferry office's Linux box
lsMakefile  fares.c  fares.h  main.c  times.c  times.h

And now that strange message. One day, a file named clean ends up in the folder. Ana types make clean, and make answers: clean is up to date. Nothing is deleted. Make did what it always does. The target exists, and nothing is newer than it, so: skip. The fix is one line..PHONY tells make that clean is not a file, so always run it. And it cleans again.

Ana's terminal: the ferry office's Linux box
touch clean
Ana's terminal: the ferry office's Linux box
make cleanmake: 'clean' is up to date.
Ana's terminal: the ferry office's Linux box
tail -n 3 Makefile.PHONY: clean
clean:
	rm -f ferry *.o
Ana's terminal: the ferry office's Linux box
make cleanrm -f ferry *.o

A target like that can have prerequisites too. Ana adds one named run. It needs the program, and its recipe starts the program. So make run first brings everything up to date, and then runs it. She touches a source file and tries it: compile, join, run, from one short command. This is how most people use make day to day, and why projects with nothing to compile still keep a Makefile: make test, make deploy: short names for long commands.

Ana's terminal: the ferry office's Linux box
tail -n 2 Makefilerun: ferry
	./ferry
Ana's terminal: the ferry office's Linux box
make run./ferry
Next ferry: 14:50
Last ferry: 22:15
Adult fare: 4.50
Ana's terminal: the ferry office's Linux box
touch times.c
Ana's terminal: the ferry office's Linux box
make runcc -c times.c
cc -o ferry main.o times.o fares.o
./ferry
Next ferry: 14:50
Last ferry: 22:15
Adult fare: 4.50

Three messages you'll meet

Which errors will you meet in other people's projects? Mostly these three. No targets specified and no makefile found: make looked in this folder and found no file named Makefile. You're in the wrong folder. No rule to make target: nothing in the Makefile has that name. Here, it's a typing mistake. And the long one: no rule to make target, needed by another. A file that a rule needs is gone, and no rule says how to make it. The words after needed by tell you which rule to open.

Ana's terminal: the ferry office's Linux box
makemake: *** No targets specified and no makefile found.  Stop.
Ana's terminal: the ferry office's Linux box
make claenmake: *** No rule to make target 'claen'.  Stop.
Ana's terminal: the ferry office's Linux box
mv fares.c fares.c.bak
Ana's terminal: the ferry office's Linux box
makemake: *** No rule to make target 'fares.c', needed by 'fares.o'.  Stop.

Reading the dollar signs

What about the Makefiles full of dollar signs? They look scarier, because nobody enjoys typing the same thing three times. Here is Ana's, rewritten the way you'll meet them in real projects. The top lines are variables: a name, an equals sign, a value. So CC is the compiler, CFLAGS holds its options, here: show every warning, and optimise, and OBJS is the list of pieces. A dollar sign and brackets read one back.

Ana's terminal: the ferry office's Linux box
head -n 9 MakefileCC = cc
CFLAGS = -Wall -O2
OBJS = main.o times.o fares.o

ferry: $(OBJS)
	$(CC) -o $@ $^

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

This one rule replaces three. The percent sign stands for any name: any piece is made from the source file with the same name. In its recipe, the variables are filled in first. Dollar less-than is the first prerequisite: for the times piece, that's its source file. And dollar at is the target itself.

The recipe of the rule %.o: %.c: diagram of $(CC) $(CFLAGS) -c $< -o $@, $(CC) = cc, $(CFLAGS) = -Wall -O2, $< = times.c, $@ = times.o, For times.o: cc -Wall -O2 -c times.c -o times.o

The program's own recipe uses one more. Dollar caret means every prerequisite: here, all three pieces. Ana runs the rewritten file, and gets the same commands as before. With those three symbols, you can read most Makefiles you will ever open.

The recipe of the rule ferry: $(OBJS): diagram of $(CC) -o $@ $^, $(CC) = cc, $@ = ferry, $^ = main.o times.o fares.o, For ferry: cc -o ferry main.o times.o fares.o

Does it matter on a real project?

Does any of this matter on a real project? This is the source code of Lua, a real programming language, with the Makefile its authors wrote. Thirty-four C files. A full build, timed, with its output saved to a file so we can count it: two point three seconds, and thirty-six compiler commands. Now touch one file, and build again. Zero point four seconds. Just three commands: one file compiled, and two programs joined again.

Ana's terminal: the ferry office's Linux box
ls src/*.c | wc -l34
Ana's terminal: the ferry office's Linux box
time make > build.logar: `u' modifier ignored since `D' is the default (see `U')

real	0m2.324s
user	0m2.206s
sys	0m0.119s
Ana's terminal: the ferry office's Linux box
grep -c gcc build.log36
Ana's terminal: the ferry office's Linux box
touch src/lvm.c
Ana's terminal: the ferry office's Linux box
time make > build.logar: `u' modifier ignored since `D' is the default (see `U')

real	0m0.440s
user	0m0.416s
sys	0m0.025s
Ana's terminal: the ferry office's Linux box
grep -c gcc build.log3

And that picture of what depends on what gives one more gift. Pieces that don't depend on each other can be built at the same time. The J stands for jobs: make -j4 runs up to four recipes at once. zero point seven seconds for the full build. Every command and the lab files are linked in the description.

Building Lua 5.4.8 (seconds, in our lab): chart of Full build: 2.324 s, One file changed: 0.44 s, Full build, make -j4: 0.724 s, One file changed: about 5x faster. Four jobs: about 3x faster.

Recap

So, a Makefile is a list of rules: a target, what it's made from, and a recipe. Make rebuilds a target only when it's missing, or older than something it's made from. And it knows only what you list.

A Makefile is this picture, written down: diagram of ferry, main.o, times.o, fares.o, main.c, times.c, fares.c, times.h

If Makefiles finally read like plain rules to you, a like helps YouTube show this to more beginners, and subscribing brings the next Linux demo your way.

Every command here was typed into a real terminal, with the real make. Thanks for watching Cloud Explainers.

Sources

Demos: a container on our own Mac standing in for a ferry office's Linux box; the ferry office is made up. Lua is the real source from lua.org. Timings are from our lab and will differ on your machine. Watch the full video on YouTube.

More videos

Auth (OAuth, JWT)9 min

SAML vs OAuth vs OpenID Connect: What's the Difference?

Python7 min

How to Fix Python cron Jobs That Never Run

Linux22 min

Linux Bash Scripting: Full Tutorial for Beginners

Networking10 min

How PXE Network Boot Works

Active Directory9 min

How Active Directory Works

Linux10 min

How to Fix a Linux systemd Service That Won't Start

Linux9 min

Linux Logs: syslog vs journald

Auth (OAuth, JWT)9 min

JWT vs Session Cookies: Which One Should You Use?

Docker10 min

Docker vs Docker Compose: Which One Should You Use?

Docker9 min

How to Fix Docker Compose Containers That Can't Talk to Each Other

Django (Python web framework)22 min

Django Full Tutorial for Beginners

Linux9 min

How Linux systemd Works

Networking8 min

How to Fix 'Windows Cannot Access' Network Share Errors (SMB)

APIs10 min

REST vs gRPC: Which One Should You Use?

Certificates & PKI (ADCS)9 min

How to Fix SSL Certificate Chain Errors (Missing Intermediate)

Python9 min

Every Python Concept for Beginners in 10 Minutes

Databases & SQL9 min

Every SQL Join Type in 10 Minutes

OpenRouter (one API for many AI models)9 min

Why OpenRouter Free Models Hit Their Limit So Fast

Gemini API & Google AI Studio9 min

Why Your Gemini API Free Tier Runs Out So Fast

OpenRouter (one API for many AI models)10 min

OpenRouter Will Finally Make Sense After This

Caching & Redis10 min

How Redis Caching Works

Gemini API & Google AI Studio10 min

Gemini API vs Vertex AI: Which One Should You Use?

Gemini API & Google AI Studio8 min

How to Fix a Leaked Gemini API Key

Go (Golang)9 min

Why Golang Uses Goroutines Instead of Threads

Gemini API & Google AI Studio9 min

How to Fix Gemini API Error 429: Resource Exhausted

Claude & the Claude Developer Platform10 min

Claude Token Pricing Will Finally Make Sense After This

Python11 min

Why Python Web Scraping Returns Nothing (and the Fix)

Python10 min

Python uv vs pip: Which One Should You Use?

Python22 min

Python Automation: Full Tutorial for Beginners

Auth (OAuth, JWT)9 min

How OAuth Login Works

Gemini API & Google AI Studio10 min

Google AI Studio Will Finally Make Sense After This

Docker10 min

What Happens When You Run a Docker Container

Claude & the Claude Developer Platform8 min

How to Fix Claude Code Billing Your API Key Instead of Your Plan

Auth (OAuth, JWT)9 min

Why Passkeys Can't Be Phished Like Passwords

PowerShell10 min

PowerShell vs Bash: Which One Should You Learn?

Linux10 min

The Linux Roadmap for Beginners

Django (Python web framework)9 min

Django vs FastAPI: Which One Should You Use?

Databases & SQL10 min

Why SQLite Is the Most Used Database in the World

Databases & SQL11 min

How a NoSQL Database Like MongoDB Works

Python11 min

How to Fix Python pip error: externally-managed-environment

Databases & SQL9 min

How to Fix MySQL Too Many Connections Database Errors

Go (Golang)9 min

Golang Concurrency Will Finally Make Sense After This

AWS10 min

How Amazon S3 Works

Kubernetes10 min

How Kubernetes Works

Git10 min

How Git Works

Docker10 min

How to Fix Docker Port Already Allocated Errors

PostgreSQL10 min

How PostgreSQL Works

Git8 min

How to Fix Git Merge Conflicts

Linux9 min

How to Fix No Space Left on Device in Linux

Networking10 min

How the Internet Works

Databases & SQL9 min

SQL vs NoSQL: When Each One Wins

Auth (OAuth, JWT)8 min

How to Fix JWT Token Expired Errors (401 Unauthorized)

Claude & the Claude Developer Platform9 min

Claude API vs Claude Pro: Why Your Subscription Doesn't Include It

AI agent skills9 min

Why AI Agent Skills Beat Giant Prompts

Linux10 min

Linux Cron Jobs Will Finally Make Sense After This

Cybersecurity10 min

Security Logs Will Finally Make Sense After This

Cybersecurity8 min

How to Fix a Security Leak: An API Key Pushed to GitHub

DNS9 min

DNS Will Finally Make Sense After This

Cybersecurity9 min

Never Ignore a Security Update

Auth (OAuth, JWT)9 min

Two-Factor Authentication Will Finally Make Sense

Networking8 min

Never Expose a Database to the Internet

Cybersecurity10 min

SSH Keys vs Passwords: Which Is Safer for Security?

Linux10 min

The 5 Levels of Linux Boot: From Power On to Login

Networking12 min

The 5 Levels of Network Firewalls

Cybersecurity10 min

Password Security: Why Hackers Crack Weak Passwords

Linux9 min

Why Your Linux Server Runs Out of Memory

Networking10 min

HTTP vs HTTPS: What Actually Changes on Your Network

Linux12 min

Every Linux File Permission You Need to Know

Linux10 min

Linux Processes vs Threads: When Each One Wins

Networking11 min

The OSI Model Will Finally Make Sense After This

Git11 min

The 7 Levels of Git

Networking10 min

Every Network Troubleshooting Command You Need

Cybersecurity10 min

Hashing vs Encryption in Cybersecurity: When Each One Wins

Networking11 min

Why the Internet Ran Out of IP Addresses

Linux11 min

Linux Package Managers: apt vs Snap vs Flatpak

Python9 min

Why Python Is Slow (and When It Doesn't Matter)

Networking11 min

TCP vs UDP: When Each One Wins

Linux8 min

Stop Running Linux as Root (Do This Instead)

Linux11 min

Linux Users and Groups Will Finally Make Sense

Cybersecurity9 min

Why Phishing Beats Good Security (and How to Spot It)

Docker12 min

The 7 Levels of Docker

AI agents & MCP12 min

AI Agents Will Finally Make Sense After This

Git8 min

Stop Using git push --force (Use This Instead)

AI agent skills10 min

Agent Skills vs MCP: When Each One Wins

Networking12 min

Networking Will Finally Make Sense After This

Linux12 min

Every Linux Command You Actually Need

Cybersecurity12 min

The 7 Levels of Cybersecurity

System design12 min

Every System Design Concept You Need, in One Diagram

PostgreSQL10 min

Why PostgreSQL Ignores Your Index