This guide explains a classic non-recursive Makefile for a C project that builds both a static library (.a) and a shared library (.so). You will learn what each target does, how make executes the workflow, and how to modernize the file while keeping its original intent.
Originally published on 2009-01-29; fully revised and updated.

What the original Makefile is doing
The original post shows an older, single-file build style: source files are listed directly, object files are derived from that list, and two library targets are built from the same objects. This is still a valid approach for small C projects.
CC = gcc
CFLAGS = -Wall -O2
LFLAGS +=
SRCS = cas.c \
client.c \
hashtable_itr.c
OBJS = $(SRCS:.c=.o)
all: libmarquis.a libmarquis.so
clean:
\trm -f libmarquis.a libmarquis.so *.o
libmarquis.a: $(OBJS)
\tar -r $@ $(OBJS)
libmarquis.so: $(OBJS)
\t$(CC) -shared $(CFLAGS) -fPIC -Wl,-soname,libmarquis.so -o $@ $(OBJS)
.c.o:
\t$(CC) $(CFLAGS) -c $<Code language: JavaScript (javascript)
Important: recipe lines in a Makefile must begin with a real tab character. If tabs are converted to spaces, builds fail with a “missing separator” error.
Build workflow at a glance
flowchart TD
A[Run make all] --> B[Compile each C file into object file]
B --> C[Archive static library libmarquis.a]
B --> D[Link shared library libmarquis.so]
C --> E[Artifacts ready]
D --> E
Static vs shared library targets
| Target | Output | Command style | Typical use |
|---|---|---|---|
libmarquis.a | Static archive | ar -r ... | Link code into binaries at build time |
libmarquis.so | Shared object | gcc -shared ... | Load library dynamically at runtime |
Useful commands while developing
# Build both outputs (default target)
make
# Build only the static library
make libmarquis.a
# Build only the shared library
make libmarquis.so
# Show commands without executing them
make -n
# Force rebuild of targets
make -B
# Remove generated artifacts
make cleanCode language: PHP (php)
Modernized template with the same goals
The following version keeps the same intent, but uses a pattern rule, marks phony targets, and tracks header dependencies automatically.
CC ?= gcc
AR ?= ar
CFLAGS ?= -Wall -O2
CPPFLAGS ?=
LDFLAGS ?=
SRCS := cas.c client.c hashtable_itr.c
OBJS := $(SRCS:.c=.o)
DEPS := $(OBJS:.o=.d)
.PHONY: all clean
all: libmarquis.a libmarquis.so
libmarquis.a: $(OBJS)
\t$(AR) rcs $@ $(OBJS)
libmarquis.so: $(OBJS)
\t$(CC) -shared -Wl,-soname,libmarquis.so -o $@ $(OBJS) $(LDFLAGS)
%.o: %.c
\t$(CC) $(CPPFLAGS) $(CFLAGS) -fPIC -MMD -MP -c $< -o $@
clean:
\trm -f $(OBJS) $(DEPS) libmarquis.a libmarquis.so
-include $(DEPS)Code language: JavaScript (javascript)
Best practices
- Keep target names predictable (
all,clean, and output names). - Use
.PHONYfor non-file targets to avoid accidental conflicts. - Prefer pattern rules (
%.o: %.c) over older suffix rules when maintaining or extending builds. - Generate and include dependency files so header changes trigger correct rebuilds.
- Treat linker options (like
-Wl,-soname,...) as platform-specific and validate them in your CI environment.
I’m Rajesh Kumar, a DevOps, SRE, DevSecOps, Cloud, and Platform Engineering expert passionate about sharing practical knowledge, real-world experiences, and industry best practices. I have worked at Cotocus and regularly write about technology, travel, investing, health, product reviews, and digital marketing through my various platforms.
I publish technical articles at DevOps School, travel stories at Holiday Landmark, stock market insights at Stocks Mantra, health and fitness guidance at My Medic Plus, product reviews at TrueReviewNow, and SEO and digital marketing strategies at Wizbrand.
Find Trusted Cardiac Hospitals
Compare heart hospitals by city and services — all in one place.
Explore Hospitals