GPRbuild

This chapter presents a brief overview of GPRbuild, the project manager of the GNAT toolchain. It can be used to manage complex builds. In terms of functionality, it's similar to make and cmake, just to name two examples.

For a detailed presentation of the tool, please refer to the GPRbuild User’s Guide.

Basic commands

As mentioned in the previous chapter, you can build a project using gprbuild from the command line:

gprbuild -P project.gpr

In order to clean the project, you can use gprclean:

gprclean -P project.gpr

Project files

You can create project files using GNAT Studio, which presents many options on its graphical interface. However, you can also edit project files manually as a normal text file in an editor, since its syntax is human readable. In fact, project files use a syntax similar to the one from the Ada language. Let's look at the basic structure of project files and how to customize them.

Basic structure

The main element of a project file is a project declaration, which contains definitions for the current project. A project file may also include other project files in order to compose a complex build. One of the simplest form of a project file is the following:

project Default is

   for Main use ("main");
   for Source_Dirs use ("src");

end Default;

In this example, we declare a project named Default. The for Main use expression indicates that the main.adb file is used as the entry point (main source-code file) of the project. The main file doesn't necessary need to be called main.adb; we could use any source-code implementing a main application, or even have a list of multiple main files. The for Source_Dirs use expression indicates that the src directory contains the source-file for the application (including the main file).

Customization

GPRbuild support scenario variables, which allow you to control the way binaries are built. For example, you may want to distinguish between debug and optimized versions of your binary. In principle, you could pass command-line options to gprbuild that turn debugging on and off, for example. However, defining this information in the project file is usually easier to handle and to maintain. Let's define a scenario variable called ver in our project:

project Default is

   Ver := external ("ver", "debug");

   for Main use ("main");
   for Source_Dirs use ("src");

end Default;

In this example, we're specifying that the scenario variable Ver is initialized with the external variable ver. Its default value is set to debug.

We can now set this variable in the call to gprbuild:

gprbuild -P project.gpr -Xver=debug

Alternatively, we can simply specify an environment variable. For example, on Unix systems, we can say:

export ver=debug

# Value from environment variable "ver" used in the following call:

gprbuild -P project.gpr

In the project file, we can use the scenario variable to customize the build:

project Default is
   Ver := external ("ver", "debug");

   for Main use ("main.adb");
   for Source_Dirs use ("src");

   --  Using "ver" variable for obj directory
   for Object_Dir use "obj/" & Ver;

   package Compiler is
      case Ver is
         when "debug" =>
            for Switches ("Ada") use ("-g");
         when "opt" =>
            for Switches ("Ada") use ("-O2");
         when others =>
            null;
      end case;
   end Compiler;

end Default;

We're now using Ver in the for Object_Dir clause to specify a subdirectory of the obj directory that contains the object files. Also, we're using Ver to select compiler options in the Compiler package declaration.

We could also specify all available options in the project file by creating a typed variable. For example:

project Default is

   type Ver_Option is ("debug", "opt");
   Ver : Ver_Option := external ("ver", "debug");

   for Source_Dirs use ("src");
   for Main use ("main.adb");

   --  Using "ver" variable for obj directory
   for Object_Dir use "obj/" & Ver;

   package Compiler is
      case Ver is
         when "debug" =>
            for Switches ("Ada") use ("-g");
         when "opt" =>
            for Switches ("Ada") use ("-O2");
         when others =>
            null;
      end case;
   end Compiler;

end Default;

The advantage of this approach is that gprbuild can now check whether the value that you provide for the ver variable is available on the list of possible values and give you an error if you're entering a wrong value.

Project dependencies

GPRbuild supports project dependencies. This allows you to reuse information from existing projects. Specifically, the keyword with allows you to include another project within the current project.

Simple dependency

Let's look at a very simple example. We have a package called Test_Pkg associated with the project file test_pkg.gpr, which contains:

project Test_Pkg is
    for Source_Dirs use ("src");
    for Object_Dir use "obj";
end Test_Pkg;

This is the code for the Test_Pkg package:

package Test_Pkg is

   type T is record
      X : Integer;
      Y : Integer;
   end record;

   function Init return T;

end Test_Pkg;
package body Test_Pkg is

   function Init return T is
   begin
      return V : T do
         V.X := 0;
         V.Y := 0;
      end return;
   end Init;

end Test_Pkg;

For this example, we use a directory test_pkg containing the project file and a subdirectory test_pkg/src containing the source files. The directory structure looks like this:

|- test_pkg
|    | test_pkg.gpr
|    |- src
|    |    | test_pkg.adb
|    |    | test_pkg.ads

Suppose we want to use the Test_Pkg package in a new application. Instead of directly including the source files of Test_Pkg in the project file of our application (either directly or indirectly), we can instead reference the existing project file for the package by using with "test_pkg.gpr". This is the resulting project file:

with "../test_pkg/test_pkg.gpr";

project Default is
    for Source_Dirs use ("src");
    for Object_Dir use "obj";
    for Main use ("main.adb");
end Default;

And this is the code for the main application:

with Test_Pkg; use Test_Pkg;

procedure Main is
   A : T;
begin
   A := Init;
end Main;

When we build the main project file (default.gpr), we're automatically building all dependent projects. More specifically, the project file for the main application automatically includes the information from the dependent projects such as test_pkg.gpr. Using a with in the main project file is all we have to do for that to happen.

Dependencies to dynamic libraries

We can structure project files to make use of dynamic (shared) libraries using a very similar approach. It's straightforward to convert the project above so that Test_Pkg is now compiled into a dynamic library and linked to our main application. All we need to do is to make a few additions to the project file for the Test_Pkg package:

library project Test_Pkg is
    for Source_Dirs use ("src");
    for Object_Dir use "obj";
    for Library_Name use "test_pkg";
    for Library_Dir use "lib";
    for Library_Kind use "Dynamic";
end Test_Pkg;

This is what we had to do:

  • We changed the project to library project.

  • We added the specification for Library_Name, Library_Dir and Library_Kind.

We don't need to change the project file for the main application because GPRbuild automatically detects the dependency information (e.g., the path to the dynamic library) from the project file for the Test_Pkg package. With these small changes, we're able to compile the Test_Pkg package to a dynamic library and link it with our main application.

Configuration pragma files

Configuration pragma files contain a set of pragmas that modify the compilation of source files according to external requirements. For example, you may use pragmas to either relax or strengthen requirements depending on your environment.

In GPRbuild, we can use Local_Configuration_Pragmas (in the Compiler package) to indicate the configuration pragmas file we want GPRbuild to use with the source files in our project.

The file gnat.adc shown here is an example of a configuration pragma file:

pragma Suppress (Overflow_Check);

We can use this in our project by declaring a Compiler package. Here's the complete project file:

project Default is

   for Source_Dirs use ("src");
   for Object_Dir use "obj";
   for Main use ("main.adb");

   package Compiler is
      for Local_Configuration_Pragmas use "gnat.adc";
   end Compiler;

end Default;

Each pragma contained in gnat.adc is used in the compilation of each file, as if that pragma was placed at the beginning of each file.

Configuration packages

You can control the compilation of your source code by creating variants for various cases and selecting the appropriate variant in the compilation package in the project file. One example where this is useful is conditional compilation using Boolean constants, shown in the code below:

with Ada.Text_IO; use Ada.Text_IO;

with Config;

procedure Main is
begin

   if Config.Debug then
      Put_Line ("Debug version");
   else
      Put_Line ("Release version");
   end if;
end Main;

In this example, we declared the Boolean constant in the Config package. By having multiple versions of that package, we can create different behavior for each usage. For this simple example, there are only two possible cases: either Debug is True or False. However, we can apply this strategy to create more complex cases.

In our next example, we store the packages in the subdirectories debug and release of the source code directory. Here's the content of the src/debug/config.ads file:

package Config is

   Debug : constant Boolean := True;

end Config;

Here's the src/release/config.ads file:

package Config is

   Debug : constant Boolean := False;

end Config;

In this case, GPRbuild selects the appropriate directory to look for the config.ads file according to information we provide for the compilation process. We do this by using a scenario type called Mode_Type in our project file:

gprbuild -P default.gpr -Xmode=release
project Default is

   type Mode_Type is ("debug", "release");

   Mode : Mode_Type := external ("mode", "debug");

   for Source_Dirs use ("src", "src/" & Mode);
   for Object_Dir use "obj";
   for Main use ("main.adb");

end Default;

We declare the scenario variable Mode and use it in the Source_Dirs declaration to add the desired path to the subdirectory containing the config.ads file. The expression "src/" & Mode concatenates the user-specified mode to select the appropriate subdirectory. For more complex cases, we could use either a tree of subdirectories or multiple scenario variables for each aspect that we need to configure.