Programming Utilities Guide

Maintaining Software Projects

make is especially useful when a software project consists of a system of programs and libraries. By taking advantage of nested make commands, you can use it to maintain object files, executables, and libraries in a whole hierarchy of directories. You can use make in conjunction with SCCS to ensure that sources are maintained in a controlled manner, and that programs built from them are consistent. You can provide other programmers with duplicates of the directory hierarchy for simultaneous development and testing if you want (although there are trade-offs to consider).

You can use make to build the entire project and install final copies of various modules onto another file system for integration and distribution.

Organizing a Project for Ease of Maintenance

As mentioned earlier, one good way to organize a project is to segregate each major piece into its own directory. A project broken out this way usually resides within a single file system or directory hierarchy. Header files could reside in one subdirectory, libraries in another, and programs in still another. Documentation, such as reference pages, can also be kept on hand in another subdirectory.

Suppose that a project is composed of one executable program, one library that you supply, a set of headers for the library routines, and some documentation, as in the following diagram.

Graphic

The makefiles in each subdirectory can be borrowed from examples in earlier sections, but something more is needed to manage the project as a whole. A carefully structured makefile in the root directory, the root makefile for the project, provides target entries for managing the project as a single entity.

As a project grows, the need for consistent, easy-to-use makefiles also grows. Macros and target names should have the same meanings no matter which makefile you are reading. Conditional macro definitions and compilation options for output variants should be consistent across the entire project.

Where feasible, a template approach to writing makefiles makes sense. With a template, you track how the project is built. All you have to do to add a new type of module is to make a new directory for it, copy an appropriate makefile into that directory, and edit a few lines. You also need to add the new module to the list of things to build in the root makefile.

Conventions for macro and target names, such as those used in the default makefile, should be instituted and observed throughout the project. Mnemonic names mean that although you might not remember the exact function of a target or value of a macro, you will know the type of function or value it represents by the name and that's usually valuable when deciphering a makefile also.

Using include Makefiles

One method of simplifying makefiles, while providing a consistent compilation environment, is to use the make:

	include filename 

This directive reads in the contents of a named makefile; if the named file is not present, make checks for a file by that name in /etc/default.

For instance, there is no need to duplicate the pattern-matching rule for processing troff sources in each makefile, when you can include its target entry, as shown below.

SOURCES= doc.ms spec.ms 
...
clean: $(SOURCES) 
include ../pm.rules.mk

Here, make reads in the contents of the ../pm.rules.mk file:

# pm.rules.mk 
# 
# Simple "include" makefile for pattern-matching 
# rules.  

%.tr: %.ms 
         	troff -t -ms $< > $@ 
%.nr: %.ms 
         	nroff -ms $< > $@

Installing Finished Programs and Libraries

When a program is ready to be released for outside testing or general use, you can use make to install it. Adding a new target and new macro definition to do so is not difficult:

DESTDIR= /proto/project/bin 

install: functions 
        	-mkdir $(DESTDIR) 
        	cp functions $(DESTDIR)

A similar target entry can be used for installing a library or a set of headers.

Building the Entire Project

Occasionally you should take a snapshot of the sources and the object files that they produce. Building an entire project involves invoking make successively in each subdirectory to build and install each module. The following example shows how to use nested make commands to build a simple project.

Assume your project is located in two different subdirectories, bin and lib, and that in both subdirectories you want make to debug, test, and install the project.

First, in the projects main, or root, directory, you put a makefile such as this:

# Root makefile for a project.  

TARGETS= debug test install 
SUBDIRS= bin lib

all: $(TARGETS)
$(TARGETS):
        	@for i in $(SUBDIRS) ; \
        	do \
               cd $$i ; \
               echo "Current directory:  $$i" ;\
               $(MAKE) $@ ; \
               cd .. ; \
        	done

Then, in each subdirectory (in this case, bin) you place a makefile of this general form:

#Sample makefile in subdirectory
debug:
        	@echo "			Building debug target"
        	@echo
test:
        	@echo "			Building test target"
        	@echo
install:
        	@echo "			Building install target"
        	@echo

When you type make (in the base directory), you get the following output:

$ make
Current directory:  bin
         	Building debugging target

Current directory:  lib
          Building debugging target

Current directory:  bin
         	Building testing target

Current directory:  lib
         	Building testing target

Current directory:  bin
         	Building install target

Current directory:  lib
         	Building install target
$

Maintaining Directory Hierarchies with the Recursive Makefiles

If you extend your project hierarchy to include more layers, chances are that not only will the makefile in each intermediate directory have to produce target files, but it will also have to invoke nested make commands for subdirectories of its own.

Files in the current directory can sometimes depend on files in subdirectories, and their target entries need to depend on their counterparts in the subdirectories.

The nested make command for each subdirectory should run before the command in the local directory. One way to ensure that the commands run in the proper order is to make a separate entry for the nested part and another for the local part. If you add these new targets to the dependency list for the original target, its action will encompass them both.

Maintaining Recursive Targets

Targets that encompass equivalent actions in both the local directory and in subdirectories are referred to as recursive targets.


Note -

Strictly speaking, any target that calls make with its name as an argument, is recursive. However, here the term is reserved for the narrower case of targets that have both nested and local actions. Targets that have only nested actions are referred to as "nested" targets.


A makefile with recursive targets is referred to as a recursive makefile.

In the case of all, the nested dependencies are NESTED_TARGETS; the local dependencies, LOCAL_TARGETS:

NESTED_TARGETS=  debug test install 
SUBDIRS= bin lib
LOCAL_TARGETS= functions 

all: $(NESTED_TARGETS) $(LOCAL_TARGETS) 

$(NESTED_TARGETS): 
        	@ for i in $(SUBDIRS) ; \
        	do \
               echo "Current directory:  $$i" ;\
               cd $$i ; \
               $(MAKE) $@ ; \
               cd .. ; \
        	done

$(LOCAL_TARGETS):
        	@ echo "Building $@ in local directory."
       (local directory commands)

The nested make must also be recursive, unless it is at the bottom of the hierarchy. In the makefile for a leaf directory (one with no subdirectories), you build only local targets.

Maintaining a Large Library as a Hierarchy of Subsidiaries

When maintaining a very large library, it is sometimes easier to break it up into smaller, subsidiary libraries, and use make to combine them into a complete package. Although you cannot combine libraries directly with ar, you can extract the member files from each subsidiary library, then archive those files in another step, as shown in the following example:

$ ar xv libx.a 
x - x1.o 
x - x2.o 
x - x3.o 
$ ar xv liby.a 
x - y1.o 
x - y2.o 
$ ar rv libz.a *.o 
a - x1.o 
a - x2.o 
a - x3.o 
a - y1.o 
a - y2.o 
ar: creating libz.a

A subsidiary library is maintained using a makefile in its own directory, along with the (object) files it is built from. The makefile for the complete library typically makes a symbolic link to each subsidiary archive, extracts their contents into a temporary subdirectory, and archives the resulting files to form the complete package.

The next example updates the subsidiary libraries, creates a temporary directory in which to put extracted the files, and extracts them. It uses the * (shell) wild card within that temporary directory to generate the collated list of files. While filename wild cards are generally frowned upon, this use of the wild card is acceptable because a new directory is created whenever the target is built. This guarantees that it contains only files extracted during the current make run.


Note -

In general, use of shell filename wild cards is considered to be bad form in a makefile. If you do use them, you need to take steps to insure that it excludes spurious files by isolating affected files in a temporary subdirectory


The example relies on a naming convention for directories. The name of the directory is taken from the basename of the library it contains. For instance, if libx.a is a subsidiary library, the directory that contains it is named libx.

It makes use of suffix replacements in dynamic-macro references to derive the directory name for each specific subdirectory. (You can verify that this is necessary.) It uses a shell for loop to successively extract each library and a shell command substitution to collate the object files into proper sequence for linking (using lorder and tsort) as it archives them into the package. Finally, it removes the temporary directory and its contents.

# Makefile for collating a library from subsidiaries.  

CFLAGS= -O 

.KEEP_STATE:
.PRECIOUS:  libz.a

all: lib.a 

libz.a: libx.a liby.a 
        	-rm -rf tmp 
        	-mkdir tmp 
        	set -x ; for i in libx.a liby.a ; \ 
              		do ( cd tmp ; ar x ../$$i ) ; done 
        	( cd tmp ; rm -f *_*_.SYMDEF ; ar cr ../$@ `lorder * | tsort` ) 
        	-rm -rf tmp libx.a liby.a 

libx.a liby.a: FORCE 
        	-cd $(@:.a=) ; $(MAKE) $@ 
        	-ln -s $(@:.a=)/$@ $@ 
FORCE: 

For the sake of clarity, this example omits support for alternate variants, as well as the targets for clean, install, and test (does not apply since the source files are in the subdirectories).

The rm -f *_*_.SYMDEF command embedded in the collating line prevents a symbol table in a subsidiary (produced by running ar on that library) from being archived in this library.

Because the nested make commands build the subsidiary libraries before the current library is processed, you can extend this makefile to account for libraries built from both subsidiaries and object files in the current directory. You need to add the list of object files to the dependency list for the library and a command to copy them into the temporary subdirectory for collation with object files extracted from subsidiary libraries.

# Makefile for collating a library from subsidiaries and local objects.  

CFLAGS= -O 

.KEEP_STATE:
.PRECIOUS:  libz.

OBJECTS= map.o calc.o draw.o

all: libz.a 

libz.a: libx.a liby.a $(OBJECTS) 
         	-rm -rf tmp 
         	-mkdir tmp 
         	-cp $(OBJECTS) tmp 
          set -x ; for i in libx.a liby.a ; \ 
              		do ( cd tmp ; ar x ../$$i ) ; done 
         	( cd tmp ; rm -f *_*_.SYMDEF ; ar cr ../$@ \
                `lorder * | tsort` ) 
          -rm -rf tmp lix.a liby.a 

libx.a liby.a: FORCE 
         	-cd $(@:.a=) ; $(MAKE) $@ 
         	-ln -s $(@:.a=)/$@ $@ 
FORCE:

Reporting Hidden Dependencies to make

You might need to write a command for processing hidden dependencies. For instance, you might need to trace document source files that are included in a troff document by way of .so requests. When .KEEP_STATE is in effect, make sets the environment variable SUNPRO_DEPENDENCIES to the value:

SUNPRO_DEPENDENCIES='report-file target'

After the command has terminated, make checks to see if the file has been created, and if it has, make reads it and writes reported dependencies to .make.state in the form:

target:dependency ...

where target is the same as in the environment variable.