Advanced Topics

The following topics are more advanced than usually presented in a tutorial, but have been included here because Opus Make has a lot of power that might be missed if you were to skim the Opus Make User's Guide.

The “Keep-Working” Mode   Search Directories   Recursive Making   Object Libraries   Version Control Systems

The “Keep-Working” Mode [Top]

Usually Make stops at the first shell line that returns an error. This is the right behavior for interactive work, but is not really what you want if you are doing a long, unattended build such as an overnight build of your entire system. The solution is to use Make's “keep-working” mode. This mode allows Make to do the maximum amount of work consistent with any errors that occur during the build.

In the keep-working mode, a shell-line error while Make is updating a target causes Make to stop working on the current target. The make process continues, with Make noting that the current target was incompletely built. Future targets are only updated if they do not depend on any incomplete targets.

Since Make does as much work as possible, you only have to fix the problem targets and start Make again. The keep-working mode does the maximum amount of safe and correct work.

The “keep-working” mode is enabled with the .KEEPWORKING makefile directive (see Page ) or “–k” command-line flag (see Page ).

 

Search Directories [Top]

A common scenario when writing software is to divide the project into production code and development code. Source files kept in the production directory are stable and are known to work. Software developers “check out” source files and header files from the production directory into a development directory, make modifications to them, then perform a “check in” to place them back into the production directory.

If the dependencies in the makefile contained “hard-coded” paths to the source files and header files, then whenever a file was checked in or out of production the dependencies would need to be updated to reflect the new location of the file. Rather than do this, the solution is to use Make's search directory support. You tell Make about multiple directories that files can reside in and Make will search them, in order, to find the files.

Make is told about search directories with special macros that have names of the form .PATH.ext where .ext is the extension of the files these search directories apply to. The value of the macro is a semi-colon separated list of directories. Here is our example again, returned to the simple makefile that only supports Borland bcc and written to use search paths to locate source files with the .c or .h extension:

.PATH.c = .;e:\make\release
.PATH.h = .;e:\make\release
OBJS = main.obj io.obj
CC = bcc
MODEL = s
CFLAGS = –m$(MODEL) 
project.exe : $(OBJS)
	tlink c0$(MODEL) $(OBJS), $(.TARGET),, c$(MODEL) /Lf:\bc\lib 
$(OBJS) : incl.h 

The .PATH.c macro, whose value is “.;e:\make\release”, specifies that .c files should be looked for first in “.” (the current directory) then in “e:\make\release”. Make will try each directory in order when it looks for .c files. Likewise for .PATH.h. Search directories only apply for target names that don't have a directory component.

Make also supports an extensionless .PATH macro which applies to all files that do not have a specific .PATH.ext. If there is no .PATH.ext and no .PATH, Make looks for files only in the current directory.

 

Recursive Making [Top]

Assume now that we want to compile the sources for two different MS-DOS memory models. The easiest way to do this is to create subdirectories under this directory and compile the sources from this directory and the production directory to produce the different memory models. For maintainability, it is preferable to use only one makefile.

One way to do this is to have Make call itself recursively. Here is our example makefile again, modified to maintain subdirectories called small and compact:

.PATH.c = ..;e:\make\release
.PATH.h = ..;e:\make\release
OBJS = main.obj io.obj
CC = bcc
CFLAGS = –m$(MODEL) 
all : small compact # `all' is the default target 
small .MAKE .ALWAYS .MISER :
	( cd small ; make $(MFLAGS) –f ../$(INPUTFILE) MODEL=s project.exe ) 
compact .MAKE .ALWAYS .MISER :
	%chdir $(.TARGET)
	make $(MFLAGS) –f ../$(INPUTFILE) MODEL=c project.exe
	%chdir $(MAKEDIR) 
project.exe : $(OBJS)
	tlink c0$(MODEL) $(OBJS), $(.TARGET),, c$(MODEL) /Lf:\bc\lib 
$(OBJS) : incl.h 

There are many ideas in this makefile:

Start with the all target. It has two sources, small and compact, which are the names of the subdirectories. When no targets are specified on the command line the first target in the makefile, all, gets made. This causes the sources of all, the targets small and compact, to be made first.

Look at the shell line after the small dependency line:

( cd small ; make $(MFLAGS) –f ../makefile MODEL=s project.exe )

There are two commands on the same shell line with “;” separating the commands. The first command changes directory into small. The second command executes a recursive make with the current command-line flags (the value of the MFLAGS macro); this makefile (accessed by ../$(INPUTFILE) because the recursive make is in the small subdirectory); a command-line macro definition, MODEL=s; and a command-line target, project.exe.

The “cd small” and the “make ...” command are together in a so-called multiple-command shell line (cf. Page ) because Make starts each shell line in the current directory. If the “cd” and “make” were on separate shell lines, the “cd” would be “forgotten” and the “make” would be executed from the current directory.

The command-line macro definition MODEL=s supplies the memory model. The command-line target, project.exe, names the target to be made and is given because we don't want Make to make the default target, all, again.

Look at the small dependency line. The target name is followed by the words: .MAKE, .ALWAYS and .MISER. These are called target attributes and are covered in detail on Page .

The .MAKE attribute overrides the “–n” (no execute) command-line flag, which is used to show but not execute the shell lines. The .MAKE attribute causes the command:

make -n small

to not just show, but to actually execute:

( cd small ; make –n –f ../$(INPUTFILE) MODEL=s project.exe )

The .ALWAYS attribute tells Make that the small target should be updated always. We need this because small is an existing target (a directory) without dependents. Make normally thinks that kind of target doesn't need updating.

The .MISER attribute tells Make to swap itself out of memory (for MS-DOS only) before executing the shell line.

Look at the compact dependency line. This shows another way to change into a subdirectory to do a recursive make. The directive:

%chdir compact

changes Make's current directory to the compact subdirectory. The:

make $(MFLAGS) –f ../makefile MODEL=s project.exe

is the recursive Make. The final directive:

%chdir $(MAKEDIR)

changes Make's directory back to its starting directory.

Finally, the .PATH.c and .PATH.h macros have been modified to point to the parent directory, “..”, and to “e:\make\release”, the source-file directories relative to the small and compact subdirectories.

How is all this used? From the current directory, execute the command:

make

Make reads makefile and makes all, which first makes small. To make small, Make executes the shell line:

( cd small ; make –f ../makefile MODEL=s project.exe )

This changes to the small subdirectory and Make starts again, reading the same makefile from the parent directory. Make defines the MODEL macro with the value “s” and builds project.exe, compiling and linking the files for the small model. After completing this, Make builds the compact target, with a similar procedure.

Notice that “make small” updates only the small directory, “make compact” updates only the compact directory, and “make” or “make all” updates them both.

Advanced Recursive Making [Top]

One problem with doing recursive makes is that, as written, we can't make selective targets in the subdirectory. For example, a command like “make small/io.obj” doesn't work because Make needs to treat it as “make io.obj in the small directory”. The following example shows how to do this. The %foreach directive (cf. Page ) is used to iterate over the elements of the MAKETARGETS macro (the list of command-line targets). If the element specifies a pathed target Make recurses into its directory and does the recursive make on the proper target:

.PATH.c = ..;e:\make\release
.PATH.h = ..;e:\make\release
OBJS = main.obj io.obj
DIRS = small compact
MODEL = $(MAKEDIR,F,S'\(.\).*'\1',LC)       # see the discussion below 
%foreach Target $(MAKETARGETS)
% if '${Target,P}' && %dir(${Target,D})     # If element has path & directory exists
$(Target) .MAKE .ALWAYS .MISER :               # ... set up a recursive make
	( cd $(.TARGET,D) ; make $(MFLAGS) –f ../$(INPUTFILE) $(.TARGET,F) )
% endif
%end 
all : $(DIRS) 
$(DIRS) .MAKE .ALWAYS .MISER :
	( cd $(.TARGET) ; make $(MFLAGS) –f ../$(INPUTFILE) project.exe ) 
project.exe : $(OBJS)
	tlink c0$(MODEL) $(OBJS), $(.TARGET),, c$(MODEL) /Lf:\bc\lib 
$(OBJS) : incl.h 

Now, doing “make small/io.obj” executes the shell line:

( cd small ; make –f ../makefile io.obj ) 

This is what we want! One final wrinkle has been added: The name of the directory provides the MODEL macro definition! The expression:

$(MAKEDIR,F,S'\(.\).*'\1',LC) 

takes the absolute path of the directory Make starts in, as kept in the MAKEDIR macro; applies the “F” modifier to reduce it to the directory name; applies “S'\(.\).*'\1'”, a substitution modifier that evaluates to the first letter of the directory name; then applies the “LC” modifier to change the letter to lower case.

Why do we go to all this effort? One reason is to have greater simplicity! We can add support for a large memory model simply by creating a large subdirectory and by changing the DIRS macro definition to “small compact large”. The name of the directory is automatically coordinated with the compiler model through the use of these macro modifiers and no other changes need to be made to the makefile.

 

Object Libraries [Top]

An object library is a file that contains a collection of object modules and serves as a means for keeping related object modules together. Object libraries are created by object librarians, including Microsoft LIB (16- and 32-bit), Borland TLIB and UNIX ar.

Make supports object libraries by being able to look inside the library for the timestamp of object modules. This is useful in two ways:

It removes the need to have both the library and the object modules on disk.

It makes it possible to state that a target depends on specific modules inside the library.

Building Object Libraries

To build a library, list the library as the target and give it the .LIBRARY target attribute. Modules inside the library are the sources. For example:

io.lib .LIBRARY : keyboard.obj screen.obj

This states that io.lib is a library dependent on modules keyboard.obj and screen.obj. When io.lib is built, keyboard.obj and screen.obj are built then added to the library. No shell lines were supplied to update io.lib so Make uses its built-in %.lib rule to update the library. The MS-DOS Make %.lib rule is:

%.lib :
	$(STAMPOBJ) $(.NEWSOURCES)
	$(LIBEXE) $(LIBFLAGS) – + $(.NEWSOURCES); 

The value of the STAMPOBJ macro is “stampobj”. The stampobj utility is supplied by us and is used to place a timestamp inside the object module. The value of the LIBEXE macro is “lib”, the Microsoft Librarian. The LIBFLAGS macro initially has no value but can be defined in your makefile.

This %.lib rule differs for the various operating systems supported by Opus Make and can be redefined by you to support other librarians. See Page for details.

 

Version Control Systems [Top]

Version control systems (VCS) store versions of a source file in an archive, maintaining an audit trail of changes and providing the ability to retrieve prior versions. Make supports VCS by being able to read the timestamp of any version stored in VCS archives. Make works with the following VCS: Intersolv PVCS v3.0 & up, Microsoft SourceSafe v3.0 & up, Burton Systems Software TLIB v4.12 & up, GNU RCS and MKS RCS and Source Integrity.

One advantage of VCS support is that it is possible for Make to determine the timestamp of source files that are absent on disk, but present inside the VCS. Another advantage of Make's VCS support is that Make can retrieve exact, named versions, making building “release” versions or rebuilding older versions well controlled.

Here is a makefile example that supports Microsoft SourceSafe, a version control system that stores version-controlled files in a project in a database:

PROJECT = make
SSPROJECT = $$/$(PROJECT)		# SourceSafe project name
SSDATA = e:\ss\data			# SourceSafe data directory 
SRCS = $(SSPROJECT,@)			# read list of sources
OBJS = $(SRCS,.c=.obj)			# create list of objects 
.SS_STORAGE : .c			# VCS support 
$(PROJECT).exe : $(OBJS)
	shell lines to produce make.exe 

The project called “make” is archived in SourceSafe. SSPROJECT and SSDATA are configuration macros that Make uses for the name of the SourceSafe project ($/make) and the location of the SourceSafe data files. The $(SSPROJECT,@) expression causes Make to read the list of source files stored in the SourceSafe project $/make. The object file list (the OBJS macro) is generated from the source file list with a macro modifier that replaces the .c in each file name with .obj.

The “.SS_STORAGE : .c” line enables Make's SourceSafe VCS support for the .c file extension. If a .c source file is needed in order to create an object file, and the .c file is absent, Make can get it from SourceSafe.