JOIN
Get Time
features   
Discuss this article
C++ tools #2 - GNU Make

By sql_lall
TopCoder Member

There's a well-known joke about two women eating lunch at a resort. "The food here is so horrible," says one. "I know," replies the other, "and the portions are so small!"

There's a somewhat similar paradox that seems to apply to most developers. No matter how much we love what we do, it seems like one of the most enjoyable elements of our work is finding tools that help us do less of it. With their large following, the C++, C#.NET and Java languages are hotbeds of development for these time-saving tools -- for example, some of the tools commonly used for short cuts and jump-starts in TopCoder component development include:

Type Java C#.NET C++
Building Ant NAnt Makefile
Documenting javadoc NDoc doxygen
Testing JUnit NUnit cppUnit


In this article, I'll review Make, "a tool which controls the generation of executables and other non-source files of a program from the program's source files" (according to GNU Make home). Created in 1977, 'make' is a Unix utility which helps developers manage how their applications are built from source code, by giving users the abilty to set up source dependancies, add callable shell scripts, and perform many levels of string manipulation. make works by building 'target' files, first recursively making any of its 'dependancy' targets, while also skipping targets whose dependancies have not changed since the last time they were made -- a useful optimization for large projects/targets. The build information is obtained from what is called a 'Makefile', either specified when make is run, or using the environment's default file.

One version of make, 'GNU make', can be found contained within most GNU/Linux distributions. As such, it has become a popular build tool among developers of both small and large projects -- if you take a look at many open-source project file-listings, chances are that the ubiquitous 'Makefile' will make an appearance.

Example Makefile
Distributions of make usually come with their own default Makefile - for instance, if you have a file 'me.cpp', and type 'make me', it will probably attempt to build your target by automatically running 'g++ me.cpp -o me'. However, you can control what make does by providing it with your own Makefile. For instance, assume you have the following file in your directory, calling it 'Makefile' (with no extension):
me.o : me.cpp
	@echo "Making object..."
	g++ -Wall -g -c me.cpp

me : me.o
	@echo "Making executable...."
	g++ -Wall -g me.o -o me
	
Calling 'make me' should result in an output similar to the following:
Making object...
g++ -Wall -g -c me.cpp
Making executable....
g++ -Wall -g me.o -o me
As you can see, make has built the 'me' target, by first building the 'me.o' target that it depends on. To build this, it has echoed a message to the screen, then run the supplied compilation command. Once this was built, the initial target could then be built by using its given commands.

Now......try it again - you should get a message telling you that 'me' is up to date. This is because it is newer than all of its dependencies, so make realizes it does not need to be built again. However, if you were to update the timestamp on me.o -- 'touch me.o', say -- then try to build it again, you would notice that only the executable is built, as the object is still more recent than its dependency (me.cpp). This optimized building is a major benefit of Makefiles, and a reason why they are so popular.

Makefile structure
All Makefiles have a command setup, as follows:
# comments start with the hash character

# string assignments are simple (name)=(value), for example:
wish=$(mood) Birthday
mood=Happy
name=coder
print=@echo

# each target description  is:
# target(s) : [optional dependency list]
#	command, starting with tab character ('\t')
#	[optional other lines, all starting with tabs]
middle : FORCE
	$(print) -e "$(wish) to you!\n\
	$(wish) dear $(name),"
end finish : FORCE
	$(print) "$(wish) to you!"
beginning start : FORCE
	$(print) "$(wish) to you!"
song : start middle end
FORCE :
Now, running 'make song' should print out:
Happy Birthday to you!
Happy Birthday to you!
Happy Birthday dear coder,
Happy Birthday to you!
The best way to think about how makefiles work is string replacements -- for example, the command for 'middle' is first joined into one line, then $(print) is replaced by @echo, $(wish) by $(mood) Birthday by Happy Birthday, etc... Finally, the whole command @echo -e "Happy Birthday to you!\nHappy Birthday dear coder," is formed, then executed, in a shell.

Some things that may be useful to know about structure when writing your own makefiles include:
  • Each line of commands is run in its own shell. This means that if you are putting some shell scripts within your commands, you have to take care to make sure variables remain in scope.
  • To help with this, you can use the slash character '\' to allow single commands to be split across multiple lines (as used above).
  • When building a target, all the following lines below it that start with a tab character are run. This means you must start the commands with that character -- if your IDE instead inserts spaces, you may have to copy \t from somewhere.
  • If you want a particular target rebuilt, you can add an empty target, such as 'FORCE' above. Putting this as a dependency will force a rebuild.
Programming in Makefiles
As Makefiles contain commands executed in a shell; it is possible to add shell scripts into the command lists, and have these executed when make-ing.

However, these may depend on the user running a given shell type -- instead, GNU Make provides its own Makefile functions, which can help add programming to a makefile. You can even define your own functions for use in a makefile. Consider the following file:
loop_demo : bash_loop make_loop
bash_loop :
	@echo "Bash loop.."
	@for ((bA=0; bA<10; bA++)); \
		do echo -n "-$- "; \
	done; echo;

make_loop :
	@echo "Make loop.."
	@echo $(foreach mA, $(sort "1" "4" "2" "8" "5" "7" "9" "3" "6" "0"), "-$(mA)-")
Executing make with this at the top of a makefile will give:
Bash loop..
-0- -1- -2- -3- -4- -5- -6- -7- -8- -9-
Make loop..
-0- -1- -2- -3- -4- -5- -6- -7- -8- -9-
Have a read through the files listed at the link above -- the important thing to keep in your head when using them is that they are all string-based, and required to be within one set of brackets. It may require a bit of getting used to, but the functions are powerful enough for most purposes -- for example, here is a snippet that compiles all .cpp files in a set of directories to .o files in another location. While not necessarily the shortest way to do it, this demonstrates the use of a few functions (nested loops, wildcard expansion and pattern replacement):
target=$(patsubst $(source_direc)/%.cpp, $(obj_direc)/%.o,$(source))
...
    $(foreach source, \
        $(foreach dir, $(direc_list),$(wildcard $(source_direc)/$(dir)/*.cpp)), \
        echo "compiling" $(source) "to" $(target) "..."; \
        g++ $(source) -c -o $(target) $(LFLAGS); \
    )
    

Automatic variables
Another useful built-in ability of GNU Make allows you to use wildcards in the targets and dependancies, and then reference the files using tokens called 'automatic variables', including:
  • $* = part matching the wildcards
  • $@ = target file name
  • $< = the first dependency only
  • $^ = space-separated list of dependencies
  • $? = space-separated list of dependencies newer than the target
For example, to compile any *.cpp file into an executable with the same name (as make does by default) you can have in your Makefile:
% : %.cpp
	g++ -o $* $<
	
Then running 'make me' will search for me.cpp, replace the $* with the matching string ('me' in this case) and $< with the dependancy (me.cpp)

Common targets
Due to its frequent use in many distributed projects, there are a few popular make targets that can often be found within Makefiles. These include:
  • make all = compile the whole program, usually placed as the first target
  • make clean = remove the files created in this directory when building
  • make install = compile the application and copy files across to desired destintaion, checking for success
  • make uninstall = remove the files created by install
  • make dist = create tar file for distribution, using the program name and version
  • make check = run tests of the code and installation
Miscellaneous - Make it so
  1. Typing simply 'make' will attempt to build the first target in the Makefile.
  2. Adding '@' to the start of a command stops make from echoing the command before it is run
  3. Adding '!' to the start of a command means make will ignore the return value, so it does not quit if the command fails. Otherwise, make will terminate on the first command which returns a non-zero value
  4. There are two 'flavors' of make variables, which determine how strings are expanded. Most often the default flavor is fine, however you may find cases where using simply-expanded variables is required.
  5. If you ever get the message "*** missing separator. Stop.", this almost always means a command line somewhere does not start with a tab character. Check the beginnings of the line that caused the error to make sure it starts with a tab (rather than, say, spaces).
  6. Some useful options for make include:
    • -B to force everything to be rebuilt
    • -f FILE to use a specified Makefile
    • -k to not bail on the first error
    • -s to run in silent mode, not printing commands as they are executed.
Further Reading: