You have built PennyWise. It has a polished GUI, database storage, charts, menus, toolbars, and file management. It works on your development machine. But a program that only runs on your machine is a program that serves exactly one person.
Learning Objectives
- Configure release vs. debug builds with appropriate compiler options
- Understand static vs. dynamic linking and its impact on deployment
- Create a Windows installer using Inno Setup
- Package applications for Linux distribution (.deb, AppImage)
- Create macOS application bundles (.app) and DMG files
- Use Free Pascal's cross-compilation capabilities
- Handle resources, data files, and version information
- Package PennyWise as an installable application for all three platforms
In This Chapter
- 32.1 From Source to Distributable Application
- 32.2 Release vs. Debug Builds
- 32.3 Static vs. Dynamic Linking
- 32.4 Windows Deployment
- 32.5 Linux Deployment
- 32.6 macOS Deployment
- 32.7 Cross-Compilation with Free Pascal
- 32.8 Handling Resources and Data Files
- 32.9 Version Information and Branding
- 32.10 Auto-Update Strategies
- 32.11 Project Checkpoint: PennyWise Deployment
- 32.12 Deployment Anti-Patterns
- 32.13 Summary
Chapter 32: Deploying Your Application — Compilation, Packaging, and Cross-Platform Considerations
"If it is not in the user's hands, it does not exist." — Shipping maxim
You have built PennyWise. It has a polished GUI, database storage, charts, menus, toolbars, and file management. It works on your development machine. But a program that only runs on your machine is a program that serves exactly one person.
This chapter is about the last mile — the distance between "it works on my computer" and "anyone can install it and use it." We cover compilation options, dependency management, platform-specific packaging, cross-compilation, resource embedding, and the practical details that turn a development project into a distributable application.
This is where Pascal's native compilation advantage becomes concrete. A PennyWise executable is a single, self-contained binary. No runtime to install. No framework to download. No package manager to configure. Copy the executable to a computer, double-click it, and it runs. Compare this to a Python application (requires Python + pip + virtualenv), a Java application (requires JRE), or an Electron application (ships a 150 MB copy of Chromium). Pascal's deployment story is remarkably clean.
32.1 From Source to Distributable Application
The journey from source code to a distributable application has several stages:
Source Code (.pas, .lfm, .lpr)
↓ [Compile]
Debug Build (large, with debug symbols)
↓ [Optimize & Strip]
Release Build (small, fast, no debug info)
↓ [Gather Dependencies]
Application + Libraries + Data Files
↓ [Package]
Installer (Windows) / .deb (Linux) / .app Bundle (macOS)
↓ [Distribute]
User's Computer
Each stage has decisions to make and pitfalls to avoid. Most development tutorials stop at "it compiles and runs." This chapter covers everything after that point — the work that separates a student project from software that real people use.
The stages are not optional. Skipping the release build means shipping a 20 MB debug binary when a 4 MB release binary would do. Skipping dependency gathering means the application crashes on the user's machine with a cryptic "DLL not found" error. Skipping packaging means the user must manually extract files, set permissions, and create shortcuts. Each stage adds polish, and the cumulative effect is the difference between "this looks amateur" and "this looks professional."
32.2 Release vs. Debug Builds
Debug Builds
During development, you compile in debug mode — Lazarus's default. A debug build includes:
- Debug symbols — mappings between compiled code and source lines, enabling the debugger to show you exactly where in your Pascal code the program is executing.
- Range checking — runtime checks that array indices are within bounds, that integer operations do not overflow, and that variant types are used correctly.
- No optimization — the compiler generates straightforward code that is easy to debug but not optimized for speed.
- Heap trace — optional tracking of memory allocations to detect memory leaks.
- Line info — stack traces in error messages show the source file and line number.
Debug builds are larger (often 15–25 MB for a GUI application) and slightly slower, but they catch bugs that would be invisible in a release build.
Release Builds
For distribution, you switch to release mode. In Lazarus:
- Go to Project > Project Options > Compiler Options > Debugging.
- Uncheck "Generate debugging info for GDB."
- Uncheck "Use line info unit."
- Uncheck "Use heap trace unit."
- Go to Compilation and Linking.
- Check "Strip symbols from executable" (
-Xs). - Optionally check "Link smart" (
-XX) — this strips unused code from the final binary, further reducing size. - Go to Code Generation.
- Set optimization level to -O2 (good optimization) or -O3 (aggressive optimization).
- Optionally enable -CX (SmartLinking for units) in combination with
-XXfor maximum size reduction.
Or use the command line:
lazbuild --build-mode=Release PennyWise.lpi
If you prefer direct fpc invocation:
fpc -O2 -Xs -XX -CX PennyWise.lpr
The flags in detail:
- -O2 — Standard optimization level. Enables constant folding, dead code elimination, register allocation optimization, and peephole optimizations. A good balance between compilation speed and output quality.
- -O3 — Aggressive optimization. Adds loop unrolling, function inlining, and instruction scheduling. May increase compilation time significantly. Occasionally changes behavior in programs with uninitialized variables.
- -Xs — Strip debug symbols from the executable. Dramatically reduces file size.
- -XX — Smart linking. Only include code that is actually referenced, removing dead code from linked units.
- -CX — Create SmartLinkable units. Required for -XX to work effectively with units.
Setting Up Build Modes in Lazarus
Rather than changing compiler settings manually each time, create named build modes:
- Go to Project > Project Options > Compiler Options.
- Click the Build Modes button (or find it in the toolbar).
- Click Create Debug and Release modes.
- Lazarus creates two modes: "Debug" with full debugging enabled, and "Release" with optimization and stripping.
- Switch between them using the dropdown in the toolbar.
You can create additional modes. A "Profile" mode, for instance, might enable optimization but keep debug symbols — useful for performance analysis with profiling tools.
Comparison
| Property | Debug | Release |
|---|---|---|
| Executable size | 15–25 MB | 3–6 MB |
| Runtime checks | Full (range, overflow, heap) | Minimal |
| Debug symbols | Included | Stripped |
| Optimization | None (-O0) | -O2 or -O3 |
| Startup time | ~200ms | ~50ms |
| Smart linking | Disabled | Enabled |
| Suitable for | Development | Distribution |
The UPX Option
After building a release binary, you can optionally compress it with UPX (Ultimate Packer for eXecutables). UPX compresses the executable and adds a small decompressor stub. When the user runs the application, UPX decompresses it in memory before execution.
upx --best PennyWise.exe
This can reduce the executable from 4 MB to 1.5 MB — useful when distribution bandwidth matters (email attachments, slow connections). The decompression adds a small startup delay (typically under 100ms) that is imperceptible to the user.
However, UPX has downsides: some antivirus scanners flag UPX-compressed executables as suspicious (because malware authors also use UPX to obfuscate their code), and the compressed executable cannot be signed with standard code signing tools without first decompressing it. For most applications, the size savings from -Xs -XX are sufficient, and UPX compression is unnecessary.
Testing in Release Mode
Always test thoroughly in Release mode before distribution. Optimization can occasionally change behavior in programs with subtle bugs:
- Uninitialized variables. In debug mode, the compiler may zero-initialize local variables as a courtesy. In release mode, they contain whatever was in memory. If you read a variable before assigning it, debug mode might give you 0 while release mode gives you garbage.
- Range checking. A debug build catches array index out of bounds. A release build does not check — the program silently reads or writes the wrong memory, causing corruption that might not manifest until much later.
- Timing. Optimized code runs faster, which can expose race conditions in threaded code that were hidden by the slower debug execution.
Run your full test suite in release mode. Exercise every feature of PennyWise in a release build before declaring it ready for distribution.
💡 Intuition: Build Modes Are Not Either/Or You can create multiple build modes in Lazarus (Project > Project Options > Build Modes). Create a "Debug" mode and a "Release" mode with different settings. Switch between them with one click. Always test thoroughly in Release mode before distribution — optimization can occasionally change behavior in programs with undefined behavior or uninitialized variables.
32.3 Static vs. Dynamic Linking
Static Linking
With static linking, all library code is compiled directly into your executable. The result is a single, standalone file with no external dependencies.
Free Pascal statically links the Pascal runtime library by default. Your PennyWise executable includes all the Pascal runtime code it needs. No separate runtime installation is required. This is a significant deployment advantage over languages that require a runtime (Java's JRE, Python's interpreter, .NET's CLR).
The LCL widgetset is also statically linked by default on most platforms. The code for buttons, labels, grids, and menus is compiled into your executable. However, the LCL ultimately calls the operating system's GUI toolkit (GTK2, Qt5, or Win32), and those native libraries are dynamically linked.
Dynamic Linking
Some libraries are dynamically linked — they exist as separate files (.dll on Windows, .so on Linux, .dylib on macOS) that the executable loads at runtime. The LCL widgetset libraries and SQLite are typically dynamically linked.
For PennyWise, the key dynamic dependencies include:
- Windows:
sqlite3.dll— Ship alongside your executable. Download the 64-bit DLL from the SQLite website (sqlite.org/download.html). The DLL is approximately 1.2 MB.-
Standard Windows libraries (
kernel32.dll,user32.dll,gdi32.dll,comctl32.dll) are present on every Windows installation. -
Linux:
libsqlite3.so— The user likely has it installed (it is standard on most distributions). If not, they install it withapt install libsqlite3-0or equivalent.libgtk-x11-2.0.so(GTK2) orlibQt5Widgets.so(Qt5) depending on the widgetset. These are standard on desktop Linux installations.-
libc.soandlibpthread.so— always present. -
macOS:
libsqlite3.dylibis included in macOS by default.- Cocoa frameworks are standard on all macOS installations.
Choosing Between Static and Dynamic Linking
| Consideration | Static | Dynamic |
|---|---|---|
| Deployment simplicity | One file, no dependencies | Must ship or document dependencies |
| File size | Larger executable | Smaller executable + separate libraries |
| Updates | Must recompile to update a library | Can update library without recompiling |
| License compatibility | Some licenses prohibit static linking | LGPL libraries must be dynamically linked |
| Platform consistency | Exact same library version everywhere | May behave differently with different library versions |
For SQLite, you can choose either approach. The sqlite3 package for Free Pascal supports both dynamic linking (the default) and static linking (by compiling the SQLite C source directly into your application using the sqlite3static unit). Static linking eliminates the need to ship sqlite3.dll on Windows.
Checking Dependencies
On Windows, use the Dependency Walker tool or the dumpbin command from Visual Studio's developer tools:
dumpbin /dependents PennyWise.exe
On Linux, use ldd to list shared library dependencies:
ldd ./PennyWise
A typical output for a GTK2-based Lazarus application:
linux-vdso.so.1
libgtk-x11-2.0.so.0 => /lib/x86_64-linux-gnu/libgtk-x11-2.0.so.0
libgdk-x11-2.0.so.0 => /lib/x86_64-linux-gnu/libgdk-x11-2.0.so.0
libglib-2.0.so.0 => /lib/x86_64-linux-gnu/libglib-2.0.so.0
libsqlite3.so.0 => /lib/x86_64-linux-gnu/libsqlite3.so.0
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
On macOS, use otool -L to list dynamic library dependencies:
otool -L ./PennyWise
⚠️ Caution: Missing Dependencies If a required DLL or .so is missing on the user's system, the application will not start — often with an unhelpful error message like "The program can't start because sqlite3.dll is missing from your computer" (Windows) or a segfault (Linux). Always test your release build on a clean system (a virtual machine or a colleague's computer) to verify that all dependencies are present.
32.4 Windows Deployment
Simple Deployment: Folder Distribution
The simplest Windows deployment is a folder containing:
PennyWise/
├── PennyWise.exe
├── sqlite3.dll (if using SQLite dynamically)
├── pennywise.ico (application icon)
└── README.txt
Zip this folder and distribute it. The user extracts it and runs the executable. No installation, no registry entries, no administrator privileges. This is called a "portable" application, and it is one of the great advantages of native compiled software.
Portable deployment is ideal for USB drives, shared network folders, or users who do not have administrator privileges on their computers. PennyWise can store its database in a user-writable location (GetAppConfigDir) so the application itself can live in a read-only directory.
Application Icon
To set your application's icon (shown in the taskbar and file explorer):
- Create or obtain a
.icofile with multiple sizes (16x16, 32x32, 48x48, 256x256). Windows uses different sizes in different contexts: 16x16 for the title bar and taskbar, 32x32 for the desktop shortcut, 256x256 for large icon view in Explorer. Include all sizes in a single.icofile. - In Lazarus, go to Project > Project Options > Application > Icon.
- Load your icon file.
- Rebuild the project.
The icon is embedded in the executable as a Windows resource. No separate icon file is needed for the application to display its icon.
DPI Awareness and High-DPI Displays
Modern Windows displays often run at 125%, 150%, or 200% scaling. Without DPI awareness, your application appears blurry on these displays because Windows scales it using bitmap stretching.
To make PennyWise DPI-aware:
- In the
.lprproject file, add:
Application.Scaled := True;
-
Lazarus generates a DPI-aware manifest automatically when
Application.Scaledis set. This tells Windows that your application handles its own scaling. -
Design your forms with anchoring and auto-sizing rather than fixed pixel positions. A button at position (200, 50) will be in the wrong place on a 150% display if the form is not scaled. Use
AnchorsandAlignproperties instead. -
Test at multiple DPI settings. Right-click the desktop, choose Display Settings, and try 100%, 125%, and 150% scaling.
Professional Deployment: Inno Setup
For a professional installer with Start Menu shortcuts, desktop icons, uninstall support, and file associations, use Inno Setup — a free, open-source installer creator for Windows (jrsoftware.org/isinfo.php).
An Inno Setup script (.iss file) describes what to install and where:
; PennyWise Installer Script for Inno Setup
[Setup]
AppName=PennyWise
AppVersion=1.0.0
AppVerName=PennyWise 1.0.0
AppPublisher=Programming with Pascal
AppPublisherURL=https://example.com/pennywise
DefaultDirName={autopf}\PennyWise
DefaultGroupName=PennyWise
OutputDir=installer_output
OutputBaseFilename=PennyWise_Setup_1.0.0
Compression=lzma2
SolidCompression=yes
SetupIconFile=pennywise.ico
UninstallDisplayIcon={app}\pennywise.ico
; Require 64-bit Windows
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
; Minimum Windows version
MinVersion=10.0
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Additional shortcuts:"; Flags: unchecked
Name: "fileassoc"; Description: "Associate .pw files with PennyWise"; GroupDescription: "File associations:"
[Files]
Source: "release\PennyWise.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "release\sqlite3.dll"; DestDir: "{app}"; Flags: ignoreversion
Source: "release\pennywise.ico"; DestDir: "{app}"; Flags: ignoreversion
Source: "release\README.txt"; DestDir: "{app}"; Flags: ignoreversion isreadme
[Icons]
Name: "{group}\PennyWise"; Filename: "{app}\PennyWise.exe"; IconFilename: "{app}\pennywise.ico"
Name: "{group}\Uninstall PennyWise"; Filename: "{uninstallexe}"
Name: "{autodesktop}\PennyWise"; Filename: "{app}\PennyWise.exe"; IconFilename: "{app}\pennywise.ico"; Tasks: desktopicon
[Registry]
Root: HKCR; Subkey: ".pw"; ValueType: string; ValueName: ""; ValueData: "PennyWiseFile"; Flags: uninsdeletevalue; Tasks: fileassoc
Root: HKCR; Subkey: "PennyWiseFile"; ValueType: string; ValueName: ""; ValueData: "PennyWise File"; Flags: uninsdeletekey; Tasks: fileassoc
Root: HKCR; Subkey: "PennyWiseFile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\PennyWise.exe"" ""%1"""; Tasks: fileassoc
[Run]
Filename: "{app}\PennyWise.exe"; Description: "Launch PennyWise"; Flags: postinstall nowait skipifsilent
Compile this with the Inno Setup compiler (ISCC.exe) to produce a single PennyWise_Setup_1.0.0.exe installer. The installer handles directory creation, file copying, Start Menu entries, desktop shortcuts, file associations, and uninstall support.
Key Inno Setup features used in this script:
{autopf}— Resolves toC:\Program Fileson 64-bit orC:\Program Files (x86)on 32-bit. Modern applications should use{autopf}rather than hardcoded paths.[Tasks]— Optional installation tasks that the user can check or uncheck (desktop icon, file associations).[Registry]— File association entries that make.pwfiles open in PennyWise when double-clicked. Theuninsdeletevalueanduninsdeletekeyflags clean up the registry on uninstall.Flags: isreadme— Marks a file as the README, which the installer offers to display after installation.lzma2compression — Produces a compact installer. A typical PennyWise installer would be 2–3 MB.
To build the installer from the command line (useful for automation):
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" PennyWise.iss
Silent Installation
Inno Setup supports silent installation for enterprise deployment:
PennyWise_Setup_1.0.0.exe /VERYSILENT /NORESTART
This installs without any user interaction — useful for deploying PennyWise to multiple machines via a script or group policy.
Additional Inno Setup capabilities worth knowing:
- License agreement page. Add a
[Setup] LicenseFile=LICENSE.txtentry to show a license that the user must accept before installation. - Custom pages. Create wizard pages that collect user input during installation — for example, asking whether to install sample data or whether to create a portable installation.
- Pre-install checks. Use Pascal scripting within Inno Setup to check for prerequisites (like a minimum Windows version or the presence of a required DLL) before installation begins.
- Upgrade detection. Inno Setup can detect a previous installation and offer to upgrade rather than install fresh. Set
AppIdto a consistent GUID across versions.
Windows Portable Application
For users who prefer not to install software (corporate environments with locked-down computers, USB-drive usage), offer a portable option alongside the installer. A portable PennyWise stores its configuration and database in the same directory as the executable rather than in GetAppConfigDir:
function GetDataDirectory: string;
begin
if FileExists(Application.Location + 'portable.flag') then
Result := Application.Location { portable mode }
else
Result := GetAppConfigDir(False); { installed mode }
end;
Include a zero-byte portable.flag file in the portable distribution. If the file exists next to the executable, PennyWise uses the local directory for data. If it does not exist (as in a normal installation), it uses the standard configuration directory. This pattern lets a single executable work in both modes.
32.5 Linux Deployment
Binary Distribution
Linux deployment starts simple: copy the compiled binary to the target system. Ensure the executable bit is set:
chmod +x PennyWise
./PennyWise
This works, but the user must manually manage the binary, create their own shortcuts, and ensure dependencies are installed. For casual distribution (sending the binary to a colleague who knows Linux), this is sufficient.
.desktop File
Before packaging, you need a .desktop file — the Linux equivalent of a Windows shortcut. This file tells the desktop environment how to display your application in the application menu:
[Desktop Entry]
Name=PennyWise
Comment=Personal Finance Manager
GenericName=Finance Manager
Exec=pennywise %f
Icon=pennywise
Terminal=false
Type=Application
Categories=Office;Finance;
Keywords=finance;budget;expense;money;
MimeType=application/x-pennywise;
StartupNotify=true
Key fields:
Exec=pennywise %f— The command to run.%fis replaced with the file path if the user opens a.pwfile.Categories— Determines where the application appears in the menu.Office;Finance;puts it in the Office category.MimeType— Associates the application with a MIME type for file association.Keywords— Additional search terms for finding the application in the menu.StartupNotify=true— Shows a loading indicator while the application starts.
.deb Package (Debian/Ubuntu)
For Debian-based distributions (Debian, Ubuntu, Linux Mint, Pop!_OS), create a .deb package. The package structure mirrors the filesystem:
pennywise_1.0.0-1_amd64/
├── DEBIAN/
│ ├── control (package metadata)
│ ├── postinst (post-install script, optional)
│ └── prerm (pre-removal script, optional)
└── usr/
├── bin/
│ └── pennywise (the executable)
├── share/
│ ├── applications/
│ │ └── pennywise.desktop (desktop entry for app menu)
│ ├── icons/hicolor/256x256/apps/
│ │ └── pennywise.png (application icon)
│ ├── icons/hicolor/48x48/apps/
│ │ └── pennywise.png (smaller icon)
│ ├── icons/hicolor/16x16/apps/
│ │ └── pennywise.png (smallest icon)
│ ├── doc/pennywise/
│ │ ├── copyright (license file, required by Debian policy)
│ │ └── changelog.gz (compressed changelog)
│ └── mime/packages/
│ └── pennywise.xml (MIME type definition)
└── lib/
└── pennywise/ (optional: additional libraries or data)
The control file contains package metadata:
Package: pennywise
Version: 1.0.0-1
Section: utils
Priority: optional
Architecture: amd64
Depends: libsqlite3-0 (>= 3.7), libgtk2.0-0 (>= 2.24)
Installed-Size: 5120
Maintainer: Your Name <you@example.com>
Homepage: https://example.com/pennywise
Description: PennyWise Personal Finance Manager
A native desktop application for tracking personal expenses,
built with Free Pascal and Lazarus. Features include expense
categorization, budget tracking, charts, and database storage.
.
PennyWise stores data in a local SQLite database and requires
no server or internet connection.
Important details:
Depends— Lists runtime dependencies. The package manager ensures these are installed before PennyWise. List the minimum acceptable version.Installed-Size— Approximate size in kilobytes when installed.Architecture: amd64— Specifies the CPU architecture. Useamd64for 64-bit x86. For ARM64 (Raspberry Pi 4, etc.), usearm64.- The long description (after the short one-line description) uses a leading space for each line and
.for blank lines — this is Debian package policy.
The optional postinst script runs after installation — useful for updating the icon cache and desktop database:
#!/bin/sh
set -e
if [ "$1" = "configure" ]; then
update-desktop-database -q /usr/share/applications || true
gtk-update-icon-cache -q /usr/share/icons/hicolor || true
fi
The optional prerm script runs before removal — a good place for cleanup.
Build the package:
# Set correct permissions
chmod 755 pennywise_1.0.0-1_amd64/DEBIAN/control
chmod 755 pennywise_1.0.0-1_amd64/DEBIAN/postinst
chmod 755 pennywise_1.0.0-1_amd64/usr/bin/pennywise
# Build the .deb
dpkg-deb --build --root-owner-group pennywise_1.0.0-1_amd64
# Verify the package
lintian pennywise_1.0.0-1_amd64.deb
The --root-owner-group flag sets file ownership to root:root, which is required for proper installation. The lintian tool checks for common packaging errors.
Install the package:
sudo dpkg -i pennywise_1.0.0-1_amd64.deb
# If dependencies are missing:
sudo apt-get install -f
RPM Package (Fedora/RHEL)
For Red Hat-based distributions (Fedora, RHEL, CentOS, openSUSE), create an RPM package. You need a .spec file:
Name: pennywise
Version: 1.0.0
Release: 1%{?dist}
Summary: Personal Finance Manager
License: MIT
URL: https://example.com/pennywise
Source0: %{name}-%{version}.tar.gz
Requires: sqlite-libs >= 3.7
Requires: gtk2 >= 2.24
%description
PennyWise is a native desktop application for tracking personal
expenses, built with Free Pascal and Lazarus.
%install
mkdir -p %{buildroot}/usr/bin
mkdir -p %{buildroot}/usr/share/applications
mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps
install -m 755 pennywise %{buildroot}/usr/bin/pennywise
install -m 644 pennywise.desktop %{buildroot}/usr/share/applications/
install -m 644 pennywise.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/
%files
/usr/bin/pennywise
/usr/share/applications/pennywise.desktop
/usr/share/icons/hicolor/256x256/apps/pennywise.png
Build the RPM:
rpmbuild -ba pennywise.spec
AppImage
For a distribution-agnostic package, create an AppImage — a single file that runs on any Linux distribution without installation. The AppImage bundles the executable, libraries, and resources into one file.
The AppImage structure:
PennyWise.AppDir/
├── AppRun (entry point script or symlink)
├── pennywise.desktop (.desktop file)
├── pennywise.png (icon)
└── usr/
├── bin/
│ └── pennywise (the executable)
└── lib/
└── libsqlite3.so.0 (bundled libraries)
The AppRun file is typically a script:
#!/bin/bash
HERE="$(dirname "$(readlink -f "${0}")")"
export LD_LIBRARY_PATH="${HERE}/usr/lib:${LD_LIBRARY_PATH}"
exec "${HERE}/usr/bin/pennywise" "$@"
Build the AppImage using appimagetool:
# Download appimagetool
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
# Build
./appimagetool-x86_64.AppImage PennyWise.AppDir PennyWise-1.0.0-x86_64.AppImage
The result is a single file (e.g., PennyWise-1.0.0-x86_64.AppImage) that the user downloads, makes executable (chmod +x), and runs. No installation, no root privileges, no package manager. AppImages are popular for distributing applications that need to work across many Linux distributions.
Flatpak and Snap
Two newer packaging formats deserve mention:
Flatpak (flathub.org) — Sandboxed applications distributed through a central repository. Flatpak handles dependencies, updates, and security isolation. Creating a Flatpak requires a manifest file (JSON or YAML) and a build environment. Flatpak applications run in a sandbox with limited filesystem access — your application must request permissions for directories it needs to read or write. This security model is excellent for users but requires some adaptation in how you handle file paths and data storage.
Snap (snapcraft.io) — Canonical's packaging format, pre-installed on Ubuntu. Similar to Flatpak but uses a different infrastructure and confinement model. Snap packages auto-update from the Snap Store, making it easy for users to stay current.
Both formats are more complex to set up than .deb or AppImage but provide automatic updates and a broader distribution channel. Flathub hosts thousands of applications and is integrated into GNOME Software Center, making it a discoverable distribution platform. For a first release, start with .deb and AppImage; consider Flatpak or Snap when you have a user base requesting it.
Choosing a Linux Distribution Strategy
With so many packaging options, which should you support? Here is a practical recommendation:
- Start with a standalone binary. This works everywhere and requires no packaging knowledge.
- Add a .deb package. Debian and Ubuntu are the most popular desktop Linux distributions. A .deb package covers the largest audience with the least effort.
- Add an AppImage. For users on Fedora, Arch, openSUSE, or other non-Debian distributions. One file, universal compatibility.
- Add Flatpak or Snap only if you have a growing user base that requests it. The additional maintenance overhead is justified only with an active user community.
32.6 macOS Deployment
Application Bundles
macOS expects applications to be packaged as .app bundles — a specific directory structure that the Finder treats as a single item:
PennyWise.app/
└── Contents/
├── Info.plist (application metadata)
├── PkgInfo (package type indicator)
├── MacOS/
│ └── PennyWise (the executable)
├── Resources/
│ ├── pennywise.icns (application icon, macOS format)
│ └── en.lproj/ (English localization, optional)
│ └── InfoPlist.strings
└── Frameworks/ (bundled libraries, if any)
└── libsqlite3.dylib (optional — macOS includes SQLite)
The Info.plist file contains application metadata in XML format:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>PennyWise</string>
<key>CFBundleDisplayName</key>
<string>PennyWise</string>
<key>CFBundleIdentifier</key>
<string>com.example.pennywise</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleExecutable</key>
<string>PennyWise</string>
<key>CFBundleIconFile</key>
<string>pennywise</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright 2025 Your Name. All rights reserved.</string>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>pw</string>
</array>
<key>CFBundleTypeName</key>
<string>PennyWise Document</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
</array>
</dict>
</plist>
Key entries:
CFBundleIdentifier— A reverse-DNS identifier unique to your application. Used by macOS for app identification, keychain access, and preferences.CFBundleVersion— The build number. Increment for every build.CFBundleShortVersionString— The user-facing version number.NSHighResolutionCapable— Set totruefor Retina display support.LSMinimumSystemVersion— The minimum macOS version required.CFBundleDocumentTypes— File type associations. This entry makes.pwfiles openable with PennyWise.
Lazarus can generate the .app bundle structure automatically in the project options (Project > Project Options > Application > Create Application Bundle).
macOS Icon Format
macOS uses the .icns format for application icons, which contains multiple resolutions (16x16, 32x32, 128x128, 256x256, 512x512, and their @2x Retina versions). Convert your PNG icon set using the iconutil command:
# Create an iconset directory with all required sizes
mkdir pennywise.iconset
cp icon_16x16.png pennywise.iconset/icon_16x16.png
cp icon_32x32.png pennywise.iconset/icon_16x16@2x.png
cp icon_32x32.png pennywise.iconset/icon_32x32.png
cp icon_64x64.png pennywise.iconset/icon_32x32@2x.png
cp icon_128x128.png pennywise.iconset/icon_128x128.png
cp icon_256x256.png pennywise.iconset/icon_128x128@2x.png
cp icon_256x256.png pennywise.iconset/icon_256x256.png
cp icon_512x512.png pennywise.iconset/icon_256x256@2x.png
cp icon_512x512.png pennywise.iconset/icon_512x512.png
cp icon_1024x1024.png pennywise.iconset/icon_512x512@2x.png
# Convert to .icns
iconutil -c icns pennywise.iconset
DMG Distribution
Users expect macOS applications to be distributed as .dmg files (disk images). The user opens the DMG, sees the application and an alias to the Applications folder, and drags the app to Applications.
Create a basic DMG:
hdiutil create -volname "PennyWise" -srcfolder PennyWise.app -ov -format UDZO PennyWise-1.0.dmg
For a polished DMG with a custom background image and icon positioning (the kind you see from professional macOS applications), use the create-dmg tool:
create-dmg \
--volname "PennyWise" \
--volicon "pennywise.icns" \
--background "dmg-background.png" \
--window-pos 200 120 \
--window-size 600 400 \
--icon-size 100 \
--icon "PennyWise.app" 175 175 \
--hide-extension "PennyWise.app" \
--app-drop-link 425 175 \
"PennyWise-1.0.dmg" \
"PennyWise.app"
Code Signing and Notarization
macOS Gatekeeper warns users about unsigned applications. Starting with macOS Catalina (10.15), unsigned applications downloaded from the internet are blocked entirely.
For distribution outside the Mac App Store, you need:
- Apple Developer ID certificate ($99/year Apple Developer Program membership).
- Code signing:
codesign --sign "Developer ID Application: Your Name (TEAMID)" \
--deep --options runtime PennyWise.app
- Notarization — Apple scans the application for malware and issues a notarization ticket:
# Create a zip for notarization
ditto -c -k --keepParent PennyWise.app PennyWise.zip
# Submit for notarization
xcrun notarytool submit PennyWise.zip --apple-id "you@example.com" \
--password "app-specific-password" --team-id "TEAMID" --wait
# Staple the notarization ticket to the app
xcrun stapler staple PennyWise.app
For personal or educational use, users can right-click the app and choose "Open" to bypass the Gatekeeper warning. This shows a dialog saying the app is from an unidentified developer, and the user can choose to open it anyway.
Homebrew Cask
For technically savvy macOS users, distribution through Homebrew Cask is increasingly popular. Users install your application with:
brew install --cask pennywise
Creating a Homebrew Cask requires submitting a Ruby formula to the homebrew-cask repository that describes where to download the DMG, how to verify its checksum, and how to install it. This is a distribution channel for established applications — submit to Homebrew once you have a stable release and a reliable download URL.
📊 Practical Advice: macOS Deployment Complexity
macOS deployment is the most complex of the three platforms due to code signing and notarization requirements. If you are distributing PennyWise informally (to classmates, for a course project), the unsigned .app bundle with a right-click workaround is sufficient. If you are distributing commercially, invest in an Apple Developer account and set up the signing/notarization pipeline.
32.7 Cross-Compilation with Free Pascal
Free Pascal supports cross-compilation: compiling for a different target platform from your current machine. This means you can build a Linux binary on Windows, or a Windows binary on Linux.
Setting Up Cross-Compilation
- Install the Free Pascal cross-compilation target. For example, on Linux to target Windows:
# Install the cross-compiler for Windows 64-bit
sudo apt install fpc-source
# Or build fpc for the target:
make crossinstall CPU_TARGET=x86_64 OS_TARGET=win64
- In Lazarus, set the target platform in Project Options > Compiler Options > Config and Target: set Target OS and Target CPU.
- Rebuild.
The resulting binary runs on the target platform, not on your development machine.
Practical Cross-Compilation
# Build for Linux x64 from any platform
lazbuild --os=linux --cpu=x86_64 PennyWise.lpi
# Build for Windows x64 from any platform
lazbuild --os=win64 --cpu=x86_64 PennyWise.lpi
# Build for macOS x64 from any platform
lazbuild --os=darwin --cpu=x86_64 PennyWise.lpi
# Build for macOS arm64 (Apple Silicon) from any platform
lazbuild --os=darwin --cpu=aarch64 PennyWise.lpi
# Build for Linux ARM64 (Raspberry Pi 4)
lazbuild --os=linux --cpu=aarch64 PennyWise.lpi
Cross-Compilation Limitations
Cross-compilation for console applications is straightforward — the compiler generates machine code for the target CPU and links against the target's runtime library. Cross-compilation for GUI applications is more complex because the LCL widgetset libraries for the target platform must be available.
For practical cross-platform releases, two approaches work well:
-
Build on the target platform. Use a VM or a CI/CD service (GitHub Actions provides Windows, Linux, and macOS runners for free). This ensures you test on the actual target platform, not just compile for it.
-
Use Docker for Linux builds. A Docker container with Free Pascal and Lazarus installed provides a reproducible Linux build environment that can run on any host OS.
Automated CI/CD Builds
For a project like PennyWise that targets multiple platforms, a CI/CD pipeline automates the build process. Here is a simplified GitHub Actions workflow:
name: Build PennyWise
on: [push, pull_request]
jobs:
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install Lazarus
uses: gcarreno/setup-lazarus@v3
with:
lazarus-version: stable
- name: Build
run: lazbuild --build-mode=Release PennyWise.lpi
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: PennyWise-Windows
path: PennyWise.exe
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Lazarus
uses: gcarreno/setup-lazarus@v3
with:
lazarus-version: stable
- name: Build
run: lazbuild --build-mode=Release PennyWise.lpi
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: PennyWise-Linux
path: PennyWise
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install Lazarus
uses: gcarreno/setup-lazarus@v3
with:
lazarus-version: stable
- name: Build
run: lazbuild --build-mode=Release PennyWise.lpi
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: PennyWise-macOS
path: PennyWise.app
Every time you push code, the pipeline builds PennyWise on all three platforms and uploads the results as downloadable artifacts. This ensures that every change is tested across platforms.
📊 Practical Advice: Use VMs or CI for Cross-Platform Builds While Free Pascal's cross-compilation is technically capable, building in a virtual machine running the target OS (or using CI/CD) is often simpler and more reliable. This ensures you test on the actual target platform, not just compile for it.
32.8 Handling Resources and Data Files
Embedded Resources with lazres
Lazarus provides the lazres tool for creating resource files that can be compiled into the executable. This is useful for embedding images, templates, default data, or other files that the application needs at runtime.
# Create a resource file from multiple sources
lazres myresources.lrs icon16.png icon32.png default_categories.json
Include the resource in your unit:
{$I myresources.lrs}
Access the embedded data at runtime:
uses LResources;
procedure LoadDefaultCategories;
var
Stream: TLazarusResourceStream;
begin
Stream := TLazarusResourceStream.Create('default_categories', nil);
try
{ Parse the JSON from the embedded resource }
LoadCategoriesFromStream(Stream);
finally
Stream.Free;
end;
end;
Windows Resource Files
Free Pascal can embed Windows-format resources using the {$R} directive:
{$R my_data.res}
{ Access embedded data at runtime: }
var
Stream: TResourceStream;
begin
Stream := TResourceStream.Create(HInstance, 'MY_DATA', RT_RCDATA);
try
{ Use the stream... }
Stream.SaveToFile(GetTempDir + 'my_data.dat');
finally
Stream.Free;
end;
end;
Create the .res file from arbitrary data using the windres tool (included with Free Pascal) or the Lazarus resource compiler.
External Data Files
For data files that should be editable or replaceable (configuration files, templates, sample data), place them alongside the executable and use platform-appropriate paths to find them:
{ Application directory — where the executable lives }
AppDir := Application.Location;
{ Per-user configuration directory }
ConfigDir := GetAppConfigDir(False);
{ Per-user data directory }
DataDir := GetAppConfigDir(False) + 'data' + PathDelim;
{ System-wide configuration directory }
GlobalConfigDir := GetAppConfigDir(True);
Platform-specific paths:
| Function | Windows | Linux | macOS |
|---|---|---|---|
Application.Location |
C:\Program Files\PennyWise\ |
/usr/bin/ |
/Applications/PennyWise.app/Contents/MacOS/ |
GetAppConfigDir(False) |
C:\Users\Name\AppData\Local\PennyWise\ |
~/.config/PennyWise/ |
~/Library/Application Support/PennyWise/ |
GetAppConfigDir(True) |
C:\ProgramData\PennyWise\ |
/etc/PennyWise/ |
/Library/Application Support/PennyWise/ |
GetUserDir |
C:\Users\Name\ |
~/ |
~/ |
GetTempDir |
C:\Users\Name\AppData\Local\Temp\ |
/tmp/ |
/tmp/ or system temp |
Always create directories before writing to them:
procedure EnsureConfigDir;
var
Dir: string;
begin
Dir := GetAppConfigDir(False);
if not DirectoryExists(Dir) then
ForceDirectories(Dir);
end;
Use PathDelim instead of hardcoded path separators (\ or /) for cross-platform compatibility. Or use ConcatPaths from SysUtils:
FullPath := ConcatPaths([GetAppConfigDir(False), 'data', 'pennywise.db']);
First-Run Setup
PennyWise should handle first-run gracefully — creating the configuration directory, initializing the database, and optionally showing a welcome dialog:
procedure TfrmMain.CheckFirstRun;
var
ConfigDir: string;
begin
ConfigDir := GetAppConfigDir(False);
if not DirectoryExists(ConfigDir) then
begin
ForceDirectories(ConfigDir);
FIsFirstRun := True;
end
else
FIsFirstRun := FileExists(ConcatPaths([ConfigDir, 'pennywise.db']));
if FIsFirstRun then
begin
InitializeDatabase;
SeedDefaultCategories;
ShowWelcomeDialog;
end;
end;
32.9 Version Information and Branding
Version Info Resource
On Windows, version information appears in the file properties dialog (right-click > Properties > Details). Configure it in Project Options > Project Options > Version Info:
- File Version: 1.0.0.0
- Product Version: 1.0.0.0
- Company Name: Your Name or Organization
- File Description: PennyWise Personal Finance Manager
- Internal Name: PennyWise
- Legal Copyright: Copyright 2025 Your Name
- Original Filename: PennyWise.exe
- Product Name: PennyWise
Check "Use version info" to enable this feature. The version info is embedded in the Windows executable as a standard VERSIONINFO resource.
Build Date and Compiler Version
Free Pascal provides compile-time macros that embed useful information:
const
BuildDate = {$I %DATE%}; { e.g., '2025/03/15' }
BuildTime = {$I %TIME%}; { e.g., '14:30:45' }
FPCVersion = {$I %FPCVERSION%}; { e.g., '3.2.2' }
TargetOS = {$I %FPCTARGETOS%}; { e.g., 'linux' }
TargetCPU = {$I %FPCTARGETCPU%}; { e.g., 'x86_64' }
These are resolved at compile time, so they reflect the actual build environment. Use them in your About dialog and for diagnostic logging.
About Dialog
Every professional application has an About dialog (Help > About). Include:
- Application name and version
- Build date and target platform
- Free Pascal and Lazarus versions
- Copyright notice
- License information
- Optionally: a link to the project website
procedure TfrmMain.actAboutExecute(Sender: TObject);
const
AboutText =
'PennyWise Personal Finance Manager' + LineEnding +
'Version 1.0.0' + LineEnding +
LineEnding +
'Build: %s %s' + LineEnding +
'Platform: %s/%s' + LineEnding +
'FPC: %s' + LineEnding +
'LCL: %s' + LineEnding +
LineEnding +
'Copyright 2025 Your Name' + LineEnding +
'Licensed under the MIT License';
begin
MessageDlg('About PennyWise',
Format(AboutText, [
{$I %DATE%}, {$I %TIME%},
{$I %FPCTARGETOS%}, {$I %FPCTARGETCPU%},
{$I %FPCVERSION%},
LCLVersion]),
mtInformation, [mbOK], 0);
end;
For a more polished About dialog, create a dedicated form (TfrmAbout) with the application icon, styled text, and a clickable URL to the project website. Use OpenURL from the LCLIntf unit to open links in the default browser:
uses LCLIntf;
procedure TfrmAbout.lblWebsiteClick(Sender: TObject);
begin
OpenURL('https://example.com/pennywise');
end;
32.10 Auto-Update Strategies
Once PennyWise is distributed, you will eventually release version 1.1. How do users get the update?
Manual Updates
The simplest approach: announce the update on your website or email list, and users download and install the new version. The installer overwrites the old files. The database is preserved (it lives in the user's config directory, not the application directory).
Check-for-Updates Dialog
Add a "Check for Updates" option in the Help menu that queries a server for the latest version:
procedure TfrmMain.actCheckUpdatesExecute(Sender: TObject);
var
Client: TFPHTTPClient;
Response, LatestVersion: string;
begin
Client := TFPHTTPClient.Create(nil);
try
try
Response := Client.Get('https://example.com/pennywise/version.txt');
LatestVersion := Trim(Response);
if LatestVersion > AppVersion then
MessageDlg('Update Available',
Format('Version %s is available. You are running %s.' + LineEnding +
'Visit the website to download the update.',
[LatestVersion, AppVersion]),
mtInformation, [mbOK], 0)
else
MessageDlg('Up to Date',
'You are running the latest version.',
mtInformation, [mbOK], 0);
except
on E: Exception do
MessageDlg('Check Failed',
'Could not check for updates: ' + E.Message,
mtWarning, [mbOK], 0);
end;
finally
Client.Free;
end;
end;
Host a simple version.txt file on your web server containing just the version number (e.g., 1.1.0). The application downloads it, compares to the current version, and informs the user.
Windows: Inno Setup Updater
Inno Setup can detect an existing installation and upgrade it. If the user runs the new installer while the old version is installed, Inno Setup updates the files in place, preserving the user's data and settings.
Linux: Package Repository
For serious distribution, set up an APT repository (for .deb packages) or a Flatpak/Snap channel. Users add your repository and receive updates through their system's package manager:
sudo apt update
sudo apt upgrade pennywise
Setting up a personal APT repository is beyond the scope of this chapter but is well-documented in the Debian Wiki.
macOS: Sparkle Framework
The Sparkle framework (sparkle-project.org) provides an auto-update mechanism for macOS applications. It checks for updates via an RSS feed, downloads the new version, and applies the update. Integration with a Lazarus application requires some bridging code but is feasible.
32.11 Project Checkpoint: PennyWise Deployment
Let us package PennyWise for all three platforms.
Build Script
Create a shell script that builds release versions for the current platform:
#!/bin/bash
# build-release.sh — Build PennyWise for distribution
PROJECT="PennyWise.lpi"
VERSION="1.0.0"
echo "Building PennyWise v$VERSION — Release Mode"
echo "============================================="
# Clean previous builds
rm -rf release/
mkdir -p release/windows release/linux release/macos
# Build for current platform (assumes native build)
lazbuild --build-mode=Release "$PROJECT"
if [ $? -ne 0 ]; then
echo "ERROR: Build failed!"
exit 1
fi
# Copy executable and dependencies based on platform
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
cp PennyWise.exe release/windows/
cp sqlite3.dll release/windows/
cp pennywise.ico release/windows/
cp README.txt release/windows/
echo "Windows build ready in release/windows/"
echo "Files:"
ls -la release/windows/
echo ""
echo "Next steps:"
echo " 1. Test: run release/windows/PennyWise.exe"
echo " 2. Package: compile PennyWise.iss with Inno Setup"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
cp PennyWise release/linux/
chmod +x release/linux/PennyWise
cp pennywise.desktop release/linux/
cp pennywise.png release/linux/
echo "Linux build ready in release/linux/"
echo "Files:"
ls -la release/linux/
echo ""
echo "Dependencies:"
ldd release/linux/PennyWise | grep -v "linux-vdso"
echo ""
echo "Next steps:"
echo " 1. Test: ./release/linux/PennyWise"
echo " 2. Package: build .deb or AppImage"
elif [[ "$OSTYPE" == "darwin"* ]]; then
cp -r PennyWise.app release/macos/
echo "macOS build ready in release/macos/"
echo "Files:"
ls -la release/macos/PennyWise.app/Contents/MacOS/
echo ""
echo "Next steps:"
echo " 1. Test: open release/macos/PennyWise.app"
echo " 2. Sign: codesign --sign 'Developer ID' --deep PennyWise.app"
echo " 3. Package: create DMG"
fi
echo ""
echo "Build complete. Version: $VERSION"
Deployment Checklist
✅ PennyWise Deployment Checklist - [ ] Release build compiled with -O2 optimization and symbol stripping (-Xs) - [ ] Smart linking enabled (-XX -CX) for minimum executable size - [ ] Application icon set in project options (multi-size .ico / .icns) - [ ] Version information configured (Windows version resource) - [ ] About dialog shows version, build date, platform, and license - [ ] DPI awareness enabled (Application.Scaled := True on Windows) - [ ] SQLite3 library included (Windows) or documented as dependency (Linux) - [ ] Configuration files use platform-appropriate paths (GetAppConfigDir) - [ ] First-run initialization creates config directory and seeds defaults - [ ] Tested on a clean system without development tools installed - [ ] Windows: Inno Setup installer created with shortcuts, icon, file association - [ ] Windows: Tested on Windows 10 and Windows 11 - [ ] Linux: .deb package created with correct dependencies in control file - [ ] Linux: .desktop file included for application menu integration - [ ] Linux: AppImage created for distribution-agnostic deployment - [ ] macOS: .app bundle created with Info.plist and .icns icon - [ ] macOS: DMG optionally prepared for distribution - [ ] macOS: Code signing and notarization completed (for public distribution) - [ ] README with installation instructions included
Final Testing
Before declaring your application ready for distribution, perform these tests on each target platform:
-
Install on a clean system. Use a virtual machine with a fresh OS installation. Does the installer work? Does the application launch? Can you add expenses, save data, and reopen the application?
-
Test on different screen resolutions. Try 1920x1080, 1366x768, and 3840x2160 (4K with DPI scaling at 150%). Do controls overlap? Is text readable? Does the pie chart render correctly?
-
Test with different regional settings. Does the number format work correctly when the system uses comma as a decimal separator (common in Europe)? Does the date format handle DD/MM/YYYY vs. MM/DD/YYYY?
-
Test the upgrade path. If a user already has version 0.9 installed with data, does installing version 1.0 preserve their data? Does the database migration run correctly?
-
Test file associations. On Windows, does double-clicking a
.pwfile open PennyWise? On macOS, does dragging a.pwfile onto the PennyWise icon open it? -
Test error conditions. What happens if the database file is locked? What happens if the disk is full? What happens if the user does not have write permission to the config directory?
-
Test uninstallation. On Windows, does the uninstaller remove all files and registry entries? On Linux, does
dpkg -r pennywiseclean up correctly?
32.12 Deployment Anti-Patterns
Before we close, here are common deployment mistakes to avoid:
Shipping a debug build. The 20 MB executable with debug symbols is not just larger — it is slower and may behave differently from the release build. Always ship release builds.
Hardcoded paths. If your code contains C:\Users\John\PennyWise\data.db, it will fail on every other computer. Always use GetAppConfigDir, Application.Location, or other platform-independent path functions.
Missing dependencies. Test on a clean system. If you developed on a machine with dozens of libraries installed, your application might depend on one of them without you realizing it.
Assuming administrator privileges. On modern operating systems, users often do not have admin rights. Your application should work without them. Write data to user-writable directories (GetAppConfigDir(False)), not to the application directory or system directories.
Ignoring regional settings. Different countries use different decimal separators (. vs ,), different date formats, and different currencies. Use FormatFloat with explicit format strings and FormatDateTime for dates. Do not assume the decimal separator is a period.
No error messages for missing libraries. If your application depends on sqlite3.dll and it is missing, display a clear error message ("PennyWise requires sqlite3.dll. Please download it from...") rather than letting the operating system display a cryptic error.
Forgetting to test the installer on a clean system. The most common deployment failure: the application works on the developer's machine but crashes on the user's. The developer's machine has libraries, environment variables, registry entries, and configuration files that accumulated during development. A fresh virtual machine reveals what the user will actually experience.
Shipping without an uninstaller. On Windows, users expect to be able to uninstall applications through Settings > Apps. Inno Setup provides this automatically. On Linux, a proper .deb package allows apt remove pennywise. Applications that scatter files across the filesystem without providing a clean removal path frustrate users.
Not handling the database location correctly. During development, PennyWise's database might live in the project directory. In the installed application, it must live in a user-writable location. Never store user data in the application directory — on modern Windows, C:\Program Files\ is read-only for non-administrator users.
32.13 Summary
This chapter covered the often-overlooked but critically important last step: getting your application from your development machine to the user's hands.
What we covered:
- Release builds use optimization (-O2 or -O3), symbol stripping (-Xs), and smart linking (-XX -CX) to produce small, fast executables. Debug builds include full debugging support for development. Build modes in Lazarus let you switch between configurations with one click.
- Static vs. dynamic linking determines whether dependencies are bundled in the executable or shipped as separate files. Pascal statically links the runtime; SQLite is typically dynamically linked. Use
ldd(Linux),otool -L(macOS), ordumpbin(Windows) to check dependencies. - Windows deployment ranges from simple folder distribution (portable app) to professional Inno Setup installers with shortcuts, icons, file associations, registry entries, and silent installation support. DPI awareness through
Application.Scaledensures crisp rendering on high-DPI displays. - Linux deployment options include raw binaries,
.debpackages for Debian/Ubuntu (withcontrolfile,.desktopfile, and dependency declarations), RPM packages for Fedora/RHEL, and AppImages for distribution-agnostic packaging. Flatpak and Snap provide additional distribution channels. - macOS deployment requires
.appbundles withInfo.plistmetadata,.icnsicons, and optionally DMG distribution. Code signing and notarization are required for public distribution. The process is the most complex of the three platforms. - Cross-compilation with Free Pascal can target different platforms from a single machine. For production releases, building on the target platform (or using CI/CD with GitHub Actions) is more reliable. Automated build pipelines ensure every change is tested across all platforms.
- Resources and data files can be embedded in the executable (using
lazresor{$R}directives) or stored alongside it. UseGetAppConfigDirfor platform-appropriate configuration paths,PathDelimfor cross-platform file paths, andForceDirectoriesto create directories safely. - Version information and branding — application icon, version resource, compile-time macros (
{$I %DATE%}`, `{$I %FPCVERSION%}), and About dialog — give your application a professional identity. - Auto-update strategies range from manual downloads to check-for-updates dialogs to platform-specific update mechanisms (Inno Setup upgrades, APT repositories, Sparkle framework).
- PennyWise is now a distributable application: a compiled, stripped, packaged binary that anyone can install and use. Rosa can hand it to a friend on a USB drive. Tomas can put it on his roommate's laptop. The Inno Setup installer makes it look professional. The AppImage makes it run anywhere.
This concludes Part V. Over six chapters, PennyWise has transformed from a console program into a professional desktop application with a GUI, database backend, charts, menus, and cross-platform packaging. The business logic from Parts I–IV carries forward unchanged — the GUI is a new presentation layer over the same well-designed core.
In Part VI, we look beyond the GUI: unit architecture, networking, threading, and the system-level capabilities that turn a good application into a great one.
"The best programs are written so that computing machines can perform them quickly and so that human beings can understand them clearly." — Donald Knuth