🔝 Retour au Sommaire
Une fois votre projet Delphi porté vers FreePascal/Lazarus (section 26.7.1), l'étape suivante consiste à l'adapter pour une véritable compatibilité Linux. La simple compilation sous Linux ne suffit pas : une application véritablement multi-plateforme doit respecter les conventions Linux, s'intégrer harmonieusement dans l'environnement desktop, et offrir une expérience utilisateur native.
Cette section vous guide à travers toutes les adaptations nécessaires pour transformer votre application Windows en une application Linux de qualité professionnelle.
Mauvaise approche :
// Application qui "fonctionne" sur Linux mais reste Windows-centrée
if FileExists('C:\Config\app.ini') then
LoadConfig('C:\Config\app.ini');Bonne approche :
// Application véritablement multi-plateforme
function GetConfigFile: String;
begin
{$IFDEF WINDOWS}
Result := GetEnvironmentVariable('APPDATA') + '\MyApp\config.ini';
{$ENDIF}
{$IFDEF UNIX}
Result := GetEnvironmentVariable('HOME') + '/.config/myapp/config.ini';
{$ENDIF}
end;| Aspect | Windows | Linux | Impact |
|---|---|---|---|
| Système de fichiers | Insensible à la casse | Sensible à la casse | |
| Séparateur de chemin | \ backslash |
/ slash |
|
| Fin de ligne | CRLF (\r\n) |
LF (\n) |
|
| Lecteurs | C:, D:, etc. | Point de montage unique / |
|
| Exécutables | .exe, .dll | Sans extension, .so | |
| Registry | Centralisée | Fichiers texte distribués | |
| Permissions | ACL complexes | Modèle Unix (rwx) | |
| Services | Service Manager | systemd/init | |
| Encodage | ANSI/UTF-16 historique | UTF-8 natif | |
| Interface | Win32 API | GTK/Qt/X11 | |
| Utilisateurs | Admin/User | root/user | |
| Raccourcis | .lnk | .desktop |
1. Système de fichiers hiérarchique unique
Windows: Linux:
C:\ /
├── Program Files\ ├── usr/
├── Users\ │ ├── bin/ (exécutables)
└── Windows\ │ ├── lib/ (bibliothèques)
│ └── share/ (données partagées)
D:\ ├── home/
└── Data\ │ └── username/ (données utilisateur)
├── etc/ (configuration système)
├── var/ (données variables)
└── tmp/ (temporaire)
2. Sensibilité à la casse
// Windows : tous équivalents
'MyFile.txt'
'myfile.txt'
'MYFILE.TXT'
// Linux : tous différents !
'MyFile.txt' ≠ 'myfile.txt' ≠ 'MYFILE.TXT'3. Permissions Unix
-rwxr-xr-x 1 user group 12345 Jan 15 10:30 myapp
│││││││││
│││││││└└─ Autres : exécution (x)
││││││└─── Autres : lecture (r)
│││││└──── Groupe : exécution (x)
││││└───── Groupe : lecture (r)
│││└────── Propriétaire : exécution (x)
││└─────── Propriétaire : écriture (w)
│└──────── Propriétaire : lecture (r)
└───────── Type : - (fichier normal), d (répertoire)FreePascal fournit des fonctions qui s'adaptent automatiquement à la plateforme.
Fonctions recommandées :
uses
SysUtils, FileUtil, LazFileUtils;
// Chemin du répertoire de l'application
function GetAppPath: String;
begin
Result := ExtractFilePath(ParamStr(0));
// Windows: C:\Program Files\MyApp\
// Linux: /opt/myapp/
end;
// Répertoire temporaire
function GetTempDir: String;
begin
Result := GetTempDir(True); // LazFileUtils
// Windows: C:\Users\xxx\AppData\Local\Temp\
// Linux: /tmp/
end;
// Répertoire utilisateur
function GetUserDir: String;
begin
Result := GetUserDir; // SysUtils
// Windows: C:\Users\username\
// Linux: /home/username/
end;
// Répertoire de configuration utilisateur
function GetAppConfigDir: String;
begin
Result := GetAppConfigDir(False); // LazFileUtils
// Windows: C:\Users\xxx\AppData\Roaming\
// Linux: /home/username/.config/
end;
// Construire un chemin multi-plateforme
function BuildPath(const Parts: array of String): String;
var
Part: String;
begin
Result := '';
for Part in Parts do
Result := IncludeTrailingPathDelimiter(Result) + Part;
// Utilise automatiquement \ ou / selon l'OS
end;
// Utilisation
var
ConfigFile: String;
begin
ConfigFile := BuildPath([GetAppConfigDir, 'myapp', 'config.ini']);
// Windows: C:\Users\xxx\AppData\Roaming\myapp\config.ini
// Linux: /home/username/.config/myapp/config.ini
end;Créez une unité centrale pour gérer tous les chemins :
unit AppPaths;
{$mode objfpc}{$H+}
interface
uses
SysUtils, FileUtil, LazFileUtils;
type
TAppPaths = class
private
class var FAppName: String;
public
class constructor Create;
// Propriétés de classe
class property AppName: String read FAppName write FAppName;
// Méthodes
class function GetAppDir: String;
class function GetConfigDir: String;
class function GetDataDir: String;
class function GetCacheDir: String;
class function GetLogDir: String;
class function GetConfigFile: String;
class function GetLogFile: String;
// Utilitaires
class procedure EnsureDirectoryExists(const Dir: String);
end;
implementation
class constructor TAppPaths.Create;
begin
FAppName := 'MyApp'; // Nom par défaut
end;
class function TAppPaths.GetAppDir: String;
begin
Result := ExtractFilePath(ParamStr(0));
end;
class function TAppPaths.GetConfigDir: String;
begin
{$IFDEF WINDOWS}
Result := GetAppConfigDir(False) + AppName + PathDelim;
{$ENDIF}
{$IFDEF UNIX}
Result := GetUserDir + '.config' + PathDelim +
LowerCase(AppName) + PathDelim;
{$ENDIF}
EnsureDirectoryExists(Result);
end;
class function TAppPaths.GetDataDir: String;
begin
{$IFDEF WINDOWS}
Result := GetAppConfigDir(False) + AppName + PathDelim + 'Data' + PathDelim;
{$ENDIF}
{$IFDEF UNIX}
Result := GetUserDir + '.local' + PathDelim + 'share' + PathDelim +
LowerCase(AppName) + PathDelim;
{$ENDIF}
EnsureDirectoryExists(Result);
end;
class function TAppPaths.GetCacheDir: String;
begin
{$IFDEF WINDOWS}
Result := GetAppConfigDir(False) + AppName + PathDelim + 'Cache' + PathDelim;
{$ENDIF}
{$IFDEF UNIX}
Result := GetUserDir + '.cache' + PathDelim +
LowerCase(AppName) + PathDelim;
{$ENDIF}
EnsureDirectoryExists(Result);
end;
class function TAppPaths.GetLogDir: String;
begin
{$IFDEF WINDOWS}
Result := GetAppConfigDir(False) + AppName + PathDelim + 'Logs' + PathDelim;
{$ENDIF}
{$IFDEF UNIX}
Result := GetUserDir + '.local' + PathDelim + 'share' + PathDelim +
LowerCase(AppName) + PathDelim + 'logs' + PathDelim;
{$ENDIF}
EnsureDirectoryExists(Result);
end;
class function TAppPaths.GetConfigFile: String;
begin
Result := GetConfigDir + 'config.ini';
end;
class function TAppPaths.GetLogFile: String;
begin
Result := GetLogDir + 'app.log';
end;
class procedure TAppPaths.EnsureDirectoryExists(const Dir: String);
begin
if not DirectoryExists(Dir) then
ForceDirectories(Dir);
end;
end.Utilisation :
uses
AppPaths;
procedure SaveSettings;
begin
TAppPaths.AppName := 'MyApplication';
SaveToFile(TAppPaths.GetConfigFile);
WriteLn('Configuration saved to: ', TAppPaths.GetConfigFile);
// Windows: C:\Users\xxx\AppData\Roaming\MyApplication\config.ini
// Linux: /home/username/.config/myapplication/config.ini
end;
procedure WriteLog(const Msg: String);
var
F: TextFile;
begin
AssignFile(F, TAppPaths.GetLogFile);
try
if FileExists(TAppPaths.GetLogFile) then
Append(F)
else
Rewrite(F);
WriteLn(F, FormatDateTime('yyyy-mm-dd hh:nn:ss', Now), ' - ', Msg);
finally
CloseFile(F);
end;
end;Problème :
// Windows : fonctionne
if FileExists('MyFile.TXT') then
LoadFile('myfile.txt'); // OK, même fichier
// Linux : échoue silencieusement
if FileExists('MyFile.TXT') then // False
LoadFile('myfile.txt'); // Pas exécuté si fichier = MyFile.TXTSolutions :
// Solution 1 : Toujours utiliser la même casse
// Convention : minuscules pour Linux
const
CONFIG_FILE = 'config.ini'; // Pas 'Config.INI' ou 'CONFIG.INI'
// Solution 2 : Recherche insensible à la casse
function FindFileIgnoreCase(const FileName: String): String;
var
Dir, Name: String;
SR: TSearchRec;
begin
Result := '';
Dir := ExtractFilePath(FileName);
Name := ExtractFileName(FileName);
if FindFirst(Dir + '*', faAnyFile, SR) = 0 then
begin
repeat
if CompareText(SR.Name, Name) = 0 then // Insensible à la casse
begin
Result := Dir + SR.Name;
Break;
end;
until FindNext(SR) <> 0;
FindClose(SR);
end;
end;
// Utilisation
var
ActualFile: String;
begin
ActualFile := FindFileIgnoreCase('/home/user/myfile.txt');
if ActualFile <> '' then
LoadFile(ActualFile);
end;
// Solution 3 : Normaliser les noms à la création
function NormalizeFileName(const FileName: String): String;
begin
{$IFDEF UNIX}
Result := LowerCase(FileName); // Tout en minuscules sur Linux
{$ELSE}
Result := FileName;
{$ENDIF}
end;Approche Windows (Registry) :
uses
Registry;
procedure SaveSettingToRegistry(const Key, Value: String);
var
Reg: TRegistry;
begin
Reg := TRegistry.Create;
try
Reg.RootKey := HKEY_CURRENT_USER;
Reg.OpenKey('Software\MyApp\Settings', True);
Reg.WriteString(Key, Value);
finally
Reg.Free;
end;
end;Approche multi-plateforme (INI) :
uses
IniFiles;
procedure SaveSetting(const Key, Value: String);
var
Ini: TIniFile;
begin
Ini := TIniFile.Create(TAppPaths.GetConfigFile);
try
Ini.WriteString('Settings', Key, Value);
finally
Ini.Free;
end;
end;Approche moderne (JSON) :
uses
fpjson, jsonparser;
procedure SaveSettings(const Settings: TJSONObject);
var
F: TextFile;
begin
AssignFile(F, TAppPaths.GetConfigDir + 'settings.json');
try
Rewrite(F);
WriteLn(F, Settings.FormatJSON);
finally
CloseFile(F);
end;
end;
procedure LoadSettings(out Settings: TJSONObject);
var
JSONString: String;
begin
JSONString := ReadFileToString(TAppPaths.GetConfigDir + 'settings.json');
Settings := TJSONObject(GetJSON(JSONString));
end;
// Utilisation
var
Settings: TJSONObject;
begin
Settings := TJSONObject.Create;
try
Settings.Add('language', 'fr');
Settings.Add('theme', 'dark');
Settings.Add('fontSize', 12);
SaveSettings(Settings);
finally
Settings.Free;
end;
end;Sous Linux, distinguer :
// Configuration GLOBALE (tous les utilisateurs)
// Nécessite sudo pour écrire
const
SYSTEM_CONFIG = '/etc/myapp/config.ini';
// Configuration UTILISATEUR (utilisateur courant)
// Pas besoin de privilèges
function GetUserConfig: String;
begin
Result := GetUserDir + '.config/myapp/config.ini';
end;
// Ordre de priorité recommandé
function LoadConfiguration: TIniFile;
begin
// 1. Config utilisateur (prioritaire)
if FileExists(GetUserConfig) then
Result := TIniFile.Create(GetUserConfig)
// 2. Sinon config système
else if FileExists(SYSTEM_CONFIG) then
Result := TIniFile.Create(SYSTEM_CONFIG)
// 3. Sinon créer config utilisateur par défaut
else
begin
Result := TIniFile.Create(GetUserConfig);
// Initialiser avec valeurs par défaut
Result.WriteString('General', 'Language', 'en');
end;
end;Lecture des permissions :
uses
BaseUnix;
function GetFilePermissions(const FileName: String): Integer;
var
Info: Stat;
begin
if FpStat(FileName, Info) = 0 then
Result := Info.st_mode and $1FF // 9 derniers bits = permissions
else
Result := 0;
end;
function IsExecutable(const FileName: String): Boolean;
var
Perm: Integer;
begin
Perm := GetFilePermissions(FileName);
Result := (Perm and S_IXUSR) <> 0; // Bit exécution propriétaire
end;
// Vérifier si le fichier est lisible
function IsReadable(const FileName: String): Boolean;
var
Perm: Integer;
begin
Perm := GetFilePermissions(FileName);
Result := (Perm and S_IRUSR) <> 0;
end;uses
BaseUnix;
// Rendre un fichier exécutable
procedure MakeExecutable(const FileName: String);
begin
FpChmod(FileName, S_IRWXU or S_IRGRP or S_IXGRP or S_IROTH or S_IXOTH);
// rwxr-xr-x : 755
end;
// Permissions lecture seule
procedure MakeReadOnly(const FileName: String);
begin
FpChmod(FileName, S_IRUSR or S_IRGRP or S_IROTH);
// r--r--r-- : 444
end;
// Permissions privées (utilisateur seulement)
procedure MakePrivate(const FileName: String);
begin
FpChmod(FileName, S_IRUSR or S_IWUSR);
// rw------- : 600
end;procedure SafeWriteToFile(const FileName, Content: String);
begin
try
WriteStringToFile(FileName, Content);
except
on E: EInOutError do
begin
// Erreur d'accès, probablement permissions
if E.ErrorCode = 13 then // Permission denied
begin
ShowMessage('Permission refusée. ' +
'Lancez l''application avec sudo ou changez les permissions.');
end
else
raise; // Autre erreur
end;
end;
end;Détecter si root :
uses
BaseUnix;
function IsRunningAsRoot: Boolean;
begin
Result := FpGetUID = 0;
end;
// Avertir l'utilisateur
procedure CheckPrivileges;
begin
if IsRunningAsRoot then
ShowMessage('Attention : Application exécutée en tant que root. ' +
'Ceci n''est pas recommandé pour des raisons de sécurité.');
end;Demander élévation (via pkexec) :
uses
Process;
function RunWithElevatedPrivileges(const Command: String): Boolean;
var
Proc: TProcess;
begin
Proc := TProcess.Create(nil);
try
Proc.Executable := 'pkexec'; // PolicyKit (remplace gksudo)
Proc.Parameters.Add(Command);
Proc.Options := [poWaitOnExit];
Proc.Execute;
Result := Proc.ExitStatus = 0;
finally
Proc.Free;
end;
end;
// Utilisation
if not IsRunningAsRoot then
begin
if MessageDlg('Cette opération nécessite des privilèges administrateur. Continuer ?',
mtConfirmation, [mbYes, mbNo], 0) = mrYes then
begin
if RunWithElevatedPrivileges('/opt/myapp/myapp --install') then
ShowMessage('Installation réussie')
else
ShowMessage('Installation annulée ou échouée');
end;
end;Service Windows (ancien) :
uses
{$IFDEF WINDOWS}
JwaTlHelp32, Windows;
{$ENDIF}
type
TMyService = class(TService)
procedure ServiceExecute(Sender: TService);
end;
procedure TMyService.ServiceExecute(Sender: TService);
begin
while not Terminated do
begin
// Logique du service
Sleep(1000);
end;
end;Démon Linux (systemd) :
program MyDaemon;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}
BaseUnix, Unix,
{$ENDIF}
SysUtils, Classes;
var
Terminated: Boolean = False;
procedure SignalHandler(Signal: LongInt); cdecl;
begin
case Signal of
SIGTERM, SIGINT:
begin
WriteLn('Received termination signal');
Terminated := True;
end;
SIGHUP:
begin
WriteLn('Received reload signal');
// Recharger la configuration
end;
end;
end;
procedure DaemonLoop;
begin
WriteLn('Daemon started');
while not Terminated do
begin
// Logique du démon
WriteLn(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now), ' - Running');
Sleep(5000);
end;
WriteLn('Daemon stopped');
end;
begin
{$IFDEF UNIX}
// Installer les gestionnaires de signaux
FpSignal(SIGTERM, @SignalHandler);
FpSignal(SIGINT, @SignalHandler);
FpSignal(SIGHUP, @SignalHandler);
// Se détacher du terminal (daemonize)
if FpFork > 0 then
Halt(0); // Le parent se termine
// Le fils continue en arrière-plan
FpUmask(0);
FpSetsid;
// Fermer stdin, stdout, stderr
FpClose(0);
FpClose(1);
FpClose(2);
// Réouvrir vers /dev/null ou fichier log
// ...
{$ENDIF}
DaemonLoop;
end.Fichier systemd unit (/etc/systemd/system/myapp.service) :
[Unit]
Description=My Application Daemon
After=network.target
[Service]
Type=simple
User=myappuser
Group=myappuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/myapp-daemon
Restart=on-failure
RestartSec=10
# Sécurité
PrivateTmp=yes
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/myapp /var/log/myapp
[Install]
WantedBy=multi-user.targetInstallation et contrôle :
# Copier le fichier unit
sudo cp myapp.service /etc/systemd/system/
# Recharger systemd
sudo systemctl daemon-reload
# Activer au démarrage
sudo systemctl enable myapp.service
# Démarrer le service
sudo systemctl start myapp.service
# Vérifier le statut
sudo systemctl status myapp.service
# Voir les logs
sudo journalctl -u myapp.service -fPlus simple pour débuter :
program SimpleDaemon;
{$mode objfpc}{$H+}
uses
SysUtils, Classes;
var
Running: Boolean = True;
procedure MainLoop;
var
LogFile: TextFile;
begin
AssignFile(LogFile, '/var/log/myapp/daemon.log');
try
if FileExists('/var/log/myapp/daemon.log') then
Append(LogFile)
else
Rewrite(LogFile);
WriteLn(LogFile, 'Daemon started at ', DateTimeToStr(Now));
Flush(LogFile);
while Running do
begin
// Votre logique
WriteLn(LogFile, 'Running at ', DateTimeToStr(Now));
Flush(LogFile);
Sleep(5000);
end;
WriteLn(LogFile, 'Daemon stopped at ', DateTimeToStr(Now));
finally
CloseFile(LogFile);
end;
end;
begin
try
MainLoop;
except
on E: Exception do
WriteLn('Error: ', E.Message);
end;
end.Systemd unit en mode simple :
[Service]
Type=simple
ExecStart=/opt/myapp/simpledaemon
StandardOutput=journal
StandardError=journalLazarus supporte plusieurs widgetsets sur Linux :
| Widgetset | Description | Avantages | Inconvénients |
|---|---|---|---|
| GTK2 | GNOME Toolkit 2 | Mature, stable | Ancien, abandonné GNOME |
| GTK3 | GNOME Toolkit 3 | Moderne, GNOME natif | Quelques bugs LCL |
| Qt5 | Qt 5 | Excellent, KDE natif | Dépendances Qt lourdes |
| Qt6 | Qt 6 | Moderne | Encore jeune en LCL |
Configuration du widgetset :
# Compilation avec GTK2 (défaut)
lazbuild --ws=gtk2 myproject.lpi
# Compilation avec Qt5
lazbuild --ws=qt5 myproject.lpi
# Compilation avec GTK3
lazbuild --ws=gtk3 myproject.lpiDans l'IDE Lazarus :
Project → Project Options → Compiler Options
→ Config and Target
→ Target OS: Linux
→ Target CPU: x86_64
→ LCL Widgetset: gtk2 / qt5 / gtk3
{$IFDEF LCLGtk2}
uses
Gtk2, Gdk2, Glib2;
procedure CustomGTK2Code;
begin
// Code spécifique GTK2
end;
{$ENDIF}
{$IFDEF LCLQt5}
uses
Qt5, QtWidgets;
procedure CustomQt5Code;
begin
// Code spécifique Qt5
end;
{$ENDIF}
// Code portable (recommandé)
procedure PortableCode;
begin
// Utiliser LCL, pas les API natives
ShowMessage('Ceci fonctionne partout');
end;Respecter le thème système :
uses
LCLIntf, LCLType;
procedure ApplySystemTheme;
begin
// LCL applique automatiquement le thème système
// Pas besoin de code spécial sur Linux
{$IFDEF LINUX}
// Le thème GTK/Qt du système est utilisé automatiquement
{$ENDIF}
end;
// Forcer un thème spécifique (déconseillé)
procedure ForceTheme(const ThemeName: String);
begin
{$IFDEF LCLGtk2}
// gtk_settings_set_string_property(...)
{$ENDIF}
// Mieux : laisser l'utilisateur choisir son thème système
end;Dark mode :
uses
Graphics, Forms;
function IsDarkTheme: Boolean;
var
BgColor: TColor;
begin
BgColor := Application.MainForm.Color;
// Si couleur sombre, probablement dark theme
Result := (GetRValue(BgColor) + GetGValue(BgColor) + GetBValue(BgColor)) < 384;
end;
procedure AdaptToDarkMode;
begin
if IsDarkTheme then
begin
// Adapter les couleurs si nécessaire
Panel1.Font.Color := clWhite;
end
else
begin
Panel1.Font.Color := clBlack;
end;
end;Créer un lanceur d'application :
# /usr/share/applications/myapp.desktop
[Desktop Entry]
Version=1.0
Type=Application
Name=My Application
Name[fr]=Mon Application
Comment=Description of my app
Comment[fr]=Description de mon application
Icon=/usr/share/icons/hicolor/256x256/apps/myapp.png
Exec=/usr/bin/myapp %F
Terminal=false
Categories=Office;Utility;
MimeType=application/x-myapp;
StartupNotify=true
StartupWMClass=myappInstallation :
# Copier le fichier .desktop
sudo cp myapp.desktop /usr/share/applications/
# Mettre à jour le cache
sudo update-desktop-database
# Icône
sudo cp myapp.png /usr/share/icons/hicolor/256x256/apps/
sudo gtk-update-icon-cache /usr/share/icons/hicolor/Créer depuis l'application :
procedure CreateDesktopEntry;
var
DesktopFile: TextFile;
DesktopPath: String;
begin
DesktopPath := GetUserDir + '.local/share/applications/myapp.desktop';
AssignFile(DesktopFile, DesktopPath);
try
Rewrite(DesktopFile);
WriteLn(DesktopFile, '[Desktop Entry]');
WriteLn(DesktopFile, 'Version=1.0');
WriteLn(DesktopFile, 'Type=Application');
WriteLn(DesktopFile, 'Name=My Application');
WriteLn(DesktopFile, 'Exec=', ParamStr(0), ' %F');
WriteLn(DesktopFile, 'Icon=myapp');
WriteLn(DesktopFile, 'Terminal=false');
WriteLn(DesktopFile, 'Categories=Utility;');
finally
CloseFile(DesktopFile);
end;
// Rendre exécutable
FpChmod(DesktopPath, S_IRWXU or S_IRGRP or S_IXGRP or S_IROTH or S_IXOTH);
end;MIME type (/usr/share/mime/packages/myapp.xml) :
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-myapp">
<comment>MyApp Document</comment>
<comment xml:lang="fr">Document MyApp</comment>
<icon name="myapp"/>
<glob pattern="*.myapp"/>
<magic priority="50">
<match type="string" offset="0" value="MYAPP"/>
</magic>
</mime-type>
</mime-info>Installation :
sudo cp myapp.xml /usr/share/mime/packages/
sudo update-mime-database /usr/share/mimeGérer les fichiers passés en paramètre :
procedure TMainForm.FormCreate(Sender: TObject);
var
i: Integer;
begin
// Vérifier les paramètres de ligne de commande
for i := 1 to ParamCount do
begin
if FileExists(ParamStr(i)) then
OpenFile(ParamStr(i));
end;
end;/opt/myapp/ # Application
├── bin/
│ └── myapp # Exécutable principal
├── lib/ # Bibliothèques privées (.so)
│ ├── libmylib.so
│ └── plugins/
├── share/ # Données partagées
│ ├── icons/
│ ├── translations/
│ └── help/
└── doc/
├── README
└── LICENSE
/usr/share/applications/ # Lanceur
└── myapp.desktop
/usr/share/icons/hicolor/ # Icônes
└── 256x256/apps/
└── myapp.png
/etc/myapp/ # Config système (optionnel)
└── config.ini
~/.config/myapp/ # Config utilisateur
└── config.ini
~/.local/share/myapp/ # Données utilisateur
└── data/
Structure du paquet :
myapp-1.0/
├── debian/
│ ├── control # Métadonnées
│ ├── changelog # Historique
│ ├── copyright # Licence
│ ├── rules # Build script
│ ├── install # Fichiers à installer
│ ├── postinst # Script post-installation
│ └── prerm # Script pré-suppression
└── [source files]
debian/control :
Source: myapp
Section: utils
Priority: optional
Maintainer: Your Name <your.email@example.com>
Build-Depends: debhelper (>= 10), lazarus, lcl, lcl-gtk2
Standards-Version: 4.1.3
Package: myapp
Architecture: amd64
Depends: ${shlibs:Depends}, ${misc:Depends}, libgtk2.0-0
Description: My Application
Long description of my application
spanning multiple lines.
debian/rules :
#!/usr/bin/make -f
%:
dh $@
override_dh_auto_build:
lazbuild --build-mode=Release myapp.lpi
override_dh_auto_install:
mkdir -p debian/myapp/opt/myapp/bin
cp myapp debian/myapp/opt/myapp/bin/
mkdir -p debian/myapp/usr/share/applications
cp myapp.desktop debian/myapp/usr/share/applications/Compilation du paquet :
# Se placer dans le répertoire du projet
cd myapp-1.0/
# Construire le paquet
dpkg-buildpackage -us -uc
# Le .deb est créé dans le répertoire parent
ls ../myapp_1.0_amd64.deb
# Installer
sudo dpkg -i ../myapp_1.0_amd64.deb
# Ou créer un repository APTAppImage = exécutable autonome avec toutes les dépendances.
Structure AppImage :
# Installer linuxdeploy
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
# Préparer l'AppDir
mkdir -p AppDir/usr/bin
cp myapp AppDir/usr/bin/
mkdir -p AppDir/usr/share/applications
cp myapp.desktop AppDir/usr/share/applications/
mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps
cp myapp.png AppDir/usr/share/icons/hicolor/256x256/apps/
# Créer l'AppImage
./linuxdeploy-x86_64.AppImage --appdir AppDir --output appimage
# Résultat : MyApp-x86_64.AppImage
chmod +x MyApp-x86_64.AppImage
./MyApp-x86_64.AppImageScript automatisé :
#!/bin/bash
# build-appimage.sh
APP_NAME="MyApp"
VERSION="1.0"
# Compiler
lazbuild --build-mode=Release myapp.lpi
# Créer AppDir
rm -rf AppDir
mkdir -p AppDir/usr/bin
cp myapp AppDir/usr/bin/
# Icône et desktop
mkdir -p AppDir/usr/share/applications
cp myapp.desktop AppDir/usr/share/applications/
mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps
cp myapp.png AppDir/usr/share/icons/hicolor/256x256/apps/
# Générer AppImage
linuxdeploy-x86_64.AppImage \
--appdir AppDir \
--output appimage \
--desktop-file=AppDir/usr/share/applications/myapp.desktop
# Renommer
mv MyApp-*.AppImage ${APP_NAME}-${VERSION}-x86_64.AppImage
echo "AppImage created: ${APP_NAME}-${VERSION}-x86_64.AppImage"snapcraft.yaml :
name: myapp
version: '1.0'
summary: My Application
description: |
Long description of my application
grade: stable
confinement: strict
apps:
myapp:
command: bin/myapp
desktop: share/applications/myapp.desktop
plugs:
- desktop
- desktop-legacy
- x11
- home
- network
parts:
myapp:
plugin: nil
source: .
override-build: |
lazbuild --build-mode=Release myapp.lpi
mkdir -p $SNAPCRAFT_PART_INSTALL/bin
cp myapp $SNAPCRAFT_PART_INSTALL/bin/
build-packages:
- lazarus
- lcl
- lcl-gtk2
stage-packages:
- libgtk2.0-0Compilation :
# Installer snapcraft
sudo snap install snapcraft --classic
# Builder le snap
snapcraft
# Résultat : myapp_1.0_amd64.snap
# Installer localement
sudo snap install --dangerous myapp_1.0_amd64.snap
# Ou publier sur Snap Store
snapcraft login
snapcraft upload myapp_1.0_amd64.snapChecklist de tests Linux :
## Interface utilisateur
- [ ] Application se lance correctement
- [ ] Toutes les fenêtres s'affichent
- [ ] Menus fonctionnent
- [ ] Raccourcis clavier corrects
- [ ] Boutons répondent aux clics
- [ ] Polices lisibles
- [ ] Icônes affichées
- [ ] Thème système respecté
## Système de fichiers
- [ ] Ouverture de fichiers
- [ ] Sauvegarde de fichiers
- [ ] Chemins corrects (pas de C:\)
- [ ] Permissions respectées
- [ ] Casse des fichiers gérée
- [ ] Configuration sauvegardée dans ~/.config
## Réseau
- [ ] Connexions HTTP/HTTPS
- [ ] Connexions base de données
- [ ] Sockets TCP/UDP
## Intégration système
- [ ] Lanceur .desktop fonctionne
- [ ] Icône apparaît dans le menu
- [ ] Association fichiers OK
- [ ] Notifications système
## Performance
- [ ] Temps de démarrage acceptable
- [ ] Utilisation mémoire normale
- [ ] Pas de fuite mémoire
- [ ] CPU non saturé# Installer VirtualBox
sudo apt install virtualbox
# Créer une VM Ubuntu
# Télécharger ISO Ubuntu depuis ubuntu.com
# Créer VM, installer Ubuntu
# Tester votre application dans la VM
# Copier via dossier partagé ou SSH
# Tests distribution
# - Ubuntu 20.04 LTS
# - Ubuntu 22.04 LTS
# - Debian 11
# - Fedora 38# Dockerfile.test
FROM ubuntu:22.04
# Installer dépendances
RUN apt-get update && apt-get install -y \
libgtk2.0-0 \
libx11-6 \
&& rm -rf /var/lib/apt/lists/*
# Copier application
COPY myapp /opt/myapp/
# Tests
CMD ["/opt/myapp/myapp", "--test"]# Builder et tester
docker build -f Dockerfile.test -t myapp-test .
docker run --rm myapp-testunit PlatformAbstraction;
interface
type
TPlatform = class
class function GetOSName: String;
class function GetUserName: String;
class function GetComputerName: String;
class function OpenURL(const URL: String): Boolean;
class function GetDefaultBrowser: String;
end;
implementation
uses
SysUtils, Process, LCLIntf
{$IFDEF UNIX}, BaseUnix{$ENDIF}
{$IFDEF WINDOWS}, Windows{$ENDIF};
class function TPlatform.GetOSName: String;
begin
{$IFDEF WINDOWS}
Result := 'Windows';
{$ENDIF}
{$IFDEF LINUX}
Result := 'Linux';
{$ENDIF}
{$IFDEF DARWIN}
Result := 'macOS';
{$ENDIF}
end;
class function TPlatform.GetUserName: String;
begin
Result := GetEnvironmentVariable('USER');
if Result = '' then
Result := GetEnvironmentVariable('USERNAME'); // Windows
end;
class function TPlatform.OpenURL(const URL: String): Boolean;
begin
Result := OpenURL(URL); // LCLIntf
end;
end.// Tests unitaires multi-plateforme
unit TestPlatform;
interface
uses
fpcunit, testregistry;
type
TPlatformTest = class(TTestCase)
published
procedure TestPaths;
procedure TestPermissions;
procedure TestFileOperations;
end;
implementation
procedure TPlatformTest.TestPaths;
var
Path: String;
begin
Path := GetAppConfigDir(False);
AssertTrue('Config dir should exist', DirectoryExists(Path));
{$IFDEF WINDOWS}
AssertTrue('Should contain AppData', Pos('AppData', Path) > 0);
{$ENDIF}
{$IFDEF UNIX}
AssertTrue('Should contain .config', Pos('.config', Path) > 0);
{$ENDIF}
end;
initialization
RegisterTest(TPlatformTest);
end.- Télécharger myapp-setup.exe
- Double-cliquer et suivre l'assistant
sudo dpkg -i myapp_1.0_amd64.debTélécharger MyApp-x86_64.AppImage et le rendre exécutable :
chmod +x MyApp-x86_64.AppImage
./MyApp-x86_64.AppImageL'adaptation d'une application pour Linux va au-delà de la simple compilation. Une application véritablement multi-plateforme doit :
✅ Respecter les conventions Linux (chemins, permissions, configuration)
✅ S'intégrer au desktop (.desktop, thèmes, icônes)
✅ Être distribuable facilement (.deb, AppImage, Snap)
✅ Fonctionner comme une application native
Checklist finale d'adaptation Linux :
- Tous les chemins sont portables (pas de C:)
- Configuration via fichiers texte (pas Registry)
- Permissions gérées correctement
- Service converti en systemd unit (si applicable)
- Fichier .desktop créé
- MIME type défini (si association fichiers)
- Paquet .deb ou AppImage créé
- Tests sur Ubuntu/Debian réalisés
- Documentation mise à jour
Votre application est maintenant prête pour Linux ! Dans les sections suivantes, nous verrons comment maintenir et faire évoluer une base de code multi-plateforme, et comment contribuer à l'écosystème FreePascal/Lazarus.