Building Projects With Imake
Imake is tool for configuring the X Window System and its components for different platforms and compilers. Imake allows you to create a generic description of how your project should be built and leaves the system-specific details to centralized configuration files.
Put simply, Imake is built on the idea of applying the C preprocessor to Makefiles, rather than C programs. Much of the redundancy in common Makefiles has been turned into preprocessor macros, which have the same effect macros have in C programs—making things easier.
Imake should have come with your installation of X. If you run one of the common distributions of Linux, you need to check that some of the “development” packages are installed. For instance, I run Red Hat 4.0 on my system, and the Imake system is contained in the XFree86-devel RPM.
If you aren't sure if you have Imake installed or have it configured properly, it is easy to check. Just run these commands:
touch Imakefile xmkmf
There should now be a new file called Makefile in the current directory. There is quite a bit of “stuff” in the new Makefile, mostly generic rules and information generated from the configuration files.
If you've gotten this far, Imake is installed and configured just fine. Now, you should be able to run make and get output. For example on my Red Hat 4.2 system, I type make and get the following output:
make: Nothing to be done for 'emptyrule'.
Besides containing many programs, there are several programs that Imake depends on to function properly. However, most of them are basic things like bash, mv, rm and so on. If these basic programs are missing, you have more severe problems than whether Imake works properly. Still, there are a couple of major components that aren't vital to a working Linux system that are vital to Imake.
make isn't necessarily vital to the functioning of Imake, but the generated Makefiles are absolutely useless by themselves. If you don't have make, you should install GNU make. For more advanced uses of Imake, it is good to have a thorough understanding of how make works. At the end of this article are some references for more information on make.
Obviously, for a Makefile generation program to be useful, you need a compiler. However, that is not the only reason for having GCC installed. Imake relies on an external C preprocessor, installed in /lib/cpp by default. If you have GCC installed and configured, you should also have cpp (the C preprocessor) installed.
In case you don't have a /lib/cpp file, the simplest way to solve the problem is to make a symbolic (soft) link (use ln) from it to GCC's cpp executable. For example, on my system, /lib/cpp is a symbolic link to /usr/lib/gcc-lib/i486-linux/2.7.2/cpp.
The major user-visible component of the Imake system is the file Imakefile, in which the project-specific information needed by Imake to build your project is kept.
At a basic level Imakefile consists of rule invocations, variable definitions and variable invocations. The first, rule invocations, resemble C-function calls. The second resemble C-variable assignments. These two should be familiar to anyone who has used macros in C code. On the other hand, variable invocations are nothing like their C counterparts, but should be familiar to anyone who has done any work with plain make. Here's a simple example:
VARIABLE = value ARule(arg1,arg2,$(VARIABLE))
As with programming in general, there are many of syntax pitfalls with Imake. The following two sections describe a few of the problems to keep in mind.
Imake (actually, the underlying C preprocessor) is fairly strict about how the various rules must be invoked. First, there must be absolutely no extraneous white space around the commas between arguments. For example, this rule is incorrect:
ARule(arg1, arg2, arg3)
and this version is correct:
ARule(arg1,arg2,arg3)To confuse things even more, in some cases, it is valid for the argument to have space. For example, many rules take a list of files as one of the arguments, where each file is delimited by a space. Typically, this is fairly intuitive, and several of the examples later in this article show the most common cases.
The second problem with argument passing in Imake is empty arguments. Not all arguments are necessary for every rule, but the C preprocessor breaks if an argument is left out or has no value. To solve the problem, Imake provides the macro NullParameter to allow empty arguments in a rule. For example:
ARule(arg1,NullParameter,arg3)
There are two different ways to include comments in your Imakefile. The first is standard C-style comments, opening with the /* characters and closing with the */ characters. Anything between the two is completely ignored. The second style of comment uses the string XCOMM to start the comment, and a line break ends it.
The basic difference between the two is what appears in the final generated Makefile. C-style comments don't show up at all in the generated Makefile, while XCOMM comments do. Put simply, instances of XCOMM are replaced with a # character. One item to remember about XCOMM comments, since the C preprocessor doesn't know to ignore them, it will gladly process anything it sees as a macro inside an XCOMM comment. In other words, an invalid rule invocation that looks to be commented out can cause the Makefile generation to fail.
There are many rules provided by the Imake configuration files. Some of the most common and useful are described here. Note that the rules used in the presented examples are not necessarily compatible with each other. Do not intermix rules from the different examples.
This rule, as you can tell by its name, is the simplest one that Imake provides. It generates a simple Makefile that compiles a single program from a single source file. Say you have a source file, prog.c, and you wish to compile it into an executable called prog. You need only a single line in the Imakefile such as this one:
SimpleProgramTarget(prog)
To try it out, write a short “hello world” type program in C and an appropriate Imakefile. Then, run xmkmf and make. Your “hello world” program should compile without problem.
Since it's difficult to write complex programs with only a single source file, this rule was created to allow a single executable to be built from multiple source files. However, with the added flexibility comes a cost, you have to add information about which source files to use. For example, if you have a project with foo.c and bar.c that will create an executable called foobar, your Imakefile should look like this:
SRCS = foo.c bar.c OBJS = foo.o bar.o ComplexProgramTarget(foobar)
There are three other rules that are similar to ComplexProgramTarget() but not quite the same. They are called ComplexProgramTarget_1(), ComplexProgramTarget_2() and ComplexProgramTarget_3(). The basic idea behind these rules is to allow a single Imakefile to generate targets for more than one program.
Although, these rules are similar to ComplexProgramTarget() there are some minor differences. The first and most obvious one, is the different way to specify source files. Instead of setting SRCS and OBJS, you use SRCS1, OBJS1, SRCS2, OBJS2, SRCS3 and OBJS3. The second is the rules take different arguments. These rules take an additional two arguments. The second and third arguments are a way to specify local and system libraries that must be linked into the final executable. Here is an example of how to use these rules:
PROGRAMS = foobar1 foobar2 foobar3 SRCS1 = foo1.c bar1.c OBJS1 = foo1.c bar1.c SRCS2 = foo2.c bar2.c OBJS2 = foo2.c bar2.c SRCS3 = foo3.c bar3.c OBJS3 = foo3.c bar3.c ComplexProgramTarget_1(foobar1,NullParameter,\ NullParameter) ComplexProgramTarget_2(foobar2,NullParameter,\ NullParameter) ComplexProgramTarget_3(foobar3,NullParameter,\ NullParameter)
Note that the last two rules will not function properly without the first. You cannot have a ComplexProgramTarget_2() or ComplexProgramTarget_3() without having a ComplexProgramTarget_1(). The reasons for this are related to the internals of the rule definitions. To discover the reason why, read the O'Reilly Imake book mentioned in Resources.
This rule is by far the most useful. It can be used an arbitrary number of times and can use an arbitrary number of source files to build the executable. Again, along with added flexibility, there is added complexity. The usage of this rule given by Imake.rules is as follows:
NormalProgramTarget( locallibs,syslibs)
This is how the arguments are defined:
program: name of the executable being built
objects: names of object files to be built from source. This argument replaces the functionality of the OBJS variables in the various ComplexProgramTarget() rules.
deplibs: libraries that the executable needs
locallibs: local (to the project) libraries to link
syslibs: system libraries to link
Note that in the last three arguments there is some redundancy. The deplibs argument is a list of the library file names which the program needs. This list is used by Imake to build a proper Makefile that will rebuild the executable if any of those files change. The last two arguments, locallibs and syslibs are the same libraries, but expressed as command-line options to tell the compiler to link those libraries. Naturally, this is done so that Imake knows how to properly build the executable with all the necessary libraries.
Listing 1 uses NormalProgramTarget() to reproduce the functionality of the ComplexProgramTarget() example. The advantage, of course, is that program targets can be added or removed without affecting each other.
Note that the use of the variables SRCS1, OBJS1 and so on are simply for convenience. Unlike ComplexProgramTarget(), these variables are not used in the definition of the rule. When it comes time to change the list of sources and objects, it's easier to find them at the front of the Imakefile as a variable definition than to search through the rule invocations to find all the instances.
On the other hand, the variable SRCS is a different case. Notice two new rules, DependTarget() and LintTarget(). These two rules create the targets depend and lint, respectively. To be able to do their job, these rules need the variable SRCS set to a list of the source files in the project. Note that in the previous examples the depend and lint targets were created automatically; the function of these targets is explained later.
There is one more new rule in this example, AllTarget(). Again, because NormalProgramTarget() does less work than the other rules, you have to do more work. Quite simply, each instance of AllTarget() adds another dependency to the “all” target in the final Makefile. AllTarget() is called by default for each program to be built.
There are various files involved in the Imake build system; most reside in the Imake configuration directory. Though only one file, Imakefile, is really important to simple uses of Imake, these descriptions are included as a sort of a road map to more in-depth understanding of how Imake works.
Imakefile is the file used to generate a Makefile. This file contains your project specific information.
Makefile is the final output file. There are many references to help you understand the contents of the Makefile. The easiest to obtain is probably the GNU Info pages on GNU make, which come with the GNU make distribution. See the References section for specifics on information available about make.
Imake.tmpl is a generic template used to fill in information not provided by the Imakefile. Most template files are created for large projects that need to customize Imake beyond what is possible through the Imakefile. (The FVWM source as an example.)
Imake.rules and other .rules files hold the C preprocessor “defines” that make up the bulk of the Imake rules. Things like SimpleProgramTarget() and DependTarget() are defined in this file.
.cf files contain system specific configuration information. Imake.cf is the file that “chooses” the proper platform-specific .cf file.
Once you've written a basic Imakefile, it's time to put Imake to work. Fortunately, the difficult part is over. Just type the following two commands:
xmkmf -a make
If you've written your Imakefile properly, your project will build with no problems.
xmkmf is a Bourne shell script that wraps up the imake command. It calls imake with convenient default arguments and does some cleanup, such as copying the old Makefile to Makefile.bak. This program generates the Makefile from your Imakefile.
imake is a program that you almost never need to run by hand. It's the program that handles the brunt of running the C preprocessor, as well as some minor tweaking (i.e., XCOMM comments) for compatibility with make.
Without any arguments, xmkmf just saves the old Makefile and runs imake to generate a new Makefile from the Imakefile. If you give xmkmf the -a switch, it runs the following commands in the given order:
make Makefile make includes make depend
See the xmkmf(1) man pages for more details on xmkmf capabilities.
Below is a list of the common make targets contained in an Imake generated Makefile. Precisely which targets end up in your Makefile depend on which rules you used in your Imakefile, of course. A given target always does the same thing regardless of the project. (That is, it will if the Imakefile has been written properly.) In fact, these targets are standard beyond Imake. For example, the GNU Coding Standards specify much the same targets.
all: Build the whole project. Usually the default target. This is the target to which the AllTarget() rule adds a dependency.
clean: Remove buildable files and backup files. Includes all object files, executables, backup Makefiles and files commonly used by editors as backup files (*~).
Makefiles: regenerates all the Makefiles using Imake.
depend: generates the dependences between source (*.c) and header (*.h) files and ensures that all object files that need recompiling are rebuilt.
tags: builds the tags database used by vi and Emacs. The tags database is simply a list of C-language symbols and their location in the source. It allows for quickly and automatically locating function definitions and other such objects. See the man pages for etags(1), ctags(1), vi(1) and the Info documentation that comes with Emacs.
install: installs the final binaries into the main file system. Defaults to a directory under /usr/X11R6/ depending on each type of component. For example, executables go in /bin; man pages go into the proper section directory under /man.
lint: This target isn't particularly useful on Linux systems, since there isn't a commonly used lint program. lint is a program that checks C code for common style problems and other non-syntactic mistakes. The GNU compiler has moved much of the functionality of lint into the compiler, compile your code with the -Wall flag set and you'll have most of lint's functionality. With a lint program installed on the system, you can use this make target to run lint on all the source files of the project.
In some cases, it is useful to write your own custom rules for generating Makefiles. The Imake system makes it easy to specify a different set of configuration files than the usual /usr/X11R6/lib/X11/config. This kind of customization is beyond the scope of this article, but O'Reilly publishes a great book on Imake that has several chapters devoted specifically to this topic. See Resources for more information on that book.
Otto is a developer at Red Hat Software who is currently very busy with the next release of Red Hat Linux. He can be contacted by e-mail at otto@redhat.com