This chapter is devoted to those facilities in PMake that allow you to do a great deal in a makefile with very little work, as well as do some things you couldn't do in Make without a great deal of work (and perhaps the use of other programs). The problem with these features, is they must be handled with care, or you will end up with a mess.
Once more, I assume a greater familiarity with UNIXor Sprite than I did in the previous two chapters.
PMake supports the dispersal of files into multiple directories by allowing you to specify places to look for sources with .PATH targets in the makefile. The directories you give as sources for these targets make up a ``search path.'' Only those files used exclusively as sources are actually sought on a search path, the assumption being that anything listed as a target in the makefile can be created by the makefile and thus should be in the current directory.
There are two types of search paths in PMake: one is used for all types of files (including included makefiles) and is specified with a plain .PATH target (e.g. ``.PATH : RCS''), while the other is specific to a certain type of file, as indicated by the file's suffix. A specific search path is indicated by immediately following the .PATH with the suffix of the file. For instance .PATH.h : /sprite/lib/include /sprite/att/lib/include would tell PMake to look in the directories /sprite/lib/include and /sprite/att/lib/include for any files whose suffix is .h.
The current directory is always consulted first to see if a file exists. Only if it cannot be found there are the directories in the specific search path, followed by those in the general search path, consulted.
A search path is also used when expanding wildcard characters. If the pattern has a recognizable suffix on it, the path for that suffix will be used for the expansion. Otherwise the default search path is employed.
When a file is found in some directory other than the current one, all local variables that would have contained the target's name (.ALLSRC, and .IMPSRC) will instead contain the path to the file, as found by PMake. Thus if you have a file ../lib/mumble.c and a makefile .PATH.c : ../lib mumble : mumble.c $(CC) -o $(.TARGET) $(.ALLSRC) the command executed to create mumble would be ``cc -o mumble ../lib/mumble.c.'' (As an aside, the command in this case isn't strictly necessary, since it will be found using transformation rules if it isn't given. This is because .out is the null suffix by default and a transformation exists from .c to .out. Just thought I'd throw that in.)
If a file exists in two directories on the same search path, the file in the first directory on the path will be the one PMake uses. So if you have a large system spread over many directories, it would behoove you to follow a naming convention that avoids such conflicts.
Something you should know about the way search paths are implemented is that each directory is read, and its contents cached, exactly once -- when it is first encountered -- so any changes to the directories while PMake is running will not be noted when searching for implicit sources, nor will they be found when PMake attempts to discover when the file was last modified, unless the file was created in the current directory. While people have suggested that PMake should read the directories each time, my experience suggests that the caching seldom causes problems. In addition, not caching the directories slows things down enormously because of PMake's attempts to apply transformation rules through non-existent files -- the number of extra file-system searches is truly staggering, especially if many files without suffixes are used and the null suffix isn't changed from .out.
UNIXand Sprite allow you to merge files into an archive using the ar command. Further, if the files are relocatable object files, you can run ranlib on the archive and get yourself a library that you can link into any program you want. The main problem with archives is they double the space you need to store the archived files, since there's one copy in the archive and one copy out by itself. The problem with libraries is you usually think of them as -lm rather than /usr/lib/libm.a and the linker thinks they're out-of-date if you so much as look at them.
PMake solves the problem with archives by allowing you to tell it to examine the files in the archives (so you can remove the individual files without having to regenerate them later). To handle the problem with libraries, PMake adds an additional way of deciding if a library is out-of-date:
A library is any target that looks like ``-lname'' or that ends in a suffix that was marked as a library using the .LIBS target. .a is so marked in the system makefile.
Members of an archive are specified as ``archive(member[ member...])''. Thus ``'libdix.a(window.o)'' specifies the file window.o in the archive libdix.a. You may also use wildcards to specify the members of the archive. Just remember that most the wildcard characters will only find existing files.
A file that is a member of an archive is treated specially. If the file doesn't exist, but it is in the archive, the modification time recorded in the archive is used for the file when determining if the file is out-of-date. When figuring out how to make an archived member target (not the file itself, but the file in the archive -- the archive(member) target), special care is taken with the transformation rules, as follows:
Thus, a program library could be created with the following makefile: .o.a : ... rm -f $(.TARGET:T) OBJS = obj1.o obj2.o obj3.o libprog.a : libprog.a($(OBJS)) ar cru $(.TARGET) $(.OODATE) ranlib $(.TARGET) This will cause the three object files to be compiled (if the corresponding source files were modified after the object file or, if that doesn't exist, the archived object file), the out-of-date ones archived in libprog.a, a table of contents placed in the archive and the newly-archived object files to be removed.
All this is used in the makelib.mk system makefile to create a single library with ease. This makefile looks like this: # # Rules for making libraries. The object files that make up the library are # removed once they are archived. # # To make several libararies in parallel, you should define the variable # "many_libraries". This will serialize the invocations of ranlib. # # To use, do something like this: # # OBJECTS = <files in the library> # # fish.a: fish.a($(OBJECTS)) MAKELIB # # #ifndef _MAKELIB_MK _MAKELIB_MK = #include <po.mk> .po.a .o.a : ... rm -f $(.MEMBER) ARFLAGS ?= crl # # Re-archive the out-of-date members and recreate the library's table of # contents using ranlib. If many_libraries is defined, put the ranlib off # til the end so many libraries can be made at once. # MAKELIB : .USE .PRECIOUS ar $(ARFLAGS) $(.TARGET) $(.OODATE) #ifndef no_ranlib # ifdef many_libraries ... # endif many_libraries ranlib $(.TARGET) #endif no_ranlib #endif _MAKELIB_MK
Like the C compiler before it, PMake allows you to configure the makefile, based on the current environment, using conditional statements. A conditional looks like this: #if boolean expression lines #elif another boolean expression more lines #else still more lines #endif They may be nested to a maximum depth of 30 and may occur anywhere (except in a comment, of course). The ``#'' must the very first character on the line.
Each boolean expression is made up of terms that look like function calls, the standard C boolean operators &&, ||, and !, and the standard relational operators ==, !=, >, >=, <, and <=, with == and != being overloaded to allow string comparisons as well. && represents logical AND; || is logical OR and ! is logical NOT. The arithmetic and string operators take precedence over all three of these operators, while NOT takes precedence over AND, which takes precedence over OR. This precedence may be overridden with parentheses, and an expression may be parenthesized to your heart's content. Each term looks like a call on one of four functions:
The arithmetic and string operators may only be used to test the value of a variable. The lefthand side must contain the variable expansion, while the righthand side contains either a string, enclosed in double-quotes, or a number. The standard C numeric conventions (except for specifying an octal number) apply to both sides. E.g. #if $(OS) == 4.3 #if $(MACHINE) == "sun3" #if $(LOAD_ADDR) < 0xc000 are all valid conditionals. In addition, the numeric value of a variable can be tested as a boolean as follows: #if $(LOAD) would see if LOAD contains a non-zero value and #if !$(LOAD) would test if LOAD contains a zero value.
In addition to the bare ``#if,'' there are other forms that apply one of the first two functions to each term. They are as follows: ifdef defined ifndef !defined ifmake make ifnmake !make There are also the ``else if'' forms: elif, elifdef, elifndef, elifmake, and elifnmake.
For instance, if you wish to create two versions of a program, one of which is optimized (the production version) and the other of which is for debugging (has symbols for dbx), you have two choices: you can create two makefiles, one of which uses the -g flag for the compilation, while the other uses the -O flag, or you can use another target (call it debug) to create the debug version. The construct below will take care of this for you. I have also made it so defining the variable DEBUG (say with pmake -D DEBUG) will also cause the debug version to be made. #if defined(DEBUG) || make(debug) CFLAGS += -g #else CFLAGS += -O #endif There are, of course, problems with this approach. The most glaring annoyance is that if you want to go from making a debug version to making a production version, you have to remove all the object files, or you will get some optimized and some debug versions in the same program. Another annoyance is you have to be careful not to make two targets that ``conflict'' because of some conditionals in the makefile. For instance #if make(print) FORMATTER = ditroff -Plaser_printer #endif #if make(draft) FORMATTER = nroff -Pdot_matrix_printer #endif would wreak havok if you tried ``pmake draft print'' since you would use the same formatter for each target. As I said, this all gets somewhat complicated.
In normal operation, the Bourne Shell (better known as ``sh'') is used to execute the commands to re-create targets. PMake also allows you to specify a different shell for it to use when executing these commands. There are several things PMake must know about the shell you wish to use. These things are specified as the sources for the .SHELL target by keyword, as follows:
The strings that follow these keywords may be enclosed in single or double quotes (the quotes will be stripped off) and may contain the usual C backslash-characters (\n is newline, \r is return, \b is backspace, \' escapes a single-quote inside single-quotes, \" escapes a double-quote inside double-quotes). Now for an example.
This is actually the contents of the <shx.mk> system makefile, and causes PMake to use the Bourne Shell in such a way that each command is printed as it is executed. That is, if more than one command is given on a line, each will be printed separately. Similarly, each time the body of a loop is executed, the commands within that loop will be printed, etc. The specification runs like this: # # This is a shell specification to have the bourne shell echo # the commands just before executing them, rather than when it reads # them. Useful if you want to see how variables are being expanded, etc. # .SHELL : path=/bin/sh \ quiet="set -" \ echo="set -x" \ filter="+ set - " \ echoFlag=x \ errFlag=e \ hasErrCtl=yes \ check="set -e" \ ignore="set +e"
It tells PMake the following:
I should note that this specification is for Bourne Shells that are not part of Berkeley UNIXas shells from Berkeley don't do error control. You can get a similar effect, however, by changing the last three lines to be: hasErrCtl=no \ check="echo \"+ %s\"\n" \ ignore="sh -c '%s || exit 0\n"
This will cause PMake to execute the two commands echo "+ cmd" sh -c 'cmd || true' for each command for which errors are to be ignored. (In case you are wondering, the thing for ignore tells the shell to execute another shell without error checking on and always exit 0, since the || causes the exit 0 to be executed only if the first command exited non-zero, and if the first command exited zero, the shell will also exit zero, since that's the last command it executed).
There are three (well, 3 ½) levels of backwards-compatibility built into PMake. Most makefiles will need none at all. Some may need a little bit of work to operate correctly when run in parallel. Each level encompasses the previous levels (e.g. -B (one shell per command) implies -V) The three levels are described in the following three sections.
As noted before, PMake will not expand a variable unless it knows of a value for it. This can cause problems for makefiles that expect to leave variables undefined except in special circumstances (e.g. if more flags need to be passed to the C compiler or the output from a text processor should be sent to a different printer). If the variables are enclosed in curly braces (``${PRINTER}''), the shell will let them pass. If they are enclosed in parentheses, however, the shell will declare a syntax error and the make will come to a grinding halt.
You have two choices: change the makefile to define the variables (their values can be overridden on the command line, since that's where they would have been set if you used Make, anyway) or always give the -V flag (this can be done with the .MAKEFLAGS target, if you want).
Then there are the makefiles that expect certain commands, such as changing to a different directory, to not affect other commands in a target's creation script. You can solve this is either by going back to executing one shell per command (which is what the -B flag forces PMake to do), which slows the process down a good bit and requires you to use semicolons and escaped newlines for shell constructs, or by changing the makefile to execute the offending command(s) in a subshell (by placing the line inside parentheses), like so: install :: .MAKE (cd src; $(.PMAKE) install) (cd lib; $(.PMAKE) install) (cd man; $(.PMAKE) install) This will always execute the three makes (even if the -n flag was given) because of the combination of the ``::'' operator and the .MAKE attribute. Each command will change to the proper directory to perform the install, leaving the main shell in the directory in which it started.
The final category of makefile is the one where every command requires input, the dependencies are incompletely specified, or you simply cannot create more than one target at a time, as mentioned earlier. In addition, you may not have the time or desire to upgrade the makefile to run smoothly with PMake. If you are the conservative sort, this is the compatibility mode for you. It is entered either by giving PMake the -M flag (for Make), or by executing PMake as ``make.'' In either case, PMake performs things exactly like Make (while still supporting most of the nice new features PMake provides). This includes:
When PMake reads the makefile, it parses sources and targets into nodes in a graph. The graph is directed only in the sense that PMake knows which way is up. Each node contains not only links to all its parents and children (the nodes that depend on it and those on which it depends, respectively), but also a count of the number of its children that have already been processed.
The most important thing to know about how PMake uses this graph is that the traversal is breadth-first and occurs in two passes.
After PMake has parsed the makefile, it begins with the nodes the user has told it to make (either on the command line, or via a .MAIN target, or by the target being the first in the file not labeled with the .NOTMAIN attribute) placed in a queue. It continues to take the node off the front of the queue, mark it as something that needs to be made, pass the node to Suff_FindDeps (mentioned earlier) to find any implicit sources for the node, and place all the node's children that have yet to be marked at the end of the queue. If any of the children is a .USE rule, its attributes are applied to the parent, then its commands are appended to the parent's list of commands and its children are linked to its parent. The parent's unmade children counter is then decremented (since the .USE node has been processed). You will note that this allows a .USE node to have children that are .USE nodes and the rules will be applied in sequence. If the node has no children, it is placed at the end of another queue to be examined in the second pass. This process continues until the first queue is empty.
At this point, all the leaves of the graph are in the examination queue. PMake removes the node at the head of the queue and sees if it is out-of-date. If it is, it is passed to a function that will execute the commands for the node asynchronously. When the commands have completed, all the node's parents have their unmade children counter decremented and, if the counter is then 0, they are placed on the examination queue. Likewise, if the node is up-to-date. Only those parents that were marked on the downward pass are processed in this way. Thus PMake traverses the graph back up to the nodes the user instructed it to create. When the examination queue is empty and no shells are running to create a target, PMake is finished.
Once all targets have been processed, PMake executes the commands attached to the .END target, either explicitly or through the use of an ellipsis in a shell script. If there were no errors during the entire process but there are still some targets unmade (PMake keeps a running count of how many targets are left to be made), there is a cycle in the graph. PMake does a depth-first traversal of the graph to find all the targets that weren't made and prints them out one by one.