So far we've only discussed the C program itself. This chapter explores the programming environment, which includes organizing your program files, and the make utility, which turns source programs into a finished work.
Small programs consisting of only a few files are easy to organize: just stick everything in one directory. But suppose you're an adventurous programmer and decide to write two programs. Do you stick them both in the same directory? No.
Put each program's files in a separate directory. That way you won't have to figure out which file goes with which program. It also keeps the number of files per directory down to a manageable level
Someday you will probably work on a series of programs, like a set of programs to manage a mailing list. There are programs to enter data, check for duplicates, print labels, and generate reports. All of these programs use a common set of low level list functions. You can't put each of these functions in each program directory. Duplicate files are very difficult to maintain. You need some way to share files.
The solution is to turn the list functions into a library. The library goes in one subdirectory while other subdirectories hold the various programs.
Suppose you have all four programs all going to the Library directory for their subroutines. But the Library directory contains both the source and the library file ( MAIL.LIB ) and headers ( MAIL.H ) used by these programs. Having access to all that data can easily confuse things. You need to limit what they can see.
The solution is to have a special directory for libraries and header files as illustrated by See : Mailing list directory tree . When a library is built it is "released" by placing it in this directory. The header files are put here as well. This directory contains the public part of the library, while the private part stays behind in the source directory.
The top level directory ( Mailing List ) should be kept free of sources, except for a Makefile , READ.ME , or other compilation. This makes the files in the top level simple. Adding programs to this level adds unneeded complexity.
Almost all C compilers come with a program building utility called make . It is designed to perform the compilation and other commands necessary to turn source files into a program.
To use make , you provide it with a description of your program in a file named Makefile . This file also contains the transformation rules that tell it how to turn source into objects.
The first things a programmer needs to know when confronting a strange Makefile are "What does it do?" and "How do I use it?" The heading comments of your Makefile should answer those questions.
The first paragraph of the heading comments explains what the Makefile creates. For example:
#################################################
# Makefile for crating the program "hello" #
#################################################
#################################################
# This Makefile creates the math libraries: #
# fft.a, curve.a and graph.a #
#################################################
Programmers use the preprocessor #ifdef to allow for compile time configuration of their program. For example, there might be a debug version and a production version. Or there might be several different flavors, one for each of the various operating systems the program is designed to run on.
The effect of these conditional compilation directives filter up to the Makefile . For example, defining the macro CFLAGS as -DDEBUG may produce a test program, while the definition -DPRODUCTION may produce the production version.
Any configuration information should be listed in the heading comments. This way a programmer can immediately see what customization must be performed on the Makefile before he starts to build the program.
# Set the variable SYSTEM to the appropriate value for your
# SYSTEM=-DBSD4_3 For Berkeley UNIX Ver. 4.3
# SYSTEM=-DSYSV For AT&T System V UNIX
The standard form of the make command is:
Here, target selects what you want to make. (Microsoft's make is a notable exception to this standard.) The programmer needs to know which targets are valid and what they do. Putting a list of targets in the heading provides this information.
#################################################
# all - create the program "hello" #
# clean - remove all object files #
# install - put the program in $(DESTDIR) #
#################################################
Over the years a standard set of four targets have developed:
all This target compiles all the programs. This is the standard default target.
install This target copies the program and associated files into the installation directory. For local commands this can be /usr/local/bin . For production software, this will be the official release directory.
clean This target removes all program binaries and object files and generally cleans up the directory.
clobber Like clean, this target removes all derived files--that is, all files that can be produced from another source. In cases where a software control system such as SCCS or RCS is used, it means removing all source files from the software control system.
This target runs the source files through the program checker
This list represents the minimum "standard" set of targets. Other optional target names have also come into use over the years.
Creates a list of dependencies automatically and edits them into the Makefile . There are several utilities to do this, including a public domain program called maketd .
srcs Checks out the sources from a software control system such as SCCS or RCS .
print Prints the sources on the line printer.
xrf Creates a cross reference printout.
debug Compiles the program with the debug flag enabled.
shar Makes a char format archive. This format is widely used to distribute sources over the Internet and USENET.
The make utility allows the user to define simple text macros, such as:
The macros are used to define a variety of items, such as the source files to be compiled, the compiler name, compilation flags, and other items.
The make program predefines a number of macros. (The actual list of Redefined macros varies, so check your manual to see which macros are defined for your version of make.)
Each macro definition should be preceded by a short, one-line comment that explains the macro. Also use white space to separate each comment/macro combination.
There are no standard macro definitions; however, the following is a list of the most common:
CFLAGS Flags supplied to the C compiler for compiling a single module.
LDFLAGS Flags supplied to the C compiler for loading all the objects into a single program.
The list of object files. Some of the newer versions of make have an extension that allows you to automatically generate this macro from the SRCS macro. For example, the following line tells Sun's make that OBJS is the same as SRCS , except change all the .c extensions to .o .
DESTDIR The destination directory, where the install target puts the files.
As mentioned earlier, macros are frequently used for configuration information. When it comes to actually defining the variable, it is useful to list all the definitions and then comment all but the selected one. For example:
# Define one of the following for your system
SYSTEM=-DBSD4 3 # For Berkeley UNIX Version 4.3
So far, we've just been defining things. At this point it's time to tell make to actually do something. This section contains the rules for all the major targets listed in the comment header. These targets are grouped just after the macros so they can be easily located.
Often a Makefile contains several intermediate or minor targets. These are to help build things for the major targets. For example, the major target all calls upon the minor target hello .
The make program knows about all or most standard compilers, such as the C compiler. Sometimes you need to define a rule for a special compiler, such as the parser generator yacc . This program takes grammars ( y files) and turns them to C code
The Makefile rule for this program is:
# Use yacc to turn xxx.y into xxx.c
Notice that every special rule has a comment explaining what it does.
This target section can also be used to override the default rules. For example, if all your C files need to run through a special pre-processor, you can install your own rule for C compilation:
# Run the files through "fixup" before compiling them
Some make programs provide you with a default rule file. Under no circumstances should you change this file. Doing so changes causes make to behave in a nonstandard way. Also, programmers expect the complete compilation instructions to be kept in the program's Makefile , not hidden in some system file.
The dependencies section shows the relationship between each of the binary files and their source. For example:
Dependency checking is the weakest point in the make command. Frequently this section is out of date or missing entirely. Advanced make programs have an automatic dependency checking, thus eliminating the need for this section.
Other solutions have also sprung up. The public domain utility maketd and other similar programs automatically generate dependency lists. They all depend on this section being at the end of the Makefile .
The full Makefile for the hello program is:
#################################################
# Makefile for creating the program "hello" #
# Set the variable SYSTEM to the appropriate #
# value for your operating system. #
# SYSTEM=-DBSD4_3 For Berkeley UNIX Version 4.3 #
# SYSTEM=-DSYSV For AT&T System V UNIX #
# SYSTEM=-DDOS For DOS (Borland Turbo C) #
# all - create the program Hello #
# clean - remove all object files #
# install - put the program in #
#################################################
# Define one of the following for your system
SYSTEM=-DBSD4_3 # For Berkeley UNIX Version 4.3
#SYSTEM=-DSYSV # For AT&T System V UNIX
#SYSTEM=-DDOS # For DOS (Borland Turbo C)
install -c hello /usr/local/bin
Whenever possible, use macros for common directories or other text. For example:
INSTALL_BIN = /usr/local/bin # Place to put the binaries
INSTALL_MAN = /usr/local/man # Place to put the man pages
INSTALL_HELP = /usr/local/lib # Place to put help info.
INSTALL_BIN = $(DESTDIR)/bin # Place to put the binaries
INSTALL_MAN = $(DESTDIR)/man # Place to put the man pages
INSTALL_HELP = $(DESTDIR)/lib # Place to put help info.
YACC_FLAGS = -c -t -I/project/include -I/general/include
CFLAGS = -c -g -I/project/include -I/general/include
INCLUDES=-I/project/include -I/general/include
Installing a program can be tricky. I've seen a shell script with more than 100 lines created just to install a single program. There is a temptation to put long, complex command sets into the Makefile. Because of the difficulties of both shell program and Makefile format, this results in a large, complex, and impossible to maintain piece of code.
In general, it is best to put large command scripts in a batch file. This makes it easier to test, debug, and comment them.
Makefile s have a standard format that is portable across most systems. However, compile time options differ from system to system. For example, a program written to work on both UNIX and DOS will require two entirely different commands sets to create it. Stuffing two sets of compilation instructions in a single Makefile can get messy. When this happens, it is best to create a separate Makefile for each system. The standard method for naming these various Makefiles is <system>.mak . Some standard names are:
turboc.mak DOS using Borland's Turbo C
msc.mak DOS using Microsoft's C compiler
This list can grow quite long as programs are ported to more and more systems. A read.me file must be distributed with the software to describe how to select the proper Makefile.
Some of the more advanced make commands have an include facility that allows the inclusion of other files in the Makefile . Some programmers have tried to create generic Makefile s, to be used like this:
# Define some macro names to be
# used by the generic Makefile
In theory, this should work nicely. There is one generic Makefile that does everything, then all you have to do is set things up properly.
In practice, though, it's not so simple. Creating a program is never a standard process and far too many have their little peculiarities. Trying to program around them in a generic Makefile is extremely tricky.
One approach is to create a generic Makefile to be used as a template for making custom Makefiles The problem with this approach is that when you want to add a new target to every Makefile . you must edit each one.
The solution? There isn't one. This is a classic trade-off of standardization vs. flexibility. Generic Makefile s are standard but inflexible. Individual Makefile s are flexible but hard to standardize.