Introduction
In my other articles, I have used manually created make files. Make is a program that runs a set of steps to produce an output. In most cases, the set of steps involves calling compilers to compile source code. Make automatically checks if the source files have been updated before it takes the steps to create the output files. This means that only changed parts of a program are recompiled rather than the entire program.
I have previously stuck to simple make files, but with the make files I have created so far, there have been a couple of problems/missing features.
- Produced files are mixed in with source files
- There is no Debug/Release functionality
In this article, I will explain the make structure I currently have, and show how I extend it to include the above features.
Background
Some people have told me that they believe hand coding make is a waste of time. People have suggested using compiler tools as well as automake etc. My thinking behind using make is as follows:
Pros
- Make files in my article simply describe the build process.
- I understand and can see all the steps in my build process.
- I don't have to figure out what compilers or automake processes are actually doing. With Make, I know exactly the commands it is running.
- People following the articles can follow using different compilers. People reading the articles can see the make file I use, then work out their own compiler specific things.
- KIS - Keep it simple. This is a golden rule I always like to follow, and making make files is simple compared with trying to get an IDE to produce the make files.
Cons
- I don't currently have in-editor debugging capability. When an error is reported, I have to manually search for the line number that the error occurred on.
- I have to put the time aside to create the make file. I think this is acceptable. I don't change the build process that often in development, and I have made a pattern which I always follow (and describe here).
Prerequisites for following this tutorial
I have created a CodeProject article that completely shows how I set up the tool chain I use for compiling C++ programs: Setting up an Open Source tool chain.
I will go through changing a make file I have used in another article. You can view it here: Starting with SQLite in C++. You should follow this before following this article to get the right files. If you want to skip this, I have included a zip file with this article. You should unzip all the files to C:\code\sqlite_hello_world.
Very simple Make file example
A make file has a 'top bit' and then any number of sections. The top bit is generally used to setup variables etc., and each section is used to supply instructions telling Make how to create a file. Follow these steps to setup the project directory and create a simple little make file. You can use Shift + Ins in MINGW to paste each command, to avoid typing errors.
- Run MINGW (Click on the Blue M)
- cd /c/code/
- mkdir make_messing
- "/c/program files/notepad++/notepad++.exe" /c/code/make_messing/makefile
- Paste in the following code, then save and close Notepad++
$(warning Starting Makefile)
CXX=g++
fin.txt:
$(warning Making fin.txt)
echo Messing about with Make > fin.txt
This is a simple Make file with the 'top bit' and one section. The first line contains a warning statement which is just a way of printing a message to the screen during build. The second line sets up a variable. The line fin.txt:
denotes the start of the first section. The layout of the section is:
<Output file>:<prerequisites ...>
command 1
command 2
...
Output file is the name of the file to output, and prerequisites is the files that must be present in order to create the output file. Command 1, 2 etc., are the shell commands used to create the file. Each command line starts with a tab character, and there is a blank line separating each section.
In our example, the file we are creating is fin.txt and we don't need any files to make it. There are two commands to make it: the first just tells us what Make is doing, and the second is just an echo command piped into a file called fin.txt. To run this, launch MINGW and follow these steps:
- cd /c/code/make_messing/makefile
- make
You can see that the file is produced.
As you can see, fin.txt now exists.
You can now see the message fin.txt is up to date, which shows us that Make is doing its job. It hasn't processed the command to make the file that is already there. Now, let's enhance the makefile a bit:
- Run MINGW (Click on the Blue M)
- "/c/program files/notepad++/notepad++.exe" /c/code/make_messing/makefile
- Paste in the following code, then save and close Notepad++
$(warning Starting Makefile)
CXX=g++
.PHONY: clean
fin.txt:
$(warning Making fin.txt)
echo Messing about with Make > fin.txt
clean:
$(warning Deleting all txt files)
rm *.txt
The first thing to note is that the file now has two sections. How does Make know which one to process? Well, when it is run with no parameters, Make will always run the first section. In this case, fin.txt is processed. If you want to make a different output file, you can specify it as the first parameter, e.g., make fin.txt or make clean, etc.
The Clean section
Another thing to note is that the Clean section is not a real section. The third line declares it as phony. This tells Make that there is no file called clean created, but the commands are run when requested. Clean is a standard section put into a lot of Make files which deletes all the files that Make produces. This is useful to force a rebuild or if you are working on changes to the makefile. You can run make clean before backing up your code to save space. Experiment with this file a bit, and note how the txt file appears and disappears and how Make will not rebuild it if it is already there.
Now, alter the makefile so it shows the following:
$(warning Starting Makefile)
CXX=g++
.PHONY: clean
fin1.txt: fin2.txt
$(warning Making fin.txt)
cat fin2.txt > fin1.txt
echo done with 2 >> fin1.txt
fin2.txt: fin3.txt
$(warning Making fin.txt)
cat fin3.txt > fin2.txt
echo done with 3 >> fin2.txt
fin3.txt: fin4.txt
$(warning Making fin.txt)
cat fin4.txt > fin3.txt
echo done with 4 >> fin3.txt
fin4.txt:
$(warning Making fin.txt)
echo We are still messing about with Make > fin4.txt
clean:
$(warning Deleting all txt files)
rm *.txt
You can see that this makefile has five sections. The Clean section we talked about earlier. You can see that fin1.txt depends on fin2, which depends on fin3, which depends on fin4. You can use Make to make the whole lot, and Make will work out which files need to be recreated. Make also looks at the date stamps of the files, so once you have made all the files, if you change fin3.txt, you will note that Make will see this and recreate fin2.txt and fin1.txt. This is useful in programming. You make objects dependant on their headers, and Make will rebuild objects that need to. This saves time as objects that haven't changed aren't rebuilt.
You can also add a section to make a final file which depends on them all:
fin.txt:fin1.txt fin2.txt fin3.txt fin4.txt
$(warning putting it all together)
cat fin1.txt >> fin.txt
cat fin2.txt >> fin.txt
cat fin3.txt >> fin.txt
cat fin4.txt >> fin.txt
Remember, Make, by default, always builds the first section it sees, so place this at the top of your Make file. As this file depends on all the other files, Make will create all the other files first. Also, all of this has lots of warnings which tell us what Make is doing here, but I wouldn't put them in a real makefile.
For a laugh, we can try and confuse Make with something like the following:
aa.txt:bb.txt
echo aa > aa.txt
bb.txt:aa.txt
echo bb > bb.txt
This can't be built, and Make warns us and tries its best.
Start using variables
Have a look at the following Make file:
$(warning Starting Makefile)
CXX=g++
.PHONY: clean
fin.txt:
echo Make variable test > fin.txt
echo CXX=${CXX} >> fin.txt
echo CFG=${CFG} >> fin.txt
clean:
rm *.txt
This file uses a built-in variable and an external variable. You can run it by using Make, or you can pass a variable to it by running a command like: make -e CFG=Test_other_var. As you can see, this will allow us to change what we do based on the command line parameters. We can also add some code to give the external variables default values, and check if they are the correct values:
$(warning Starting Makefile)
CXX=g++
.PHONY: clean
ifndef CFG
CFG=DEBUG
$(warning Defaulting to $(CFG))
endif
ifneq ($(CFG),DEBUG)
ifneq ($(CFG),RELEASE)
$(error error is CFG must be DEBUG or RELEASE)
endif
endif
fin.txt:
echo Make variable test > fin.txt
echo CXX=${CXX} >> fin.txt
echo CFG=${CFG} >> fin.txt
clean:
rm *.txt
The Error command is different from the Warning command because Error will exit the Make process. You can see here that the variable CFG
is defaulted to DEBUG
, but the user can enter a value. If the value is not DEBUG
or RELEASE
, the build process errors. You can test this Make file with the commands:
make
make -e CFG=ddd
make -e CFG=RELEASE
Note: Remember to use make clean in between tests.
Putting what we know to use
OK, now we need to use this to allow us to make debug and release builds and store the created files somewhere other than our code directory. I will do this using the SQLite example project I created. Follow these instructions:
- Run MINGW
- "/c/program files/notepad++/notepad++.exe" /c/code/sqlite_hello_world/makefile
- Paste in the following code, then save and close Notepad++
$(warning Starting Makefile)
CXX=g++
ifndef CFG
CFG=DEBUG
$(warning Defaulting to $(CFG))
endif
ifneq ($(CFG),DEBUG)
ifneq ($(CFG),RELEASE)
$(error error is CFG must be DEBUG or RELEASE)
endif
endif
.PHONY: clean
${CFG}/main.exe: ${CFG}/dir_exists.obj ${CFG}/sqlite_demo.lib main.cpp
$(CXX) -s main.cpp -o ${CFG}/main.exe -Wl,${CFG}/sqlite_demo.lib
${CFG}/sqlite_demo.lib: ${CFG}/dir_exists.obj ${CFG}/sqlite3.obj
${CFG}/RJM_SQLite_Resultset.obj ${CFG}/RJMFTime.obj
ar cq $@ ${CFG}/RJMFTime.obj
ar cq $@ ${CFG}/RJM_SQLite_Resultset.obj
ar cq $@ ${CFG}/sqlite3.obj
${CFG}/sqlite3.obj: ${CFG}/dir_exists.obj sqlite3.c
gcc -c sqlite3.c -o ${CFG}/sqlite3.obj -DTHREADSAFE=1
${CFG}/RJM_SQLite_Resultset.obj: ${CFG}/dir_exists.obj
RJM_SQLite_Resultset.cpp RJM_SQLite_Resultset.h
Glob_Defs.h RJMFTime.h sqlite3.h
$(CXX) -c RJM_SQLite_Resultset.cpp -o ${CFG}/RJM_SQLite_Resultset.obj
${CFG}/RJMFTime.obj: ${CFG}/dir_exists.obj RJMFTime.cpp RJMFTime.h
$(CXX) -c RJMFTime.cpp -o ${CFG}/RJMFTime.obj
${CFG}/dir_exists.obj:
-mkdir ${CFG}
-echo dir exists > ${CFG}/dir_exists.obj
clean:
-rm DEBUG/*.*
-rm RELEASE/*.*
-rmdir DEBUG
-rmdir RELEASE
Notes
- The Clean section will delete all files from DEBUG and RELEASE, and deletes the directories themselves. It does all the files, which is useful for cleaning up directories before back ups.
- If you didn't do a make clean before you changed the makefile, you may have to delete the .obj, .lib, and .exe files from /c/code/sqlite_hello_world/ (you might also want to delete datafile.sqlite).
- There is a fake object file: dir_exists.obj. This allows Make to create the directory if it doesn't exist.
Explanation
Basically, what I have done is added ${CFG} in front of the object files. This means that the files are created in the relevant directories. I also made all the files dependant on ${CFG}/dir_exists.obj. This means that the directory will be created if it doesn't exist. You can also see an enhanced Clean section to deal with the sub-directories.
Fixing Notepad++ bat files to work with this
When I set up the tool chain, I added menu items into the Notepad++ menu. This automatically launched the program. This won't now work as the program is in a different place. To fix this, follow these instructions:
- "/c/program files/notepad++/notepad++.exe" /c/code/run.bat
- Paste in the following code, then save and close Notepad++
:##BATCH to run fron notepad++
c:
cd\
cd %1
make
pause
cd DEBUG
main.exe
pause
This default menu option will automatically run the DEBUG version of the code. You should note that the data files used in this example are also stored in the DEBUG directory and are always recreated when the program is rebuilt. This behaviour is program specific, and if you wish data files to be kept, you will need to locate them elsewhere.
Finally, I create a new menu to produce the Release build:
- "/c/program files/notepad++/notepad++.exe" /c/code/run_release.bat
- Paste in the following code, then save and close Notepad++
:##BATCH to run fron notepad++
c:
cd\
cd %1
make -e CFG=RELEASE
pause
cd RELEASE
main.exe
pause
- Load Notepad++
- Press F5
- Enter c:\code\run_release.bat $(CURRENT_DIRECTORY)
- Save the command as RUN RELEASE CODE
Clean cleans both RELEASE and DEBUG, so there is no reason to add it here.
Rolling your own
We now have a much simpler and cleaner way to build our programs, separating the source files from the created files. You now should be able to create your own projects that follow this system. Things you need to remember when building the makefile are:
- Include the basic start section, setting up the
${CFG}
and ${CXX}
vars - Make all objects dependent on ${CFG}/dir_exists.obj
- Include instructions to build ${CFG}/dir_exists.obj
- Make the Clean section deal with the sub-dirs
If you follow these guides, you can easily create your own projects following this system.
Final comments
Make is very powerful, and using just the features I have described, you can produce complex build processes. One thing I have done before is to use Make to build a program, then run it. The program I wrote produced source code which I then used Make to build into my main project. This way, I achieved auto-generated code being directly included in the build process.
Some people say Make is complicated and time sucking. I do not think what I have explained here is too complicated, especially since people here will be programmers anyway and variables etc., should be second nature. I personally like the idea of having control over the build process and manually setting up the dependencies, rather than allowing the IDE to do it. It is also useful as it exposes me to common compiler directives used in libraries I use, and I can see what is going on. Of course, there are other methods available, and you might decide something else is better. Even if you move on to something else, you might be glad that you spent this time studying Make and the build process at a detailed level.
Link
You can view the Make manual at http://www.gnu.org/software/make/manual/.
History
- 04-Jul-2009 - First version.