2
\$\begingroup\$

This is my "general purpose" makefile that I wrote with the following objectives in mind:

  • Ready to use (copy/paste in a simple directory and it works out of the box)
  • Flexible (easy to add compiler flags, link libraries, add other source directories)
  • Settings to choose by passing them as arguments (optimizations, compiler, warnings)
  • "IDE friendly" (multiple targets to choose from)

That last point is probably somewhat subjective (I mainly use CLion and the makefile support is quite good).

Obviously what seems easy to me like add a compiler flag or link a library might not be that straight forward so I would like to know what you think.

In addition to your opinion on the code, its functionnalities and its ease of use, I have a few specific questions:

  • The all target builds and then cleans the project only to leave the executable. I am also having it run the executable so it is very to use in my IDE (one click and my entire project is built and executed). What do you think about that?
  • About the default compiler flags: would it be good practice to add others? If so, which ones? (the current flags are from CPP Best Practices)

I don't have years of experience in software engineering (still a student) so I am having troubles seeing the limits of such a makefile: at what point would it be mandatory to switch to a more "advanced" build system ?

#~~~~ control global settings ~~~~
# make opt=2 --> optimization for size + no debug
# make opt=1 --> optimization          + no debug
# make opt=0 --> no optimization       + debug
opt=0
# make clang=1 --> use clang/clang++
# make clang=0 --> use gcc/g++
clang=0
# make no-warn=1 --> disable all warnings
# make no-warn=0 --> enable all warnings
no-warn=0

#~~~~ build program ~~~~
EXE_PREFIX=main

#~~~~ detect operating system ~~~~
ifneq (${OS},Windows_NT)
  ifneq (,${findstring Ubuntu,${shell lsb_release -i 2>/dev/null}})
    OS:=Ubuntu
  endif
endif

#~~~~ adjust project-specific settings ~~~~
CFLAGS   =
CPPFLAGS =
CXXFLAGS =
LDFLAGS  =
BINFLAGS =

# additional include directory  : CPPFLAGS+=-I header/path
# linking precompiled libraries : LDFLAGS+=-L library/path -Wl,-rpath,library/path -l library_name

#~~~~ source directories ~~~~#
SRC_DIRS = .

#~~~~ adjust platform-specific features ~~~~
ifneq (,${findstring Windows_NT,${OS}})
  EXE_SUFFIX=.exe
  SKIP_LINE=echo.
  REMOVE=del /q
else
  EXE_SUFFIX=
  SKIP_LINE=echo
  REMOVE=rm -rf
endif

#~~~~ deduce file names ~~~~
MAIN_C_FILES=${foreach d,${SRC_DIRS},${wildcard ${d}/${strip ${EXE_PREFIX}}*.c}}
MAIN_CXX_FILES=${foreach d,${SRC_DIRS},${wildcard ${d}/${strip ${EXE_PREFIX}}*.cpp}}

COMMON_C_FILES=${filter-out ${MAIN_C_FILES},${foreach d,${SRC_DIRS},${wildcard ${d}/*.c}}}
COMMON_CXX_FILES=${filter-out ${MAIN_CXX_FILES},${foreach d,${SRC_DIRS},${wildcard ${d}/*.cpp}}}

MAIN_OBJECT_FILES=${sort ${patsubst %.c,%.o,${MAIN_C_FILES}} \
                         ${patsubst %.cpp,%.o,${MAIN_CXX_FILES}}}
COMMON_OBJECT_FILES=${sort ${patsubst %.c,%.o,${COMMON_C_FILES}} \
                           ${patsubst %.cpp,%.o,${COMMON_CXX_FILES}}}

OBJECT_FILES=${MAIN_OBJECT_FILES} ${COMMON_OBJECT_FILES}
DEPEND_FILES=${patsubst %.o,%.d,${OBJECT_FILES}}

EXE_FILES=${sort ${patsubst %.c,%${EXE_SUFFIX},${MAIN_C_FILES}} \
                 ${patsubst %.cpp,%${EXE_SUFFIX},${MAIN_CXX_FILES}}}

GENERATED_FILES=${DEPEND_FILES} ${OBJECT_FILES} ${EXE_FILES}

GENERATED_FILES+=${wildcard output_* *~}

WINDOWS_GENERATED_FILES:=${subst /,\,${GENERATED_FILES}}

ifeq (${OS},Windows_NT)
    GENERATED_FILES=${WINDOWS_GENERATED_FILES}
endif

#~~~~ compiler/linker settings ~~~~
CFLAGS   += -std=c99
CPPFLAGS += -Wall -Wextra -Wshadow -pedantic
CXXFLAGS += -std=c++17 -Wnon-virtual-dtor
LDFLAGS  +=
BINFLAGS +=

# consider these :
# -Wold-style-cast -Wcast-align -Wunused -Woverloaded-virtual -Wpedantic -Wconversion -Wsign-conversion
# -Wmisleading-indentation -Wduplicated-cond -Wduplicated-branches -Wlogical-op -Wnull-dereference
# -Wuseless-cast -Wdouble-promotion -Wformat=2 -Wlifetime

ifeq (${strip ${clang}},1)
  CC=clang
  CXX=clang++
else
  CC=gcc
  CXX=g++
endif

ifneq (,${strip ${MAIN_CXX_FILES} ${COMMON_CXX_FILES}})
  # force c++ link if there is at least one c++ source file
  LD:=${CXX}
else
  LD:=${CC}
endif

#~~~~ debug/optimisation settings ~~~~
ifeq (${strip ${opt}},0)
  BINFLAGS+=-O0 -g
endif
ifeq (${strip ${opt}},1)
  BINFLAGS+=-O3 -s -DNDEBUG
endif
ifeq (${strip ${opt}},2)
  BINFLAGS+=-Os -DNDEBUG
endif

#~~~~ no warnings option ~~~~
ifeq (${strip ${no-warn}},1)
  BINFLAGS+=-w
endif

#~~~~ main target ~~~~
build : ${EXE_FILES}

all : rebuild run

rebuild : clean build clear

.SUFFIXES:
.SECONDARY:
.PHONY: clean clear build rebuild run all

#~~~~ linker command to produce the executable files (if any) ~~~~
%${EXE_SUFFIX} : %.o ${COMMON_OBJECT_FILES}
    @echo ==== linking [opt=${opt}] $@ ====
    ${LD} -o $@ $^ ${BINFLAGS} ${LDFLAGS}
    @${SKIP_LINE}

#~~~~ compiler command for every source file ~~~~
%.o : %.c
    @echo ==== compiling [opt=${opt}] $< ====
    ${CC} -o $@ $< -c ${BINFLAGS} ${CPPFLAGS} ${CFLAGS}
    @${SKIP_LINE}

%.o : %.cpp
    @echo ==== compiling [opt=${opt}] $< ====
    ${CXX} -o $@ $< -c ${BINFLAGS} ${CPPFLAGS} ${CXXFLAGS}
    @${SKIP_LINE}

-include ${DEPEND_FILES}

#~~~~ remove all files ~~~~
clean :
    @echo ==== cleaning ====
    ${REMOVE} ${GENERATED_FILES}
    @${SKIP_LINE}

#~~~~ remove generated files ~~~~
clear :
    @echo ==== clearing ====
    ${REMOVE} ${filter-out ${subst /,\,${EXE_FILES}},${GENERATED_FILES}}
    @${SKIP_LINE}

#~~~~ run main file ~~~~
run :
    @echo ==== running ====
    ${EXE_FILES}
    @${SKIP_LINE}
\$\endgroup\$
4
  • \$\begingroup\$ This is my general purpose Makefile Then project specific makefile simply include this one. Example: ThorsSerializer/Makefile \$\endgroup\$ Apr 13, 2022 at 0:14
  • 1
    \$\begingroup\$ more "advanced" build system ?. Make is the pinnacle of build systems. The problem is that it is extremely complex to use correctly. All other build systems are dumbed down and/or simplified versions of make. By dumbing them down you make them easier to use but loose the power (though you don't need most of the power most of the time). \$\endgroup\$ Apr 13, 2022 at 5:54
  • 1
    \$\begingroup\$ Always compile with -Werror. Compile warnings are logical errors in your thinking. They should be fixed or explicitly muted after you have verified they are not a problem. Compiler with zero warnings in user code. \$\endgroup\$ Apr 13, 2022 at 6:00
  • \$\begingroup\$ Curious that CFLAGS += -std=c99 uses a 23 year-old standard, rather than C11 or later. \$\endgroup\$ Apr 13, 2022 at 6:56

1 Answer 1

2
\$\begingroup\$

The one thing I would note is that you are building object files into the same directory as the source. This means that you can potentially build debug objects and release objects in the same directory and then link them together.

> make opt=1
> # Change some code in one file
> make
> # This make hasbuilt debug code while all the original files are
> # built in release.

Not all systems behave well when you link debug and release objects into the same lib/executable (they may have different ways of packing the structures).

As a result, I tend to build object files into their own specific directory depending on type debug release profile. Then I know that when I build an executable / library, I simply link all the objects in a particular directory.


Also a side nit for me is that dumping the whole command for building a C++ object dumps a lot of repetative stuff into the terminal making it hard to read. So I have made my Makefile simply echo a few important parameters when things are going well, but if there is an error then it dumps the full command the terminal.

$(TARGET_MODE)/%.o: %.cpp | $(TARGET_MODE).Dir
    @if ( test "$(VERBOSE)" = "Off" ); then             \
        $(ECHO) $(call colour_text, GRAY, "$(CXX) -c $< $(OPTIMIZER_FLAGS_DISP)  $(call expandFlag,$($*_CXXFLAGS))") | awk '{printf "%-80s", $$0}' ; \
    elif ( test "$(VERBOSE)" = "On" ); then             \
        $(ECHO) '$(CXX) -c $< -o $@ $(CPPFLAGS) $(CXXFLAGS) $(call expandFlag,$($*_CXXFLAGS))' ;        \
    fi
    @export tmpfile=$(shell $(MKTEMP));                 \
    $(ECHO) $(call colour_text, GRAY, "$(CXX) -c $(OPTIMIZER_FLAGS_DISP)  $(call expandFlag,$($*_CXXFLAGS))") $< | awk '{printf "%-80s", $$0}' ; \
    $(CXX) -c $< -o $@ $(CPPFLAGS) $(CXXFLAGS) $(MOCK_HEADERS)  $(call expandFlag,$($*_CXXFLAGS)) 2>$${tmpfile};    \
    if [ $$? != 0 ];                                    \
    then                                                \
        $(ECHO) $(RED_ERROR);                           \
        $(ECHO) $(CXX) -c $< -o $@ $(CPPFLAGS) $(CXXFLAGS) $(MOCK_HEADERS) $(call expandFlag,$($*_CXXFLAGS));\
        $(ECHO) "========================================";\
        cat $${tmpfile};                                \
        exit 1;                                         \
    else                                                \
        $(ECHO) $(GREEN_OK);                            \
        $(RM) $${tmpfile};                              \
    fi

So if everything is compiling OK it looks like this:

g++ -c -DCOVERAGE_test -DTHOR_COVERAGE  FrameTest.cpp                OK
g++ -c -DCOVERAGE_test -DTHOR_COVERAGE  unittest.cpp                 OK
g++ -c -DCOVERAGE_test -DTHOR_COVERAGE  BasicUpTest.cpp              OK
g++ -o coverage/unittest.app -DCOVERAGE_test -DTHOR_COVERAGE         OK

But if there is an error you get the full output like this:

g++ -o coverage/unittest.app -DCOVERAGE_test -DTHOR_COVERAGE         ERROR
g++ -o coverage/unittest.app coverage/stringTest.o coverage/indexTest.o coverage/readTest.o coverage/unittest.o coverage/openTest.o coverage/tellTest.o coverage/writeTest.o coverage/seekTest.o coverage/Logging.o coverage/closeTest.o coverage/unittest.o -L./coverage -lUnitTest -L../coverage -L/home/travis/build/Loki-Astari/ThorsSerializer/third/ThorsStorage/build/dlib -L/home/travis/build/Loki-Astari/ThorsSerializer/third/ThorsStorage/build/lib -lgtest -fprofile-arcs -ftest-coverage -lpthread -L/home/travis/build/Loki-Astari/ThorsSerializer/third/ThorsStorage/build/lib -lThorsLogging17D
===================================================
/home/travis/build/Loki-Astari/ThorsSerializer/third/ThorsStorage/build/lib/libThorsLogging17D.so: undefined reference to `dladdr'
collect2: error: ld returned 1 exit status
/home/travis/build/Loki-Astari/ThorsSerializer/third/ThorsStorage/build/tools/Makefile:438: recipe for target 'coverage/unittest.app' failed
make[4]: *** [coverage/unittest.app] Error 1
/home/travis/build/Loki-Astari/ThorsSerializer/third/ThorsStorage/build/tools/Build/test.Makefile:45: recipe for target 'test/coverage/unittest.app' failed
make[3]: *** [test/coverage/unittest.app] Error 2
/home/travis/build/Loki-Astari/ThorsSerializer/third/ThorsStorage/build/tools/Build/test.Makefile:28: recipe for target 'report/test' failed
make[2]: *** [report/test] Error 2
/home/travis/build/Loki-Astari/ThorsSerializer/build/tools/Project.Makefile:49: recipe for target 'ThorsStorage.dir' failed
make[1]: *** [ThorsStorage.dir] Error 2
/home/travis/build/Loki-Astari/ThorsSerializer/build/tools/Project.Makefile:49: recipe for target 'src.dir' failed
make: *** [src.dir] Error 2
The command "export PATH=${PATH}:$(pwd)/build/bin;make" exited with 2.
\$\endgroup\$
4
  • \$\begingroup\$ Thanks for your feedback. I will definetly look into building in different directories so debug and release objects don't get mixed up. About the second part of your answer it is true that mine is very verbose in the terminal and I'd to change that. I need take time to look at the code you provided as it is not crystal clear to me what this does. \$\endgroup\$
    – t9dupuy
    Apr 13, 2022 at 11:42
  • \$\begingroup\$ Oh, that's not a great way to put differently-built objects into separate directories - you'll end up having to re-write all the built-in rules. Much simpler to invoke a sub-make in each target directory ($(MAKE) -C debug -f ../Makefile VPATH=..). \$\endgroup\$ Apr 13, 2022 at 13:51
  • \$\begingroup\$ @TobySpeight Thanks. I may update. But I still have to rewrite the built in rules to get the nice output (where it only prints minimal info when everything is OK and dumps all info on a fail). If there was a way to limit what was printed on each command, that would be a cool feature (i.e. The output of each command was passed to a filter before the output). \$\endgroup\$ Apr 15, 2022 at 16:16
  • \$\begingroup\$ If you have chronic available in your build environment, that might be helpful for dealing with overly-chatty compilers (with CXX := chronic $(CXX)). That doesn't help with your desire to obscure the command arguments, but that's not a problem present in the question code. \$\endgroup\$ Apr 18, 2022 at 7:17

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.