Scenarios

A scenario is a named configuration of a project - for example, a debug build, a release build, or a platform-specific variant. GPR expresses scenarios through typed string variables whose values are supplied from outside the project file, allowing a single project to describe multiple build configurations without duplication.

External variables

An external variable is a string value passed to GPR tools via the -X command-line switch:

$ gprbuild -P my_proj.gpr -XBUILD=release

Inside the project file, the value is read with the external function:

Build : Build_Type := external ("BUILD", "debug");

The second argument is the default value used when the variable is not set on the command line.

External_As_List

When an external variable carries a list of values encoded as a single string, External_As_List splits it on a given separator and returns a string list:

for Source_Dirs use External_As_List ("EXTRA_DIRS", ":");

If the environment contains EXTRA_DIRS=/opt/include:/home/user/src, the attribute receives ("/opt/include", "/home/user/src"). This is useful for injecting additional source directories, switches, or other list-valued attributes from the build environment without modifying the project file.

Typed variables

To constrain the set of accepted values and enable case statements, declare a string type first:

type Build_Type is ("debug", "release", "release_checks");

Build : Build_Type := external ("BUILD", "debug");

If the value passed via -X is not in the declared type, a load error is reported. This catches typos early.

case statements

A case statement selects attribute values based on the current scenario variable:

project My_App is
   type Build_Type is ("debug", "release");
   Build : Build_Type := external ("BUILD", "debug");

   for Source_Dirs use ("src");
   for Object_Dir  use "obj/" & Build;
   for Exec_Dir    use "bin/" & Build;
   for Main        use ("main.adb");

   package Compiler is
      case Build is
         when "debug" =>
            for Switches ("Ada") use ("-g", "-gnatwa", "-gnatVa");
         when "release" =>
            for Switches ("Ada") use ("-O2", "-gnatn");
      end case;
   end Compiler;
end My_App;

A few things to note:

  • The case expression must be a typed scenario variable.

  • Every value of the type must be covered; use when others to provide a catch-all.

  • case statements can appear at the project level and inside packages.

  • Variable references (& Build) are allowed in attribute values, making it easy to keep debug and release objects in separate directories.

With this project, gprbuild -P my_proj.gpr builds a debug binary; gprbuild -P my_proj.gpr -XBUILD=release builds an optimized one.

Selecting platform-specific sources

When a case statement contains Ada naming exceptions (for Body or for Spec clauses in the Naming package), the source files named in the inactive branches are automatically removed from the project view. This makes it straightforward to keep platform-specific variants in the same source directory without triggering duplicate-unit errors:

type OS_Type is ("linux", "windows");
OS : OS_Type := external ("OS", "linux");

package Naming is
   case OS is
      when "linux"   => for Body ("Foo") use "foo_linux.adb";
      when "windows" => for Body ("Foo") use "foo_windows.adb";
   end case;
end Naming;

When building with -XOS=linux, foo_windows.adb is excluded from the project view even though it lives in the source directory.

The same result can be achieved for any language using Excluded_Source_Files in a case statement:

case OS is
   when "linux"   => for Excluded_Source_Files use ("foo_windows.c");
   when "windows" => for Excluded_Source_Files use ("foo_linux.c");
end case;

A third approach is to place target-specific sources in dedicated subdirectories. Declare the common directory first, then extend Source_Dirs in the case statement using the project’s own attribute value:

src/
├── common/
├── linux/
└── windows/
for Source_Dirs use ("src/common");

case OS is
   when "linux"   =>
      for Source_Dirs use My_Proj'Source_Dirs & ("src/linux");
   when "windows" =>
      for Source_Dirs use My_Proj'Source_Dirs & ("src/windows");
end case;

This layout scales well when many files differ between targets and avoids the need for per-file exclusions or naming exceptions.

Multiple scenario variables

A project can declare any number of scenario variables, each controlling an independent dimension of the configuration:

project My_App is
   type Build_Type is ("debug", "release");
   type OS_Type    is ("linux", "windows", "macos");

   Build : Build_Type := external ("BUILD", "debug");
   OS    : OS_Type    := external ("OS",    "linux");

   for Source_Dirs use ("src/common");
   for Object_Dir  use "obj/" & Build & "-" & OS;
   for Exec_Dir    use "bin/" & Build & "-" & OS;
   for Main        use ("main.adb");

   package Compiler is
      case Build is
         when "debug"   =>
            for Switches ("Ada") use ("-g", "-gnatwa");
         when "release" =>
            for Switches ("Ada") use ("-O2", "-gnatn");
      end case;
   end Compiler;

   case OS is
      when "windows" =>
         for Source_Dirs use My_App'Source_Dirs & ("src/windows");
      when others =>  --  linux, macos
         for Source_Dirs use My_App'Source_Dirs & ("src/posix");
   end case;
   ...
end My_App;

Tip

Keep the number of scenario variables small and their names consistent across projects in the same tree. Convention: BUILD for debug/release, TARGET for cross-compilation target, RUNTIME for Ada runtime variant. Consistent naming lets callers pass -X switches once at the root and have them apply everywhere.

Sharing scenario variables

When a project tree has multiple projects, all should use the same typed variable declaration for a given scenario. The common pattern is to declare the type and variable in an abstract project and import it everywhere:

--  config.gpr
abstract project Config is
   type Build_Type is ("debug", "release");
   Build : Build_Type := external ("BUILD", "debug");
end Config;
--  my_comp.gpr
with "config.gpr";
project My_Comp is
   package Compiler is
      case Config.Build is
         when "debug"   => for Switches ("Ada") use ("-g");
         when "release" => for Switches ("Ada") use ("-O2");
      end case;
   end Compiler;
   ...
end My_Comp;
--  my_app.gpr
with "config.gpr";
with "my_comp.gpr";
project My_App is
   package Compiler is
      case Config.Build is
         when "debug"   => for Switches ("Ada") use ("-g", "-gnatwa");
         when "release" => for Switches ("Ada") use ("-O2");
      end case;
   end Compiler;
   ...
end My_App;

A single -XBUILD=release on the command line then applies consistently to the whole tree.

Warning

External variable names are global to the project tree: two projects that both read the same variable name will always receive the same value. If their typed declarations differ - for example one accepts ("debug", "release") and another accepts ("debug", "release", "release_checks") - then passing a value valid for the wider type but not the narrower one causes a load error. To avoid this, share a single type declaration (as shown above) so that all projects in the tree agree on the set of accepted values.