Elementary Use of the Program "make"

Environment: DOS/Unix console

These days, programmers generally develop using some GUI IDE; however, it may happen that one is given only traditional command-line tools. In this case, using the classic program "make" to generate/update a program, especially when source code is spread across many unit files, may be a solution.

I will briefly show where "make" fits in the process of developing an application and introduce a simple "makefile" for a C++ program made of three unit files; finally, I will outline a simple method to automate the generation of makefiles.

The approach I suggest is aimed towards the development of C++ console applications, and is presented using the GNU tools on Windows; however, it can be adapted to other compiled programming languages/tools/systems as long as a compiler and a "make" are available.

Index:

  • Doing it all by hand
  • Using "make"
  • Automatically generating simple makefiles
  • Some extra thoughts
  • About the program gm
  • About the GNU tools
  • Downloads
  • History

Doing It All by Hand

Consider the following program (it is a multi-source program consisting of three unit files. hello.cpp and print.cpp are source files; print.hpp is a user header file):

// hello.cpp                   // print.hpp                // print.cpp
#include "print.hpp"           #ifndef print_hpp           #include <iostream>
                               #define print_hpp           #include "print.hpp"
///////////////////////        ///////////////////////     ///////////////////////
int                            void                        void
main()                         print(const char*);         print(const char* s)
{                              ///////////////////////     {
  print("Hello world!");       #endif                        std::cout << s << '\n';
}                                                          }

The basic operations needed to generate the binary file (which I'll call hello.exe) are:

  • Compile all source files (hello.cpp and print.cpp), generating object files (hello.o and print.o).
  • Link the object files making the binary.

Using GNU tools (I assume that the binary files of the tools are in c:\gnu\bin, which is included in the path and that C++ standard header files are in c:\gnu\include\g++), one would use the commands:

g++ hello.cpp -c -o hello.o -Ic:/gnu/include/g++/
g++ print.cpp -c -o print.o -Ic:/gnu/include/g++/
g++ hello.o print.o -o hello.exe

The first says to use g++ to compile hello.cpp, generating hello.o. The options used have this meaning:

  • c = only compile file, do not invoke linker
  • o = next comes the name of the output
  • I = after the "I" comes the place where you can look for header files not found in the current directory

The second command is the same as the first, but is referred to as print.cpp.

The third command says to link hello.o and print.o, generating hello.exe.

Note: I'll write Unix-style paths, using a slash to delimit directories, because that is safer when dealing with GNU tools (even if they are running on Windows).

If, later, one of the source files is edited (say hello.cpp), one has to do the following to update the binary file:

  • Compile only the updated source file (hello.cpp).
  • Link the object files making the binary.

So the commands are:

g++ hello.cpp -c -o hello.o -Ic:/gnu/include/g++/
g++ hello.o print.o -o hello.exe

Generally, besides sources, an application also has user headers (just one in this case, print.hpp) storing information to be shared among source files (in the example, the declaration of function "print"), and so included in one or more sources.

Upon editing a header file, one is actually editing each of the sources that include it, even if the time stamp of the sources did not change. This is somehow an extension of the notion of editing a source file to include the case of editing one of its header files. Additionally, header files may themselves include other headers; thus, in the end, a source file shall be considered updated not only when its time stamp or that of an header has changed, but even when the stamp of an header of an header changes.

Of course, when a header is edited, all source files including it have to be compiled, and the binary accordingly updated.

In other words, should I ever modify print.hpp, I would need to recompile both hello.cpp and print.cpp and, of course, update the binary.

Using "make"

The task of keeping a binary up to date can be much simpler if there is a file telling:

  • Which header is included by each source file (so that one knows when a source file has been updated).
  • How a source file is translated into the corresponding object (that is, which commands are to be used to compile).
  • How the object files are linked to make the binary.

And a program that, given the above information, will:

  • Check if there is anything to be compiled.
  • Check if the binary has to be updated.

The file I outlined above contains more or less what goes into a simple "makefile", and the program that interprets makefiles is "make".

Here is the makefile for the "hello" program. Lines with a a leading "#" are comments; those with leading white spaces are indented using a tab character.

######################################################################
# Makefile for program : hello
# this file is written : manually
######################################################################


# generating/updating the binary file

hello.exe : hello.o print.o
  g++ hello.o print.o -o hello.exe

# Remarks:
# 1. Consider the first line: the file placed on the left of the
#    colon (':') is the "target" (here, hello.exe)
#
# 2. Files placed on the right of the colon tell the "dependency" of
#    the target (that is, if at least one of them is newer than the
#    target itself, update the target)
#
# 3. Lines following the first one are indented (here there is only
#    one such line) and tell which commands are to be executed to
#    update the target; in this case the command is the one that
#    links the objects.
# --------------------------------------------------------------------

# generating/updating object file hello.o

hello.o : hello.cpp print.hpp
  g++ hello.cpp -c -o hello.o -Ic:/gnu/include/g++/

# Remarks:
# target is hello.o, it depends upon hello.cpp and print.hpp and
# is updated by compiling the source file hello.cpp

# --------------------------------------------------------------------


# generating/updating object file print.o

print.o : print.cpp print.hpp
  g++ print.cpp -c -o print.o -Ic:/gnu/include/g++/

######################################################################
#end of file

Assuming the above is saved on disk as "makefile", let's see how "make" uses it.

The first "time" make is invoked -- by the way this is done by typing "make" -- assume that only the unit files exist (there is no binary file named hello.exe and none of the object files hello.o and print.o).

"make" finds instructions about generating hello.exe; because the file does not exist, it has to be made. Also, the file depends on two object files.

The object files do not exist, so "make" looks for instructions on how to make them; if none are found, an error results. In our case, "make" finds that hello.o depends on hello.cpp and print.hpp. There is no need to search for how to generate these unit files because they exist. So, the instructions to generate hello.o are executed.

Something similar occurs for print.o.

After that, "make" goes back to generating the binary and executes the instructions required to link it.

Consider now the case when one edits hello.cpp, and runs "make".

As before, "make" finds that hello.exe depends on hello.o and print.o. The files both exist, but when it checks the time stamp of hello.o, it realizes that the object file is older than the source it depends on, so it will update the object hello.o; accordingly, it will update the binary.

If print.hpp is edited and "make" invoked, the program will update both object files (and then the binary as well) because the user header file is listed in the dependency of both the objects.

This is what one would have done manually, so I guess that both "make" and the makefile may be trusted when updating binaries.

Automatically Generating Simple Makefiles

Manually writing a makefile is easy when the program to be generated is simple; thus, next step is to write a program that, given the unit files, generates a simple makefile for me.

Here is how such a program, say named "gm" (generate makefile), could work.

gm is given the name of the sole source file hosting function "main" (I term this the "main source"; such a file also exists for every ANSI C++ program). In the "hello" example, the main source is: hello.cpp.

Right away, gm knows that:

  • The binary file is named "hello.exe" (that is, it has the same name of main source, but with a ".exe" extension).
  • There is an object file named "hello.o" (the same name as source file, but with an ".o" extension), to be compiled.
  • The binary depends on the object hello.o.
  • The object depends on "hello.cpp".

Next, gm will examine the source "hello.cpp", looking for links to other unit files (that is, user headers or source files). It comes across the include directive related to "print.hpp". That file is a user header file (because it appears enclosed in double quotes), thus gm will:

  • Add print.hpp to the list of headers included by hello.cpp (in other words, it will add print.hpp to the dependency of hello.o).
  • Examine the header file print.hpp as soon as it finishes with hello.cpp.

There are no other links in hello.cpp, so the operation of scanning it is over; examination of print.hpp begins. There is no link in this file, so no further information is collected. Because there are no other units to scan, gm prints the makefile and terminates.

Here is the output:

######################################################################
# Makefile for program : hello
# generated by program : gm.exe
######################################################################

# target to generate/update binary file
hello.exe : hello.o
  g++ hello.o -o hello.exe

hello.o : hello.cpp print.hpp
  g++ hello.cpp -c -o hello.o -Ic:/gnu/include/g++/

######################################################################
#end of file

Note that the makefile is not complete; it lacks information about the other source file. In fact, if you type the command "make", the linker will complain that the definition of the "print" function is missing.

Assume now that one adds the following line to the user header file:

# pragma defs_in print.cpp

So, now the header file is:

// print.hpp
#ifndef print_hpp
#define print_hpp
#pragma defs_in print.cpp // this is the extra line
///////////////////////
void
print(const char*);
///////////////////////
#endif

Let's run gm again. This time, when it examines the header print.hpp, it will find the link pointing to print.cpp and so will scan that source too.

So, there is another source file (besides hello.cpp). This means that:

  • The binary depends on hello.o and print.o
  • There is another target in the makefile, named "print.o", and it depends on print.cpp.

gm will then scan print.cpp and find:

  • The include directive pointing to "iostream".
  • the include directive pointing to "print.hpp".

The first link is ignored because, related to a standard header file (in fact, the file name is wrapped by angle brackets), the second link points to a file previously examined, so won't be scanned again. However, it tells that print.hpp is to be added to the list of headers included by print.cpp (in other words, print.o will depend upon print.hpp, too).

Because there are no other files to scan, gm prints the makefile and terminates. This time the output is:

######################################################################
# Makefile for program : hello
# generated by program : gm.exe
######################################################################

# target to generate/update binary file
hello.exe : hello.o print.o
  g++ -o hello.exe hello.o print.o

hello.o : hello.cpp print.hpp
  g++ hello.cpp -c -o hello.o -Ic:/gnu/include/g++/

print.o : print.cpp print.hpp
  g++ print.cpp -c -o print.o -Ic:/gnu/include/g++/

######################################################################
#end of file

This is about what I expected.

Some Extra Thoughts

I will now introduce a few things that will make the picture just a little bit more complicated, but hopefully more realistic.

Consider the case when the user header file "print.hpp" itself includes a user header file, for instance "foo.hpp". As I already said, one expects that foo.hpp will be added to the list of header files included by each source file that "includes" print.hpp (in my example, hello.cpp and print.cpp), so that it will end up in the dependency of the corresponding object files. The program gm will look for headers of headers, and will correctly add them to the dependency.

Consider now, the situation where hello.cpp is in one directory, for instance "c:/develop", while print.hpp and print.cpp are in another place, say "c:/mylib". However, gm is launched from "c:/develop" (that is, from the location of the main source).

A good question is: How does gm locate the files print.hpp and print.cpp?

In other words, until now all files were in the same directory; thus, I had directives without a path. Now, shall the include directive for print.hpp and the "pragma defs_in" directive for print.cpp be modified, so that they have a path in front of them?

The answer is: They could be modified, but they should not.

In fact, changing the directives would be editing unit files to keep directives up to date with the personal directory structure you chose on some system. This means that you are editing units for purposes not related to the programming language and so is best avoided; anyway, it would soon become tedious.

The solution I use is the following: Write, in a file, the paths where non-local pieces of code are stored (in my example there is only the path c:/mylib), and then hand the name of this file (which I term the "configuration" file), together with the one of the main source, to gm.

gm will read the configuration file and use its information to trace files that it can find in the directory where it is launched from; that is, files it can find in the same directory of the main source.

If it is unable to locate a unit, the user will be warned but execution continues (you'll soon learn the reason why it is better not to stop execution upon not finding a unit).

Because there is a configuration file, it could be used to store values that help parameterize gm; for instance, compiler options or linker options (the first I can think of are names of libraries). Actually, the name of the compiler itself could be kept there (instead of being built in the program gm).

A configuration file could be (lines with a leading "#" are comments, thus discarded):

# paths where non-local units are stored: one path on each line
# note that each path may be absolute or relative and always ends
# with a slash
c:/mylib/
c:/another_lib/
subdir/

# macros related to GNU tools and to the Windows system
# the BIN_EXT is because of Windows
# note that the extension begins with a dot
BIN_EXT   = .exe
COMPILER  = g++
LINKER    = g++
CMP_OPT   = -Ic:/gnu/include/g++/ -Wall -ggdb
LNK_OPT   =

As you can see there are two paragraphs:

  • The first has the paths for non-local files.
  • The second has "macros", sort of constants that will be copied into the makefile and used here to make files look shorter.

Here is, in detail, the information each macro conveys:

BIN_EXT  = extension of binary files (clearly, an OS-related issue)
COMPILER = name of compiler
LINKER   = name of linker
CMP_OPT  = options to be used by the compiler
LNK_OPT  = options to be used by the linker

Given the above, the makefile produced by gm is:

######################################################################
# Makefile for program : hello
# generated by program : gm.exe
######################################################################

# macros
BIN_EXT  = .exe
COMPILER = g++
LINKER   = g++
CMP_OPT  = -Ic:/gnu/include/g++/ -Wall -ggdb
LNK_OPT  =

# object files
OBJS = hello.o print.o

# target to generate/update binary file
hello$(BIN_EXT) : $(OBJS)
  $(LINKER) $(OBJS) -o hello$(BIN_EXT) $(LNK_OPT)

hello$OBJ_EXT) : hello.cpp print.hpp
  $(COMPILER) hello.cpp -c -o hello.o $(CMP_OPT)

print$OBJ_EXT) : print.cpp print.hpp
  $(COMPILER) print.cpp -c -o print.o $(CMP_OPT)

######################################################################
#end of file

There was no suitable linker option above, so here is a situation where one is needed.

Assume this:

  • I wish to place the "print" function in a library named "test" (actually, the name of the library is "libtest.a").
  • I use program "ar" to create the library (the command is ar -sr libtest.a print.o).
  • The file libtest.a (the library) is in the subdirectory "libs/".

The options to allow linking against this user library are "-Llib/ -ltest" (that is: tell where non-local libraries are, and tell which library should be linked).

If I have the definition of "print.o" stored in a library, chances are that I do not have the source code for the function (that is, print.cpp can't be found by gm). When gm runs, because it still finds a link pointing to "print.cpp" in the user header file "print.hpp", a warning message will show up, telling that print.cpp could not be examined. Of course, there will be no target describing how the object file print.o has to be generated; however, your application will be linked as expected because the apparently missing definition is actually found, by the linker, in the library file libtest.a. This is the reason why not finding a unit is not considered a fatal error by gm.

Note that if instead you have both the library definition of the function and the source file, even if you add the required linker options, the file print.o will be generated and used when linking (in place of the definition stored in the library).

There are two different ways to make sure the library is used, even if print.cpp is around:

  • Remove the pragma defs_in directive pointing to print.cpp.
  • "Hide" the source file where gm can't find it.

About the Program gm

The gm I'm shipping is a C++ implementation of the few ideas expressed above; of course, gm is a console application and as such should be used from a DOS box.

There are a few things about the code you should be aware of:

  • The code is distributed under the GNU license.
  • I only have limited experience, so my code is simple and, hopefully, easy to modify.
  • The code uses ANSI C++ (as much as g++ 2.95 allows me to), so it should be portable.
  • gm writes its output in the file named "makefile".
  • Compiling with the macro VERBOSE defined (that is, list "-DVERBOSE" among the compiler options) yields some additional output (sent to the console), which I used when debugging.
  • Issues related to the names of files are addressed in "filename.hpp" and "filename.cpp".
  • Do not forget to edit the configuration file I supply. It needs to be tailored to your installation of the compiler.
  • To change the extension of header files and source files recognized as unit files by gm, edit the file "gm.hpp".

I provide no binary file; instead, there is a script (build.bat) that can be used to generate one. Here is how:

  • Make sure "c:\gnu\bin" (or the equivalent on your system) is in the path.
  • Type "build DIR", assuming that "DIR" the directory where the g++ includes are stored on your system. For instance, on my system, DIR is "c:/gnu/include/g++/".

As soon as you have a "gm.exe" file, edit the configuration file "gm.con", changing the location of C++ header files according to your system; then use it on gm.cpp by typing the command "gm gm.cpp gm.con". Take a look at the makefile produced. It should be the almost the same as the file "makefile.ok" which I ship; "almost" because of the C++ standard headers' location issue.

An alternative way to build the binary file is to edit "makefile.ok", changing the location of the C++ system header files, and then using the command "make -f makefile.ok".

About the GNU Tools

The programs I used are ports to Windows, by MinGW, of:

  • The GNU make (generally shipped together with the compiler).
  • The GNU C++ compiler: g++ (it is part of "gcc", so it comes bundled with that program).
  • The GNU binary utility (found in the "binutils"): ar.

These are all free (in the GNU sense), well-documented, and easily available on the Web. Here are just a couple of places where you can get them:

Any search engine will yield more locations.

Downloads

The archive "gm.tgz" (Winzip will extract it) contains the source code of the program "gm". The subdirectory "example" contains the three-file version of the "hello world" program I used in this document, and a configuration file for gm, named gm.con (albeit you should tailor it to your system by editing the location of system headers).

Download source - 16 Kb

DISCLAIMER: You use the code contained in the archive at your own risk; in no way shall I be held responsible of any damage caused by using it.



Comments

  • Broken link!

    Posted by Aidman on 01/11/2006 01:42am

    The download link seems to be broken, any mirror site?

    Reply
  • Very useful

    Posted by Legacy on 06/27/2002 12:00am

    Originally posted by: Alex Farber

    Thank you for this article. If you are working also in Windows, it would be nice to get such explanations about Visial Studio makefiles. In any case, very useful.

    Reply
  • Compiling resource files into Proxy/Stub DLLs

    Posted by Legacy on 06/25/2002 12:00am

    Originally posted by: Tron

    This seems to be right in line with what I need. However would you happen to know how to compile resource files into Proxy/Stub DLLs?

    Thank you for any assistance.

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: October 29, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you interested in building a cognitive application using the power of IBM Watson? Need a platform that provides speed and ease for rapidly deploying this application? Join Chris Madison, Watson Solution Architect, as he walks through the process of building a Watson powered application on IBM Bluemix. Chris will talk about the new Watson Services just released on IBM bluemix, but more importantly he will do a step by step cognitive …

  • It's no secret what keeps CIOs up at night. Mobile, cloud, data, security, and social have become the "five imperatives," the drivers of business progress, innovation, and competitive differentiation. Business leaders around the world want to hear how other companies are succeeding. How are they applying the latest technologies? How did they get started? What outcomes are they achieving? Read this online magazine for success stories from organizations like the NBA, Pfizer, and San Jose State University as they …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds