Lazarus / Free Pascal CI without running your own build server
Daraja is a compact and flexible HTTP server application framework for Object Pascal, built on Internet Direct (Indy). It comes with a unit test suite, and I wanted every push and pull request to run it automatically on both Windows and Linux, using Free Pascal and Lazarus.
This post walks through the GitHub Actions workflow that does this. You can read the full file here: .github/workflows/tests.yml.
Disclosure
Two things up front:
- The workflow was created with the help of Claude, Anthropic’s AI assistant. I reviewed and tested the result, but you should read any generated CI configuration critically before reusing it.
- It runs on a free, public GitHub account. Daraja is an open source project in a public repository, and as far as I know GitHub does not charge for standard hosted runner minutes on public repositories, so there is no practical limit on workflow executions for a project like this. Please check GitHub’s current Actions billing documentation for the exact terms, since they can change.
What the workflow does
In short: it checks out Daraja and its two dependencies, installs Free Pascal and Lazarus, builds the FPCUnit test runner with lazbuild, and runs it headless. It does this on windows-latest and ubuntu-latest in parallel.
Triggers
on:
push:
branches: [master]
paths:
- 'source/**'
- 'test/**'
- '.github/workflows/tests.yml'
pull_request:
paths:
- 'source/**'
- 'test/**'
- '.github/workflows/tests.yml'
workflow_dispatch:
The workflow runs on pushes to master and on pull requests, but only when something relevant changed: the framework sources, the tests, or the workflow file itself. Documentation-only changes don’t start a build. workflow_dispatch adds a “Run workflow” button for manual runs.
Build matrix
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
Both operating systems run side by side. With fail-fast: false, a failure on one platform doesn’t cancel the other, so you see both results every time. The 20-minute timeout stops a stalled job from hanging around.
Checking out the dependencies
Daraja needs Indy and slf4p (a logging facade). The workflow checks out all three repositories side by side:
- name: Checkout daraja-framework
uses: actions/checkout@v5
with:
path: daraja-framework
- name: Checkout Indy (dependency)
uses: actions/checkout@v5
with:
repository: IndySockets/Indy
path: Indy
- name: Checkout slf4p (dependency)
uses: actions/checkout@v5
with:
repository: michaelJustin/slf4p
path: slf4p
Installing FPC and Lazarus
This is the fiddliest part, and the two platforms are handled differently.
Linux uses the Ubuntu package archive, which provides FPC 3.2.2 and Lazarus:
- name: Install FPC and Lazarus (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y lazarus libgtk2.0-dev
The popular setup-lazarus action downloads .deb files from SourceForge, which on hosted runners can stall for many minutes. Using apt avoids that.
Windows does use gcarreno/setup-lazarus, but SourceForge throttling can be a problem there too. So the workflow caches the installed Lazarus/FPC tree. It is self-contained and includes its own fpc.cfg:
- name: Cache Lazarus / FPC (Windows)
if: runner.os == 'Windows'
id: cache-lazarus
uses: actions/cache@v5
with:
path: ${{ runner.temp }}/installers/lazarus
key: lazarus-${{ runner.os }}-stable-4.4-fpc-3.2.2
- name: Install Lazarus / FPC (Windows)
if: runner.os == 'Windows' && steps.cache-lazarus.outputs.cache-hit != 'true'
timeout-minutes: 10
uses: gcarreno/setup-lazarus@v3
with:
lazarus-version: stable
- name: Add cached Lazarus to PATH (Windows)
if: runner.os == 'Windows' && steps.cache-lazarus.outputs.cache-hit == 'true'
shell: bash
run: |
echo "${RUNNER_TEMP}/installers/lazarus" >> "$GITHUB_PATH"
echo "${RUNNER_TEMP}/installers/lazarus/fpc/3.2.2/bin/x86_64-win64" >> "$GITHUB_PATH"
On a cache miss, the action installs Lazarus. On a cache hit, the download is skipped, and the workflow adds the two directories to PATH that the action would normally have added (the IDE/lazbuild directory and the bundled FPC bin directory). When you upgrade Lazarus or FPC, bump the version in the cache key to force a fresh install.
Building the test runner
- name: Build the test runner (ConsoleCI build mode)
working-directory: daraja-framework/test/unittests
run: lazbuild -B --build-mode=ConsoleCI Unittests.lpi
shell: bash
The test project Unittests.lpi has a dedicated ConsoleCI build mode. It is the regular Console mode plus the define -dDARAJA_SKIP_SERVER_TESTS, which excludes the integration test suites that start a loopback HTTP server (TSessionTests and TAPIConfigTests). That keeps CI runs fast. Those suites are still exercised by the local run-fpc and run-delphi scripts.
The define lives in the build mode rather than being passed as lazbuild --opt=... because Lazarus 3.0’s lazbuild rejects --opt. A build mode works with every Lazarus version.
Running the tests headless
- name: Run the test suite (headless)
working-directory: daraja-framework/test/unittests
shell: bash
run: |
exe=./UnittestsConsoleCI
[ -f "$exe.exe" ] && exe="$exe.exe"
if [ "$RUNNER_OS" = "Linux" ]; then
xvfb-run --auto-servernum "$exe" --all --format=plain
else
"$exe" --all --format=plain
fi
A few details here:
- FPCUnit’s console runner is started with
--allto run every registered test, and--format=plainfor readable log output. If any test fails, the process exits with a non-zero code and the job turns red. xvfb-runon Linux: the test runner links the LCL through theInterfacesunit, whose initialization opens the (GTK2) display even though no window is ever shown. Running under a virtual X server (Xvfb) makes that work on a headless runner.- The
.execheck: FPC only adds the.exesuffix on Windows, so the script looks for either name and one step works on both platforms.
And Delphi?
The GitHub workflow covers Free Pascal and Lazarus, but Daraja is not FPC-only. The framework also runs on Delphi 2009 or later, and the repository includes a run-delphi script for running the same tests locally with Delphi, next to the run-fpc script for Free Pascal.
Takeaways
If you want CI for your own Lazarus project, these are the ideas worth borrowing:
- Use
pathsfilters so only relevant changes trigger a build. - Run a Windows and Linux matrix with
fail-fast: falseto catch platform-specific issues. - Avoid slow third-party download hosts: use
apton Linux and cache the toolchain on Windows. - Put CI-specific compiler defines in a Lazarus build mode, not on the
lazbuildcommand line. - Use
xvfb-runif your test runner touches the LCL, even without showing any windows. - Skip slow integration tests in CI if you run them elsewhere, and say so in a comment.
Links
- Daraja HTTP Framework: https://github.com/michaelJustin/daraja-framework
- The workflow file: https://github.com/michaelJustin/daraja-framework/blob/master/.github/workflows/tests.yml
- Workflow run history: https://github.com/michaelJustin/daraja-framework/actions/workflows/tests.yml