Building Projects With Imake

Here's an explanation of how Imake works and how you can use it to build your executables—an article for programmers with C and Unix programming skills.
Comment Warnings

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.

The (Common) Rules

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.

Rule 1. SimpleProgramTarget()

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:


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.

Rule 2. ComplexProgramTarget()

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

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

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.

Rule 3. NormalProgramTarget()

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:


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.