There are many features in TMKMAKE to help you automate and extend the capabilities of your software build process. Learn how Make variables allow you to create multiple configurations of software from one Makefile. Use and extend TMKMAKEs built-in rules for multiple projects. Tighten up your Makefiles with implicit targets. An example RPG project detailed in this article can be modified and used for your first TMKMAKE-enabled project.
Last month, I introduced TMKMAKE, a recompilation tool that simplifies the chore of rebuilding objects (makefile targets) when the objects upon which they depend (dependents) change. I promised you that I would tell you about some of Makes more esoteric features this month. Ill begin with a discussion of Make variables.
Variables
A very powerful feature of Make is its ability to recognize and evaluate Makefile variables, sometimes called macros. A variable can be defined and used in a Makefile in much the same way you would use variables in a procedural programming language. One difference is that Make variables can be assigned from the command line (as an argument to TMKMAKE). Variables in TMKMAKE (and most other Make implementations) can be assigned only string values. The syntax for defining and assigning a variable in a Makefile is as follows:
var = value
In this syntax, var is the name of the variable, and value is the string to be assigned to it. The first character of var must be in the first column of the Makefile. To reference a variable within a Makefile, enclose it in parentheses and prefix it all with a dollar sign ($)
(e.g., to reference the Makefile variable named MYVAR, use $(MYVAR)). Although variables can be named using uppercase and/or lowercase characters, most conventional Makefiles use only uppercase characters in variable names. Variables can be referenced anywhere within the description block. When Make encounters a variable reference, it substitutes the reference with the value of the variable in its place. If a variable is referenced
before it is given a value, or if it is not assigned at all, TMKMAKE replaces the reference with an empty string.
As a simple example of the use of variables, suppose a project is to be built into two separate configurations, one for the current release of OS/400 and the other for the previous release. You could create a variable named CONFIG and refer to it in description blocks that create program and module objects, as in Figure 1.
Now, with the TMKMAKE command, give CONFIG a value, as shown here:
TMKMAKE +
MAKETARG(debug02
MACRO(CONFIG=tgtrls(*prv))
The MACRO parameter of TMKMAKE allows you to give a value to one or more variables referenced in a Makefile. Thus, the TGTRLS parameter of the Create RPG Module (CRTRPGMOD) command would be substituted for the $(CONFIG) reference, and the module would be built for the previous release of the operating system. Note that the TGTRLS parameter is used in many other compilation and program generation commands, so the $(CONFIG) reference could be used in each of the description blocks using these commands. Note also that if CONFIG had not been given a value in the MACRO parameter of TMKMAKE, the $(CONFIG) reference would be translated into an empty string, and hence the default value of TGTRLS would be implied.
Conditional Preprocessing
When Make evaluates variables and makes substitutions for the variables references in the Makefile, it does so at preprocessing time in the Make process. That is, Make performs all variable substitutions before evaluating any description blocks. Another action that Make can perform prior to description block evaluation is conditional directive evaluation. An if-then-else structure is provided to allow sections of the Makefile to be alternately switched on or off, depending on the value or existence of a Make variable.
In the previous example, if the value for CONFIG had not been specified on the command line, the programs would be built with the default value for the TGTRLS parameter of the build commands, which is generally *CURRENT. Suppose the *PRV configuration should be built by default instead of the *CURRENT configuration. This function could be performed by means of the following conditional statement:
!ifndef CONFIG
CONFIG=TGTRLS(*PRV)
!endif
The exclamation point (!) indicates to TMKMAKE that the line contains a preprocessing directive that should be evaluated prior to description block evaluation. All preprocessing directives should be given in lowercase (but not the parameters to the directive). The !ifndef directive is used to enable the section of the Makefile between a subsequent !endif directive and itself, but only if the variable named by its argument is defined either previously in the Makefile or as an argument in the MACRO parameter. Similarly, but inversely defined, is the !ifdef directive. In the example, the effect is to assign the value TGTRLS(*PRV) to CONFIG only if it has not already been assigned in the MACRO parameter of TMKMAKE (or in a previous line of the Makefile). Thus, the build will deviate from the previous version of OS/400 only if the CONFIG variable is set on the command line.
You can also use the !else directive following one of the ifs, as in the following:
!ifndef CONFIG
CONFIG=TGTRLS(*PRV)
TEXT=Default build
!else
TEXT=Special build
!endif
Two occassionally useful preprocessing directives are also available: !undef allows you to revoke a macro definition. It takes a macro name as a parameter and, if processed, removes the macro from Makes current dictionary of macros. Thus, any subsequent
reference of the macro will return an empty string. !error may be used to halt Make at the point it is processed, and it may be followed by a text string that will be output. !error can be helpful when debugging a complex Makefile.
Implicit Targets
With TMKMAKE and other modern Make programs, it is possible to specify implicit targets to create a set of objects with a given extension from another set of objects with another extension. Instead of explicitly specifying the full names of the targets and dependencies, the first line of the description block specifies the extensions of the targets and dependencies. This can save a lot of redundant description blocks, which may be identical with the exception of the basenames of the targets and dependents. Figure 2 shows two description blocks that create two separate ILE modules from RPG code.
Note that the only differences between the two description blocks are the basenames of the targets, dependents, and references to the target in the compiler command. Instead, you could specify an implicit target to combine the two description blocks, as in Figure 3.
When using implicit targets, you have to give TMKMAKE a hint about which extensions to use. The .SUFFIXES special target tells TMKMAKE that its list of dependents is a list of extensions to be used in resolving implicit targets. .SUFFIXES needs to be defined only once in the Makefile, usually before the implicit target definitions. In Figure 3, the implicit target description block follows the .SUFFIXES block. The target specification and the apparent lack of dependencies are what distinguish this description block as an implicit target block. The target specification is actually a specification of one class of target and one class of dependent. It is a wildcard specification, and it is read as *.qrpgsrc
Built-in Variables
Notice the odd-looking variable reference in the command list for the example in Figure 3. $(@F) is one of TMKMAKEs built-in variables and is usable only in description blocks. These variables are very special in that they are resolved not at preprocessing time, but rather at description block evaluation time. $(@F) is substituted with the object name (mnemonic F) of the target (mnemonic @). For a target named test
Notice that the name of the variable reference is not enclosed in parentheses for the regular built-in variables. The extended built-in variables are variations of the regular builtins with this syntax:
$({regvarname}{modifier})
Here, {regvarname} refers to one of the variable names of the regular built-in variables: @, ?, <, *, or **. {modifier} is one of the modifiers listed in Figure 5.
In the example above, $(@F) gave us the file name of the target. For another example, suppose there were a target with an explicit path, such as this:
mylib/myprog
crtpgm pgm(mylib/myprog) module(mylib/mymod)
In the command section of the description block, the library name of the target could be referenced with $(@L), instead of the explicit references to mylib.
More Fun with Implicit Targets
In Part 1, I showed you how to construct a pseudotarget description block with no command section. TMKMAKE actually handles the case of no command section by
checking for any implicit target description blocks that match the targets and dependents of the commandless description block. For clarity, a description block such as the following could be added:
test
This matches the previously defined implicit target description block for building RPG modules since the target has the
As a final example of implicit targets, suppose that there were only single-module, single-language programs in a different project. While this is an impractical use of ILE RPG, it is an effective demonstration; with modification, the example could be used with OPM RPG. There will be three programs, and the source members for the modules will have the same name as the programs. These programs can be built with one implicit target block and one pseudotarget, as follows:
.SUFFIXES : .qrpgsrc
all : debug
.qrpgsrc
crtrpgmod module(*curlib/$(@F)) source(*curlib/qrpgsrc)
crtpgm pgm(*curlib/$(@F)) module($(@F))
Invoking TMKMAKE using this Makefile with all as the argument in the MAKETARG parameter will invoke the RPG compiler and the bind command for three source members and three ILE modules to create the three programs.
TMKMAKE matches undefined targets to the extensions specified as dependents in the .SUFFIXES description block in left-to-right order. The first extension matching the undefined target identifies the implicit target description block to use to generate the target. If no suffix matches or no implicit target for the suffix exists, then the build of the undefined target fails.
Whenever you define new implicit targets in a Makefile, you should add the extensions of the target and dependent as dependents of the .SUFFIXES target block at the top of the Makefile. Otherwise, Make may not be able to correctly match the implicit targets.
Built-in Targets
TMKMAKE includes some built-in implicit targets, too. These can simplify a Makefile even more, to the point that it is possible to write an effective Makefile that contains no commands at all! The built-in implicit target blocks are actually configurablethey are contained in the QMAKSRC source file in the library where TMKMAKE is installed, in the member BUILTIN. TMKMAKE reads this file member before reading your Makefile, effectively appending its contents as a prefix to your Makefile. You can examine this file to determine if any of the built-in targets are usable in your projects.
Unfortunately, in TMKMAKE as delivered from IBM, there are several implicit rules missing from BUILTIN that should probably be there. There are no implicit targets to build ILE RPG modules from the QRPGSRC file source members like the one defined in the first implicit target example. Since BUILTIN is a source file member, however, you can add your own implicit targets here, if you wish, to suit the needs of your projects.
Tips for Writing Useful Makefiles
A well-written Makefile will most likely contain elements of all the features of Make outlined in the previous sections. The goals of Makefile programming are similar to the goals of applications programming. A good Makefile is concise, flexible, easy to understand, easy to maintain, and easy to extend.
Conciseness is achieved mainly via the implicit targets construct, which reduces the number and size of description blocks. Large projects may contain dozens of modules. It makes better sense to use implicit targets to build the modules for such projects thereby
reducing the number of description blocks from dozens to as few as one. This greatly simplifies changing the configuration of the build as well, as changes often need to be made to only a few implicit targets.
Makefile variables can be used to increase the utility of a build process. By defining and/or overriding the assignment of Makefile variables in a build process, several different build configurations are possible using a single Makefile. Makefile variables can also help to reduce the complexity of description blocks by gathering strings of similar command parameters or options under a single variable. Pseudotargets, with or without implicit targets, can help to build multiple independent parts in a single step and provide for future extendibility of the build.
As a final example, examine the smallish RPG project whose source is given in Figure 6. Suppose that the project will probably grow in size and scope. The Makefile for the project will be designed accordingly.
The first elements you should notice about this Makefile are the lines containing comments. The pound sign (#) indicates the beginning of a comment, which extends to the end of the current line. The comment can appear anywhere on any line and may be preceded by Makefile directives or description block code.
At the top of this Makefile, a few variables that control the build configuration are outlined. These variables are placed at the top of the Makefile so that they are easily accessible by the user of the Makefile, who may change or override them to suit a particular build configuration. Many seemingly static values are defined here as variable values, such as location of the development library. This makes it possible for build configuration changes to be as simple as changing a variable assignment. For the same reason, compiler and binder options variables are assigned here, such as RPGCOPTS. Note that the definition of RPGCOPTS contains a reference to an undefined variable RPGFLAGS, allowing you to specify other compiler options on the command line. When you want to do this, simply add an assignment to RPGFLAGS in the MACRO parameter of the TMKMAKE command.
Next are defined variables enumerating the lists of objects to generate. PGMS is the list of programs to be built. In this example, the project currently builds only one program, but when requirements change and it needs to build more, the names of the new programs can be added here.
Following the programs list are lists of modules required to build the programs. Each program to be built will have a separate list of modules to be built (its OK if the lists overlap). Here, DEBUG01MODSTYPE and DEBUG01MODS are two different expressions of the same list. The first is used as a dependency list for the DEBUG01 program target, and the second is used as an argument to the MODULE parameter of CRTPGM.
The first target in the Makefile is a pseudotarget, all. Most Makefiles have a pseudotarget similar to this one, which is invoked to build all the major objects of the project. All modules are built with implicit target description blocks, but programs, which may have several module dependencies, are built with explicit targets. Again, since there is currently only one program target, DEBUG01, there is only one explicit target in the Makefile, in the description block following the all pseudotarget description block.
Sometimes a clean pseudotarget is useful, so there is one defined in this example. When clean is invoked, all the programs and modules associated with the project are deleted. This might be necessary when you are unsure of the contents of the development library. Note that in the command list of the clean target, the commands are prefixed with a hyphen (-). This tells TMKMAKE to ignore errors in these commands if they occur. So, if one of the objects does not exist, TMKMAKE does not abort because of the error, but rather continues to delete the other objects.
At the end of the Makefile, the set of implicit target description blocks required to build the module lists is given. Currently, only RPG and CL modules are constructed by this build.
This Makefile generates program and module objects to the library whose name is assigned to the LIB variable. The source files it uses to do this, however, must be contained in the current library. Generally, you would invoke one of the two pseudotargets, all or clean, to use it. You could however, invoke other explicit or implicit targets, such as PROJLIB/DEBUG01C
This Makefile can be used as a prototype for your projects. In most cases, it will be possible to change only a few configuration variables and the object lists to make this Makefile suitable for your project.
TMKMAKE Command Options
There are eight command parameters available with TMKMAKE. The default options are best in most cases, but its nice to know what the other options are. Refer to Figure 7.
The default options will do the trick for you if the Makefile and library list are set up correctly. As mentioned before, the MACRO parameter can be used to override Makefile variables. And if you have more than one primary target in your Makefile, you may need to use MAKETARG. The MSGLOG parameter can usefully be changed from the default value. By specifying MSGLOG(*SESSION), all TMKMAKE-generated output goes to the session log rather than the job log, which some may find handy. Also, if a certain Makefile just isnt working as you think it should, the OPTION(*DEBUG) parameter can sometimes be helpful.
More Information
TMKMAKE has many more features than have been introduced here. Many of these, including support for binding directories and link editor libraries, are presented in a document supplied with QUSRTOOL. The member TMKINFO in the QATTINFO file of QUSRTOOL contains this document, and it is a must-read for any programmers seriously considering using TMKMAKE in their development plan. A word of warning, though: Some of the features described in the document simply do not seem to work, such as the Output Translation feature. The basic functionality, however, is robust enough to work around this problem.
For information on Make in general, there are almost as many books about the program as there are platform implementations of it. The computer science section of any good library or bookstore will have at least one title that describes Make. Most modern C/C++ compilers bundle with, or at least assume the use of, a version of Make. Make is installed on almost every machine in the world running the UNIX operating system. Since 1998, the Free Software Foundation has been publishing a freeware version of Make for UNIX called GNU Make, for which it regularly releases updates and produces bug fixes. It also has extensive documentation that outlines the basics of Make and the enhancements implemented by the GNU Project. GNU Make and its documentation are available at http://www.gnu.org.
In the bookstores, look for the OReilly & Associates, Inc. book, Managing Projects with Make (by Andrew Oram and Steve Talbott), which covers the basics well. It also provides insight into the different implementations of Make.
Finally, if you are truly brave in your quest for knowledge, you have the source code to TMKMAKE sitting there on your AS/400 right now in QUSRTOOL. It could use an update and probably a bug fix or two as well, if you are so inclined.
Reference
Oram, Andrew and Steve Talbott. Managing Projects with Make. Sebastopol, California: OReilly & Associates, Inc., 1991.
debug02
crtrpgmod module(*curlib/debug01)
srcfile(*curlib/qrpgsrc) $(CONFIG)
Figure 1: Using a Makefile variable
debug01
crtrpgmod module(*curlib/debug01) srcfile(*curlib/qrpgsrc)
debug02
crtrpgmod module(*curlib/debug02) srcfile(*curlib/qrpgsrc)
Figure 2: Explicit description blocks tend to be redundant
.SUFFIXES : .qrpgsrc
.qrpgsrc
crtrpgmod module(*curlib/$(@F)) srcfile(*curlib/qrpgsrc)
Figure 3: This implicit target (following the required .SUFFIXES target) combines the two description blocks in Figure 2
$@ The full name of the target, including object tags,
$< The name of the first dependent causing execution of the command list (implicit target description blocks only)
$* The basename of the target and dependent causing execution of the command list (implicit target description blocks only)
$** The list of all dependents in the current description block
Figure 4: Built-in macros
L The library name part of the regular built-in variables value M The member name part of the regular built-in variables value (empty string if not a database file)
F The file name part of the regular built-in variables value (empty string if not a database file)
T The object type part of the regular built-in variables value (as in
Figure 5: Modifiers used by extended variables
#
# Typical RPG/CL project makefile
#
#
# configuration options
#
LIB=PROJLIB # primary development library
RPGC=CRTRPGMOD # rpg compiler
CLCL=CRTCLPGM # cl compile & bind
CLC=CRTCLMOD # cl compiler
BIND=CRTPGM # bind program
TGTRLS=TGTRLS(*PRV) # default target release is *PRV
# compiler option strings
RPGCOPTS=$(RPGFLAGS) $(TGTRLS) DBGVIEW(*ALL)
CLCOPTS=DBGVIEW(*ALL) $(TGTRLS)
BINDOPTS=REPLACE(*YES) $(TGTRLS)
#
# programs
#
PGMS=$(LIB)/DEBUG01C
# modules to bind defined two ways.
#
DEBUG01MODSTYPE = $(LIB)/DEBUG01
$(LIB)/DEBUG01C
DEBUG01MODS = $(LIB)/DEBUG01 $(LIB)/DEBUG02 $(LIB)/DEBUG01C #
# primary targets
#
all : $(PGMS)
$(LIB)/DEBUG01C
$(BIND) PGM($(LIB)/$(@F)) MODULE($(DEBUG01MODS)) $(BINDOPTS)
clean :
-DLTPGM PGM($(LIB)/DEBUG01C)
-DLTMOD MODULE($(LIB)/DEBUG01)
-DLTMOD MODULE($(LIB)/DEBUG02)
-DLTMOD MODULE($(LIB)/DEBUG01C) #
# implicit rules
#
.SUFFIXES : .
.qrpgsrc.
$(RPGC) MODULE($(LIB)/$(@F)) SRCFILE(*CURLIB/$(
.qclsrc.
$(CLC) MODULE($(LIB)/$(@F)) SRCFILE(*CURLIB/$(
Figure 6: A Makefile for a more realistic project
MAKETARG This parameter is the target in the Makefile to evaluate. It must be bound by single-quote () characters if it contains lowercase characters.
It defaults to *FIRST, which TMKMAKE interprets as the target of the first description block of the Makefile. SRCFILE This parameter is the library and file containing the Makefile. The library defaults to *LIBL, and the file defaults to QMAKSRC. SRCMBR This parameter is the member containing the Makefile. It defaults to *FIRST.
MACRO This parameter comprises the makefile variable assignments. Up to 50 variable assignments of the form var=value can be specified in this parameter. Variables assigned via this parameter override variable assignments in the Makefile. It defaults to *NONE.
OPTION This parameter is a set of the following keyword options:
*NOIGNORE causes TMKMAKE to abort when commands return error or exceptional conditions. *IGNORE causes TMKMAKE to ignore all errors and exceptions. *NOSILENT prints the command text to the output device prior to invoking the command. *SILENT does not print the commands. *BIRULES causes the built-in description blocks contained in the BUILTIN member of QMAKSRC in the installation library to be used. *NOBIRULES does not use BUILTIN. *EXEC causes commands in the command list to be executed, whereas *NOEXEC causes the commands not to be executed. *TOUCH causes the update time of the target and all of its dependents to be changed explicitly rather than by the commands that generate them. *NOTOUCH does not explicitly change the update time. *DEBUG displays debug information that may be useful for debugging errors in Makefiles. *NODEBUG does not display debug information. If conflicting options are specified, the last one specified in the argument list is used. The default options are *NOIGNORE, *NOSILENT, *BIRULES, *EXEC, *NOTOUCH, and *NODEBUG.
MSGLOG This parameter is the output device to which TMKMAKE-invoked commands and messages are directed. It will be either *JOBLOG or
*SESSION, and it defaults to *SESSION. MARGINS This parameter will specify Makefile margin column numbers as in MARGINS(left,right). It can also take the keyword default value, *SRCFILE, which for source physical files indicates column 1 for the left margin, and the record length for the right margin.
RTNCODE This parameter will specify the severity level of exceptions and/or return codes that cause TMKMAKE to interpret a command execution as successful or unsuccessful. It will specify one of *EXCEPTION, *LURC, or *BOTH to indicate that the following severity value is for exceptions, return codes, or both, respectively. Then, it will specify an integer severity value. It defaults to RTNCODE(*EXCEPTION 30).
SRVOPT This parameter comprises undocumented internal debugging options for TMKMAKE.
Figure 7: TMKMAKE command parameters
LATEST COMMENTS
MC Press Online