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
- Set
Application.Scaled := Truein the .lpr file. - Use
AnchorsandAligninstead of absolute positions. - Use
Font.Size(points) instead ofFont.Height(pixels). - 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, orConcatPaths - [ ] No hardcoded font names without platform conditionals
- [ ]
Application.Scaled := Trueis 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
- "Write once, compile everywhere" is real but not automatic. The code compiles, but visual polish and edge cases require platform-specific attention.
- Use platform-agnostic APIs. Free Pascal and the LCL provide functions for every platform difference — paths, line endings, fonts, DPI. Use them.
- Test on real platforms. Virtual machines are your best friend. A bug you catch in a VM is a bug your users never see.
- The LCL handles most differences. Button order, menu bar location, file dialogs — the LCL adapts automatically. Resist the urge to override platform conventions.
- Respect the user's system. Do not hardcode colors, fonts, or paths. Let the operating system's choices flow through to your application.