Organizing Multi-Project Systems
Real-world software is rarely a single component. This chapter explains how to split a system into multiple project files, express dependencies between them, and control what is visible across project boundaries.
Why multiple projects?
A single project file works well for a self-contained component. As a system grows, splitting it into multiple projects brings several benefits:
Incremental builds: only the components that changed are recompiled.
Reuse: a library project can be imported by many applications without copying sources.
Encapsulation: each project controls which of its sources are visible to importers (see Libraries).
Separate build settings: each project can have its own compiler switches, object directory, and languages.
Importing a project with with
A project declares its dependencies at the top of the file using with
clauses, one per imported project:
with "libs/utils/utils.gpr";
with "libs/crypto/crypto.gpr";
project My_App is
for Source_Dirs use ("src");
for Object_Dir use "obj";
for Exec_Dir use "bin";
for Main use ("main.adb");
end My_App;
The path in a with clause is relative to the importing project file.
GPRbuild loads all projects in the tree, then builds each source in the
order required by the actual source dependency graph.
GPR also searches for project files in the directories listed in the
GPR_PROJECT_PATH environment variable, so installed libraries can be
imported by name without a path:
with "gnatcoll.gpr";
Accessing attributes and variables of a withed project
After importing a project, you can read its attributes and variables using
the Project.Attribute notation. This is commonly used to reuse
settings declared in a shared project:
with "common_settings.gpr";
project My_App is
-- Read a variable defined in Common_Settings
Build_Mode : Common_Settings.Build_Mode_Type :=
Common_Settings.Build_Mode;
-- Reuse the Compiler package verbatim
package Compiler renames Common_Settings.Compiler;
-- Append to the imported switches
package Linker is
for Switches ("Ada") use
Common_Settings.Linker'Switches ("Ada") & ("-lm");
end Linker;
end My_App;
The renames clause makes the package an alias of the original; any
change in Common_Settings.Compiler is automatically reflected.
Limited with
A plain with gives the importing project full visibility over the withed
project’s sources, attributes, variables, and packages. Circular plain
with dependencies are not allowed: if project A imports B and B imports
A, a load error is reported.
When two projects need their sources to be mutually visible - for example,
two components that are mutually recursive at the Ada unit level - a
limited with can be used:
limited with "component_b.gpr";
project Component_A is
for Source_Dirs use ("src_a");
...
end Component_A;
With a limited with, the sources of the withed project are visible for
compilation, but its attributes, variables, and packages are not accessible
from the importing project.
Use limited with sparingly. A need for it often indicates that the
components involved should be reorganized into a shared lower-level library
that both can depend on.
The project tree
The set of projects reachable from the root project through with and
limited with clauses forms the project tree. Loading the tree resolves
all source directories, attributes, and naming conventions across all
projects. The actual compilation order is then determined by the source
dependency graph, independently of the project import structure.
Typical layout
A common layout for a multi-project system:
myproject/
├── myproject.gpr <- root project (executable)
├── src/
├── obj/
├── libs/
│ ├── utils/
│ │ ├── utils.gpr <- library project
│ │ ├── src/
│ │ └── obj/
│ └── crypto/
│ ├── crypto.gpr <- library project
│ ├── src/
│ └── obj/
└── common_settings.gpr <- abstract project (shared settings)
Each library has its own project file, source directory, and object directory. The root project imports all libraries it needs.