make Command: Tutorial & Examples

The oldest robot in Unix — it rebuilds only what changed, decided purely by timestamps.

What It Is

make is the command that turns a folder of source files into a finished program — but calling it a "build tool" sells it short and hides the one idea that makes it magic. make is a dependency engine. You hand it a list of this is made from that, and it figures out — entirely on its own — the smallest set of steps needed to bring everything up to date. Change one file out of a thousand, type make, and it rebuilds exactly that file and the things downstream of it, then stops. Nothing else gets touched. On a big project, that's the difference between a coffee break and an instant.

If you've cloned a project from GitHub and seen the holy trinity — ./configure, make, make install — you've already met it. make has been the heartbeat of compiling software on Unix since 1976, it ships on essentially every Linux box, and the Linux kernel itself — twenty-some million lines of C — is built by typing exactly four letters. This page takes you from "what even is a Makefile" to reading and writing them like someone who's done it for years, and along the way you'll pick up how the filesystem tracks time, why your CPU cores sit idle during a build, and one famous design mistake that has cost the human race more debugging hours than almost any other character in computing.

Your First Look

A make setup is two things: a file called Makefile that describes the work, and the make command that reads it. Here's the smallest useful one — it builds a C program called hello from hello.c:

hello: hello.c
	gcc -o hello hello.c

clean:
	rm -f hello

Run it:

$ make
gcc -o hello hello.c

make read the file, saw that hello is built from hello.c, and ran the gcc line to make it. Notice it echoes the command it runs — that's not noise, it's make showing its work. Now run it a second time without changing anything:

$ make
make: 'hello' is up to date.

Nothing happened — and that's the whole point. make looked at the two files, saw that hello is newer than hello.c, concluded there was nothing to do, and said so. Now touch the source to make it look freshly edited, and try again:

$ touch hello.c
$ make
gcc -o hello hello.c

It rebuilds. That little dance — build, do-nothing, change-one-thing, rebuild-just-that — is make in a nutshell, and we're about to take it apart.

How It Decides: The Timestamp Trick

Here's the magic, and it's so simple it feels like a trick: make does not understand your code at all. It doesn't parse C, it doesn't know what a compiler is, it has no idea what "building" means. It makes its entire decision on one piece of information the filesystem hands it for free — the modification time (mtime) stamped on every file.

The rule is one sentence: a target is rebuilt if any of its prerequisites is newer than it. That's it. make looks at hello, looks at hello.c, compares their two timestamps, and:

  • if hello.c is newer than hello → the source changed since the last build → rebuild.
  • if hello is newer → already up to date → skip.

You can see the exact same logic the ls command shows you — every file carries a timestamp, and make is just doing the comparison you'd do by eye, a million times faster. This is why touch (which does nothing but bump a file's mtime to now) tricks make into rebuilding: you didn't change a byte of code, but you changed the number make reads, so off it goes.

Why

This is also the source of the single most baffling make bug there is: you fix a file, rebuild, and nothing changes — because your computer's clock or a copied file left the source with a timestamp older than the output. make trusts the clock absolutely, and a clock that lies makes make lie too. If a build refuses to pick up a change you know you made, suspect timestamps before you suspect anything else.

Once this lands, make stops being a build tool in your head and becomes what it actually is: a tiny, relentless engine for the question "what is older than the thing it depends on?" — applied to a graph of files. Compiling code is just the most famous thing you can point it at.

Anatomy Of A Makefile

Every Makefile is built from one repeating shape, called a rule:

target: prerequisites
	recipe

Three parts, and learning their real names is half the battle:

  • target — the thing being made. Usually a file (hello, app.o), sometimes just a name for a job (clean, install).
  • prerequisites (also called dependencies) — the files the target is built from. These are what make checks the timestamps of. A target with newer prerequisites is "out of date."
  • recipe — the shell commands that actually do the work. Each line is handed to /bin/sh.

And now the most infamous detail in all of Unix, the one that has driven generations of newcomers to despair. The recipe lines must begin with a real TAB character, not spaces. Get this wrong and you see one of computing's most cryptic error messages:

$ make
Makefile:2: *** missing separator.  Stop.

"Missing separator" means "I expected a tab and you gave me spaces." It does not say "tab." It does not say "line 2 starts with spaces." It says "missing separator," and walks away. This single rule — a leading tab is semantically different from leading spaces, in a way your eyes cannot see — is responsible for an uncountable number of lost evenings. Configure your editor to show whitespace, or to keep tabs literal in Makefiles, and you'll never be bitten.

Backstory

The tab rule wasn't a grand design decision; it was a quick hack the author regretted almost immediately. Stuart Feldman, who wrote make at Bell Labs in 1976, later said: "Why the tab in column 1? Yacc was new, Lex was newer. I didn't know either well enough to do something reasonable... So I made something that worked, and I didn't realize I was making something that I'd have to live with for the rest of my life." By the time he saw the mistake, perhaps a dozen people were already using it — and he couldn't bring himself to break their Makefiles. A throwaway shortcut became a permanent law of the universe because two early users would have been mildly inconvenienced. Software is held together by exactly this kind of kindness, and we all pay for it forever.

How I Read It

When I open someone else's Makefile, here's the order my eyes move — because a Makefile reads backwards from how it runs.

First I look for the first target, or one named all. By default, typing plain make builds the first target in the file (or all, by convention, if it's listed first). Everything else only gets built if something depends on it. So the top of the file is the "front door" — it tells me what the project produces.

Then I trace the prerequisites like a tree. A real build is a chain: the final program depends on object files, each object file depends on a source file. make walks this dependency graph from the goal downward, and rebuilds bottom-up — it'll recompile the one .c file you edited into its .o, then relink the final binary, and skip every other file whose source you didn't touch. That selective, surgical rebuild is the entire reason make exists and why it beats a "just recompile everything" shell script every time.

Then I scan for .PHONY and the job-style targets. Targets like clean, install, test aren't files — they're commands dressed as targets. More on the subtle trap there in Gotchas.

Last I read the variables at the top. Real Makefiles hoist settings — CC = gcc, CFLAGS = -O2 -Wall — to the top so they're tunable in one place. Once I know those, the recipes below read cleanly.

Variables And Automatic Variables

Makefiles get DRY fast with variables. You set them with = and expand them with $(NAME):

CC = gcc
CFLAGS = -O2 -Wall
hello: hello.o util.o
	$(CC) $(CFLAGS) -o $@ $^
$ make
gcc -O2 -Wall -o hello hello.o util.o

See how $(CC) became gcc and $(CFLAGS) became the flags? Change the compiler in one line and the whole project follows. But the genuinely clever bit is $@ and $^ — those are automatic variables, and they're the secret to writing rules that don't repeat themselves:

  • $@ — the target (here, hello). "Whatever I'm building right now."
  • $^all prerequisites (here, hello.o util.o). The full input list.
  • $< — the first prerequisite only. Indispensable for compile rules, where the input is one source file.
  • $? — only the prerequisites newer than the target — the files that actually changed. Niche, but lovely.
  • $* — the stem, the part a pattern rule matched with %.

Read $(CC) $(CFLAGS) -o $@ $^ aloud and it says: "run the compiler with the flags, output to whatever I'm building, from all its inputs." The rule no longer mentions hello by name at all — which is exactly what makes the next idea possible.

Pattern Rules And Built-In Magic

Here's a thing that surprises everyone: that first hello: hello.c example had no recipe telling it how to compile C — yet it compiled. How? make ships with a library of built-in implicit rules, and one of them knows how to turn any .c into a binary. You can read them straight out of make's own brain:

$ make -p -f /dev/null
%.o: %.c
#  recipe to execute (built-in):
	$(COMPILE.c) $(OUTPUT_OPTION) $<

That %.o: %.c is a pattern rule: the % is a wildcard that matches a stem. It reads as "to make any foo.o, look for foo.c and compile it." Write that pattern once and make knows how to build a hundred object files from a hundred sources, no per-file rules needed. This is how a serious Makefile stays short: a couple of pattern rules do the repetitive work, and you only spell out the special cases by hand. (make -p dumps the entire built-in database — every rule and variable it knows before reading a single line of your file. It's a wonderful, overwhelming thing to scroll through once.)

Use Every Core: make -j

Now the move that feels like cheating and that most people never turn on. By default, make builds one thing at a time, in order:

$ make
building a
building b
building c

But your dependency graph already encodes which steps don't depend on each other — and independent steps can run simultaneously, one per CPU core. The -j flag (for "jobs") unleashes that:

$ make -j4
building a
building b
building c

Same result, but a, b, and c ran at the same time instead of waiting in line. On a real compile of hundreds of independent source files, this is night and day. And there's a perfect, self-tuning way to set it: ask the kernel how many cores you have with nproc and hand that straight to make:

make -j$(nproc)

On an 8-core box, nproc prints 8, so this runs up to eight compilers at once and saturates every core. This one habit can cut a from-scratch kernel build from forty minutes to five. If you've ever stared at top, watched a make crawl with seven of your eight cores sitting idle, and thought "why won't it use the whole machine?" — this is the answer. make won't parallelize unless you tell it to.

Pro Tip

Pair -j with -l (load limit): make -j$(nproc) -l$(nproc) runs flat-out but pauses launching new jobs whenever the load average climbs above your core count — so a heavy build won't drag the rest of the machine to its knees. It's the polite way to use every core without making everything else unusable.

The Flags, Explained

make has a deep flag set; these are the ones worth carrying. Captured from make --help on GNU Make 4.4.1:

  • -j [N] — run up to N jobs in parallel (no number = unlimited, which can fork-bomb your RAM — always give a number). The single biggest speed win.
  • -l [N] — don't start new jobs while the load average is above N. The throttle for -j.
  • -n, --dry-run — print the commands make would run, without running any. The safest way to understand a Makefile before trusting it. Pure gold.
  • -B, --always-make — rebuild everything unconditionally, ignoring timestamps. The "I don't care, just do it all" button.
  • -C DIR — change into DIR first. How a top-level Makefile drives builds in subdirectories.
  • -f FILE — use FILE instead of Makefile. For when your build file has a different name.
  • -k, --keep-going — on an error, keep building everything that doesn't depend on the failed part, instead of stopping dead. Great for seeing all your compile errors in one pass.
  • -i, --ignore-errors — ignore failures from recipe commands entirely. Use with care.
  • -s, --silent — don't echo the commands as they run. Quiet builds.
  • -p, --print-data-base — dump make's entire internal database of rules and variables. The X-ray.
  • -d / --debug — print lots of reasoning: why each target was or wasn't rebuilt. When make does something baffling, -d tells you which timestamp made the call.
  • -q, --question — run nothing; just exit 0 if everything's up to date, non-zero if not. The scriptable "is a build needed?" check.
  • -t, --touch — mark targets up-to-date (bump their timestamps) without building. Occasionally handy, usually a footgun.
  • -W FILE — pretend FILE was just modified ("what-if"); shows what a change to it would trigger. The flip side of -o FILE, which pretends a file is old and won't be rebuilt.
  • -e — let environment variables override Makefile variables (normally it's the other way around).
  • -r / -R — disable built-in rules / built-in variables, for a faster, more predictable build with no implicit magic.

Reading It by Example

Build instinct fast — output → what it means:

make: 'hello' is up to date. → the target exists and is newer than every prerequisite. make did its job by doing nothing. This is success, not an error.

make: *** No rule to make target 'nope'. Stop. → you asked for a target (make nope) that isn't in the Makefile and isn't a file make knows how to produce. Either a typo or a missing rule. Also appears when a prerequisite file is missing and there's no rule to create it — a classic "you forgot to list how foo.h gets generated."

Makefile:2: *** missing separator. Stop. → spaces where a tab belongs at the start of a recipe line. The number one beginner trap; see Anatomy.

make: Nothing to be done for 'all'. → the all target has no recipe and all its prerequisites are up to date. Usually harmless; occasionally means you defined a target but forgot to make anything depend on the real work.

make: *** [Makefile:3: hello] Error 1 → a recipe command itself failed (returned non-zero). The Error 1 is the command's exit code, not make's — make stops because a step it ran broke. Scroll up to find the real compiler error; this line is just make reporting the body, not the cause.

How You'll Actually Use It

The honest truth: most of the time you type make and make install on someone else's project and never write a Makefile at all. The everyday moves:

make              # build the default target
make clean        # run the cleanup target
make -j$(nproc)   # build fast, using every core
make -n install   # SEE what 'make install' would do — before it does it

That make -n habit is worth gold. Before running an unfamiliar make install — which often copies files into system directories as root-n prints every command it would run so you can read it first. Trust, but verify.

When you do write one, the pattern is small: variables at the top, a couple of pattern rules, an all target first, and a .PHONY line. And make isn't only for C — people drive Docker builds, LaTeX documents, static sites, and database migrations with it, because the core idea ("only redo what's out of date") is universal. If your task is "a pile of steps where some depend on others, and I don't want to redo unchanged work," make already solved it in 1976.

Gotchas

  • Tabs, not spaces — said it twice because it'll bite you thrice. missing separator = spaces in a recipe line.
  • .PHONY for job targets — if a file named clean ever appears in your directory, make clean will look at it, decide it's "up to date," and silently stop cleaning. The fix is to declare such targets phony: .PHONY: clean all install test. That tells make "these are commands, not files — always run them, never check timestamps." Forgetting it is a quiet, maddening bug.
  • Each recipe line is a separate shellcd build on one line does not affect the next line; that next command runs back in the original directory. To chain, join with && and \ on one logical line, or set .ONESHELL:.
  • A failed line stops the whole build — by default make halts the instant any command returns non-zero. Usually what you want; use -k when you'd rather see every error at once.
  • make -j without a number = unlimited parallelism. On a big project that can fork hundreds of compilers and exhaust your RAM into swap. Always write -j$(nproc) or a fixed number.
  • Timestamps, not contentsmake never hashes your files; it only compares clocks. A restored backup, a git checkout, or a skewed clock can leave a source "older" than its output and silently skip a needed rebuild. When in doubt, make -B forces a full rebuild.

History & Philosophy

make was written in 1976 by Stuart Feldman at Bell Labs, born — like so much good software — out of personal pain. A colleague had spent half a day chasing a bug that turned out to be a program that simply hadn't been recompiled after a source edit; the old binary was still being tested. Feldman's insight was to stop trusting humans to remember what needed rebuilding and let the machine work it out from file timestamps. It earned him a share of the 2003 ACM Software System Award, and the design has been so durable that the make you ran today is recognizably the same tool, half a century on.

What makes it endure is a philosophy worth absorbing, because it shows up everywhere once you see it: declare the relationships, not the steps. You don't tell make how to build in what order — you tell it what depends on what, and it derives the order, the parallelism, and the minimal work itself. That's declarative thinking, and it's the same idea behind everything from a database query (you say what rows you want, not how to fetch them) to modern infrastructure-as-code. Learn to see your problem as a graph of dependencies and a whole class of tools — make first among them — does the hard part for you.

And here's the thread to pull on some evening: GNU Make is what you almost certainly have, but it spawned a whole dynasty of children who each thought they could do better. cmake and autotools generate Makefiles so you don't write them by hand; ninja is a stripped-down speed demon that cmake emits for huge C++ projects; Bazel (Google's) and Buck (Meta's) scaled the same dependency-graph idea to monorepos with millions of files and cached, shared build results across an entire company. Every one of them is a descendant of one person's annoyance at a colleague testing a stale binary in 1976. The tab is still there in all of them, too. Some inheritances you can't escape.

See Also

  • gcc — the compiler make usually drives
  • nproc — count cores for make -j$(nproc)
  • ls — see the timestamps make makes its decisions from
  • touch — bump an mtime to force or fake a rebuild
  • top — watch a parallel build use (or not use) your cores
  • load average — the number make -l throttles against
  • kernel — the famous thing you build with four letters and -j$(nproc)
  • high load — what an unthrottled make -j can do to a box

Did a runaway build just bury your server in compilers and drag everything to a crawl?

CleverUptime watches the symptoms a heavy build creates — load spiking past your core count, memory vanishing into swap, one process tree eating the whole machine — and flags the cause in plain language, so you catch the box choking before your users do.

Want to see your own server's health right now? One command, no signup, no install.

Check your server →