4
\$\begingroup\$

For my own projects, I regularly use GNU make and often had the same requirements like

  • It should work in windows cmd as well as in a *nix shell
  • It should work for parallel builds and without recursion
  • It should support cross compiling
  • It should generally be configurable at the command line
  • It should be reliable wrt which object files need rebuilding

This ultimately led to a lot of copy-pasting from project to project with the newest project having the newest makefile features, so I decided to finally create a project only for the build system.

It consists of several included modules, I'll present the most important ones here. The first one is a relatively simple module determining the host environment and setting some variables based on it:

ifeq ($(OS),Windows_NT)

undefine POSIXSHELL
ifneq ($(strip $(filter %sh,$(basename $(realpath $(SHELL))))),)
POSIXSHELL := 1
endif

OSVER := $(subst ],,$(lastword $(shell cmd /c ver)))
_ZIMK__OSVER := $(subst ., ,$(OSVER))
OSVER_MAJ := $(firstword $(_ZIMK__OSVER))
OSVER_MIN := $(word 2, $(_ZIMK__OSVER))
OSVER_REV := $(word 3, $(_ZIMK__OSVER))

else

POSIXSHELL := 1

endif

ifdef POSIXSHELL

CMDSEP := ;
PSEP := /
CPF := cp -f
RMF := rm -f
RMFR := rm -fr
MDP := mkdir -p
MV := mv
STAMP := touch
XIF := if [ -x
XTHEN := ]; then
XFI := ; fi
CATIN := cat
CATADD :=
CATOUT := >
EQT := "
#" make vim syntax highlight happy
CMDQUIET := >/dev/null 2>&1
CODNOERR := 2>/dev/null
CMDNOIN := </dev/null

INSTALL ?= install
INSTDIR := $(INSTALL) -d

instfile = $(INSTDIR) $(2) $(CMDSEP) $(INSTALL) -m$(3) $(1) $(2)
geq = $(shell if test $(1) -ge $(2); then echo 1; fi)

touch = touch $(1)

SYSNAME := $(shell uname 2>/dev/null)

else

CMDSEP := &
PSEP := \\
CPF := copy /y
RMF := del /f /q
RMFR := -rd /s /q
MDP := -md
MV := move
STAMP := copy /y NUL
XIF := if exist
XTHEN := (
XFI := )
CATIN := copy /b
CATADD := +
CATOUT :=
EQT :=
CMDQUIET := >nul 2>nul & verify >nul
CMDNOERR := 2>nul & verify >nul
CMDNOIN := <nul

INSTDIR := $(MDP)

instfile = $(MDP) $(dir $(2)) $(CMDQUIET) $(CMDSEP) copy $(1) $(2) $(CMDQUIET)
geq = $(shell if $(1) geq $(2) echo 1)

touch = copy /b $(1) +,,

SYSNAME := $(shell uname 2>nul & verify >nul)

endif

After that, I know whether I'm in cmd or a *nix shell and I have some variables and functions defined to do the right thing in $(shell ) or in recipes.

A little more complicated is the module that handles configurations. It can save a given set of configuration variables to config files and use them automatically. There is support for using DEFAULT_[name] variants if the user didn't specify a value for a variable. This module is also responsible for determining the target platform and set the default output directories based on build configuration and platform:

define ZIMK__UNIQ
$(strip $(eval undefine __ZIMK__UNIQ__SEEN)$(foreach \
    _v,$1,$(if $(filter $(_v),$(__ZIMK__UNIQ__SEEN)),,$(eval \
        __ZIMK__UNIQ__SEEN += $(_v))))$(__ZIMK__UNIQ__SEEN))
endef
SINGLECONFVARS += prefix exec_prefix bindir sbindir libexecdir datarootdir \
              sysconfdir sharedstatedir localstatedir runstatedir \
              includedir docrootdir libdir localedir
SINGLECONFVARS := $(call ZIMK__UNIQ,CC CXX CPP AR STRIP MOC $(SINGLECONFVARS))
LISTCONFVARS := $(call ZIMK__UNIQ,CFLAGS CXXFLAGS DEFINES INCLUDES LDFLAGS \
    $(LISTCONFVARS))
CONFVARS := $(SINGLECONFVARS) $(LISTCONFVARS)
BUILDCFGS := $(call ZIMK__UNIQ,release debug $(BUILDCFGS))
NOBUILDTARGETS := $(sort clean distclean config changeconfig showconfig \
    _build_config _build_changeconfig $(NOBUILDTARGETS))
MAKECMDGOALS ?= all

-include global.cfg

undefine ZIMK__EMPTY
ZIMK__EMPTY :=
ZIMK__TAB := $(ZIMK__EMPTY) $(ZIMK__EMPTY)

#default config
DEFAULT_BUILDCFG ?= release
BUILDCFG ?= $(DEFAULT_BUILDCFG)
BUILDCFG := $(strip $(BUILDCFG))

ZIMK__BUILDCFG := $(filter $(BUILDCFG),$(BUILDCFGS))
ifndef ZIMK__BUILDCFG
$(error Unknown BUILDCFG $(BUILDCFG))
endif

ZIMK__DOUBLECONFVARS := $(filter $(SINGLECONFVARS),$(LISTCONFVARS))
ifdef ZIMK__DOUBLECONFVARS
$(error variables can't be in both SINGLECONFVARS and LISTCONFVARS: $(ZIMK__DOUBLECONFVARS))
#' make vim syntax highlight happy
endif

USERCONFIG:=$(BUILDCFG).cfg
ZIMK__CFGCACHE:=.cache_$(BUILDCFG).cfg

define ZIMK__WRITECACHELINE

$(ZIMK__TAB)$$(VR)echo $$(EQT)C_$(_cv) := $$(strip $($(_cv)))$$(EQT) >>$$(ZIMK__CFGCACHE)
endef
define ZIMK__WRITECACHE
$$(ZIMK__CFGCACHE):
    $$(VR)echo $$(EQT)# generated file, do not edit!$$(EQT) >$$(ZIMK__CFGCACHE)$(foreach _cv,$(CONFVARS),$(if $(strip $($(_cv))),$(ZIMK__WRITECACHELINE),))
endef
define ZIMK__WRITECFGLINE

$(ZIMK__TAB)$$(VR)echo $$(EQT)$(_cv) ?= $$(strip $($(_cv)))$$(EQT) >>$$(USERCONFIG)
endef
define ZIMK__WRITECFG
$(ZIMK__CFGTARGET): $$(USERCONFIG)
    $$(VCFG)
    $$(VR)echo $$(EQT)# generated file, do not edit!$$(EQT) >$$(USERCONFIG)$(foreach _cv,$(CONFVARS),$(if $(strip $($(_cv))),$(ZIMK__WRITECFGLINE),))
endef
define ZIMK__WRITECFGTAG
undefine ZIMK__CFGTAG
$$(foreach _cv,$$(CONFVARS), \
    $$(if $$($$(_cv)),$$(eval ZIMK__CFGTAG += $$(_cv)=$$$$(strip $$$$($$(_cv)))),))
ifdef ZIMK__CFGTAG
ZIMK__CFGTAG := $$(ZIMK__PRBOLD)[$$(ZIMK__PRRED)$$(BUILDCFG)$$(ZIMK__PRNORM)$$(ZIMK__PRBOLD): $$(ZIMK__PRYELLOW)$$(ZIMK__CFGTAG)$$(ZIMK__PRNORM)$$(ZIMK__PRBOLD)]$$(ZIMK__PRNORM)
else
ZIMK__CFGTAG := $$(ZIMK__PRBOLD)[$$(ZIMK__PRRED)$$(BUILDCFG)$$(ZIMK__PRNORM)$$(ZIMK__PRBOLD)]$$(ZIMK__PRNORM)
endif
ZIMK__CFGMSG :=
ZIMK__CFGMSG +=
ZIMK__CFGMSG +=
ZIMK__CFGMSG += $$(ZIMK__PRBOLD)$$(ZIMK__PRYELLOW)[CFG]$$(ZIMK__PRNORM)  $$(ZIMK__CFGTAG)
$$(info $$(ZIMK__CFGMSG))
endef

ifndef MAKE_RESTARTS
ifneq ($(filter config,$(MAKECMDGOALS)),)
$(eval $(ZIMK__WRITECFGTAG))
endif
endif

# save userconfig
ZIMK__CFGTARGET := _build_config
$(eval $(ZIMK__WRITECFG))

-include $(USERCONFIG)

ZIMK__CFGTARGET := _build_changeconfig
$(eval $(ZIMK__WRITECFG))

# save / compare config cache
ifneq ($(filter-out $(NOBUILDTARGETS),$(MAKECMDGOALS)),)
$(eval $(ZIMK__WRITECACHE))

-include $(ZIMK__CFGCACHE)

ifneq ($(foreach _cv,$(CONFVARS),$(_cv):$(strip $(C_$(_cv)))),$(foreach _cv,$(CONFVARS),$(_cv):$(strip $($(_cv)))))
.PHONY: $(ZIMK__CFGCACHE)
endif
endif

config: global.cfg _build_config
    $(VCFG)
    $(VR)echo $(EQT)# generated file, do not edit!$(EQT) >$<
    $(VR)echo $(EQT)BUILDCFG ?= $(BUILDCFG)$(EQT) >>$<

changeconfig: global.cfg _build_changeconfig
    $(VCFG)
    $(VR)echo $(EQT)# generated file, do not edit!$(EQT) >$<
    $(VR)echo $(EQT)BUILDCFG ?= $(BUILDCFG)$(EQT) >>$<

global.cfg: ;

$(USERCONFIG): ;

ifndef MAKE_RESTARTS
ifneq ($(filter-out $(filter-out changeconfig,$(NOBUILDTARGETS)),$(MAKECMDGOALS)),)
$(eval $(ZIMK__WRITECFGTAG))
endif
endif

DEFAULT_CC ?= cc
DEFAULT_CXX ?= c++
DEFAULT_CPP ?= cpp
DEFAULT_AR ?= ar
DEFAULT_STRIP ?= strip
DEFAULT_MOC ?= moc

DEFAULT_CFLAGS ?= -std=c11 -Wall -Wextra -Wshadow -pedantic
DEFAULT_CXXFLAGS ?= -std=c++11 -Wall -Wextra -pedantic
DEFAULT_LDFLAGS ?= -L$(LIBDIR)

PLATFORM_win32_CFLAGS ?= -Wno-pedantic-ms-format
PLATFORM_win32_CXXFLAGS ?= -Wno-pedantic-ms-format
PLATFORM_win32_LDFLAGS ?= -static-libgcc -static-libstdc++

BUILD_debug_CFLAGS ?= -g3 -O0
BUILD_debug_CXXFLAGS ?= -g3 -O0
BUILD_debug_DEFINES ?= -DDEBUG

BUILD_release_CFLAGS ?= -g0 -O2 -ffunction-sections -fdata-sections
BUILD_release_CXXFLAGS ?= -g0 -O2 -ffunction-sections -fdata-sections
BUILD_release_LDFLAGS ?= -O2 -Wl,--gc-sections

ifdef POSIXSHELL
prefix ?= /usr/local
exec_prefix ?= $(prefix)
bindir ?= $(exec_prefix)/bin
sbindir ?= $(exec_prefix)/sbin
libexecdir ?= $(exec_prefix)/libexec
datarootdir ?= $(prefix)/share
sysconfdir ?= $(prefix)/etc
sharedstatedir ?= $(prefix)/com
localstatedir ?= $(prefix)/var
runstatedir ?= $(localstatedir)/run
includedir ?= $(prefix)/include
docrootdir ?= $(datarootdir)/doc
libdir ?= $(exec_prefix)/lib
localedir ?= $(datarootdir)/locale
else
DESTDIR ?= dist
prefix ?= $(PSEP).
exec_prefix ?= $(prefix)
bindir ?= $(exec_prefix)
sbindir ?= $(exec_prefix)
libexecdir ?= $(exec_prefix)
datarootdir ?= $(prefix)
sysconfdir ?= $(prefix)
sharedstatedir ?= $(prefix)
localstatedir ?= $(prefix)
runstatedir ?= $(localstatedir)
includedir ?= $(prefix)
docrootdir ?= $(datarootdir)
libdir ?= $(exec_prefix)
localedir ?= $(datarootdir)
endif

define ZIMK__UPDATESINGLECFGVARS
ifeq ($$(strip $$(origin $(_cv))$$($(_cv))),command line)
override undefine $(_cv)
endif
$(_cv) := $$(if $$($(_cv)),$$($(_cv)),$$(DEFAULT_$(_cv)))
$(_cv) := $$(if $$($(_cv)),$$($(_cv)),$$(PLATFORM_$(PLATFORM)_$(_cv)))
$(_cv) := $$(if $$($(_cv)),$$($(_cv)),$$(BUILD_$(BUILDCFG)_$(_cv)))
endef
$(foreach _cv,CC,$(eval $(ZIMK__UPDATESINGLECFGVARS)))
ZIMK__DEFDEFINES:= $(shell $(CROSS_COMPILE)$(CC) -dM -E - $(CMDNOIN))
ifeq ($(filter _WIN32,$(ZIMK__DEFDEFINES)),)
PLATFORM:= posix
EXE:=
else
PLATFORM:= win32
EXE:=.exe
endif

TARGETARCH:= $(strip $(shell $(CROSS_COMPILE)$(CC) -dumpmachine))
ifeq ($(TARGETARCH),)
TARGETARCH:= unknown
endif

OBJBASEDIR ?= obj
BINBASEDIR ?= bin
LIBBASEDIR ?= lib
TESTBASEDIR ?= test

OBJDIR ?= $(OBJBASEDIR)$(PSEP)$(TARGETARCH)$(PSEP)$(BUILDCFG)
BINDIR ?= $(BINBASEDIR)$(PSEP)$(TARGETARCH)$(PSEP)$(BUILDCFG)
LIBDIR ?= $(LIBBASEDIR)$(PSEP)$(TARGETARCH)$(PSEP)$(BUILDCFG)
TESTDIR ?= $(TESTBASEDIR)$(PSEP)$(TARGETARCH)$(PSEP)$(BUILDCFG)

$(foreach _cv,$(SINGLECONFVARS),$(eval $(ZIMK__UPDATESINGLECFGVARS)))

define ZIMK__UPDATELISTCFGVARS
ifeq ($$(strip $$(origin $(_cv))$$($(_cv))),command line)
override undefine $(_cv)
endif
$(_cv) := $$(if $$($(_cv)),$$($(_cv)),$$(DEFAULT_$(_cv)))
$(_cv) := $$(strip $$(BUILD_$(BUILDCFG)_$(_cv)) $$($(_cv)))
$(_cv) := $$(strip $$(PLATFORM_$(PLATFORM)_$(_cv)) $$($(_cv)))
endef
$(foreach _cv,$(LISTCONFVARS),$(eval $(ZIMK__UPDATELISTCFGVARS)))

ifneq ($(filter showconfig,$(MAKECMDGOALS)),)
$(foreach _cv,BUILDCFG PLATFORM TARGETARCH $(CONFVARS),$(info $(_cv) = $($(_cv))))
endif

CLEAN += $(ZIMK__CFGCACHE)

showconfig:
    @:

.PHONY: config changeconfig _build_config _build_changeconfig _cfg_message showconfig

Another module provides some helper functions. The most interesting of them is zinc. This is my replacement for include, it operates on relative paths and automatically sets a variable to the path where the included file is found, so I can use relative paths there as well:

ZIMK__DIR :=
ZIMK__PDIRS :=
ZIMK__MK :=
ZIMK__PMKS :=
define __F_ZINC
ZIMK__PDIRS := $$(ZIMK__DIR) $$(ZIMK__PDIRS)
ZIMK__PMKS := $$(ZIMK__MK) $$(ZIMK__PMKS)
ZIMK__DIR := $$(ZIMK__DIR)$$(strip $$(subst /,$$(PSEP),$$(dir $(1))))
ZIMK__MK := $$(ZIMK__DIR)$$(notdir $(1))
include $$(ZIMK__MK)
ZIMK__MK := $$(firstword $$(ZIMK__PMKS))
ZIMK__DIR := $$(firstword $$(ZIMK__PDIRS))
ZIMK__PMKS := $$(wordlist 2, $$(words $$(ZIMK__PMKS)), $$(ZIMK__PMKS))
ZIMK__PDIRS := $$(wordlist 2, $$(words $$(ZIMK__PDIRS)), $$(ZIMK__PDIRS))
endef

define zinc
$(eval $(__F_ZINC))
endef

define __F_BINRULES
_T := $$(strip $(1))
$$(eval $$(BINRULES))
endef

define binrules
$(eval $(__F_BINRULES))
endef

define __F_LIBRULES
_T := $$(strip $(1))
$$(eval $$(LIBRULES))
endef

define librules
$(eval $(__F_LIBRULES))
endef

toupper = $(subst a,A,$(subst b,B,$(subst c,C,$(subst d,D,$(subst e,E,$(subst \
      f,F,$(subst g,G,$(subst h,H,$(subst i,I,$(subst j,J,$(subst \
      k,K,$(subst l,L,$(subst m,M,$(subst n,N,$(subst o,O,$(subst \
      p,P,$(subst q,Q,$(subst r,R,$(subst s,S,$(subst t,T,$(subst \
      u,U,$(subst v,V,$(subst w,W,$(subst x,X,$(subst y,Y,$(subst \
      z,Z,$1))))))))))))))))))))))))))

tolower = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst \
      F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst \
      K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst \
      P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst \
      U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst \
      Z,z,$1))))))))))))))))))))))))))

Then the heart of the whole thing are the modules that generate the required rules based on variables set per build target, here's the part that's common between executable and library targets:

PREPROC_MOC_suffix := moc
PREPROC_MOC_intype := h
PREPROC_MOC_outtype := cpp
PREPROC_MOC_preproc := $(MOC)

define OBJRULES

$(_T)_MAKEFILES ?= $$(ZIMK__MK)
$(_T)_SRCDIR ?= $$(patsubst %$$(PSEP),%,$$(ZIMK__DIR))
$(_T)_SRCDIR := $$(strip $$($(_T)_SRCDIR))
$(_T)_OBJDIR ?= $$(OBJDIR)$$(PSEP)$$($(_T)_SRCDIR)
$(_T)_OBJDIR := $$(patsubst %$$(PSEP),%,$$($(_T)_OBJDIR))
ifeq ($$($(_T)_SRCDIR),)
$(_T)_SRCDIR := .$$(PSEP)
endif

$(_T)_SOURCES := $$(addprefix $$($(_T)_SRCDIR)$$(PSEP), \
    $$(addsuffix .c,$$($(_T)_MODULES)))
$(_T)_OBJS := $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix .o,$$($(_T)_MODULES)))
$(_T)_SOBJS := $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _s.o,$$($(_T)_MODULES)))

ifneq ($$(strip $$($(_T)_CXXMODULES)),)
$(_T)_SOURCES += $$(addprefix $$($(_T)_SRCDIR)$$(PSEP), \
    $$(addsuffix .cpp,$$($(_T)_CXXMODULES)))
$(_T)_OBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix .o,$$($(_T)_CXXMODULES)))
$(_T)_SOBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _s.o,$$($(_T)_CXXMODULES)))
endif

ifneq ($$(strip $$($(_T)_ASMMODULES)),)
$(_T)_SOURCES += $$(addprefix $$($(_T)_SRCDIR)$$(PSEP), \
    $$(addsuffix .S,$$($(_T)_ASMMODULES)))
$(_T)_OBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix .o,$$($(_T)_ASMMODULES)))
$(_T)_SOBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _s.o,$$($(_T)_ASMMODULES)))
endif

ifneq ($$(strip $$($(_T)_PLATFORMMODULES)),)
$(_T)_SOURCES += $$(addprefix $$($(_T)_SRCDIR)$$(PSEP), \
    $$(addsuffix _$$(PLATFORM).c,$$($(_T)_PLATFORMMODULES)))
$(_T)_OBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _$$(PLATFORM).o,$$($(_T)_PLATFORMMODULES)))
$(_T)_SOBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _$$(PLATFORM)_s.o,$$($(_T)_PLATFORMMODULES)))
endif

ifneq ($$(strip $$($(_T)_PLATFORMCXXMODULES)),)
$(_T)_SOURCES += $$(addprefix $$($(_T)_SRCDIR)$$(PSEP), \
    $$(addsuffix _$$(PLATFORM).cpp,$$($(_T)_PLATFORMCXXMODULES)))
$(_T)_OBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _$$(PLATFORM).o,$$($(_T)_PLATFORMCXXMODULES)))
$(_T)_SOBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _$$(PLATFORM)_s.o,$$($(_T)_PLATFORMCXXMODULES)))
endif

ifneq ($$(strip $$($(_T)_PLATFORMASMMODULES)),)
$(_T)_SOURCES += $$(addprefix $$($(_T)_SRCDIR)$$(PSEP), \
    $$(addsuffix _$$(PLATFORM).S,$$($(_T)_PLATFORMASMMODULES)))
$(_T)_OBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _$$(PLATFORM).o,$$($(_T)_PLATFORMASMMODULES)))
$(_T)_SOBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _$$(PLATFORM)_s.o,$$($(_T)_PLATFORMASMMODULES)))
endif

ifeq ($$(PLATFORM),win32)
ifneq ($$(strip $$($(_T)_win32_RES)),)
$(_T)_SOURCES += $$(addprefix $$($(_T)_SRCDIR)$$(PSEP), \
    $$(addsuffix .rc,$$($(_T)_win32_RES)))
$(_T)_OBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix .o,$$($(_T)_win32_RES)))
$(_T)_SOBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix .o,$$($(_T)_win32_RES)))
endif
endif

ifneq ($$(strip $$($(_T)_PREPROC)),)
$(_T)_PPSRCDIR ?= $$($(_T)_SRCDIR)

$(_T)_OBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _$$(PREPROC_$$($(_T)_PREPROC)_suffix).o, \
    $$($(_T)_PREPROCMODULES)))
$(_T)_SOBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _$$(PREPROC_$$($(_T)_PREPROC)_suffix)_s.o, \
    $$($(_T)_PREPROCMODULES)))
$(_T)_OBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _$$(PLATFORM)_$$(PREPROC_$$($(_T)_PREPROC)_suffix).o, \
    $$($(_T)_PLATFORMPREPROCMODULES)))
$(_T)_SOBJS += $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _$$(PLATFORM)_$$(PREPROC_$$($(_T)_PREPROC)_suffix)_s.o, \
    $$($(_T)_PLATFORMPREPROCMODULES)))

$(_T)_PPSOURCES := $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _$$(PREPROC_$$($(_T)_PREPROC)_suffix).c, \
    $$($(_T)_PREPROCMODULES))) \
    $$(addprefix $$($(_T)_OBJDIR)$$(PSEP), \
    $$(addsuffix _$$(PLATFORM)_$$(PREPROC_$$($(_T)_PREPROC)_suffix).c, \
    $$($(_T)_PLATFORMPREPROCMODULES)))

CLEAN += $$($(_T)_PPSOURCES)

$$($(_T)_OBJDIR)$$(PSEP)%_$$(PREPROC_$$($(_T)_PREPROC)_suffix).$$(PREPROC_$$($(_T)_PREPROC)_outtype): \
            $$($(_T)_PPSRCDIR)$$(PSEP)%.$$(PREPROC_$$($(_T)_PREPROC)_intype) \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) \
    | $$(_$(_T)_DIRS) $$($(_T)_DEPS)
    $$(VGEN)
    $$(VR)$$(PREPROC_$$($(_T)_PREPROC)_preproc) \
            $$($(_T)_PREPROCFLAGS) $$< >$$@

endif

CLEAN += $$($(_T)_OBJS:.o=.d) $$($(_T)_OBJS)

OUTFILES := $$($(_T)_OBJS)
$(DIRRULES)

%.o: %.c

$$($(_T)_OBJDIR)$$(PSEP)%.d: $$($(_T)_SRCDIR)$$(PSEP)%.c \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VDEP)
    $$(VR)$$(CROSS_COMPILE)$$(CC) -MM -MT"$$@ $$(@:.d=.o)" -MF$$@ \
            $$($(_T)_$$(PLATFORM)_CFLAGS) $$($(_T)_CFLAGS) $$(CFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

$$($(_T)_OBJDIR)$$(PSEP)%.d: $$($(_T)_SRCDIR)$$(PSEP)%.cpp \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VDEP)
    $$(VR)$$(CROSS_COMPILE)$$(CXX) -MM -MT"$$@ $$(@:.d=.o)" -MF$$@ \
            $$($(_T)_$$(PLATFORM)_CXXFLAGS) $$($(_T)_CXXFLAGS) $$(CXXFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

$$($(_T)_OBJDIR)$$(PSEP)%.d: $$($(_T)_SRCDIR)$$(PSEP)%.S \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VDEP)
    $$(VR)$$(CROSS_COMPILE)$$(CC) -MM -MT"$$@ $$(@:.d=.o)" -MF$$@ \
            $$($(_T)_$$(PLATFORM)_CFLAGS) $$($(_T)_CFLAGS) $$(CFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

$$($(_T)_OBJDIR)$$(PSEP)%.d: $$($(_T)_SRCDIR)$$(PSEP)%.rc \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VDEP)
    $$(VR)$$(CROSS_COMPILE)$$(CPP) -MM -MT"$$@ $$(@:.d=.o)" -MF$$@ \
            $$($(_T)_$$(PLATFORM)_CFLAGS) $$($(_T)_CFLAGS) $$(CFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

ifneq ($(filter-out $(NOBUILDTARGETS),$(MAKECMDGOALS)),)
-include $$($(_T)_OBJS:.o=.d)
endif

ifeq ($$(PLATFORM),win32)
$$($(_T)_OBJDIR)$$(PSEP)%.o: $$($(_T)_SRCDIR)$$(PSEP)%.rc \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VRES)
    $$(VR)$$(CROSS_COMPILE)windres $$< $$@

endif

$$($(_T)_OBJDIR)$$(PSEP)%.o: $$($(_T)_SRCDIR)$$(PSEP)%.c \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VCC)
    $$(VR)$$(CROSS_COMPILE)$$(CC) -c -o$$@ \
            $$($(_T)_$$(PLATFORM)_CFLAGS_STATIC) $$($(_T)_CFLAGS_STATIC) \
            $$(CFLAGS_STATIC) \
            $$($(_T)_$$(PLATFORM)_CFLAGS) $$($(_T)_CFLAGS) $$(CFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

$$($(_T)_OBJDIR)$$(PSEP)%_s.o: $$($(_T)_SRCDIR)$$(PSEP)%.c \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VCC)
    $$(VR)$$(CROSS_COMPILE)$$(CC) -c -o$$@ \
            $$($(_T)_$$(PLATFORM)_CFLAGS_SHARED) $$($(_T)_CFLAGS_SHARED) \
            $$(CFLAGS_SHARED) \
            $$($(_T)_$$(PLATFORM)_CFLAGS) $$($(_T)_CFLAGS) $$(CFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

$$($(_T)_OBJDIR)$$(PSEP)%.o: $$($(_T)_SRCDIR)$$(PSEP)%.cpp \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VCXX)
    $$(VR)$$(CROSS_COMPILE)$$(CXX) -c -o$$@ \
            $$($(_T)_$$(PLATFORM)_CXXFLAGS_STATIC) $$($(_T)_CXXFLAGS_STATIC) \
            $$(CXXFLAGS_STATIC) \
            $$($(_T)_$$(PLATFORM)_CXXFLAGS) $$($(_T)_CXXFLAGS) $$(CXXFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

$$($(_T)_OBJDIR)$$(PSEP)%_s.o: $$($(_T)_SRCDIR)$$(PSEP)%.cpp \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VCXX)
    $$(VR)$$(CROSS_COMPILE)$$(CXX) -c -o$$@ \
            $$($(_T)_$$(PLATFORM)_CXXFLAGS_SHARED) $$($(_T)_CXXFLAGS_SHARED) \
            $$(CXXFLAGS_SHARED) \
            $$($(_T)_$$(PLATFORM)_CXXFLAGS) $$($(_T)_CXXFLAGS) $$(CXXFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

$$($(_T)_OBJDIR)$$(PSEP)%.o: $$($(_T)_SRCDIR)$$(PSEP)%.S \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VCAS)
    $$(VR)$$(CROSS_COMPILE)$$(CC) -c -o$$@ \
            $$($(_T)_$$(PLATFORM)_CFLAGS_STATIC) $$($(_T)_CFLAGS_STATIC) \
            $$(CFLAGS_STATIC) \
            $$($(_T)_$$(PLATFORM)_CFLAGS) $$($(_T)_CFLAGS) $$(CFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

$$($(_T)_OBJDIR)$$(PSEP)%_s.o: $$($(_T)_SRCDIR)$$(PSEP)%.S \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VCAS)
    $$(VR)$$(CROSS_COMPILE)$$(CC) -c -o$$@ \
            $$($(_T)_$$(PLATFORM)_CFLAGS_SHARED) $$($(_T)_CFLAGS_SHARED) \
            $$(CFLAGS_SHARED) \
            $$($(_T)_$$(PLATFORM)_CFLAGS) $$($(_T)_CFLAGS) $$(CFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

ifneq ($$(strip $$($(_T)_PREPROC)),)
$$($(_T)_OBJDIR)$$(PSEP)%_$$(PREPROC_$$($(_T)_PREPROC)_suffix).o: \
            $$($(_T)_OBJDIR)$$(PSEP)%_$$(PREPROC_$$($(_T)_PREPROC)_suffix).c \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VCC)
    $$(VR)$$(CROSS_COMPILE)$$(CC) -c -o$$@ \
            $$($(_T)_$$(PLATFORM)_CFLAGS_STATIC) $$($(_T)_CFLAGS_STATIC) \
            $$(CFLAGS_STATIC) \
            $$($(_T)_$$(PLATFORM)_CFLAGS) $$($(_T)_CFLAGS) $$(CFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

$$($(_T)_OBJDIR)$$(PSEP)%_$$(PREPROC_$$($(_T)_PREPROC)_suffix)_s.o: \
            $$($(_T)_OBJDIR)$$(PSEP)%_$$(PREPROC_$$($(_T)_PREPROC)_suffix).c \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VCC)
    $$(VR)$$(CROSS_COMPILE)$$(CC) -c -o$$@ \
            $$($(_T)_$$(PLATFORM)_CFLAGS_SHARED) $$($(_T)_CFLAGS_SHARED) \
            $$(CFLAGS_SHARED) \
            $$($(_T)_$$(PLATFORM)_CFLAGS) $$($(_T)_CFLAGS) $$(CFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

$$($(_T)_OBJDIR)$$(PSEP)%_$$(PREPROC_$$($(_T)_PREPROC)_suffix).o: \
            $$($(_T)_OBJDIR)$$(PSEP)%_$$(PREPROC_$$($(_T)_PREPROC)_suffix).cpp \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VCXX)
    $$(VR)$$(CROSS_COMPILE)$$(CC) -c -o$$@ \
            $$($(_T)_$$(PLATFORM)_CXXFLAGS_STATIC) $$($(_T)_CXXFLAGS_STATIC) \
            $$(CXXFLAGS_STATIC) \
            $$($(_T)_$$(PLATFORM)_CXXFLAGS) $$($(_T)_CXXFLAGS) $$(CXXFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

$$($(_T)_OBJDIR)$$(PSEP)%_$$(PREPROC_$$($(_T)_PREPROC)_suffix)_s.o: \
            $$($(_T)_OBJDIR)$$(PSEP)%_$$(PREPROC_$$($(_T)_PREPROC)_suffix).cpp \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VCXX)
    $$(VR)$$(CROSS_COMPILE)$$(CC) -c -o$$@ \
            $$($(_T)_$$(PLATFORM)_CXXFLAGS_SHARED) $$($(_T)_CXXFLAGS_SHARED) \
            $$(CXXFLAGS_SHARED) \
            $$($(_T)_$$(PLATFORM)_CXXFLAGS) $$($(_T)_CXXFLAGS) $$(CXXFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

$$($(_T)_OBJDIR)$$(PSEP)%_$$(PREPROC_$$($(_T)_PREPROC)_suffix).o: \
            $$($(_T)_OBJDIR)$$(PSEP)%_$$(PREPROC_$$($(_T)_PREPROC)_suffix).S \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VCC)
    $$(VR)$$(CROSS_COMPILE)$$(CC) -c -o$$@ \
            $$($(_T)_$$(PLATFORM)_CFLAGS_STATIC) $$($(_T)_CFLAGS_STATIC) \
            $$(CFLAGS_STATIC) \
            $$($(_T)_$$(PLATFORM)_CFLAGS) $$($(_T)_CFLAGS) $$(CFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

$$($(_T)_OBJDIR)$$(PSEP)%_$$(PREPROC_$$($(_T)_PREPROC)_suffix)_s.o: \
            $$($(_T)_OBJDIR)$$(PSEP)%_$$(PREPROC_$$($(_T)_PREPROC)_suffix).S \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VCC)
    $$(VR)$$(CROSS_COMPILE)$$(CC) -c -o$$@ \
            $$($(_T)_$$(PLATFORM)_CFLAGS_SHARED) $$($(_T)_CFLAGS_SHARED) \
            $$(CFLAGS_SHARED) \
            $$($(_T)_$$(PLATFORM)_CFLAGS) $$($(_T)_CFLAGS) $$(CFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

endif
endef

.SECONDARY:

I know this is a lot of code, but it makes the project-specific makefiles (in my opinion) very concise and easy to understand. A single-file snake game for example uses this Makefile:

include zimk/zimk.mk

minsnake_MODULES:= minsnake
minsnake_win32_STATICLIBS:= pdcurses
minsnake_posix_LIBS:= ncurses
$(call binrules,minsnake)

For a more complex setup, I have for example this:

[...]
include zimk/zimk.mk

INCLUDES += -I.$(PSEP)include

$(call zinc, src/lib/core/core.mk)
[...]

and in core.mk:

pocascore_MODULES:= cmdline string stringbuilder list hashtable plugin mtwaitqueue event eventloop textcolor
pocascore_PLATFORMMODULES := textcolor dso plugin eventloop file processhost
pocascore_DEFINES:= -DBUILDING_POCAS_CORE
pocascore_posix_LDFLAGS:= -pthread
pocascore_win32_STATICLIBS:= pthread
pocascore_posix_LIBS:= dl
pocascore_V_MAJ:= 0
pocascore_V_MIN:= 0
pocascore_V_REV:= 1
$(call librules, pocascore)

This creates a libpocascore.so.0 or a pocascore-0.dll, depending on the target system, and optionally also static libs.

Now, do you think this is a decent approach? Am I maybe missing something important that's not doable with this design?

Here's a link to the whole project for reference.


UPDATE: I just found one shortcoming (or: BUG, depending on your point of view) myself by accident. Right now, the $(eval )-generated rules only work correctly if every target has its own source directory exclusively. Although this is a good rule for project layout in general, it might be broken for really small targets, like test cases. I'm not sure yet whether I'll try to update the code to support this. It would be a major change because it would mean not to use pattern rules at all.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

When I read the title, I was looking forward to a makefile that let Make use its own built-in pattern rules. Instead, we seem to have constructed a domain-specific language, built on monstrosities such as

$$($(_T)_OBJDIR)$$(PSEP)%.o: $$($(_T)_SRCDIR)$$(PSEP)%.c \
    $$($(_T)_MAKEFILES) $$(ZIMK__CFGCACHE) | $$(_$(_T)_DIRS)
    $$(VCC)
    $$(VR)$$(CROSS_COMPILE)$$(CC) -c -o$$@ \
            $$($(_T)_$$(PLATFORM)_CFLAGS_STATIC) $$($(_T)_CFLAGS_STATIC) \
            $$(CFLAGS_STATIC) \
            $$($(_T)_$$(PLATFORM)_CFLAGS) $$($(_T)_CFLAGS) $$(CFLAGS) \
            $$($(_T)_$$(PLATFORM)_DEFINES) $$($(_T)_DEFINES) $$(DEFINES) \
            $$($(_T)_$$(PLATFORM)_INCLUDES) $$($(_T)_INCLUDES) \
            $$(INCLUDES) $$<

So what happened here?

I think the first problem is that we're fighting Make to put the generated files in a subdirectory rather than in the working directory. It's something I've seen several times in review, but never with any motivating use-case; I'm not sure where this meme started, but it seems to have poisoned much of the world.

If you want to separate the build products for different platforms or those with different optimisation and/or debug levels (and that's a valid thing to want, otherwise you run the risk of attempting to link mismatched objects), then the easy thing to do is create a new directory for each build, and from that directory execute make -f $src/Makefile VPATH=$src - this is the natural thing to do when the source directory isn't writeable, of course. It's not hard to create a wrapper script such as this in each one, to pass the build's options:

#!/bin/sh
src=../project-name

exec make -f $src/Makefile VPATH=$src \
          NDEBUG=1 EXTRA_CFLAGS=-O3 \
          "$@"

Alternatively/additionally, have the makefile -include an optional per-build file for these options.

Even with the object-directory shenanigans, I'd expect the body of the pattern rule to be copied from the built-in one:

%.o: %.c
#  recipe to execute (built-in):
  $(COMPILE.c) $(OUTPUT_OPTION) $<

Why is ours so much more complex?

The main difference is that we're adding arguments in non-standard variables such as CFLAGS_STATIC rather than incorporating them into CFLAGS:

CFLAGS += CFLAGS_STATIC
CFLAGS += DEFINES
CFLAGS += INCLUDES
CFLAGS += EXTRA_CFLAGS  # allow user to add options without overriding existing ones

Similarly, passing in cross-compilation options is normally done by passing in the appropriate compiler at invocation time:

make CC=${CROSS_COMPILE-}${CC:-gcc}

The stuff with $(_T) looks like an ignorance of target-specific variable definitions to me.


I don't see why have this definition:

 RMF := rm -f

when Make has built-in:

# default
RM = rm -f

And these "Legoish" definitions that seem intended for use together would be better written as a $(call)able function:

XIF := if [ -x
XTHEN := ]; then
XFI := ; fi

e.g.

define ifexists
if [ -x $1 ]; then $2; fi
endef

Is it really right to use ; rather than && as a command joiner?


These functions are really stretching Make's text processing:

toupper = $(subst a,A,$(subst b,B,$(subst c,C,$(subst d,D,$(subst e,E,$(subst \
      f,F,$(subst g,G,$(subst h,H,$(subst i,I,$(subst j,J,$(subst \
      k,K,$(subst l,L,$(subst m,M,$(subst n,N,$(subst o,O,$(subst \
      p,P,$(subst q,Q,$(subst r,R,$(subst s,S,$(subst t,T,$(subst \
      u,U,$(subst v,V,$(subst w,W,$(subst x,X,$(subst y,Y,$(subst \
      z,Z,$1))))))))))))))))))))))))))

tolower = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst \
      F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst \
      K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst \
      P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst \
      U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst \
      Z,z,$1))))))))))))))))))))))))))

It's possibly more efficient to shell out to tr; it would certainly be more readable.


We're missing the standard .DELETE_ON_ERROR: target. Without that, we run the risk of erroneous files being treated as up-to-date.


I think that perhaps what you're pining for here is a makefile generator such as CMake or QMake. I don't really have the experience with those to know whether they suit your needs, but one of those may reduce your maintenance burden if used well.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.