Skip to content

Latest commit

 

History

History
1573 lines (1258 loc) · 33.6 KB

File metadata and controls

1573 lines (1258 loc) · 33.6 KB

🔝 Retour au Sommaire

26.7.2 Adaptation pour Linux

Introduction

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.

Philosophie : Penser multi-plateforme

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;

Différences fondamentales Windows ↔ Linux

Tableau comparatif

Aspect Windows Linux Impact
Système de fichiers Insensible à la casse Sensible à la casse ⚠️ Élevé
Séparateur de chemin \ backslash / slash ⚠️ Élevé
Fin de ligne CRLF (\r\n) LF (\n) ⚠️ Moyen
Lecteurs C:, D:, etc. Point de montage unique / ⚠️ Élevé
Exécutables .exe, .dll Sans extension, .so ⚠️ Moyen
Registry Centralisée Fichiers texte distribués ⚠️ Élevé
Permissions ACL complexes Modèle Unix (rwx) ⚠️ Moyen
Services Service Manager systemd/init ⚠️ Élevé
Encodage ANSI/UTF-16 historique UTF-8 natif ⚠️ Moyen
Interface Win32 API GTK/Qt/X11 ⚠️ Faible (LCL abstrait)
Utilisateurs Admin/User root/user ⚠️ Moyen
Raccourcis .lnk .desktop ⚠️ Faible

Concepts clés à comprendre

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)

Adaptation des chemins de fichiers

Utiliser les fonctions portables

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;

Abstraction complète des chemins

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;

Gestion de la casse des fichiers

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.TXT

Solutions :

// 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;

Configuration : Registry → Fichiers texte

Migrer depuis le Registry Windows

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;

Configuration système vs utilisateur

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;

Permissions et sécurité

Comprendre les permissions Linux

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;

Définir les permissions

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;

Gestion des erreurs de permissions

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;

Élévation de privilèges

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;

Services et démons

Migrer un service Windows vers systemd

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.target

Installation 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 -f

Approche simplifiée : daemon en mode "foreground"

Plus 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=journal

Interface graphique : Widgetsets

Choisir le widgetset Linux

Lazarus 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.lpi

Dans l'IDE Lazarus :

Project → Project Options → Compiler Options
→ Config and Target
  → Target OS: Linux
  → Target CPU: x86_64
  → LCL Widgetset: gtk2 / qt5 / gtk3

Code spécifique au widgetset

{$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;

Thèmes et apparence

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;

Intégration desktop Linux

Fichiers .desktop

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=myapp

Installation :

# 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;

Association de fichiers

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/mime

Gé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;

Packaging et distribution

Structure de déploiement Linux

/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/

Création d'un paquet .deb (Debian/Ubuntu)

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 APT

AppImage (portable)

AppImage = 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.AppImage

Script 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"

Snap (Ubuntu moderne)

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-0

Compilation :

# 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.snap

Tests sous Linux

Tests de compatibilité

Checklist 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é

Tests en machine virtuelle

# 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

Tests avec Docker

# 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-test

Bonnes pratiques multi-plateforme

1. Abstraire les différences

unit 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.

2. Tester sur toutes les plateformes

// 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.

3. Documentation multi-plateforme

Installation

Windows

  1. Télécharger myapp-setup.exe
  2. Double-cliquer et suivre l'assistant

Linux (Debian/Ubuntu)

sudo dpkg -i myapp_1.0_amd64.deb

Linux (autres distributions)

Télécharger MyApp-x86_64.AppImage et le rendre exécutable :

chmod +x MyApp-x86_64.AppImage
./MyApp-x86_64.AppImage

Conclusion

L'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.

⏭️ Opportunités professionnelles