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
caseexpression must be a typed scenario variable.Every value of the type must be covered; use
when othersto provide a catch-all.casestatements 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.