Skip to content

Latest commit

 

History

History
678 lines (506 loc) · 16.6 KB

File metadata and controls

678 lines (506 loc) · 16.6 KB

PasBuild Multi-Module Tutorial

This tutorial demonstrates how to create and build multi-module Free Pascal projects using PasBuild.

1. Overview

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

2. Project Structure

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.pas

3. Step 1: Create the Aggregator

Create 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=pom marks 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

4. Step 2: Create Library Modules

Create each library module with packaging=library:

4.1. core/project.xml

<?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>

4.2. ui/project.xml

<?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/

5. Step 3: Create Application Modules

Create application modules with packaging=application (default):

5.1. demo/project.xml

<?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)

6. Building Multi-Module Projects

6.1. Build All Modules

Build the entire project (all modules in dependency order):

cd my-framework
pasbuild compile

Output 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 built

6.2. Inspect Dependencies Before Building

Before 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 dependencies

6.3. Build Specific Module

Build only a specific module (and its dependencies):

pasbuild compile -m demo

This builds core and ui first, then demo.

6.4. Skip Aggregator

Aggregators are automatically skipped - they don’t compile to anything:

pasbuild compile

Only core, ui, and demo are built (not the aggregator itself).

7. Dependency Resolution

PasBuild automatically resolves and validates dependencies:

7.1. Valid Dependency Chain

demo (application)
  └── ui (library)
      └── core (library)

Build order: core → ui → demo ✓

7.2. Dependency on Library

Libraries automatically produce compiled units in target/units/:

  • core/target/units/ contains core unit files

  • UI’s compiler gets -Fucore/target/units automatically

  • Demo’s compiler gets -Fuui/target/units and -Fucore/target/units

7.3. Error: Circular Dependency

If you create a cycle, PasBuild detects it:

Fatal: Cyclic dependency detected: a

Build is aborted before any compilation.

7.4. Error: Invalid Dependency

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.

7.5. External Dependencies

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.

8. Project Paths

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).

9. Testing Multi-Module Projects

Run tests across all modules:

pasbuild test

This 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 core

10. Packaging Multi-Module Projects

Create a release archive:

pasbuild package

Creates my-framework-1.0.0.zip containing: - All compiled executables from application modules - All compiled units from library modules - Source files - Documentation

11. CLI Reference

11.1. Module Selection

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 library

11.2. Dependency Tree

Display the full dependency graph without compiling anything:

pasbuild dependency-tree

Output 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.

11.3. Verbose Dependency Graph During Build

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 -v

Use dependency-tree when you want to inspect dependencies in isolation; use -v when you want extra detail alongside the full build output.

11.4. Combined with Other Flags

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 file

Examples 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.xml

12. Nested Aggregators

For projects with many modules, you can group related modules under a nested aggregator. This is common for organising examples, plugins, or other logical groups.

12.1. Example: Framework with Nested Examples

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/

12.2. Root Aggregator

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>

12.3. Nested Aggregator

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>

12.4. Leaf Modules with Cross-Level Dependencies

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>

12.5. Custom Source Directory for Examples

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.pas

This 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.

12.6. How It Works

  • 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, demo1 inherits version 2.0.0 from examples, 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, core builds first, then demo1 and demo2.

  • POM modules are skipped: Nested aggregator modules appear in the reactor summary but are not compiled (they have no source code).

12.7. Building

cd my-framework
pasbuild compile              # Builds: core, demo1, demo2
                              # (examples aggregator is skipped)
pasbuild compile -m demo1    # Builds: core, demo1

13. Advanced Topics

13.1. Module Naming Conventions

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.

13.2. Build Profiles with Multi-Module

Apply profiles to multi-module builds:

pasbuild compile -p debug             # All modules use debug profile
pasbuild compile -p release -m demo   # Demo uses release profile

Each module’s build inherits the active profiles.

13.3. Multi-Level Dependencies

Transitive dependencies are automatically resolved:

demo → ui → core
       ↓
    core (same)

Demo’s compiler gets paths for both ui and core automatically.

14. Troubleshooting

14.1. Module Not Found

Fatal: Module "unknown" referenced by "demo" not found

Check the module name in <modules> matches the module’s <name> in its project.xml.

14.2. Path Outside Project Tree

Fatal: Module path outside project tree: ../../other

Module paths must stay within the aggregator’s root directory.

14.3. Circular Dependency

Fatal: Cyclic dependency detected: a

Check that dependencies don’t create cycles. Graph your modules and verify no cycles exist.

14.4. Circular Aggregator Reference

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.

14.5. Missing project.xml

Fatal: Module project.xml not found: path/to/module/project.xml

Ensure each module directory contains a valid project.xml file.

15. Complete Example

For a complete working example, see the design document at docs/multi-module-design.adoc.