Case Study 2: Cross-Platform Gotchas

Overview

Lazarus promises cross-platform development. The promise is real — the same code does compile and run on Windows, Linux, and macOS. But "compiles and runs" is not the same as "works correctly and looks good." This case study catalogs the most common cross-platform issues and their solutions, drawn from real-world Lazarus development experience.


Gotcha 1: File Paths

The Problem

{ This works on Windows but fails on Linux/macOS }
ConfigFile := 'C:\Users\Rosa\AppData\PennyWise\config.ini';

{ This works on Linux but fails on Windows }
ConfigFile := '/home/rosa/.config/pennywise/config.ini';

The Solution

Never hardcode paths. Use platform-agnostic functions:

ConfigFile := GetAppConfigDir(False) + 'config.ini';
DataFile := ConcatPaths([Application.Location, 'data', 'sample.csv']);
TempFile := GetTempDir + 'pennywise_export.csv';
HomeDir := GetUserDir;

Always use PathDelim or ConcatPaths for path construction. Never assume \ or /.


Gotcha 2: Line Endings

The Problem

Text files created on Windows use CR+LF (carriage return + line feed), while Linux uses LF only and classic macOS used CR only. Reading a Windows file on Linux may show ^M characters at the end of each line.

The Solution

Free Pascal's TStringList.LoadFromFile and TMemo.Lines.LoadFromFile handle line ending conversion automatically. For manual text processing, use LineEnding constant instead of hardcoded #13#10:

Output := 'Line 1' + LineEnding + 'Line 2' + LineEnding;

For files that must use a specific line ending (e.g., CSV exports for Windows Excel), write explicitly:

Write(F, 'Header1,Header2');
Write(F, #13#10);  { force Windows line endings }

Gotcha 3: Font Differences

The Problem

Fonts that exist on Windows may not exist on Linux or macOS:

Memo1.Font.Name := 'Consolas';    { Windows only }
Memo1.Font.Name := 'SF Mono';      { macOS only }
Memo1.Font.Name := 'Ubuntu Mono';  { Ubuntu only }

The Solution

Use generic font family names or platform conditionals:

{$IFDEF WINDOWS}
  Memo1.Font.Name := 'Consolas';
{$ENDIF}
{$IFDEF DARWIN}
  Memo1.Font.Name := 'Menlo';
{$ENDIF}
{$IFDEF LINUX}
  Memo1.Font.Name := 'DejaVu Sans Mono';
{$ENDIF}

Or use 'monospace' as a generic name that each platform resolves to its default monospaced font.


Gotcha 4: DPI and Scaling

The Problem

Modern displays range from 96 DPI (standard) to 288 DPI (Retina/4K). Controls positioned with absolute pixel coordinates at 96 DPI become tiny on 4K displays.

The Solution

  1. Set Application.Scaled := True in the .lpr file.
  2. Use Anchors and Align instead of absolute positions.
  3. Use Font.Size (points) instead of Font.Height (pixels).
  4. Test at 100%, 150%, and 200% scaling.

For custom drawing, account for the scaling factor:

var
  Scale: Double;
begin
  Scale := Screen.PixelsPerInch / 96;
  Canvas.Pen.Width := Round(2 * Scale);
  Canvas.Font.Size := Round(10 * Scale);
end;

Gotcha 5: Menu Bar Location (macOS)

The Problem

On macOS, the menu bar is at the top of the screen, not on the application window. If your application relies on the menu being part of the window layout (e.g., calculating available space by subtracting menu height), the calculation will be wrong on macOS.

The Solution

Do not make assumptions about menu bar position. The LCL handles the macOS menu bar automatically. Use Align := alClient for the main content area, and it will fill the correct space on each platform.


Gotcha 6: Dialog Button Order

The Problem

Windows convention: OK on the left, Cancel on the right. macOS convention: Cancel on the left, OK on the right (and the default/action button is rightmost). Linux: Follows GTK or Qt conventions, which vary.

The Solution

Use MessageDlg instead of custom dialog forms for standard confirmations — the LCL handles button ordering for each platform. For custom dialogs, use TButtonPanel which automatically arranges buttons according to platform conventions:

ButtonPanel1.ShowButtons := [pbOK, pbCancel];
{ The LCL orders them correctly for each platform }

Gotcha 7: File System Case Sensitivity

The Problem

Windows file systems are case-insensitive: Config.ini and config.ini are the same file. Linux file systems are case-sensitive: they are different files. macOS is case-insensitive by default but case-preserving.

If your code saves a file as PennyWise.db but loads it as pennywise.db, it works on Windows and macOS but fails on Linux.

The Solution

Be consistent with file names. Choose a convention (all lowercase is safest) and stick to it. Never rely on case-insensitive matching.

{ Safe: consistent case }
const
  DatabaseFileName = 'pennywise.db';
  ConfigFileName = 'config.ini';

Gotcha 8: Executable Permissions (Linux/macOS)

The Problem

On Linux and macOS, files are not executable by default. If you distribute a binary in a ZIP archive, the executable permission bit may be lost during extraction.

The Solution

Include a README that tells users to run chmod +x PennyWise. Or distribute as a .deb package or AppImage, which preserves permissions. In a shell script:

chmod +x "$APP_DIR/PennyWise"

Gotcha 9: GTK Dark Themes (Linux)

The Problem

If a Linux user has a dark GTK theme, your application inherits it. Text with hardcoded black color becomes invisible on a dark background. Panels with hardcoded white backgrounds look wrong.

The Solution

Avoid hardcoding colors. Use clDefault and system color constants:

{ Bad — invisible on dark themes }
Label1.Font.Color := clBlack;
Panel1.Color := clWhite;

{ Good — respects system theme }
Label1.Font.Color := clDefault;
Panel1.Color := clDefault;
{ Or use: Panel1.ParentColor := True; }

For charts and custom drawing where you need specific colors, draw on a white background that you control:

Canvas.Brush.Color := clWhite;
Canvas.FillRect(0, 0, Width, Height);
{ Now you know the background is white, so dark text works }

Testing Checklist

Before declaring your application cross-platform ready:

  • [ ] All file paths use GetAppConfigDir, Application.Location, PathDelim, or ConcatPaths
  • [ ] No hardcoded font names without platform conditionals
  • [ ] Application.Scaled := True is set
  • [ ] No hardcoded colors on standard controls (use clDefault)
  • [ ] Custom drawing uses a controlled background color
  • [ ] File names use consistent casing (preferably all lowercase)
  • [ ] Tested on Windows 10+, Ubuntu 22.04+, and macOS 12+
  • [ ] Tested at 100% and 150%+ DPI scaling
  • [ ] Tested with non-US regional settings (comma decimal separator)
  • [ ] MessageDlg used for standard confirmations (correct button order)

Lessons Learned

  1. "Write once, compile everywhere" is real but not automatic. The code compiles, but visual polish and edge cases require platform-specific attention.
  2. Use platform-agnostic APIs. Free Pascal and the LCL provide functions for every platform difference — paths, line endings, fonts, DPI. Use them.
  3. Test on real platforms. Virtual machines are your best friend. A bug you catch in a VM is a bug your users never see.
  4. The LCL handles most differences. Button order, menu bar location, file dialogs — the LCL adapts automatically. Resist the urge to override platform conventions.
  5. Respect the user's system. Do not hardcode colors, fonts, or paths. Let the operating system's choices flow through to your application.