This post exists also in english

Ce billet fait partie d'une série sur comment déployer une application XULRunner. Voir le préambule pour le contexte.

3 - Windows

3.1 Icônes pour Windows

Nous avons besoin d'une icône au format .ico. Elle sera utilisée par nos fenêtres, et par notre lanceur.

Il est facile de faire un fichier .ico sur Linux, nous avons juste besoin du programme icotool, disponible dans le paquet icoutils.
Sur Debian/Ubuntu :

apt-get install icoutils

Puis nous créons le fichier ico à partir de plusieurs png de différentes tailles, 16x16 px, 32x32 px, et 48x48 px. Il serait possible d'utiliser plus de png, avec différentes profondeurs de couleur par exemple.

icotool -c -o icon.ico icon16.png icon32.png icon48.png

Vous trouverez un script bash avec cette ligne de commande dans l'archive example de ce chapitre 3 (dans samples_chapter_3/data/icons/).

Pour utiliser notre icône pour nos fenêtres, plutôt que celle de firefox ou de xulrunner, nous avons juste à mettre cette icône, renommée default.ico, dans le dossier icons/default situé dans notre dossier chrome principal.
Cette icône sera utilisée par tous les <window> XUL sans un attribut id.
Mais le <window> XUL principal de myapp a l'attribut id="main", donc pour cette fenêtre nous devons avoir une icône nommée main.ico, située dans le même dossier.

    |- samples_chapter_3/
|- myapp/
|- chrome
|- icons/
|- default/
|- default.ico
|- main.ico

3.2 Créer un lanceur (batch ou C)

Le lanceur le plus simple est un script batch. Voici le contenu de myapp.bat :

set CUR_DIR=%~dp0

START firefox -app $CUR_DIR\application.ini

Ce fichier doit être placé dans le dossier principal de notre appli. Ça marche, il lance notre appli via le fichier application.ini de notre appli.
Mais il se passe quelque chose de gênant, une fenêtre noire de commande est également ouverte.

Pour éviter ce defaut, nous allons créer un lanceur en C.

Nous utiliserons le code de XAL (XUL Application Launcher). C'est un programme léger en C, sous licence MIT, il lance une application XULRunner avec Firefox, avec l'argument -app et application.ini. Il doit être placé dans le dossier principal de l'application (comme le batch précédent). Bonus, il prend en charge d'éventuels arguments supplémentaires (comme -jsconsole), quand il est utilisé en ligne de commande. Et nous pouvons ajouter notre icône, et quelques autres informations sur l'application, via un fichier .rc.

Cet exécutable peut etre compilé avec n'importe quel compilateur C, son code est indépendant du code de Mozilla. Personnellement je le compile avec MinGW, sur Linux, avec succés. Si vous voulez utiliser un autre compilateur, éditer le fichier build, et adapter les variables CC et RESCOMP.

pour installer MinGW sur Debian/Ubuntu:

apt-get install mingw32

Je ne publie pas ici le code source C, vous le trouverez dans l'archive relative à ce chapitre, où dans sa page dédiée.

Nous pouvons personnaliser ce lanceur, en utilisant un fichier resource (.rc), insérer notre icône et spécifier quelques informations (vendeur, nom de l'appli, version,...).

Voici le contenu du fichier myapp-res.rc :

APPLICATION_ICON ICON "myapp.ico"

1 VERSIONINFO
FILEVERSION 0,1,0,0
PRODUCTVERSION 0,0,0,0
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "000004B0"
BEGIN
VALUE "Comments", "Published under the MPL 1.1/GPL 2.0/LGPL 2.1 licenses"
VALUE "CompanyName", "John Doe Organization"
VALUE "FileDescription", "MyApp"
VALUE "FileVersion", "1.0.0"
VALUE "InternalName", "MyApp"
VALUE "LegalCopyright", "(c) 2010 John Doe"
VALUE "ProductName", "MyApp"
VALUE "ProductVersion", "0.0.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x000, 1200
END
END

Quelques détails sur ce contenu :

La ligne APPLICATION_ICON ICON "myapp.ico" insère notre icône

Les entrées FILEVERSION et PRODUCTVERSION utilisent un format spécial, en résumé entier virgule entier virgule entier virgule entier.

La ligne VALUE "Translation", 0x000, 1200 spécifie que les chaînes (string) utilisées dans les champs d'information sont en Unicode.

Nous pouvons compiler ce lanceur, en utilisant notre fichier resource, avec le script build.sh fourni avec XAL :

sh build.sh myapp myapp-res.rc

Après compilation, la taille du fichier créé est de ~21Ko, donc vraiment léger (notez que le poids de l'icône est incluse).

3.3 Créer un script NSIS

Il existe plusieurs solution pour créer des installeurs pour Windows. Ici nous utiliserons NSIS, car : il est open source, nous pouvons construire notre installeur depuis Linux, et qu'il est facile et puissant.
Firefox lui-même utilise NSIS pour son installeur Windows.

Pour installer les outils NSIS sur Debian/Ubuntu :

apt-get install nsis

NSIS utilise son propre language de script pour créer les installeurs. Nous pouvons utiliser des pages par défaut, en créer de nouvelles personnalisées, gérer les fichiers lors de l'installation/désinstallation, agir sur le Registre Windows,...
Je ne vais pas faire une documentation complète sur NSIS ici, voyez leur wiki, il y a beaucoup d'explications et d'exemples.
Vous devriez avoir des exemples et une doc disponibles en local, une fois nsis installé, dans /usr/share/doc/nsis/Examples et /usr/share/doc/nsis/Doc.

Mais voilà le principal script proposé, et j'explique par la suite quelles actions sont réalisées :

!define PRODUCT_NAME "MyApp"
!define PRODUCT_INTERNAL_NAME "myapp"
!define PRODUCT_VERSION "1.0"
!define PRODUCT_WIN_VERSION "1.0.0.0"

!define MESSAGEWINDOW_NAME "${PRODUCT_NAME}MessageWindow"

!define HKEY_ROOT "HKLM"
!define UN_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"

!define LICENSE_PATH "${PRODUCT_INTERNAL_NAME}\LICENSE.txt"

!define INSTALLER_NAME "${PRODUCT_NAME}-${PRODUCT_VERSION}-install.exe"
!define TMP_UNINSTALL_EXE "${PRODUCT_INTERNAL_NAME}_uninstall.exe"

;--------------------------------
;Variables

; The name of the product installed
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"

; The file to write
OutFile "${INSTALLER_NAME}"

SetCompressor /final /solid lzma
ShowInstDetails show
ShowUninstDetails show

; The default installation directory
InstallDir $PROGRAMFILES\${PRODUCT_NAME}

; Request application privileges for Windows Vista
RequestExecutionLevel admin

Var Shortcuts_Dialog
Var Shortcuts_Label
Var Shortcuts_SM_Checkbox
Var Shortcuts_SM_Checkbox_State
Var Shortcuts_D_Checkbox
Var Shortcuts_D_Checkbox_State

Var Previous_Uninstall
Var Previous_Uninstall_dir
Var TempUninstallPath

!include "MUI2.nsh"
!include "FileFunc.nsh"

VIProductVersion "${PRODUCT_WIN_VERSION}"

VIAddVersionKey "ProductName" "${PRODUCT_NAME}"
;VIAddVersionKey "CompanyName" "${CompanyName}"
;VIAddVersionKey "LegalTrademarks" "${BrandShortName} is a Trademark of"
;VIAddVersionKey "LegalCopyright" "${CompanyName}"
VIAddVersionKey "LegalCopyright" ""
VIAddVersionKey "FileVersion" "${PRODUCT_VERSION}"
VIAddVersionKey "ProductVersion" "${PRODUCT_VERSION}"
VIAddVersionKey "FileDescription" "${PRODUCT_NAME} Installer"
VIAddVersionKey "OriginalFilename" "${INSTALLER_NAME}"

!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_INTERNAL_NAME}.exe"

;--------------------------------
; Pages

!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "${LICENSE_PATH}"
!insertmacro MUI_PAGE_DIRECTORY
Page custom onShortcutsPageCreate
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH

!insertmacro MUI_UNPAGE_WELCOME
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES


!insertmacro MUI_LANGUAGE "English"
!insertmacro MUI_LANGUAGE "French"

!include ./l10n/fr.nsh
!include ./l10n/en_US.nsh


;--------------------------------

Function .onInit
; an eventual previous version of the app should not be currently running.
; Abort if any.
; Explanation, when the application is running, a window with the className
; productnameMessageWindow exists
FindWindow $0 "${MESSAGEWINDOW_NAME}"
StrCmp $0 0 +3
MessageBox MB_OK|MB_ICONEXCLAMATION "${PRODUCT_NAME} is running. Please close it first" /SD IDOK
Abort

StrCpy $Shortcuts_SM_Checkbox_State 1
StrCpy $Shortcuts_D_Checkbox_State 1
FunctionEnd

Function un.onInit
; see Function .onInit
FindWindow $0 "${MESSAGEWINDOW_NAME}"
StrCmp $0 0 +3
MessageBox MB_OK|MB_ICONEXCLAMATION "${PRODUCT_NAME} is running. Please close it first" /SD IDOK
Abort
FunctionEnd

; custom page creation, for the shortcuts installation, using nsDialog
Function onShortcutsPageCreate
!insertmacro MUI_HEADER_TEXT $(l10n_SHORTCUTS_PAGE_TITLE) \
$(l10n_SHORTCUTS_PAGE_SUBTITLE)

nsDialogs::Create 1018
Pop $Shortcuts_Dialog

${If} $Shortcuts_Dialog == error
Abort
${EndIf}

${NSD_CreateLabel} 0 6 100% 12u $(l10n_CREATE_ICONS_DESC)
Pop $Shortcuts_Label

${NSD_CreateCheckbox} 15u 20u 100% 10u $(l10n_ICONS_STARTMENU)
Pop $Shortcuts_SM_Checkbox
GetFunctionAddress $0 OnSMCheckbox
nsDialogs::OnClick $Shortcuts_SM_Checkbox $0

${If} $Shortcuts_SM_Checkbox_State == ${BST_CHECKED}
${NSD_Check} $Shortcuts_SM_Checkbox
${EndIf}

${NSD_CreateCheckbox} 15u 40u 100% 10u $(l10n_ICONS_DESKTOP)
Pop $Shortcuts_D_Checkbox
GetFunctionAddress $0 OnDCheckbox
nsDialogs::OnClick $Shortcuts_D_Checkbox $0

${If} $Shortcuts_D_Checkbox_State == ${BST_CHECKED}
${NSD_Check} $Shortcuts_D_Checkbox
${EndIf}

nsDialogs::Show
FunctionEnd

; event when the Start Menu shortcut is (un)checked in the custom page
Function OnSMCheckbox
${NSD_GetState} $Shortcuts_SM_Checkbox $Shortcuts_SM_Checkbox_State
Pop $0 # HWND
FunctionEnd

; event when the Desktop shortcut is (un)checked in the custom page
Function OnDCheckbox
${NSD_GetState} $Shortcuts_D_Checkbox $Shortcuts_D_Checkbox_State
Pop $0 # HWND
FunctionEnd

Function WriteUninstallReg
WriteRegStr ${HKEY_ROOT} ${UN_KEY} "DisplayName" \
"${PRODUCT_NAME} (${PRODUCT_VERSION})"
WriteRegStr ${HKEY_ROOT} ${UN_KEY} "UninstallString" \
"$INSTDIR\uninstall.exe"
WriteRegStr ${HKEY_ROOT} ${UN_KEY} "QuietUninstallString" \
"$INSTDIR\uninstall.exe /S"
WriteRegStr ${HKEY_ROOT} ${UN_KEY} "InstallLocation" \
"$INSTDIR"
WriteRegStr ${HKEY_ROOT} ${UN_KEY} "DisplayIcon" \
"$INSTDIR\${PRODUCT_INTERNAL_NAME}.exe"
WriteRegStr ${HKEY_ROOT} ${UN_KEY} "DisplayVersion" \
"${PRODUCT_VERSION}"

${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD ${HKEY_ROOT} ${UN_KEY} "EstimatedSize" "$0"
FunctionEnd

; The stuff to install
Section ""
; uninstall an eventual previous installation
ReadRegStr $Previous_Uninstall ${HKEY_ROOT} ${UN_KEY} "UninstallString"
ClearErrors
${If} $Previous_Uninstall != ""
StrCpy $Previous_Uninstall_dir $Previous_Uninstall
${GetParent} $Previous_Uninstall $Previous_Uninstall_dir

IfFileExists "$Previous_Uninstall" myUninstallPrevious myInstall
${Else}
goto myInstall
${EndIf}

myUninstallPrevious:
; copy the previous uninstaller into TEMP
ClearErrors
StrCpy $TempUninstallPath "$TEMP\${TMP_UNINSTALL_EXE}"
CopyFiles /SILENT "$Previous_Uninstall" "$TempUninstallPath"
IfErrors myInstall

ClearErrors
ExecWait '"$TempUninstallPath" /S _?=$Previous_Uninstall_dir'

ClearErrors
Delete "$TempUninstallPath"

;MessageBox MB_OK "UNINSTALL: finished"

myInstall:
SetOutPath $INSTDIR

; copy the files
File /r ${PRODUCT_INTERNAL_NAME}\*

WriteUninstaller "uninstall.exe"

Call WriteUninstallReg

${If} $Shortcuts_SM_Checkbox_State == ${BST_CHECKED}
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" \
"$INSTDIR\${PRODUCT_INTERNAL_NAME}.exe"
${EndIf}

${If} $Shortcuts_D_Checkbox_State == ${BST_CHECKED}
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" \
"$INSTDIR\${PRODUCT_INTERNAL_NAME}.exe"
${EndIf}
SectionEnd

;--------------------------------
; Uninstaller

Section "Uninstall"
MessageBox MB_OK|MB_ICONEXCLAMATION "$INSTDIR" /SD IDOK
; Remove installed files and uninstaller
!include ./uninstall_files.nsi
Delete "$INSTDIR\uninstall.exe"

; remove installed directories
!include ./uninstall_dirs.nsi
RMDir /r "$INSTDIR\extensions"

; Remove shortcuts, if any
Delete "$SMPROGRAMS\${PRODUCT_NAME}.lnk"
Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
;TODO remove eventual quicklaunch Too

; Remove the installation directory used (if empty)
RMDir "$INSTDIR"

; and delete the registry key for uninstall
DeleteRegKey ${HKEY_ROOT} ${UN_KEY}
SectionEnd

Quelques explications sur ce script :

Il utilise le thème par défaut "modern". Mais il serait possible de le personnaliser.

Certaines parties sont localisées ("traduites"), en insérant d'autres scripts nsis (décrits plus loin).

Il utilise quelques pages nsis par défaut, et une personnalisée pour la création des raccourcis sur le bureau et dans le menu démarrez.

L'installation et la désinstallation sont annulées si notre application est actuellement lancée.
En fait, quand notre application est en fonctionnement, XULRunner/Firefox crée une fenêtre Windows native avec une classe MyAppMessageWindow, le nom de cette classe est la valeur du champ Name dans application.ini concaténée avec "MessageWindow".
Le script vérifie juste si une telle fenêtre avec ce nom de classe est ouverte, et annule le traitement.

Il crée quelques entrées minimales dans La Base de Registre, pour permettre la désinstallation du programme avec l'outil "ajouter/supprimer des programmes" de Windows.

Si notre appli est déjà installée, la précédente version est désinstallée avant la nouvelle installation, en utilisant le désinstalleur précédent. Et, pour être plus précis, ce précédent désinstalleur est copié et lancé depuis le dossier "Temp" de Windows.

Pour la désinstallation, nous avons besoin de la liste complète des fichiers et dossiers installés. Ces listes seront créés dynamiquement plus tard, pour le moment dans ce script nous les insérons en tant que scripts nsis supplémentaires (uninstall_files.nsi et uninstall_dirs.nsi).

Maintenant voici le contenu de l'un des scripts localisés ("traduit"), en_US.nsh :

;!!! IMPORTANT: this must file must be edited with ANSI charset (in fact the
; Windows CP-1252 charset), but if there's no special character, UTF-8 is ok,
; because it's a subset ;)

LangString l10n_SHORTCUTS_PAGE_TITLE ${LANG_ENGLISH} \
"Set Up Shortcuts"
LangString l10n_SHORTCUTS_PAGE_SUBTITLE ${LANG_ENGLISH} \
"Create Program Icons"
LangString l10n_CREATE_ICONS_DESC ${LANG_ENGLISH} \
"Create icons for ${PRODUCT_NAME}:"
LangString l10n_ICONS_DESKTOP ${LANG_ENGLISH} \
"On my &Desktop"
LangString l10n_ICONS_STARTMENU ${LANG_ENGLISH} \
"In my &Start Menu Programs folder"

Nous devons faire attention à l'encodage des ces fichiers, celui-ci en anglais ne devrait pas poser de problème, mais celui en français doit être édité avec le charset Windows CP-1252 par exemple. Ces variables sont utilisées simplement dans notre script principal avec, par exemple, $(l10n_SHORTCUTS_PAGE_TITLE).

Notez que l'esperluette & dans les définitions de chaîne définissent des raccourcis clavier pour les contrôles dans l'interface utilisateur, ici il y a alt+D et alt+S.

3.4 Créer l'installeur

Nous avons maintenant tous les fichiers pour créer l'installeur. Voyons l'arborescence de nos sources :

    |-samples_chapter_3
|- data/
|- icons/
|- icon.ico
|- myapp.bat
|- myapp-res.rc
|- myapp/
|- win
|- nsis/
|- l10n/
|- en_US.nsh
|- fr.nsh
|- myapp.nsi
|- xal-src/
|- build.sh
|- clean.sh
|- main.c

Nous devons construire notre lanceur à partir des sources C et de nos données (icône et resource). Et créer 2 scripts nsis additionnels, pour lister les fichiers de notre appli. Puis nous pourrons créer notre installeur avec makensis. Nous allons faire celà dans un dossier temporaire. Voici le script, nommé build_win.sh :

#!/bin/bash

# exit the script on errors
set -e

TMP_DIR=./tmp

# get the absolute path of our TMP_DIR folder
TMP_DIR=$(readlink -f -- "$TMP_DIR")

# re-create the TMP_DIR folder
rm -rfv "$TMP_DIR"
mkdir -v "$TMP_DIR"

# copy our app
cp -rv myapp "$TMP_DIR/"

# copy the XAL launcher sources
cp -rv win/xal-src "$TMP_DIR/xal"
# and our icon and resource file
cp -v data/icons/icon.ico "$TMP_DIR/xal/myapp.ico"
cp -v data/myapp-res.rc "$TMP_DIR/xal/"

# build the launcher
cd "$TMP_DIR/xal/"
sh build.sh myapp myapp-res.rc
cd -

# copy the launchers in the right folder
cp -v data/myapp.bat "$TMP_DIR/myapp/"
cp -v "$TMP_DIR/xal/myapp.exe" "$TMP_DIR/myapp/"

# delete the xal sources
rm -rv "$TMP_DIR/xal"

# create the nsis script listing the files to unsinstall
cd "$TMP_DIR/myapp"
find . -maxdepth 1 -type f > ../uninstall_files.nsi
sed -i -r "s|^\./(.*)$| Delete \"\$INSTDIR\\\\\1\"|" ../uninstall_files.nsi

# and the list of directory
ls -d */ > ../uninstall_dirs.nsi
sed -i -r "s|^(.*)/$| RMDir /r \"\$INSTDIR\\\\\1\"|" ../uninstall_dirs.nsi
cd -

# copy the other nsis scripts
cp -rv win/nsis/* "$TMP_DIR/"

# and create the installer
makensis "$TMP_DIR/myapp.nsi"

# finally, copy our installer in the root sources dir
cp -v "$TMP_DIR/MyApp-1.0-install.exe" ./

echo "Windows installer for myapp created."

Pour lancer ce script, dans un terminal :

  • cd samples_chapter_3
  • sh ./build_win.sh

Et en résultat nous avons finalement le fichier MyApp-1.0-install.exe dans le dossier samples_chapter_3 :) .

Nicolas Martin

Vous pouvez télécharger tous les exemples de ce chapitre 3 (Windows) dans l'archive samples_chapter_3.tar.gz .

L'application myapp, de developer.mozilla.org, est dans le Domaine Public.

L'icône utilisée est issue du Tango Desktop Project, et est dans le Domaine Public.

le lanceur C XUL App Launcher (XAL) est sous licence MIT.

Toutes les autres données ajoutées, et les fichiers en exemple, de ce chapitre 3, sont dans le Domain Public également.