Table of Contents
KEY TAKEAWAYS
- Make automates the build process by tracking file dependencies and only recompiling changed files
- A Makefile consists of rules with targets, prerequisites, and recipes (commands)
- Variables (
CC,CFLAGS,LDFLAGS) make Makefiles flexible and maintainable - Phony targets like
cleanandallperform actions that do not produce output files
make utility can come in handy. The make utility requires a file, Makefile (or makefile), which defines set of tasks to be executed. You may have used make to compile a program from source code. Most open source projects use make to compile a final executable binary, which can then be installed using make install.In this article, we’ll explore make and Makefile using basic and advanced examples. Before you start, ensure that make is installed in your system.Basic examples of Makefile
Let’s start by printing the classic “Hello World” on the terminal. Create a empty directorymyproject containing a file Makefile with this content:say_hello: echo "Hello World"Now run the file by typing
make inside the directory myproject. The output will be:$ make echo "Hello World" Hello WorldIn the example above,
say_hello behaves like a function name, as in any programming language. This is called the target. The prerequisites or dependencies follow the target. For the sake of simplicity, we have not defined any prerequisites in this example. The command echo "Hello World" is called the recipe. The recipe uses prerequisites to make a target. The target, prerequisites, and recipes together make a rule.target: prerequisites <TAB> recipe
One very important thing to note is that there is a tab before the gcc command in the makefile. There must be a tab at the beginning of any command, and make will not be happy if it’s not there.As an example, a target might be a binary file that depends on prerequisites (source files). On the other hand, a prerequisite can also be a target that depends on other dependencies:
final_target: sub_target final_target.c Recipe_to_create_final_target sub_target: sub_target.c Recipe_to_create_sub_targetIt is not necessary for the target to be a file; it could be just a name for the recipe, as in our example. We call these “phony targets.”
make was executed, the entire command echo "Hello World" was displayed, followed by actual command output. We often don’t want that. To suppress echoing the actual command, we need to start echo with @:say_hello: @echo "Hello World"Now try to run
make again. The output should display only this:$ make Hello WorldLet’s add a few more phony targets:
generate and clean to the Makefile:say_hello:
@echo "Hello World"
generate:
@echo "Creating empty text files..."
touch file-{1..10}.txt
clean:
@echo "Cleaning up..."
rm *.txtIf we try to run make after the changes, only the target say_hello will be executed. That’s because only the first target in the makefile is the default target. Often called the default goal, this is the reason you will see all as the first target in most projects. It is the responsibility of all to call other targets. We can override this behavior using a special phony target called .DEFAULT_GOAL..DEFAULT_GOAL := generateThis will run the target
generate as the default:$ make
Creating empty text files...
touch file-{1..10}.txtAs the name suggests, the phony target .DEFAULT_GOAL can run only one target at a time. This is why most makefiles include all as a target that can call as many targets as needed.all and remove .DEFAULT_GOAL: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 *.txtBefore running make, let’s include another special phony target, .PHONY, where we define all the targets that are not files. make will run its recipe regardless of whether a file with that name exists or what its last modification time is. Here is the complete makefile:.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 *.txtThe make should call say_hello and generate:$ make
Hello World
Creating empty text files...
touch file-{1..10}.txt
clean in all or put it as the first target. clean should be called manually when cleaning is needed as a first argument to make:$ make clean Cleaning up... rm *.txtNow that you have an idea of how a basic makefile works and how to write a simple makefile, let’s look at some more advanced examples.
Advanced examples of Makefile
Variables
In the above example, most target and prerequisite values are hard-coded, but in real projects, these are replaced with variables and patterns.The simplest way to define a variable in a makefile is to use the= operator. For example, to assign the command gcc to a variable CC:CC = gccThis is also called a recursive expanded variable, and it is used in a rule as shown below:
hello: hello.c
${CC} hello.c -o helloAs you may have guessed, the recipe expands as below when it is passed to the terminal:gcc hello.c -o helloBoth
${CC} and $(CC) are valid references to call gcc. But if one tries to reassign a variable to itself, it will cause an infinite loop. Let’s verify this:CC = gcc
CC = ${CC}
all:
@echo ${CC}Running make will result in:$ make Makefile:8: *** Recursive variable 'CC' references itself (eventually). Stop.To avoid this scenario, we can use the
:= operator (this is also called the simply expanded variable). We should have no problem running the makefile below:CC := gcc
CC := ${CC}
all:
@echo ${CC}Patterns and functions# Usage:
# make # compile all binary
# make clean # remove ALL binaries and objects
.PHONY = all clean
CC = gcc # compiler to use
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} - Lines starting with
#are comments. - Line
.PHONY = all cleandefines phony targetsallandclean. - Variable
LINKERFLAGdefines flags to be used withgccin a recipe. SRCS := $(wildcard *.c):$(wildcard pattern)is one of the functions for filenames. In this case, all files with the.cextension will be stored in a variableSRCS.BINS := $(SRCS:%.c=%): This is called as substitution reference. In this case, ifSRCShas values'foo.c bar.c',BINSwill have'foo bar'.- Line
all: ${BINS}: The phony targetallcalls values in${BINS}as individual targets. - Rule:Let’s look at an example to understand this rule. Suppose
%: %.o @echo "Checking.." ${CC} ${LINKERFLAG} $< -o $@foois one of the values in${BINS}. Then%will matchfoo(%can match any target name). Below is the rule in its expanded form:As shown,foo: foo.o @echo "Checking.." gcc -lm foo.o -o foo
%is replaced byfoo.$<is replaced byfoo.o.$<is patterned to match prerequisites and$@matches the target. This rule will be called for every value in${BINS} - Rule:
%.o: %.c @echo "Creating object.." ${CC} -c $< Every prerequisite in the previous rule is considered a target for this rule. Below is the rule in its expanded form: foo.o: foo.c @echo "Creating object.." gcc -c foo.c - Finally, we remove all binaries and object files in target
clean.
foo.c:# Usage: # make # compile all binary # make clean # remove ALL binaries and objects .PHONY = all clean CC = gcc # compiler to use LINKERFLAG = -lm SRCS := foo.c BINS := foo all: foo foo: foo.o @echo "Checking.." gcc -lm foo.o -o foo foo.o: foo.c @echo "Creating object.." gcc -c foo.c clean: @echo "Cleaning up..." rm -rvf foo.o foo
Complete Makefile for AVR Projects (Copy-Paste Template)
Here is a production-ready Makefile for a typical embedded C project targeting an ATmega328P with avr-gcc. Copy this into your project directory, adjust the three variables at the top, and run make.
# ============================================ # AVR Project Makefile — ATmega328P + avr-gcc # ============================================ # Adjust these three variables for your project: MCU = atmega328p F_CPU = 16000000UL TARGET = main # Toolchain CC = avr-gcc OBJCOPY = avr-objcopy OBJDUMP = avr-objdump SIZE = avr-size AVRDUDE = avrdude # Programmer (change for your setup) PROGRAMMER = arduino PORT = /dev/ttyACM0 BAUD = 115200 # Compiler flags CFLAGS = -mmcu=$(MCU) -DF_CPU=$(F_CPU) CFLAGS += -Os -Wall -Wextra -std=c11 CFLAGS += -funsigned-char -funsigned-bitfields CFLAGS += -fpack-struct -fshort-enums CFLAGS += -ffunction-sections -fdata-sections # Linker flags — remove unused sections to shrink binary LDFLAGS = -mmcu=$(MCU) -Wl,--gc-sections # Source files — automatically finds all .c files in the directory SRC = $(wildcard *.c) OBJ = $(SRC:.c=.o) # ── Build targets ── all: $(TARGET).hex size $(TARGET).elf: $(OBJ) $(CC) $(LDFLAGS) -o $@ $^ $(TARGET).hex: $(TARGET).elf $(OBJCOPY) -O ihex -R .eeprom $< $@ %.o: %.c $(CC) $(CFLAGS) -c -o $@ $< # ── Utility targets ── size: $(TARGET).elf @echo "" @$(SIZE) --mcu=$(MCU) --format=avr $(TARGET).elf flash: $(TARGET).hex $(AVRDUDE) -c $(PROGRAMMER) -p $(MCU) -P $(PORT) -b $(BAUD) -U flash:w:$< disasm: $(TARGET).elf $(OBJDUMP) -d -S $< > $(TARGET).lst clean: rm -f $(OBJ) $(TARGET).elf $(TARGET).hex $(TARGET).lst .PHONY: all size flash disasm clean
How to Use This Makefile
make— Compiles all.cfiles, links them, generates the.hexfile, and prints memory usage.make flash— Uploads the hex file to your board via avrdude. ChangePROGRAMMER,PORT, andBAUDfor your setup.make disasm— Generates a disassembly listing (.lstfile) so you can inspect the generated assembly.make clean— Removes all build artifacts.
Adapting for Other MCUs
- ATmega2560: Change
MCU = atmega2560— everything else stays the same. - ATtiny85: Change
MCU = attiny85andF_CPU = 8000000UL(internal 8 MHz oscillator). - STM32 (ARM): Replace avr-gcc with
arm-none-eabi-gcc, add a linker script (-T linker.ld), and usest-flashoropenocdinstead of avrdude.
The key insight of Makefiles is dependency tracking: make only recompiles files that changed. In a project with 20 source files, editing one file recompiles just that file and re-links — saving significant time compared to recompiling everything.
📖 Related: Most common pitfalls in C Programming Language and how to avoid them

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.







