- 1. Overview
- 2. Project Structure
- 3. Step 1: Create the Aggregator
- 4. Step 2: Create Library Modules
- 5. Step 3: Create Application Modules
- 6. Building Multi-Module Projects
- 7. Dependency Resolution
- 8. Project Paths
- 9. Testing Multi-Module Projects
- 10. Packaging Multi-Module Projects
- 11. CLI Reference
- 12. Nested Aggregators
- 13. Advanced Topics
- 14. Troubleshooting
- 15. Complete Example
This tutorial demonstrates how to create and build multi-module Free Pascal projects using PasBuild.
PasBuild supports Maven-style multi-module projects with:
-
Aggregator projects (packaging=pom) that coordinate multiple modules
-
Library modules that provide reusable components
-
Application modules that depend on libraries
-
Version inheritance from aggregator to child modules
-
Automatic dependency resolution between modules
-
Topological build ordering with cycle detection
A typical multi-module project looks like:
my-framework/ # Aggregator (packaging=pom)
├── project.xml # Aggregator config
├── core/ # Library module
│ ├── project.xml
│ └── src/main/pascal/
│ ├── Core.Utils.pas
│ └── Core.Logger.pas
├── ui/ # Library module (depends on core)
│ ├── project.xml
│ └── src/main/pascal/
│ ├── UI.Widgets.pas
│ └── UI.Forms.pas
└── demo/ # Application module (depends on ui)
├── project.xml
└── src/main/pascal/
└── Demo.Main.pasCreate the root project.xml with packaging type "pom":
<?xml version="1.0" encoding="UTF-8"?>
<project>
<name>my-framework</name>
<version>1.0.0</version>
<author>Your Name</author>
<license>BSD-3-Clause</license>
<build>
<!-- packaging=pom means this is an aggregator -->
<packaging>pom</packaging>
</build>
<!-- List child modules -->
<modules>
<module>core</module>
<module>ui</module>
<module>demo</module>
</modules>
</project>Key points:
-
packaging=pommarks this as an aggregator -
Aggregators cannot have
mainSource(they don’t compile to anything) -
The
<modules>list contains paths to child module directories -
The
<version>is defined here and inherited by all child modules
Create each library module with packaging=library:
<?xml version="1.0" encoding="UTF-8"?>
<project>
<name>core</name>
<!-- version inherited from aggregator -->
<author>Your Name</author>
<license>BSD-3-Clause</license>
<build>
<packaging>library</packaging>
<!-- Library has optional mainSource -->
</build>
</project><?xml version="1.0" encoding="UTF-8"?>
<project>
<name>ui</name>
<!-- version inherited from aggregator -->
<author>Your Name</author>
<license>BSD-3-Clause</license>
<build>
<packaging>library</packaging>
</build>
<!-- UI depends on core library -->
<moduleDependencies>
<module>../core</module>
</moduleDependencies>
</project>Key points:
-
Libraries use
packaging=library -
<version>is omitted — it is inherited from the aggregator -
Specify dependencies in
<moduleDependencies>section (relative paths) -
Libraries produce compiled units in
target/units/
Create application modules with packaging=application (default):
<?xml version="1.0" encoding="UTF-8"?>
<project>
<name>demo</name>
<!-- version inherited from aggregator -->
<author>Your Name</author>
<license>BSD-3-Clause</license>
<build>
<mainSource>Demo.Main.pas</mainSource>
<executableName>demo</executableName>
</build>
<!-- Demo depends on UI (which transitively depends on core) -->
<moduleDependencies>
<module>../ui</module>
</moduleDependencies>
</project>Key points:
-
Applications use
packaging=application(default) -
Applications require
mainSource -
<version>is inherited from the aggregator, same as libraries -
Dependencies are automatically resolved (UI → core)
Build the entire project (all modules in dependency order):
cd my-framework
pasbuild compileOutput shows build progress:
[INFO] Building 3 modules in dependency order
[INFO] Building module 1/3: core
[INFO] Building module 2/3: ui
[INFO] Building module 3/3: demo
[INFO] Reactor build complete: 3/3 modules builtBefore building, use dependency-tree to verify module relationships without
invoking the compiler:
pasbuild dependency-tree # Show all modules and their dependencies
pasbuild dependency-tree -m demo # Show only demo's dependenciesBuild only a specific module (and its dependencies):
pasbuild compile -m demoThis builds core and ui first, then demo.
PasBuild automatically resolves and validates dependencies:
demo (application)
└── ui (library)
└── core (library)Build order: core → ui → demo ✓
Libraries automatically produce compiled units in target/units/:
-
core/target/units/contains core unit files -
UI’s compiler gets
-Fucore/target/unitsautomatically -
Demo’s compiler gets
-Fuui/target/unitsand-Fucore/target/units
If you create a cycle, PasBuild detects it:
Fatal: Cyclic dependency detected: aBuild is aborted before any compilation.
Depending on an aggregator (pom) is forbidden:
Error: Module "app" cannot depend on aggregator "aggregator" (packaging=pom)Only libraries and applications can be compiled and depended upon.
In addition to <moduleDependencies> (between modules in the same aggregator), modules can also declare <dependencies> on projects installed to the local repository:
<dependencies>
<dependency>
<name>some-external-lib</name>
<version>1.0.0</version>
</dependency>
</dependencies>External dependencies are resolved from ~/.pasbuild/repository/ at compile time. Use pasbuild install in the dependency project to make it available. See Dependency Management Design for details.
Paths in <modules> are relative to the module’s directory:
<!-- In my-framework/project.xml -->
<modules>
<module>core</module> <!-- Relative to my-framework -->
<module>ui</module>
</modules>
<!-- In my-framework/ui/project.xml -->
<modules>
<module>../core</module> <!-- Relative to ui -->
</modules>Paths are validated to prevent escaping the project tree (security).
Run tests across all modules:
pasbuild testThis runs: clean → compile → process-test-resources → test-compile → test
Each module’s tests are compiled and run in dependency order.
Test a specific module:
pasbuild test -m coreCreate a release archive:
pasbuild packageCreates my-framework-1.0.0.zip containing:
- All compiled executables from application modules
- All compiled units from library modules
- Source files
- Documentation
pasbuild <goal> -m <module-name>
pasbuild <goal> --module <module-name>Examples:
pasbuild compile -m core # Build only core
pasbuild test -m demo # Test demo module
pasbuild package -m ui # Package UI libraryDisplay the full dependency graph without compiling anything:
pasbuild dependency-treeOutput lists every module in topological order (dependencies first), with each dependency labelled by kind:
[INFO] ------------------------------------------------------------------------
[INFO] Dependency Tree
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] core 1.0.0 [library]
[INFO] (no dependencies)
[INFO]
[INFO] ui 1.0.0 [library]
[INFO] └─ core [module]
[INFO]
[INFO] demo 1.0.0 [application]
[INFO] ├─ ui [module]
[INFO] └─ some-ext:2.0.0 [external]Restrict the output to a single named module with -m:
pasbuild dependency-tree -m demo[INFO] ------------------------------------------------------------------------
[INFO] Dependency Tree for module: demo
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] demo 1.0.0 [application]
[INFO] ├─ ui [module]
[INFO] └─ some-ext:2.0.0 [external]dependency-tree is particularly useful for large multi-module projects where
running compile -v buries the dependency graph under screens of compiler
output. The goal exits immediately after printing the tree — no compilation
occurs.
The -v (verbose) flag still shows a compact dependency summary at the start
of a compile run, but the output is interleaved with FPC compiler lines:
pasbuild compile -vUse dependency-tree when you want to inspect dependencies in isolation;
use -v when you want extra detail alongside the full build output.
pasbuild compile -v -m demo # Verbose output for demo build
pasbuild compile -p debug -m core # Debug profile for core
pasbuild compile -v -p release # Verbose with release profile
pasbuild test -m ui -f custom.xml # Use custom project fileExamples combining multiple flags:
# View dependency graph before building specific module
pasbuild compile -v -m demo
# Build with debug profile and verbose output
pasbuild compile -p debug -v
# Run tests with verbose graph and custom project file
pasbuild test -v -f myproject.xmlFor projects with many modules, you can group related modules under a nested aggregator. This is common for organising examples, plugins, or other logical groups.
my-framework/ # Root aggregator
├── project.xml # packaging=pom, modules: core, examples
├── core/
│ ├── project.xml # packaging=library
│ └── src/main/pascal/
└── examples/ # Nested aggregator
├── project.xml # packaging=pom, modules: demo1, demo2
├── demo1/
│ ├── project.xml # packaging=application
│ └── src/main/pascal/
└── demo2/
├── project.xml # packaging=application
└── src/main/pascal/The root project.xml lists examples as a module alongside core:
<?xml version="1.0" encoding="UTF-8"?>
<project>
<name>my-framework</name>
<version>2.0.0</version>
<build>
<packaging>pom</packaging>
</build>
<modules>
<module>core</module>
<module>examples</module>
</modules>
</project>The examples/project.xml is itself a POM aggregator with its own modules:
<?xml version="1.0" encoding="UTF-8"?>
<project>
<name>examples</name>
<!-- version inherited from root aggregator -->
<build>
<packaging>pom</packaging>
</build>
<modules>
<module>demo1</module>
<module>demo2</module>
</modules>
</project>Leaf modules under a nested aggregator can depend on modules at any level using relative paths:
<?xml version="1.0" encoding="UTF-8"?>
<project>
<name>demo1</name>
<!-- version inherited through: root -> examples -> demo1 -->
<build>
<mainSource>Demo1.pas</mainSource>
<executableName>demo1</executableName>
</build>
<moduleDependencies>
<module>../../core</module>
</moduleDependencies>
</project>Example projects typically do not need the full src/main/pascal/ directory layout. Use <sourceDirectory> to place source files directly in the project root:
<?xml version="1.0" encoding="UTF-8"?>
<project>
<name>demo1</name>
<build>
<mainSource>Demo1.pas</mainSource>
<executableName>demo1</executableName>
<sourceDirectory>.</sourceDirectory> <!-- Source files in project root -->
</build>
<moduleDependencies>
<module>../../core</module>
</moduleDependencies>
</project>With this configuration, the directory structure becomes:
examples/
├── project.xml # Nested aggregator
├── demo1/
│ ├── project.xml
│ └── Demo1.pas # Source directly in project root
└── demo2/
├── project.xml
└── Demo2.pasThis avoids the overhead of creating src/main/pascal/ for each small example.
Valid <sourceDirectory> values include . (project root), pascal (a pascal/ subdirectory), or any relative path. When omitted, the default src/main/pascal is used.
-
Recursive discovery: PasBuild walks the
<modules>tree recursively. When it encounters a POM-type module with its own<modules>, it descends into those children. The result is a single flat module list. -
Version inheritance chains: Version flows through the full aggregator hierarchy. In the example above,
demo1inherits version2.0.0fromexamples, which inherited it from the root. -
Cycle detection: If a nested aggregator references a parent aggregator (directly or indirectly), PasBuild detects the cycle and raises an error.
-
Arbitrary depth: There is no limit to nesting depth. You can nest aggregators three or more levels deep if your project requires it.
-
Build order: The topological sort operates on the flat module list. Dependencies determine order, not nesting level. In this example,
corebuilds first, thendemo1anddemo2. -
POM modules are skipped: Nested aggregator modules appear in the reactor summary but are not compiled (they have no source code).
Module names can include:
-
Lowercase letters:
core,ui -
Numbers:
lib1,module2 -
Hyphens:
core-utils,ui-widgets -
Camel case:
CoreUtils,UIWidgets
Choose a consistent naming convention for your project.
Apply profiles to multi-module builds:
pasbuild compile -p debug # All modules use debug profile
pasbuild compile -p release -m demo # Demo uses release profileEach module’s build inherits the active profiles.
Fatal: Module "unknown" referenced by "demo" not foundCheck the module name in <modules> matches the module’s <name> in its project.xml.
Fatal: Module path outside project tree: ../../otherModule paths must stay within the aggregator’s root directory.
Fatal: Cyclic dependency detected: aCheck that dependencies don’t create cycles. Graph your modules and verify no cycles exist.
Fatal: Circular aggregator reference detected: .. (from /path/to/child-agg)A nested aggregator’s <modules> list points back to a parent aggregator. Ensure that nested aggregator module paths only point downwards into child directories.