Makefiles are the build definitions consumed by make, and GNU Make is the implementation almost everyone actually runs. The model is small enough to state in one sentence: a rule declares a target, the prerequisites the target depends on, and a recipe that produces the target from them. Make builds a directed graph from those rules and rebuilds a target when any prerequisite is newer than it. That is the whole engine, and its economy is why Makefiles are still everywhere fifty years on — in kernels, compilers, embedded firmware, scientific software, and as the task interface on top of otherwise modern toolchains.
The difficulty is that the language around that engine is unusual. Variables have two assignment semantics — recursively expanded and simply expanded — and choosing the wrong one produces bugs that appear only under specific conditions. Recipes run in a fresh shell per line unless told otherwise. Pattern rules, implicit rules and the built-in rule database mean make will often do something plausible without being asked. Functions such as wildcard, patsubst, foreach, eval and call operate on text at parse time, before any rule runs, and the difference between parse time and recipe time explains most surprising behaviour in real Makefiles.
Most problems attributed to make are actually incomplete dependency graphs. A build that requires make clean before it produces correct output has a graph that does not describe reality — usually missing header dependencies, missing order-only prerequisites for generated directories, or a recipe that produces outputs make was never told about. The same defects are what break parallel builds under -j, because parallelism simply exposes ordering that was previously accidental. Learning Makefiles properly means learning to make that graph complete.
Why this skill matters now
Every organisation with software older than a decade has Makefiles, and a meaningful share of them are load-bearing. Compilers, kernels, drivers, embedded firmware, database engines and numerical libraries are built this way, and those builds do not get rewritten because a build system rewrite is a multi-quarter project with no visible feature output.
At the same time make has quietly returned as a task interface. A Makefile with phony targets is the most portable way to give a repository a common entry point — make build, make test, make lint, make deploy — that works identically on a laptop, in a container and in CI, without adopting another tool. That usage is growing, and it is written by engineers who have never learned make properly, which produces its own class of subtle breakage.
The practical driver is usually performance and correctness. Builds that take twenty minutes because they cannot run in parallel, builds that produce wrong output unless cleaned first, builds that behave differently on a developer machine and on an agent — these are all dependency-graph defects, and they are fixable by someone who understands what make is actually doing rather than adding another clean step.