Gem #65: gprbuild

Let's get started…


GNAT allows for a great deal of flexibility in compiling applications. The traditional method is to use gnatmake on the command line, specifying the list of source directories by using "-I" switches, and letting gnatmake decide which files need to be recompiled. One drawback of this approach is that it only works for Ada files. If your application uses sources written in other languages (which is probably the case for the majority of serious applications), then you need some kind of wrapper around gnatmake. This is typically done via a Makefile: first you recompile C, C++, and other source files using standard Makefile techniques, then invoke gnatmake to handle the Ada sources, and finally (although this can also be done as part of gnatmake) bind and link your application.

Gnatmake is what we call a builder. Its role is to examine all source files in your application and decide which ones need to be recompiled. It then relies on other tools to do the actual compilation (gcc), bind (gnatbind) and link (gnatlink).

A few releases ago, a new tool called gprbuild was introduced in the GNAT technology. It is also a builder, reusing and extending gnatmake to support multilanguage applications. This means it knows how to handle C, C++, and many other languages, and is able to determine which source files for each language need to be recompiled. One benefit is that you no longer need to depend on a Makefile to compile those source files prior to binding and linking.

Gprbuild has been carefully designed so that no hard coding was done for any of the languages. Instead, all the information resides in a configuration file (typically having the extension ".cgpr"): how to compile a file, how to decide whether it is up to date, what files get generated as a result of the compilation, and so on.

These configuration files have a syntax similar to that of the project files themselves. They can be hand-edited as required, and maintained in your version control system as for any other component of the environment.

However, developing a configuration file is not always easy, especially when you are using different compiler chains (such as GNAT for Ada files and Diab for C). In particular, the link phase becomes harder to describe. Add to that support for multiple platforms, cross-compilation, libraries, and other subtleties, and the complexity increases quickly.

AdaCore has already taken care of a lot of possible compilers and combinations, so that you do not have to redo that complex setup yourself. We have created a knowledge base that describes the configuration for various common scenarios.

GNAT comes with an additional command-line tool, called gprconfig, which is generally spawned transparently by gprbuild, so you don't have to invoke it directly in most cases. The role of gprconfig is to create the configuration file based on your specific set of compilers, getting information from the knowledge base.

Now that we've described the general organization of things, let's show a concrete example.

Let's assume your application is made up of two files, bar.adb and foo.c, with the following code (not very interesting, admittedly, since it will do exactly nothing):

procedure Bar is
   procedure Foo;
   pragma Import (C, Foo);
begin
   Foo;
end Bar;
int foo () {}

gprbuild only works with project files (there are no command-line options to specify source directories, for instance). So you need to write one. The basic elements of information you need to specify in a project file are: languages (C and Ada in our case), source directories and source files (implicit in our case), and main source files (bar.adb in our case). So the project file default.gpr looks like:

project Default is
   for Languages use ("C", "Ada");
   for Main use ("bar.adb");
end Default;

At this point everything is set up, and we can just spawn gprbuild with the command:

gprbuild -Pdefault

Since we did not specify a configuration file through a --config switch, gprbuild will automatically generate one by spawning gprconfig and telling it we want to find the first available Ada and C compilers on the PATH. Gprconfig will then create a configuration file called auto.cgpr, which gprbuild in turn uses to build our executable.

If you relaunch the same command again immediately, no recompilation takes place, not even for the C file.

Notice also how gprconfig selects the first matching compiler on the PATH for each language. If you want to force the use of specific compilers (that might not even be on your PATH), you will need to spawn gprconfig manually yourself and pass it a --config switch. This creates a configuration file which you can then pass along on your invocation of gprbuild.