- 1. Overview
- 2. Project Structure
- 3. Configuration Format
- 4. Build System Architecture
- 5. Build Goals
- 6. Profile System
- 7. Compiler Integration
- 8. Error Handling Strategy
- 9. Template Generation (init Goal)
- 10. Implemented Features
- 11. Version Injection (Achieved via Resource Filtering)
- 12. Semantic Versioning 2.0.0 Support
- 13. Dependency Management
- 14. Future Enhancements
- 15. Implementation Phases
- 16. Multi-Module Projects
- 17. Appendix: Technology Choices
PasBuild is a build automation tool for Free Pascal projects, inspired by Apache Maven. It provides standardized project structure, declarative configuration, and repeatable builds through a goal-based execution model.
-
Standardization: Eliminate per-project build script variations
-
Simplicity: Zero configuration for conforming projects
-
Extensibility: Support platform-specific and debug/release builds via profiles
-
Cross-platform: Work identically on Linux, Windows, macOS
-
Self-hosting: Tool builds itself using its own project.xml
All PasBuild projects follow the Maven Standard Directory Layout adapted for Pascal:
MyProject/
├── project.xml # Project configuration (POM equivalent)
├── LICENSE # Project license (optional)
├── README.adoc # Project documentation (optional)
├── src/
│ └── main/
│ └── pascal/ # Application source code (*.pas, *.pp, *.lpr)
│ ├── Main.pas # Entry point
│ ├── core/ # Optional: subdirectories for organization
│ └── gui/ # Optional: more subdirectories
└── target/ # Build output (created by pasbuild)
├── units/ # Compiled units (*.o, *.ppu)
└── <executable> # Final binaryRules:
-
project.xmlMUST exist in project root -
src/main/pascal/MUST exist -
Subdirectories under
pascal/are automatically scanned for-Fupaths -
target/is generated; MUST NOT be committed to version control
Why strict layout?
-
Eliminates "where should I put this file?" decisions
-
Tools can make assumptions (automation)
-
New contributors immediately understand structure
-
Cross-project consistency
Why src/main/pascal/ instead of just src/?
-
Future-proofs for
src/test/pascal/(see [future-test-phase]) -
Mirrors Maven exactly (easier mental model for Java developers)
-
Allows for
src/main/resources/later (embedded resources, help files)
<?xml version="1.0" encoding="UTF-8"?>
<project>
<!-- Metadata -->
<name>MyPascalApp</name> <!-- REQUIRED: Project identifier -->
<version>1.0.0</version> <!-- REQUIRED: Semantic version -->
<author>Jane Doe</author> <!-- OPTIONAL: Creator -->
<license>MIT</license> <!-- OPTIONAL: SPDX identifier or "Proprietary" -->
<!-- Build Configuration -->
<build>
<mainSource>Main.pas</mainSource> <!-- REQUIRED: Entry point relative to src/main/pascal/ -->
<outputDirectory>target</outputDirectory> <!-- OPTIONAL: Default = "target" -->
<executableName>myapp</executableName> <!-- OPTIONAL: Default = <name> lowercased -->
<!-- Global compiler defines (always active) -->
<defines>
<define>UseCThreads</define>
</defines>
<!-- Global compiler options (extends defaults: -Mobjfpc -O1) -->
<compilerOptions>
<option>-vh</option> <!-- Show hints -->
</compilerOptions>
</build>
<!-- Build Profiles (environment-specific settings) -->
<profiles>
<profile>
<id>debug</id>
<defines>
<define>DEBUG</define>
<define>VERBOSE_LOGGING</define>
</defines>
<compilerOptions>
<option>-O1</option>
<option>-g</option>
<option>-gl</option>
</compilerOptions>
</profile>
<profile>
<id>release</id>
<defines>
<define>RELEASE</define>
<define>INLINE_FUNCTIONS</define>
</defines>
<compilerOptions>
<option>-O3</option>
<option>-CX</option>
<option>-XX</option>
</compilerOptions>
</profile>
</profiles>
</project>| Field | Type | Validation |
|---|---|---|
|
String |
Alphanumeric + hyphens/underscores only. Max 64 chars. |
|
String |
Semantic versioning: |
|
Path |
Must exist in |
| Field | Default Value | Notes |
|---|---|---|
|
|
Included in package metadata |
|
|
SPDX identifier (MIT, BSD-3-Clause, GPL-3.0, Apache-2.0, etc.) or "Proprietary" |
|
|
Relative to project root |
|
Lowercased |
Platform suffix added automatically ( |
Why XML over JSON?
-
Schema validation (XSD) for configuration correctness
-
Comments supported (users can document profiles)
-
Extensibility via namespaces (future custom goals)
-
Consistency with Maven (familiar to enterprise developers)
-
FPC’s
DOM/XMLReadunits are mature and stable
Why semantic versioning?
-
Industry standard
-
Clear upgrade impact (
MAJOR= breaking changes) -
Enables dependency management and version resolution
pasbuild (executable)
├── PasBuild.CLI → Argument parsing, goal dispatch
├── PasBuild.Config → XML parsing, validation
├── PasBuild.Types → Shared data structures
├── PasBuild.Cleaner → "clean" goal implementation
├── PasBuild.Builder → "compile" goal implementation
├── PasBuild.Archiver → "package" goal implementation
└── PasBuild.Utils → File operations, process executionPasBuild follows Maven’s lifecycle model where goals can have dependencies on other goals.
Goal Dependencies:
| Goal | Depends On | Execution Order |
|---|---|---|
|
None |
clean |
|
None |
compile |
|
clean, compile |
clean → compile → package |
|
process-resources, compile |
process-resources → compile → install |
|
None |
init |
Behavior:
-
When a goal declares dependencies, those goals execute first in order
-
Each dependency goal runs once (no duplicate execution)
-
If any dependency fails (exit code ≠ 0), execution stops immediately
-
Goals can be run individually or as part of a dependency chain
Example:
pasbuild package
# Executes: clean → compile → package
# If compile fails, package never runsPurpose: Delete all build artifacts
Usage:
pasbuild cleanBehavior:
-
Read
<build><outputDirectory>fromproject.xml(default:target) -
If directory exists:
-
Recursively delete all contents
-
Delete directory itself
-
-
Display:
"[INFO] Cleaned target directory"
Implementation Notes:
-
Safety: Only delete if path == configured output directory
-
No confirmation: Matches Maven behavior (add
--forcelater if needed) -
Use FPC’s
DeleteDirectoryor manual recursive delete viaFindFirst/FindNext
Purpose: Build the executable
Usage:
pasbuild compile # Default build
pasbuild compile -p debug # With profileBehavior:
-
Validate
src/main/pascal/exists -
Scan subdirectories for automatic
-Fupaths -
Construct FPC command:
-
Base:
fpc -Mobjfpc -O1 -
Source:
src/main/pascal/<mainSource> -
Output:
-FE<outputDirectory> -FU<outputDirectory>/units -
Defines:
-d<define>for each global + profile define -
Unit paths:
-Fu<dir>for each scanned subdirectory
-
-
Execute via
TProcess -
Stream compiler output to console in real-time
-
Exit with FPC’s exit code (0 = success)
Example Generated Command:
fpc -Mobjfpc -O1 \
src/main/pascal/Main.pas \
-FEtarget \
-FUtarget/units \
-dUseCThreads \
-dDEBUG \
-Fusrc/main/pascal/core \
-Fusrc/main/pascal/gui \
-omyappDirectory Scanning Algorithm:
procedure ScanForUnitPaths(BaseDir: string; var Paths: TStringList);
// Recursively find all subdirectories under src/main/pascal/
// Add each as -Fu<path>
// Exclude: ., .., .git, .svn, backup~
end;Purpose: Create release archive
Usage:
pasbuild package # Automatically runs: clean → compile → packageBehavior:
-
Execute
cleangoal (dependency) -
Execute
compilegoal (dependency) -
If both dependencies succeed:
-
Create zip:
<name>-<version>.zip -
Include:
-
Compiled executable from
target/ -
LICENSEfile (if exists) -
README.adocorREADME.md(if exists)
-
-
Exclude:
-
.o,.ppufiles (intermediate artifacts)
-
-
-
Save archive to
targetdirectory. -
Display:
"[INFO] Created myapp-1.0.0.zip"
Archive Structure:
myapp-1.0.0.zip
├── myapp (or myapp.exe on Windows)
├── LICENSE
└── README.adocRationale:
-
"Clean then build" ensures reproducibility
-
Flat structure (no intermediate directory) for simplicity
-
Only user-facing files (no build artifacts)
Allow environment-specific compiler settings without modifying source code.
Use Cases:
-
Debug vs Release builds
-
Platform-specific backends (GTK2 vs Qt)
-
Feature flags (trial version vs full version)
Active Defines = Global Defines + Profile Defines
Given:
<build>
<defines>
<define>UseCThreads</define>
</defines>
</build>
<profiles>
<profile>
<id>release</id>
<defines>
<define>RELEASE</define>
</defines>
</profile>
</profiles>Result of pasbuild compile -p release:
fpc ... -dUseCThreads -dRELEASE ...Rules:
-
Profiles are additive (not replacement)
-
Unknown profile ID → Warning + ignore profile
-
No profile specified → Only global defines used
-
Multiple profiles supported via comma-separated list:
-p profile1,profile2
Strategy: Require fpc in system PATH
Validation:
-
Execute:
fpc -i(orfpc -iVfor short version) -
If exit code ≠ 0 → Error:
"FPC not found in PATH" -
Parse version from output (informational only)
Note: FPC 3.2.2+ uses -i for full info, -iV for short version string, -iW for full version string
Future Enhancement: <build><compilerPath> override
Always Applied:
| Flag | Purpose |
|---|---|
|
Free Pascal object model (required for modern code) |
|
Level 1 optimization (debug-friendly, reasonable speed) |
|
Executable output directory |
|
Unit files output directory (keeps |
|
Executable name |
Automatically Generated:
| Flag | Generated By |
|---|---|
|
Scanned from all subdirectories under |
|
From |
Real-time Streaming:
-
Use
TProcess.Options := [poUsePipes, poStderrToOutPut] -
Read
TProcess.Outputduring compilation -
Display to console immediately (UX: user sees progress)
Exit Code Propagation:
-
If FPC returns non-zero → pasbuild returns same code
-
Allows CI systems to detect build failures
Philosophy: Clear error messages, no automatic recovery
Error Categories:
| Category | Detection | Response |
|---|---|---|
Missing |
File not found |
|
Invalid XML |
Parse exception |
|
Missing required field |
XPath check |
|
Invalid directory layout |
|
|
FPC not in PATH |
|
|
Compilation failure |
FPC exit code ≠ 0 |
Display FPC output, exit with same code |
Dependency goal failure |
Goal exit code ≠ 0 |
|
No Silent Failures:
-
Every error writes to stderr
-
Exit code always reflects success (0) or failure (≠0)
Purpose: Bootstrap new PasBuild project
Usage:
cd MyNewProject
pasbuild initInteractive Prompts:
Project name [MyNewProject]:
Version [1.0.0]:
Author [Your Name]:
License (MIT/BSD-3-Clause/GPL-3.0/Apache-2.0/Proprietary) [MIT]:Generated Structure:
MyNewProject/
├── project.xml
├── LICENSE (Generated from SPDX template if MIT/GPL/Apache chosen)
├── src/
│ └── main/
│ └── pascal/
│ └── Main.pasGenerated project.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project>
<name>MyNewProject</name>
<version>1.0.0</version>
<author>Your Name</author>
<license>MIT</license>
<build>
<mainSource>Main.pas</mainSource>
<executableName>mynewproject</executableName>
</build>
</project>Generated Main.pas:
program Main;
{$mode objfpc}{$H+}
uses
SysUtils;
begin
WriteLn('Hello from MyNewProject!');
WriteLn('Build tool: PasBuild');
end.Behavior:
-
If
project.xmlexists → Error:"Project already initialized" -
Non-interactive mode (future):
pasbuild init --name Foo --version 1.0.0
The following features have been implemented and are available in PasBuild:
Status: ✅ Implemented
Goals: pasbuild test-compile, pasbuild test
Features:
-
Separate
src/test/pascal/directory support -
Test runner compilation with dependency on main compilation
-
Separate test units directory (
target/test-units/) -
Test execution with configurable framework options
-
Dependency chain: compile → test-compile → test
Configuration:
<test>
<testSource>TestRunner.pas</testSource>
<frameworkOptions>
<option>--all</option>
<option>--format=plain</option>
</frameworkOptions>
</test>Status: ✅ Implemented
Goal: pasbuild source-package
Features:
-
Creates source archive:
target/<name>-<version>-src.zip -
Default includes:
src/,project.xml,LICENSE*,README*,BOOTSTRAP*,INSTALL* -
Optional includes via configuration
-
Security: validates paths are within project root
-
Archive structure with version prefix (Maven-style)
Configuration:
<build>
<sourcePackage>
<include>docs</include>
<include>examples</include>
<include>scripts</include>
</sourcePackage>
</build>Use Cases:
-
Distribution to software repositories (Debian, FreeBSD ports)
-
Conference/journal paper submissions
-
Users without Git access
-
Official release archives
Status: ✅ Implemented
Features:
-
Global compiler options in
<build><compilerOptions> -
Profile-specific compiler options in
<profile><compilerOptions> -
Additive application: hardcoded defaults → global → profile
-
Later options override earlier ones (FPC behavior)
Configuration:
<build>
<compilerOptions>
<option>-vh</option> <!-- Show hints globally -->
</compilerOptions>
</build>
<profiles>
<profile>
<id>release</id>
<compilerOptions>
<option>-O3</option> <!-- Override default -O1 -->
<option>-CX</option>
<option>-XX</option>
</compilerOptions>
</profile>
</profiles>Status: ✅ Implemented
Features:
-
Activate multiple profiles via comma-separated list:
-p base,debug,logging -
Profiles applied in order (left to right)
-
Each profile’s defines and options added sequentially
-
Composable build configurations
Status: ✅ Implemented
Features:
-
Specify alternate project file:
-f custom.xmlor--file custom.xml -
Default:
project.xml -
Supports relative and absolute paths
-
Clear error messages for missing files
Use Cases:
-
Multiple build configurations (dev/prod/ci)
-
Testing configurations without modifying main project.xml
-
Build system integration
Status: ✅ Implemented
Features:
-
-vor--verboseflag shows full FPC compiler output -
Default mode: clean console with INFO/ERROR only, full log to
target/pasbuild-status/<goal>/compiler.log -
Build status tracking: Maven-style status directories
-
Input file tracking:
inputUnits.lst,inputIncludeFiles.lst
Status: ✅ Implemented
Goals: pasbuild process-resources, pasbuild process-test-resources
Features:
-
Copies resources from
src/main/resources/→target/ -
Copies test resources from
src/test/resources/→target/ -
Preserves directory structure during copy
-
Optional variable filtering for project metadata substitution
-
Integrated into build lifecycle:
-
compiledepends onprocess-resources -
test-compiledepends onprocess-test-resources
-
Configuration:
<build>
<resources>
<directory>src/main/resources</directory> <!-- optional, default shown -->
<filtering>true</filtering> <!-- optional, default: false -->
</resources>
</build>
<test>
<resources>
<directory>src/test/resources</directory>
<filtering>true</filtering>
</resources>
</test>Variable Filtering:
When filtering=true, the following variables are substituted in resource files:
-
${project.name}→ Project name -
${project.version}→ Project version -
${project.author}→ Project author -
${project.license}→ Project license
Use Cases:
-
Help files (
.html,.chm) -
Icons, images
-
Configuration templates with version/author info
-
Translation files (
.po,.mo) -
Database configuration files
-
Version manifests
Version injection is already possible using the resource copying feature with filtering enabled.
Implementation:
Create src/main/resources/version.inc:
'${project.version}'Enable filtering in project.xml:
<build>
<resources>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resources>
</build>Usage in Code:
const
APP_VERSION = {$I version.inc};
begin
WriteLn('MyApp version: ', APP_VERSION);
end.How it works:
-
During
compile,process-resourcesruns first -
version.incis copied totarget/with${project.version}replaced by actual version -
FPC automatically includes
target/in include path -
Code includes the filtered version constant
For more metadata:
Create src/main/resources/project.inc:
{ Auto-generated project metadata - DO NOT EDIT }
const
PROJECT_NAME = '${project.name}';
PROJECT_VERSION = '${project.version}';
PROJECT_AUTHOR = '${project.author}';
PROJECT_LICENSE = '${project.license}';Then in your code:
{$I project.inc}
begin
WriteLn(PROJECT_NAME, ' version ', PROJECT_VERSION);
WriteLn('By ', PROJECT_AUTHOR);
end.Note: FPC does not support compiler defines with values (e.g., -dVERSION='1.0.0' is invalid in FPC). Include files are the standard approach for compile-time constants.
Status: ✅ Implemented
PasBuild supports full Semantic Versioning 2.0.0 including pre-release identifiers.
Valid version formats:
-
Release versions:
MAJOR.MINOR.PATCH-
Examples:
1.0.0,2.1.0,3.4.5
-
-
Pre-release versions:
MAJOR.MINOR.PATCH-PRERELEASE-
Examples:
1.0.0-alpha,1.0.0-beta.2,1.0.0-rc.1,2.0.0-SNAPSHOT
-
Pre-release identifier:
-
Can contain alphanumeric characters, dots, and hyphens:
[a-zA-Z0-9.-]+ -
Regex validation:
^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?$
Use cases:
-
Beta/alpha releases
-
Release candidates
-
Development snapshots (e.g.,
-SNAPSHOTfor Maven compatibility) -
Nightly builds
-
Feature branch builds
Package naming:
Pre-release versions are included in package filenames:
-
myapp-1.0.0-beta.zip -
myapp-2.0.0-SNAPSHOT-src.zip
PasBuild supports Maven-style dependency management through a local repository. Projects can declare external dependencies in project.xml, and PasBuild resolves them at compile time by locating pre-compiled units in the local repository.
Installed artifacts are stored under ~/.pasbuild/repository/ with the following structure:
~/.pasbuild/repository/
<name>/
<version>/
<cpu>-<os>-<fpcVersion>/ # Target triplet
units/ # Compiled .ppu and .o files
metadata.xml # Artifact metadata and dependency listThe target triplet (e.g., x86_64-linux-3.2.2) ensures compiled units are only used with a matching FPC version and target architecture. This prevents .ppu version mismatch errors.
Paths containing spaces in the user’s home directory (e.g., C:\Users\John Smith\) are handled correctly through path quoting.
The install goal compiles the project and copies compiled units to the local repository:
pasbuild installThis executes: process-resources → compile → install. The install step:
-
Detects the target triplet via
fpc -iTP,fpc -iTO,fpc -iV -
Creates the repository directory structure
-
Copies all
.ppuand.ofiles fromtarget/units/to the repository -
Generates
metadata.xmlwith artifact identity, build info, and dependency list
For multi-module aggregator projects, install runs on each non-aggregator module in dependency order.
Consumer projects declare external dependencies in project.xml:
<project>
<name>my-app</name>
<version>1.0.0</version>
<build>
<mainSource>MyApp.pas</mainSource>
</build>
<dependencies>
<dependency>
<name>fpgui-framework</name>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>Validation rules:
-
Each
<dependency>requires both<name>and<version> -
Version must be valid semantic version format
-
Duplicate dependency names are rejected
When a project with <dependencies> is compiled, PasBuild resolves each dependency before invoking the compiler:
-
Detect the current target triplet
-
For each declared dependency, locate the artifact in the local repository
-
Read
metadata.xmlto discover transitive dependencies -
Recursively resolve transitive dependencies (with cycle detection)
-
Add all resolved unit paths to the compiler’s
-Fusearch paths
If a dependency is not found, PasBuild reports a descriptive error indicating whether the artifact is missing entirely or available for a different target.
Each installed artifact includes a metadata.xml file:
<artifact>
<name>fpgui-framework</name>
<version>1.0.0</version>
<packaging>library</packaging>
<build>
<fpcVersion>3.2.2</fpcVersion>
<targetCPU>x86_64</targetCPU>
<targetOS>linux</targetOS>
<timestamp>2026-02-13T00:24:26</timestamp>
</build>
<dependencies>
<dependency>
<name>some-base-lib</name>
<version>1.0.0</version>
</dependency>
</dependencies>
</artifact>The <dependencies> section enables transitive dependency resolution. When a consumer depends on fpgui-framework, PasBuild automatically resolves some-base-lib as well.
Future enhancements will be driven by user feedback and community requests. Potential areas include:
-
Remote artifact repository (similar to Maven Central)
-
Dependency version range resolution
See implementation-progress.adoc for detailed task breakdown and current status.
PasBuild supports Maven-style multi-module projects with aggregators, libraries, and applications.
Three packaging types organize multi-module projects:
| Type | Purpose | Example |
|---|---|---|
|
Coordinates multiple modules |
Root project with |
|
Provides reusable components |
Framework, utilities, UI toolkit |
|
Final executable |
Demo, tool, end-user application |
my-framework/ # Aggregator (packaging=pom)
├── project.xml
├── core/ # Library (packaging=library)
│ ├── project.xml
│ └── src/main/pascal/
├── ui/ # Library (depends on core)
│ ├── project.xml
│ └── src/main/pascal/
└── demo/ # Application (depends on ui)
├── project.xml
└── src/main/pascal/Aggregators list child modules:
<project>
<build>
<packaging>pom</packaging>
</build>
<modules>
<module>core</module>
<module>ui</module>
<module>demo</module>
</modules>
</project>Modules declare dependencies:
<project>
<!-- UI depends on core -->
<modules>
<module>../core</module>
</modules>
</project>PasBuild automatically:
-
Discovers all modules from aggregator
-
Resolves dependencies between modules
-
Detects circular dependencies
-
Computes topological build order
-
Adds resolved module paths to
-Fuflags
Uses depth-first topological sort to determine order:
demo (application)
└── ui (library)
└── core (library)
Build order: core → ui → demoCycle detection prevents:
A → B → A (cycle detected, build fails)All configurations are validated:
-
Aggregators (pom) MUST have
<modules>, forbid<mainSource> -
Libraries can optionally have
<mainSource> -
Applications MUST have
<mainSource> -
Only libraries and applications produce artifacts
-
Module dependencies cannot reference aggregators
-
Paths must remain within project tree (security)
Build specific modules:
pasbuild compile -m demo # Build demo and dependencies
pasbuild test -m core # Test core only
pasbuild package -m ui -p release # Package with profileDisplay dependency graph in verbose mode:
pasbuild compile -v # Show dependency graph
pasbuild compile -v -m demo # Show graph and build demoExample output:
[INFO] Dependency Graph:
[INFO]
[INFO] core (no dependencies)
[INFO] ui (depends on: core)
[INFO] └─ core
[INFO] demo (depends on: ui)
[INFO] └─ uiFor details and examples, see multi-module-tutorial.adoc.
Free Pascal 3.2.2 in -Mobjfpc mode
Justification:
-
Self-hosting (tool builds itself)
-
Demonstrates best practices to users
-
Cross-platform without dependencies
| Unit | Purpose |
|---|---|
|
Parse |
|
File/directory operations |
|
Execute FPC compiler |
|
Create release archives |