A Makefile is a text file that instructs the `make` build utility how to compile, link, and otherwise process a project's source files by defining targets, their dependencies, and the shell commands needed to produce them. It automates the build process and enables incremental builds, rebuilding only the files that have changed since the last successful build.
In practice
In embedded development, a Makefile typically coordinates the entire toolchain invocation: calling the cross-compiler (such as `arm-none-eabi-gcc`), assembler, linker, and post-processing tools (such as `objcopy` to produce a flashable `.hex` or `.bin`). Variables at the top of the file usually capture the toolchain prefix, CPU flags (e.g., `-mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard`), include paths, and linker script paths, making it straightforward to retarget the build for a different device by changing a handful of lines. The blog post "Coding Step 1 - Hello World and Makefiles" walks through this kind of setup from scratch.
Makefiles rely on a dependency graph: each target lists the files it depends on, and `make` typically compares timestamps to decide what needs rebuilding, though some implementations and workflows layer additional logic on top of this. For a project with many source files, this can dramatically shorten rebuild times during iterative development. A common pitfall is an incomplete or incorrect dependency list -- if a header file is not listed as a dependency of the object files that include it, changing that header will not trigger a rebuild, leading to stale binaries. Many Makefiles address this by using GCC's `-MMD`/`-MP` flags to auto-generate `.d` dependency files that `make` then includes automatically.
Vendor IDEs (Keil MDK, IAR Embedded Workbench, STM32CubeIDE) manage their own internal build systems and may not expose a Makefile directly. However, many teams prefer hand-written or generated Makefiles for command-line builds, CI/CD pipelines, or editor-agnostic workflows. Tools such as CMake, SCons, or Meson can generate Makefiles (or Ninja build files) as an output, so developers sometimes work with a higher-level build description and never edit the raw Makefile. The blog post "Coding - Step 0: Setting Up a Development Environment" covers the broader toolchain context in which Makefiles sit.
Discussed on EmbeddedRelated
Frequently asked
What does a minimal embedded Makefile look like?
At minimum it needs a
toolchain prefix variable (e.g., `CC = arm-none-eabi-gcc`), CPU/architecture flags, a rule to compile each `.c` file to a `.o` object, a link rule that produces an `.
elf`, and usually an `objcopy` rule to convert the ELF to a flashable `.hex` or `.bin`. A `clean` target that deletes build artifacts is also standard practice.
How does `make` know what needs to be rebuilt?
`make` compares the modification timestamp of each target file against the timestamps of its listed prerequisites in the typical case. If any prerequisite is newer than the target, or if the target does not exist, `make` runs that target's recipe. Note that GNU make's exact behavior is more nuanced for order-only prerequisites, phony targets, and certain special targets. This is why correct and complete dependency lists matter: a missing dependency means `make` will not detect that a rebuild is necessary.
How do I handle header file dependencies automatically?
With
GCC or Clang, pass `-MMD -MP` to the compiler in your compile rule. The compiler will emit a `.d` file alongside each `.o` that lists the headers the source included. Add `-include $(OBJS:.o=.d)` (or equivalent) near the bottom of your Makefile so `make` reads those dependency files on the next invocation. This way, editing any included header triggers a rebuild of all affected translation units without manually tracking includes. Other
toolchains may provide different mechanisms for generating dependency information.
Should I write Makefiles by hand or use a generator like CMake?
Both approaches are common in embedded projects. Hand-written Makefiles give full visibility and control and have no extra tool dependency, which is valuable in constrained or offline build environments. Generators like
CMake add a layer of abstraction that can simplify managing multiple targets, platforms, or configurations, and they integrate well with IDEs and package managers. The right choice depends on project size, team familiarity, and CI infrastructure requirements.
My Makefile rebuilds everything every time even when nothing changed. Why?
Common causes include: a target name that does not match the actual output file (so `make` never sees the file as up to date), a `PHONY` declaration missing or incorrectly applied, auto-generated `.d` dependency files that pull in a path `make` cannot resolve (triggering a forced rebuild), or a recipe that does not actually create the target file. Adding `$(info ...)` statements or running `make --debug=v` helps trace which rule is firing and why.
Differentiators vs similar concepts
Makefiles are the input to the `make` utility specifically. Adjacent build systems such as
CMake, Meson, and SCons use their own description files (`CMakeLists.txt`, `meson.build`, `SConstruct`) and are not Makefiles, though CMake and Meson can emit Makefiles as one of their back-end output formats. Ninja is a separate, lower-level build tool that uses `.ninja` build files and is designed primarily for speed; it is usually generated by a higher-level tool rather than hand-authored, though manual authoring is possible. CMake and Meson can target Ninja instead of Make. Shell scripts can also drive a build but provide no dependency tracking or incremental rebuild capability.