Skip to content

Non-recursive Makefile for multiple targets and configurations with MINGW

Here is a problem: how to compile multiple libraries in different directories without using recursive Makefile? Inspired by Peter Millers paper about recursive Make been harmful, I thought I would try my shot on the theme and learn how to write a proper Makefle. Project I have in hand is an old parser for .obj (3d model format) and .mtl (material files) of mine. It is written in ANSI C, with VS2008 and it compiles into two shared libraries: libobj.dll and libmtl.dll. There are corresponding debug versions too.
My goal is to create a build somewhat similar in spirit to what VS does to keep sources separate from intermediate files based on configurations. To start with here is a listing of my project directories:
Listing of project directory

Goal is to create one Makefile in top level directory and use it to compile both libraries and to end up with DLLs in bin and include libraries in LIB directory (needed for VS compiler). Intermediate object files are kept in bin/debug and bin/release directories in order to keep source clean.
Code is written in C (not C++) and contains a parser for .obj and .mtl formats in respective src directory. There is one file per function, as C programs are traditionally written. For example this is content of src/libobj:

Listing of libobj source directory

As seen there are quite many files, and there are not much fewer for .mtl parser either, so I am obviously not interested to type in all those names manually into makefile. So Makefile will also have to automatically include any source files in source directories too.
Those were my goals and actually it turned out not to be that hard to do it. But it did took me a while to figure out how due to my Make illiteracy. Here is what I ended up:

#system: win32, linux
#build: debug release
#cc: gcc, mingw, vs

system = win32
cc     = mingw
build  = release

CFLAGS = -DLIBAPI

# Compiler specific options
ifeq ($(cc),mingw)
CC        =  GCC
CFLAGS   += -Wall -D_WIN32
LDFLAGS   = -Wl,--subsystem,console -shared 
LDFLAGS  += -Wl,--out-implib,$(TARGET).a
endif

# System specific tools we need in make
ifeq ($(system),win32)
CP = xcopy /y
RM = del /f /q
SHELL  = cmd
OUTDIR = bin\$(build)\\
DBGDIR = bin\debug\\
RLSDIR = bin\release\\
endif

ifeq ($(system),linux)
mkdir  = mkdir -p
CP     = cp -f
OUTDIR = bin/$(build)/
DBGDIR = bin/debug/
RLSDIR = bin/release/
endif

# Build configuration
ifeq ($(build),debug)
  TARGET = $(OUTDIR)$@d
  CFLAGS += -O0 -g -DDEBUG
else
  TARGET = $(OUTDIR)$@
  CFLAGS += -O2
endif

objsrc = $(wildcard src/libobj/*.c)
mtlsrc = $(wildcard src/libmtl/*.c)
cmnsrc = $(wildcard src/common/*.c)

objobjs = $(patsubst src/libobj/%.c, $(OUTDIR)%.o, $(objsrc))
mtlobjs = $(patsubst src/libmtl/%.c, $(OUTDIR)%.o, $(mtlsrc))
cmnobjs = $(patsubst src/common/%.c, $(OUTDIR)%.o, $(cmnsrc))

VPATH = src/libobj:src/common:src/libmtl:include

all: libobj libmtl install

$(OUTDIR)%.o: %.c
	$(CC) -shared -c $^ -o $@

libobj: $(objobjs)
	$(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET).dll

libmtl: $(mtlobjs)
	$(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET).dll

install:
	$(CP) $(OUTDIR)*.a lib
	$(CP) $(OUTDIR)*.dll bin

clean:
	$(RM) $(DBGDIR)*.*
	$(RM) $(RLSDIR)*.*

.PHONY: all clean install

I am not an expert in writing Makefiles, so there is probably better way to do it, but this one works fine for a small homemade project of limited complexity as mine. I cannot explain it line by line, but I can spend a few word to explain my thoughts around it. To recap this is how proj dir looks with makefile in it:

First I had to configure some paths and tools so that Make can find and work with my source. I am using GCC and GNU Make from MINGW distribution which have some assumptions about environment it runs in. To make it work without complaints, I had to tell it it runs from cmd.exe and not bash. Install and clean goals also need few simple tools like cp and rm which on Windows machines are known as copy and del. So purpose of ifeq blocks is to set those variables, and also paths with correct directory separator based on system, build and compiler. Here I am using Makes built in feature to pass value of flags on command line (for example: make build=”release”). Currently I have written only what I need to run this Makefile with GCC from MINGW, but it is just matter of adding ifeq for some other compilers if needed.
After configuration I am adding source files with built in function wildcard. It lists all sources in a directory and puts them into a list which is transformed with patsubst to automatically rename it to list of object files which is later on passed to compiler. I have seen some other ways to do it, but I think this one is quite clean and easy to understand.

    objsrc = $(wildcard src/libobj/*.c)
    objobjs = $(patsubst src/libobj/%.c, $(OUTDIR)%.o, $(objsrc))

Last part are rules to compile sources and link files into final library. This rule tells compiler how to produce each object in list we get from Patsub function.

    $(OUTDIR)%.o: %.c
    $(CC) -shared -c $^ -o $@

Finally this rule will invoke compilation of objects and create a library:

    libobj: $(objobjs)
    $(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET).dll

So to add yet another library, say libxxx, I would have to add declarations for xxxsrc and xxxobjs directories, and to create rule for libxxx. To add a test app, is similar, but we have to change compiler flags not to produce dll, but an executable (which I haven’t put in my Makefile yet).

Also an important thing is to add sources to VPATH so that Make can find them. You can download complete Makefile here.

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*