Have you ever tried to write Makefile for your project? If not then I can tell you - its a mess. It was so painful for me that I even made my own build script.
But, eventually I had to learn Makefiles because my script was hitting its limits and I didn’t want to reinvent the wheel.
Sadly, I don’t know how to write good Makefiles so I took some generic Makefiles and cut them in pieces. After a while I had what I wanted - the core of a Makefile. Here it is!
app: ./bin/main.o
gcc -o app ./bin/main.o
./bin/main.o: ./bin/main.o : ./src/main.c
gcc -c -o ./bin/main.o ./src/main.c
These rules describe precisely the build project for simple main.c project. It works like that:
- make has to build the
app
file but it depends on the./bin/main.o
, ./bin/main.o
depends on the./src/main.c
,./src/main.c
exists, somain.o
can be built,./bin/main.o
is built as single object file (the-c
flag assures it),app
can now be built from because all dependencies are met
It’s plain and simple but it will work only for single source file named main.c
and that’s a problem. But we can fix it with variables:
$(TARGET): $(OBJS)
gcc -o $@ $^
$(OBJS): $(OBJDIR)/%$(OBJEXT) : $(SRCDIR)/%$(SRCEXT)
gcc -c -o $@ $?
Here’s a description of these variables:
$^
is list of all files after the target name (all object files)$?
is list of all modified files that have to be compiled (modified source files)$(TARGET)
is just the name of a compiled app,$(OBJS)
is a list of files based on source files,$(SRCDIR)
and$(OBJDIR)
are the names of source and binary directories,$(SRCEXT)
and$(OBJEXT)
contain file extensions for source and binary files.
One thing that might concern you is the fourth line:
$(OBJS): $(OBJDIR)/%$(OBJEXT) : $(SRCDIR)/%$(SRCEXT)
Thanks to this the input files $(OBJDIR)/%$(OBJEXT)
will be created from $(SRCDIR)/%$(SRCEXT)
. The %
sign symbolizes currently processed file name, so all it does is replaces ./src/<file>.c
to ./bin/<file>.o
.
Next, we can add few more standard rules and variables.
all: $(TARGET)
@echo Done.
run: $(TARGET)
@./$(TARGET)
clean:
@rm -r $(TARGET) $(OBJS) $(OBJDIR)
$(TARGET): $(OBJS)
@$(LD) $(LDFLAGS) -o $@ $^
$(OBJS): $(OBJDIR)/%$(OBJEXT) : $(SRCDIR)/%$(SRCEXT) | $(OBJDIR)
@$(CC) $(CCFLAGS) -c -o $@ $?
$(OBJDIR):
@mkdir -p $(OBJDIR)
.PHONY: all run clean
I’ve added the all
, run
and clean
rules and new variables: $(LD)
/$(LDFLAGS)
and $(CC)
/$(CCFLAGS)
for linker/flags and compiler/flags and a new rule for creating $(OBJDIR)
if it doesn’t exist yet ($(OBJDIR)
is often removed by the clean
rule).
There’s also a $(OBJDIR)
for specifying output directory and $(OBJEXT)
for object file extension. The @
sign mutes printing of currently executed command (we only want the output).
This makefile is pretty much done but it still won’t work because these variables are never defined. We can change that with some shell commands.
TARGET = $(notdir $(CURDIR))
SRCEXT = .c
INCEXT = .h
OBJEXT = .o
SRCDIR = ./src
INCDIR = ./src
OBJDIR = ./bin
CC = gcc
LD = gcc
LDFLAGS = -lm
CCFLAGS = -std=gnu99 -g -ggdb -Og -Wall -Wextra -pedantic
SRCTREE = $(shell find $(SRCDIR) -type d)
INCS = $(shell find $(INCDIR) -type f -name '*$(INCEXT)')
SRCS = $(shell find $(SRCDIR) -type f -name '*$(SRCEXT)')
OBJTREE = $(foreach D,$(SRCTREE),$(shell echo $(D) | sed 's/$(SRCDIR)/$(OBJDIR)/'))
OBJSTMP = $(foreach F,$(SRCS),$(shell echo $(F) | sed -e 's/$(SRCDIR)/$(OBJDIR)/'))
OBJS = $(foreach O,$(OBJSTMP),$(shell echo $(O) | sed -e 's/\$(SRCEXT)/\$(OBJEXT)/'))
The simple ones:
TARGET
is the name of current directory,SRCEXT
,INCEXT
andOBJEXT
source code, header files and object files extensions,SRCDIR
,INCDIR
,OBJDIR
are set up for source and header files directory and output directory,CC
,LD
,LDFLAGS
andCCFLAGS
compiler, linker, compiler flags and linker flags.
And the cool ones:
SRCTREE
contains all directories found in$(SRCDIR)
,INCS
has all header files in$(INCDIR)
,SRCS
has all source files in$(SRCDIR)
,OBJTREE
has all dirs fromSRCTREE
but with replaced source directory name for the object directory one,OBJSTMP
has all files fromSRCS
with the same rule as above applied,OBJS
all files fromOBJSTMP
with replaced source file extension for object file extension.
Those few lines will basically generate three file lists - SRCS
, INCS
and OBJS
all nicely filled from SRCDIR
and INCDIR
.
That’s it! Here’s the final makefile - it contains our clever variables and simple rules. Together it allows us to easily compile C/C++ projects.
TARGET = $(notdir $(CURDIR))
SRCEXT = .c
INCEXT = .h
OBJEXT = .o
SRCDIR = src
INCDIR = src
OBJDIR = bin
CC = gcc
LD = gcc
LDFLAGS = -lm -lpthread
CCFLAGS = -std=gnu99 -g -ggdb -Og -Wall -Wextra -pedantic
SRCTREE = $(shell find $(SRCDIR) -type d)
INCS = $(shell find $(INCDIR) -type f -name '*$(INCEXT)')
SRCS = $(shell find $(SRCDIR) -type f -name '*$(SRCEXT)')
OBJTREE = $(foreach D,$(SRCTREE),$(shell echo $(D) | sed 's/$(SRCDIR)/$(OBJDIR)/'))
OBJSTMP = $(foreach F,$(SRCS),$(shell echo $(F) | sed -e 's/$(SRCDIR)/$(OBJDIR)/'))
OBJS = $(foreach O,$(OBJSTMP),$(shell echo $(O) | sed -e 's/\$(SRCEXT)/\$(OBJEXT)/'))
all: $(TARGET)
@echo Done.
run: $(TARGET)
@./$(TARGET)
clean:
@rm -r $(TARGET) $(OBJS) $(OBJDIR) 2>/dev/null || true
$(TARGET): $(OBJS) | $(OBJDIR)
@$(LD) $(LDFLAGS) -L$(OBJDIR) -o $@ $^
$(OBJS): $(OBJDIR)/%$(OBJEXT) : $(SRCDIR)/%$(SRCEXT) | $(OBJDIR)
@$(CC) $(CCFLAGS) -I$(INCDIR) -c -o $@ $?
$(OBJDIR):
@mkdir -p $(OBJDIR) $(OBJTREE)
# comment
.PHONY: all run clean