Chapter 2. Compiling a Program

A "Hello world" (of sorts)

Most programming languages start with a short example that prints a "hello world" message. With Aap, this is also possible. In a file called main.aap, enter the following:

    :print Hello, World!

Now run Aap by entering aap at the command line. Aap will respond something like this:

    % aap
    Hello, World!
    Aap: No target on the command line and no $TARGET, build rules or "all" target in a recipe

As you can see, Aap outputs the desired text, but also prints an error message. This is because Aap is not a programming language, but a language for describing how to compile and build programs (written in other languages). In other words, if you have written a "hello world" program in some language, then you can use Aap to compile that program.

Using Aap to compile "hello.c"

Suppose you have written a "hello world" program in C, and the sources are stored in a file called hello.c. Aap already knows about the C language (and several others), so the instructions to Aap about how to compile this program are very short. Instructions for Aap are stored in a file with the extension .aap. Such a file is called a recipe.

This is the recipe for compiling such a program with Aap:

    :program hello : hello.c

Write this text in a file main.aap, in the same directory as hello.c. Now invoke Aap to compile hello.c into the program hello:

    % ls
    hello.c             main.aap
    % aap
1   Aap: Creating directory "/home/mool/tmp/build-FreeBSD4_5_RELEASE"
2   Aap: cc -I/usr/local/include -g -O2 -E -MM hello.c > build-FreeBSD4_5_RELEASE/hello.c.aap
3   Aap: cc -I/usr/local/include -g -O2 -c -o build-FreeBSD4_5_RELEASE/hello.o hello.c
4   Aap: cc -L/usr/local/lib -g -O2 -o hello build-FreeBSD4_5_RELEASE/hello.o

You see the commands Aap uses to compile the program:

  1. A directory is created to write the intermediate results in. This directory is different for each platform, thus you can compile the same program for different systems without cleaning up.

  2. Dependencies are figured out for the source file. Aap will automatically detect dependencies on included files and knows that if one of the included files changed compilation needs to be done, even when the file itself didn't change. In this example, Aap uses the C compiler with the -MM option to determine the included files.

  3. The "hello.c" program file is compiled into the "hello.o" object file (on MS-Windows that would be "hello.obj").

  4. The "hello.o" object file is linked to produce the "hello" program (on MS-Windows this would be "hello.exe", the ".exe" is added automatically).

Other things to do with "hello world"

The same simple recipe not only specifies how to build the "hello" program, it can also be used to install the program:

    % aap install PREFIX=try
    Aap: Creating directory "try/bin/"
    Aap: Copied "test/hello" to "try/bin/hello"
    Aap: /usr/bin/strip 'try/bin/hello'

The PREFIX variable specifies where to install the program. The default is /usr/local. For the example we use the try directory, which doesn't exist. Aap creates it for you.

Other ways that this recipe can be used:

    aap uninstall    undo installing the program
    aap clean          cleanup the generated files
    aap cleanALL     cleanup all files (careful!)

See the reference manual for details about :program.

Several Source Files

When you have several files with source code you can specify them as a list:

    :program myprogram : main.c
                         version.c
                         help.c

There are three source files: main.c, version.c and help.c. Notice that it is not necessary to use a line continuation character, as you would have to do in a Makefile. The list ends at a line where the indent is equal to or less than what the assignment started with. The amount of indent for the continuation lines is irrelevant, so long as it's more than the indent of the first line.

The Makefile-style line continuation with a backslash just before the line break can also be used, by the way.

Indents are very important, just like in a Python script. Make sure your tabstop is always set to the standard value of eight, otherwise you might run into trouble when mixing tabs and spaces!

When you give a list of files to :program, Aap will determine dependencies and compile each of the source files in turn, and then link them all together into an executable.

Variables and Assignments

Sometimes it is convenient to have an abbreviation for a long list of files. Aap supports this through variables (just like the make command and the shell).

An assignment has the form:

    variablename = expression

The variable name is the usual combination of letters, digits and underscore. It must start with a letter. Upper and lower case letters can be used and case matters. To see this in action, write this recipe in a file with the name try.aap:

    foo = one
    Foo = two
    FOO = three
    :print $foo $Foo $FOO

Aap normally reads the recipe from main.aap, but you can tell it to read a different file if you want to. Use the -f flag for this. Now execute the recipe:

    % aap -f try.aap
    one two three
    Aap: No target on the command line and no build rules or "all" target in a recipe

The :print command prints its argument. You can see that a variable name preceded with a dollar is replaced by the value of the variable. The three variables that only differ by case each have a different value. Aap also complains that there is nothing to build, just like in the hello world example.

If you want text directly after the variable's value, for example, to append an extension to the value of a variable, the text may be impossible to distinguish from a variable name. In these cases you must put parenthesis around the variable name, so that Aap knows where it ends:

    all:
      MakeName = Make
      :print $(MakeName)file    # 'f' can be in a variable name
      :print $(MakeName).txt    # '.' can be in a variable name
      :print $MakeName-more     # '-' is not in a variable name

    % aap -f try.aap
    Makefile
    Make.txt
    Make-more
    %

All Aap commands, except the assignment, start with a colon. That makes them easy to recognize.

Some characters in the expression have a special meaning. The :print command also handles a few arguments in a special way. To avoid the special meaning use the $(x) form, where "x" is the special character. For example, to print a literal dollar use $($). See the user manual for a complete list.

Comments

Someone who sees this recipe would like to know what it's for. This requires adding comments. These start with a "#" character and extend until the end of the line (like in a Makefile and Python script).

It is also possible to associate a comment with a specific item:

    # A-A-P recipe for compiling "myprogram"
    :program myprogram { comment = MyProgram is really great } :
            main.c            # startup stuff
            version.c         # just the date stamp
            help.c            # display a help message

Now run Aap with a "comment" argument:

    % aap comment
    target "myprogram": MyProgram is really great
    target "clean": delete generated files that are not distributed
    target "cleanmore": delete all generated files
    target "cleanALL": delete all generated files, AAPDIR and build-* directories
    target "install": install files
    target "uninstall": delete installed files
    %

The text inside curly braces is called an attribute. In this case the attribute name is "comment" and the attribute value is "MyProgram is really great". An attribute can be used to attach extra information to a file name. We will encounter more attributes later on.

Dependencies

Let's go back to the "Hello world" example and find out what happens when you change a source file. Use this hello.c file:

    #include <stdio.h>
    #include "hello.h"
    main()
    {
        printf("Hello %s\n", world);
    }

The included "hello.h" file defines "world":

    #define world "World!"

If you run Aap, the "hello" program will be built as before. If you run Aap again you will notice that nothing happens. Aap remembers that "hello.c" was already compiled. Now try this:

    % touch hello.c
    % aap
    %

If you have been using the "make" program you would expect something to happen. But Aap checks the contents of the file, not the timestamp. A signature of "hello.c" is computed and if it is still the same as before Aap knows that it does not need to be compiled, even though "hello.c" is newer than the "hello" program.

Aap uses the mechanism of dependencies. When you use the :program command Aap knows that the target depends on the sources. When one of the sources changes, the commands to build the target from the sources must be executed. This can also be specified explicitly:

      hello$EXESUF : $BDIR/hello$OBJSUF
          :do build $source

      $BDIR/hello$OBJSUF : hello.c
          :do compile $source

The generic form of a dependency is:

      target : list-of-sources
           build-commands

The colon after the target is important, it separates the target from the sources. It is not required to put a space before it, but there must be a space after it. We mostly put white space before the colon, so that it is easy to spot. There could be several targets, but that is unusual.

There are two dependencies in the example. In the first one the target is "hello$EXESUF", the source file is "$BDIR/hello$OBJSUF" and the build command is ":do build $source". This specifies how to build the "hello$EXESUF" program from the "$BDIR/hello$OBJSUF" object file. The second dependency specifies how to compile "hello.c" into "$BDIR/hello$OBJSUF" with the command ":do compile $source". The "BDIR" variable holds the name of the platform-dependent directory for intermediate results, as mentioned in the first example of this chapter. In case you need it, the $EXESUF variable Aap is empty on Unix and ".exe" on MS-Windows.

The relation between the two dependencies in the example is that the source of the first one is the target in the second one. The logic is that Aap follows the dependencies and executes the associated build commands. In this case "hello$EXESUF" depends on "$BDIR/hello$OBJSUF", which then depends on "hello.c". The last dependency is handled first, thus first hello.c is compiled by the build command of the second dependency, and then linked into "hello$EXESUF" by the build command of the first dependency.

Now change the "hello.h" file by replacing "World" with 'Universe":

    #define world "Universe!"

If you now run Aap with "aap hello" or "aap hello.exe" the "hello" program will be built. But you never mentioned the "hello.h" file in the recipe. How did Aap find out the change in this file matters? When Aap is run to update the "hello" program, this is what will happen:

  1. The first dependency with "hello$EXESUF" as the target is found, it depends on "$BDIR/hello$OBJSUF".

  2. The second dependency with "$BDIR/hello$OBJSUF" as the target is found. The source file "hello.c" is recognized as a C program file. It is inspected for included files. This finds the "hello.h" file. "stdio.h" is ignored, since it is a system file. "hello.h" is added to the list of files that the target depends on.

  3. Each file that the target depends on is updated. In this case "hello.c" and "hello.h". No dependency has been specified for them and the files exist, thus nothing happens.

  4. Aap computes signatures for "hello.c" and "hello.h". It also computes a signature for the build commands. If one of them changed since the last time the target was built, or the target was never built before, the target is considered "outdated" and the build commands are executed.

  5. The second dependency is now finished, "$BDIR/hello$OBJSUF" is up-to-date. Aap goes back to the first dependency.

  6. Aap computes a signature for "$BDIR/hello$OBJSUF". Note that this happens after the second dependency was handled, it may have changed the file. It also computes a signature for the build command. If one of them changed since the last time the target was built, or the target was never built before, the target is considered "outdated" and the build commands are executed.

Now try this: Append a comment to one of the lines in the "hello.c" file. This means the file is changed, thus when invoking Aap it will compile "hello.c". But the program is not built, because the produced intermediate file "$BDIR/hello$OBJSUF" is still equal to what it was the last time. When compiling a large program with many dependencies this mechanism avoids that adding a comment may cause a snowball effect. (Note: some compilers include line numbers or a timestamp in the object file, in that case building the program will happen anyway).

Compiling Multiple Programs

Suppose you have a number of sources files that are used to build two programs. You need to specify which files are used for which program. Here is an example:

1.    Common = help.c util.c
2.
3.    all : foo bar
4.
5.    :program foo : $Common foo.c
6.
7.    :program bar : $Common bar.c

This recipe defines three targets: "all", "foo" and "bar". "foo" and "bar are programs that Aap can build from source files. But the "all" target is not a file. This is called a virtual target: A name used for a target that does not exist as a file. Let's list the terminology of the items in a dependency:

Table 2.1. items in a dependency

sourceitem on the right hand side of a dependency
source filesource that is a file
virtual sourcesource that is NOT a file
targeton the left hand side of a dependency
target filetarget that is a file
virtual targettarget that is NOT a file
nodesource or target
file nodesource or target that is a file
virtual nodesource or target that is NOT a file

Aap knows the target with the name "all" is always used as a virtual target. There are a few other names which Aap knows are virtual, see Table 37.1, “Virtual Targets”. For other targets you need to specify it with the "{virtual}" attribute.

The first dependency has no build commands. This only specifies that "all" depends on "foo" and "bar". Thus when Aap updates the "all" target, this dependency specifies that "foo" and "bar" need to be updated. Since the "all" target is the default target, this dependency causes both "foo" and "bar" to be updated when Aap is started without an argument. You can use "aap foo" to build "foo" only. The dependencies for "all" and "bar" will not be used then.

The two files help.c and util.c are used by both the "foo" and the "bar" program. To avoid having to type the file names twice, the "Common" variable is used.

Kinds of things you can build

Not everything you want to build is a program. Your recipe might need too build a library or a libtool archive. In these cases, :lib, :dll or :ltlib provide the same level of automation as :program does for programs. The :produce command is more generic, you can use this to build various kinds of things.

If all else fails, you can use Aap like the make program and explicitly list the commands you need to build your project.