Makefile Build Tools
Intro
Assumptions
How to Use the Tool
Including and managing object libraries
Calling make directly
Configuring Compiler Commands
Managing object compilation
Corrupt Compilation
How my coding has improved
Program crashes and gdb
Performance and Motivation
Why not use Makefiles to update themselves
Bash Integration
Issues
This is a building tool to make Makefiles for a simple C++ environment within Bash.
Features:
mkupdate to build the makefilemkerrors to compile the codeThere are three main areas. Building the makefile. Compiling the makefile and unit testing/CI feedback.
There are two versions, the main one written in C++ called mkupdate
written in C++ see
proj/makefilebuildtool,
and a bash version called mkupdatebash see
proj/ide/MakefileUpdate.sh.
Similarly there is mkerrors0 largely deprecated and
mkerrors the C++ implementation.
This is the main document for describing these tools.
mkupdate called from within a projects directory to build the makefile Call $ mkupdate in the directory
containing the source files. Whenever a file or another header
is included the makefile needs to be regenerated.
The makefile and messages are output to
the screen and the makefile is written to "Makefile".
For example in proj/visit directory.
# This makefile was generated using the build tool found at
# http://www.fluxionsdividebyzero.com/p1/misc/makefilebuildtool.html
CC=g++ -Wall
INC=-I../misclib/ -I../visit/ -I./
OBJ=visitprint.o visitdataC.o main.o
LIB=
main: $(OBJ)
$(CC) $(INC) -o main $(OBJ) $(LIB)
visitprint.o: ../misclib/typedefs.h ../visit/visitbase.h ../visit/visitdataA.h ../visit/visitdataB.h ../visit/visitprint.h ../visit/visitprint.cpp
$(CC) $(INC) -c ../visit/visitprint.cpp
visitdataC.o: ../visit/visitbase.h ../misclib/typedefs.h visitprint.o ../visit/visitdataC.h ../visit/visitdataC.cpp ../visit/visitdataC.h
$(CC) $(INC) -c ../visit/visitdataC.cpp
main.o: ../visit/visitbase.h ../misclib/typedefs.h ../visit/visitdataA.h ../visit/visitdataB.h visitprint.o visitdataC.o ./main.cpp
$(CC) $(INC) -c ./main.cpp
clean:
rm *.o *.order *.out gmon.* main
# Re-compile specific parts of the program. e.g.
# $ make del targ=windowscale
targ=__nopattern__
del:
rm *${targ}*.o; make
At the terminal information is printed about processed files and any unknown files found.
mkupdate
processed: visitdataB.h visitprint.h typedefs.h visitprint.cpp visitdataA.h main.cpp visitdataC.h main.h visitdataC.cpp visitbase.h
unknown:
To make this easy I have a function which captures the first few lines of errors, and inserts the appropriate options.
Compiling the module.
$ mkerrors
id=debug
command=make
libraries=
exitstatus=0
g++ -Wall -I../misclib/ -I../visit/ -I./ -c ../visit/visitprint.cpp
g++ -Wall -I../misclib/ -I../visit/ -I./ -c ../visit/visitdataC.cpp
g++ -Wall -I../misclib/ -I../visit/ -I./ -c ./main.cpp
g++ -Wall -I../misclib/ -I../visit/ -I./ -o main visitprint.o visitdataC.o main.o
The important information to look at the is exitstatus. If this is non-zero
then there is a problem.
More detailed information is stored in projcompile.txt, and is used
by the CI(Continuous Integration) html reports.
When compiling the two places of interest are commands at the start of the compiler call and commands at the end. Library linking are commands at the end of the compiler call.
I decided on a very simple model that looks for a file called
libraries and appends its contents at the end of the compiler call.
For example to include OpenGL graphics the file "libraries" contains
the following line of text.
-lGLU -lGL -lglut
This approach has a per directory compiler linking options approach. This is an issue of portability as different systems have different library linkage.
To address this projlibsave projlibload projlibclear can
be used to save, load and clear proj/xxx/library files.
[proj/ide/projlib.sh]
For example to set up the libraries on my Mac
$ cd ${projdirectory}/ide [ $ proj ide ]
$ projlibload librariesMacOSX.txt
Lets say I made some minor configuration changes that I want to occasionally use. Put them in a text file and call to overwrite current library files (as specified). Save your original configuration before to revert changes.
The library configuration format is the module/project directories name, a "," and then the options.
Typical examples
zpr,-framework GLUT -framework OpenGL in MacOSX
zpr,-lGLU -lGL -lglut in Linux
zpr,-lopengl32 -lglu32 -lglut32 in Cygwin
Managing compiler configurations and linkage supported. Edit xml configuration file proj/mkerrorsconfig.txt.
<command> </command> file.cpp <libraries> </libraries>
$ g++ -Wall file01.cpp --framework GLUT -framework OpenGL
The start and end of commands can be overridden by the command and libraries tags respectively. Each compiler option has a id which uniquely associates the command and optionally the linkage.
The presence of library file overrides the configuration file and affects only the libraries tag. e.g. set all options to include this library irrespective of their different compile options.
Some reasonable defaults for Linux were given. These can be overridden.
mkerrors - compiles with "g++ -Wall"
mkerrors clean - removes *.o and main .
mkerrors gdb - compiles with "g++ -g -Wall".
mkerrors release - compiles with
"g++ -DNDEBUG -03 -Wall".
$ make clean deletes
everything and is the safest.
$ make del targ=windowscale $ touch file.cpp
to re-arm dependencies and then recompile.
make clean.
make CC="g++ -DNDEBUG -O3 -Wall"
make CC="g++ -g -Wall"
Advantage: know exactly what is going on. Do this before automating.
Disadvantage: can not use id's/configuration file.
If changing strategies for example compiling release code where before you were
compiling debug code then delete the object files.
$ make clean
For large projects deleting all object code is time consuming because of the cost in time when you recompile the project.
In big projects for code edits near the root of the dependency tree use Corrupt Compilation.
For leaf compilation - compilation at
the leafs of the dependency tree,
generally use
$ mkerrors.
Consider editing code A which depends on code B. Code C also depends on code B. Now if we edit code B then code A and C will be recompiled.
For large programs the results in unnecessary recompilation of areas of code which are not used.
To correct this support for limited(or corrupt) compilation is supported. Here only the target object and its dependencies are recompiled. As long as other parts of the application are not used everything is ok.
Example
$ mkupdate - to build the Makefile
$ make - to build the object code and binaries
$ mkupdate corrupt=randomtest.o - changes the makefile to
main: randomtest.o
Edit random.h, now we can recompile the application without other object
files which depend on random.h being recompiled.
$ make - compiles randomtest.cpp only.
The advantage of what I have called corrupt compilation is that as the size of the source code increases, the compilation time need not increase. This is of course in situations where this technique is useful.
The client then uses the tool as follows. First build the application.
mkupdate
make
This ensures that all the object files exist. Now rebuild the makefile
by adding the target dependency as an argument to mkupdate.
mkupdate targ.o
Freely edit targ.o's source dependencies and compile in
the usual way. When finished convert back to the correct makefile
with mkupdate.
In the previous example there was over 3 times gain in compilation time, and this will increase as the application grows.
The technique of corrupt compilation results in significant compilation time savings. The project can grow (independently) without effecting the ability to compile low level source files.
This makefile update tool is excellent value for several reasons. Firstly it automates one of the most unpleasant and error prone programming tasks - that of generating the makefile.
I first wrote the makefile generation utility in bash (mkupdatebash) and eventually in C++.
In practise updating the makefile is not done less frequently compared with compiling, but it is common so having the process as fast as possible.
Motivation comes from being dissatisfied with complex Makefile technology - it should be easy to debug a Makefile, by making everything explicit (what you see is what you get) and restricting the use of Makefile to a subset of its capabilities, dependencies can be tracked.
The bash version is still used to bootstrap mkupdate so that it can be compiled (built).
mkupdatebash:
The way dependency is calculated is recursive and does not re use
dependency information.
The Corrupt Compilation can lift performance as it allows the programmer to take direct control of compilation dependency. You know when to use this tool when you see files that you are not using continually being recompiled.
Different environments lead to very different wait times for building the makefile.
For example making the Makefile in the proj/graphicslib/ module takes a few minutes under Windows in Cygwin on an old P2, but takes 20s on a Pentium D with Fedora 6 os.
I was unhappy with the complexity of Makefiles. In my opinion a technology should be easy to use and seeing the errors is critical.
I had a problem with the GSL makefile and it was so complicated that I could not debug it. Similarly Qt's makefile generation tool is pretty complicated too. So for now I want a simple tool that generates makefiles which are easy to see what is going wrong.
To this effect my tool explicitly writes/realizes dependency paths - uses variables simply and only a few. Effectively using a simple subset of Makefile technology and throwing the rest away.
To conclude my aim was to generate a simple makefile so that if there is a problem it can easily be seen. For example if a source file was not processed it would not appear in the makefile so the makefile generated was wrong. Then back track by looking at the error messages, back track further to the logic of the program if necessary.
For an alternative solution with Makefiles which I do not recommend see "GNU Make" 3rd edition page 33 where dependency information is included in the Makefile, pages 149+.
When things go wrong gdb ddd is there. Delete the old object and executable code, then recompile with -g option. Then enter a gdb session.
$ make cleangenerated the new code.
$ mkerrors gdb
Start gdb, kill annoying confirm messages, load and
run the program for a back trace.
$ gdb
(gdb) set confirm off
(gdb) file ./main
(gdb) r prog=34
(gdb) bt
(gdb) kill
This is generally enough to solve most of my bugs as I am an intuitional programmer I put the software in my head.
This is a command line IDE(Integrated Development Environment) in a Bash OS.
proj/makefilebuildtool
is compiled
in release and copied to mainrelease.
.bashrc loads the integrated bash code by sourcing in
proj/ide/ide.sh which defines mkupdate.
[bootstrapping mkupdate] To compile the makefile build tool the bash version is used to build ${projdirectory}/makefilebuildtool. See proj/ide/MakefileUpdate.sh.
mkupdate(C++)
Set limits on file length names. For the hash data structure it needs fixed size allocations, so for massive projects the numbers would need to be changed - but it has been designed for this.
mkupdatebash
Circular dependencies are not addressed, but in practise make itself discards them.
Commented out files are still parsed. Header #ifndef and other C++ directives are ignored
so files can not be excluded from the makefile compilation with compiler directives.
For example dead code
//#include <global.h>
sucked in and compiled 5 other irrelevant files.
When things are set up correctly there are no problems and the makefile is generated. When things are set up incorrectly there can be logical errors in the script, circular dependencies... This program does not report the errors in a clear way, it sooner crashes.
Each header file must be unique else find will break in
the script. if [ -e $hfilename ]; then will
complain about a ternary operator when expecting a binary
one. This translates to the find returning two arguments
so there are two file names. Both should be sent to the
errors list.