joliclic blog

Aller au contenu | Aller au menu | Aller à la recherche

mercredi, mai 25 2011

Distribuer votre appli XULRunner - 3 - Windows

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.

Distribute your XULRunner app - 3 - Windows

Ce billet existe aussi en français

This post is part of a series about how to package a XULRunner application.
See the preamble for the context case.

3 - Windows

3.1 Icons for Windows

We need a icon in .ico format. It will be used by ours windows, and by our launcher.

It's easy to make a .ico file on Linux, we just need to install the program icotool, from the icoutils package.
On Debian/Ubuntu:

apt-get install icoutils

Then we create a ico file from several png at different size, 16x16 px, 32x32 px, and 48x48 px. It would be possible to add more png, with different color depth as well.

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

You can find a bash script with this command line in the sample archive related to this chapter 3 (in samples_chapter_3/data/icons/).

To add our icon to our windows, rather than the firefox or xulrunner one, we have just to put this icon, renamed as default.ico, into a icons/default folder situated in our main chrome folder.
This icon will be used by all XUL <window> without an id attribute.
But the main XUL <window> of myapp has the attribute id="main", so for this window we must have an icon named main.ico, situated in the same folder:

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

3.2 Create a launcher (batch or C)

The simplest launcher is a batch script. Here the content of myapp.bat:

set CUR_DIR=%~dp0

START firefox -app $CUR_DIR\application.ini

This file must be placed into the main folder of our app. This works, it launches our app via the application.ini file of our app.
But there's something annoying, a black command window is opened too.

To avoid the previously mentioned default, we will create a launcher in C.

We use the code of XAL (XUL Application Launcher). This is a light C program, MIT licensed, it launches a XULRunner application with Firefox, with the -app argument and the application.ini. It must be placed in the main application folder (like the previous batch). Bonus, it handles additional arguments (like -jsconsole) if used in command line. And we can add our icon, and some application information via a .rc file.

This executable can be compiled with any C compiler, its code is independent of the Mozilla code. I personaly compile it with MinGW, on Linux, with success. If you plan to use another compiler, edit the build file, and adapt the CC and RESCOMP variables.

To install MinGW on Debian/Ubuntu:

apt-get install mingw32

I don't publish here the C source of this code, you'll find it in the archive related to this chapter 3, or in its dedicated page.

We can customize this launcher, using a resource (.rc) file, insert our icon and specify some information (vendor, app name, version,...).

Here the content of the myapp-res.rc file:

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

Some details about this content:

The line APPLICATION_ICON ICON "myapp.ico" integrate our icon

The FILEVERSION and PRODUCTVERSION entries use a special format, in short integer comma integer comma integer comma integer.

The line VALUE "Translation", 0x000, 1200 specify that the strings for information use Unicode.

We can compile this launcher, using our resource file, with the build.sh script distributed with XAL:

sh build.sh myapp myapp-res.rc

After compilation, the size of the result file is ~21Kb, so very small (note that the size of the icon is included).

3.3 Create a NSIS script

There are several solution to create Windows installers. Here, we will use NSIS, because: it's open source, we can build our installer from Linux, and it's easy and powerful.
Firefox itself use NSIS for its Windows installer.

To install NSIS tools on Debian/Ubuntu:

apt-get install nsis

NSIS uses its own script language to create installers. We can use some default pages, create some custom ones, manage files (un)installation, act on the Windows Registry,...
I will not do a complete NSIS documentation here, see their wiki for that, there's a lot of explanations and examples.
You should have some local examples and doc after installing nsis, in /usr/share/doc/nsis/Examples and /usr/share/doc/nsis/Doc.

But here's the main proposed script here, and I will explain what actions are performed after:

!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

Some descriptions about this script:

It uses the default "modern" skin. But it's possible to customize it.

It uses some localized parts, including some other nsis scripts (described later).

It uses some default nsis pages, and a custom one to allow the creation of shortcuts on the desktop and in the start menu.

The installation and uninstallation are aborted if our application is currently running.
In fact, when our app is running, XULRunner/Firefox create a native Windows window with a class MyAppMessageWindow, this class name is the value of the field Name of the application.ini concatenated with "MessageWindow".
The script just checks if such a window with this class name is opened, then abort.

It creates some minimal Registry entries to allow the uninstallation of the app with the "add/remove programs" tool.

If our app is already installed, the previous version is uninstalled before the new installation, using our previous uninstaller. And, to be more precise, this previous uninstaller is copied and launched from the Windows temp folder.

For the uninstaller, we need the complete list of installed files and directories. These lists will be dynamically created later, for the moment in this script we just include them as other nsis scripts (uninstall_files.nsi and uninstall_dirs.nsi).

Now here the content of one of the localized included script, 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"

We have to be careful with the encoding of these files, this English one is not a problem, but the French one must be edited with the Windows CP-1252 charset for exemple. These variables are simply used in our main script with, for example, $(l10n_SHORTCUTS_PAGE_TITLE).

Note that the ampersand & in the string definitions define an access key for controls in the UI, here there's alt+D and alt+S.

3.4 Create the installer

We have now all the files to create the installer. Let see our sources tree:

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

We have to build our launcher from the C source and our data (icon and resource). And create 2 additional nsis scripts, listing our app files. Then we can create the installer with makensis. We will doing this in a temporary folder. Here's the script, named 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."

To launch this script, in a terminal:

  • cd samples_chapter_3
  • sh ./build_win.sh

And as result we have finally the file MyApp-1.0-install.exe in the samples_chapter_3 folder :) .

Nicolas Martin

You can download all the samples of this chapter 3 (Windows) in the samples_chapter_3.tar.gz archive.

The myapp application, from developer.mozilla.org is in the Public Domain.

The icon used is from the Tango Desktop Project, and is in the Public Domain.

The C launcher XUL App Launcher (XAL) is under the MIT license.

All other added data, and sample files, of this chapter 3, are in the Public Domain too.

mardi, mai 24 2011

Distribuer votre appli XULRunner - 2.4 - Empaqueter un rpm

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.

2 - Linux

2.4 Empaqueter un rpm

2.4.1 Préalable

Dans cette partie nous allons créer un paquet rpm, pour les distributions basée sur rpm, comme Red Hat, Fedora, Suse,... (j'ai testé uniquement sur Fedora, merci de me faire savoir si vous rencontrez des problèmes sur les autres).

Nous pouvons créer ce rpm aussi bien depuis d'autres distributions Linux, basées sur Debian par exemple (c'est ce que je fais), nous avons juste besoin de l'executable rpmbuild, et pour celà il suffit d'installer rpm :

apt-get install rpm

Quelques liens de documentation :

Je suppose que nous n'avons pas besoin de compilation pour notre appli, mais si ce n'est pas le cas, compiler simplement avant l'empaquètement (malgré le fait que ce soit déconseillé dans ce guide).

2.4.2 Fichiers RPM

pour créer un rpm, nous sommes supposé paramétrer une configuration utilisateur pour la création de tout rpm. Mais il est possible de faire celà dans un dossier local, c'est ce que nous allons faire.

Nous devons créer les dossiers suivants, et un fichier myapp.spec :

    |- samples_chapter_2/
|- myapp/
|- rpmbuild/
|- BUILD/
|- RPMS/
|- SOURCES/
|- SPECS/
|- SRPMS/
|- myapp.spec

En fait c'est la même structure que pour un environnement général de création rpm, mais local.

Le fichier .spec est le 'script' utilisé par rpmbuild pour créer le rpm. Il est supposé utiliser une archive source au format tar.gz, que nous allons créer spécialement pour lui plus tard.

Voici le contenu de notre spec :

Name:           myapp
Version: 1.0
Release: 1%{?dist}
Summary: simple Hello World powered by XULRunner.

Group: Development/Tools
License: MPLv1.1 or GPLv2+ or LGPLv2+
URL: http://example.com/myapp/
Source: myapp-1.0.tar.gz
BuildArch: noarch
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id} -un)

#BuildRequires:
Requires: firefox >= 3.6

%description
simple Hello World
Powered by XULRunner.

%prep
%setup

%build

%install
rm -rf %{buildroot}
mkdir -p %{buildroot}%{_libdir}
cp -R myapp %{buildroot}%{_libdir}/
mkdir -p %{buildroot}%{_bindir}

ln -s %{_libdir}/myapp/myapp.sh %{buildroot}%{_bindir}/myapp

mkdir -p %{buildroot}%{_datadir}/applications
cp data/myapp.desktop %{buildroot}%{_datadir}/applications/

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/16x16/apps
cp data/icons/icon16.png \
%{buildroot}%{_datadir}/icons/hicolor/16x16/apps/myapp.png

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/22x22/apps
cp data/icons/icon22.png \
%{buildroot}%{_datadir}/icons/hicolor/22x22/apps/myapp.png

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/32x32/apps
cp data/icons/icon32.png \
%{buildroot}%{_datadir}/icons/hicolor/32x32/apps/myapp.png

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/48x48/apps
cp data/icons/icon48.png \
%{buildroot}%{_datadir}/icons/hicolor/48x48/apps/myapp.png

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/scalable/apps
cp data/icons/icon48.svg \
%{buildroot}%{_datadir}/icons/hicolor/scalable/apps/myapp.svg

%post
touch --no-create %{_datadir}/icons/hicolor &>/dev/null || :
if [ -x %{_bindir}/gtk-update-icon-cache ]; then
%{_bindir}/gtk-update-icon-cache -q %{_datadir}/icons/hicolor;
fi

%postun
touch --no-create %{_datadir}/icons/hicolor &>/dev/null || :
if [ -x %{_bindir}/gtk-update-icon-cache ]; then
%{_bindir}/gtk-update-icon-cache -q %{_datadir}/icons/hicolor;
fi


%clean
rm -rf $RPM_BUILD_ROOT


%files
%defattr(-,root,root,-)
%{_bindir}/myapp
%{_libdir}/myapp
%{_datadir}/applications/myapp.desktop
%{_datadir}/icons/hicolor/16x16/apps/myapp.png
%{_datadir}/icons/hicolor/22x22/apps/myapp.png
%{_datadir}/icons/hicolor/32x32/apps/myapp.png
%{_datadir}/icons/hicolor/48x48/apps/myapp.png
%{_datadir}/icons/hicolor/scalable/apps/myapp.svg
#%doc

%changelog
* Fri Oct 8 2010 John Doe <johndoe@example.com> 1.0-1
- first publication

Voyons en détail certaines entrées de ce fichier :

Name ne devrait contenir aucun caractère blanc.

Version doit contenir uniquement des chiffres (et des points), pas de chaîne comme "beta".

L'entrée Group doit être choisie dans la liste du fichier /usr/share/doc/rpm/GROUPS . Vous devez l'avoir sur votre ordinateur avec l'installation de rpm.

Release est la version du rpm pour cette version de l'appli, et %{?dist} sera remplacé par rpmbuild par la distribution courante. Celà ne sera pas complété sur une distribution non rpm (comme Debian), vous pouvez le supprimer si vous le désirez.

Pour l'entrée License, vous devez choisir un mot clé court (short keyword) dans cette liste.

L'entrée Source doit correspondre exactement au tar.gz que nous créerons plus tard.

La ligne BuildArch: noarch spécifie que notre appli n'est pas spécifique à l'architecture. Si votre appli contient du code compilé, vous devez supprimer cette ligne, et un rpm spécifique à l'architecture courante sera construit.

La valeur de BuildRoot est une valeur habituelle.

BuildRequires est commenté (avec un #), comme nous n'avons pas de compilation.

Enfin, l'entrée Requires contient les autres paquets qui sont nécessaires au fonctionnement de notre appli. Vous pourriez en ajouter d'autres, séparés par des virgules (,). Notez que vous ne pouvez pas déclarer de dépendances alternatives, comme pour un deb. C'est pourquoi nous déclarons Firefox et non XULRunner comme dépendance, dans le cas éventuel où le Firefox de la distribution n'est pas basée sur XULRunner.

Par la suite nous copierons notre archive tar.gz dans le dossier SOURCES.
Puis la section %prep et l'instruction %setup la décompresseront dans le dossier BUILD.
La section %build est vide, parce que nous n'avons pas de compilation à faire.

Dans la section %install, nous installons notre appli.
Ce sont des commandes shell, le dossier courant de travail est la source tar.gz non compressée. %{buildroot} est la racine de l'installation local de notre appli, ce sera la racine de l'ordinateur / lors de l'installation réelle.
%{_libdir}, %{_bindir}, et %{_datadir} correspondent respectivement à /usr/lib/, /usr/bin/, et /usr/share/. Je suppose que c'est le cas pour les principales distributions basées sur rpm, si je me trompe, merci de me corriger, car il faudrait alors redéfinir certaines variables.
Donc dans cette section, nous recréons le dossier initial buildroot. Puis nous créons le lien vers notre lanceur dans /usr/bin/, copions notre appli dans /usr/lib/, et copions notre fichier desktop et nos icônes aux bons emplacements dans /usr/share/.

les sections %post et %postun déclarent quelques actions après que notre appli sera installée ou désinstallée. Nous mettons à jour la base des icônes pour le gestionnaire de fenêtre.
Si l'appli prend en charge certains types mime, i.e. peut ouvrir certains type de fichier, celà doit être spécifier dans le fichier .desktop . Et vous devez ajouter le code suivant dans ces sections %post et %postun :

if [ -x /usr/bin/update-desktop-database ]; then
update-desktop-database &> /dev/null || :
fi

La section %files est obligatoire, et contient la liste complète des fichiers/dossiers installés par notre appli.

Enfin, le format de la section %changelog, est décrite dans cette doc.

2.4.3 Créer le rpm

Maintenant que nous avons tout ce dont nous avons besoin, nous allons créer un script pour créer une "fausse" archive source pour rpmbuild, qui contiendra notre appli, notre fichier desktop, et nos icônes. Nous placerons cette archive dans le dossiers SOURCES, puis invoquerons rpmbuild localement avec notre fichier spec. Et en fait, nous ferons tout celà dans un dossier temporaire, pour laisser nos sources propres.

Note: rmpbuild semble bugger si il y a des espaces dans le chemin du dossier de travail, donc faites attention avec le nom des (sous)dossiers où vous placez les sources.
Le script suivant s'arrêtera si c'est le cas.

Voilà ce script :

#!/bin/bash

# exit the script on errors
set -e

TMP_DIR=./tmp
SRC_VERSION=1.0

# 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 the rmpbuild tree folder
cp -rv rpmbuild/* "$TMP_DIR/"

# create a source tar.gz archive for rpmbuild into SOURCES
mkdir -v "$TMP_DIR/myapp-$SRC_VERSION"
cp -rv myapp "$TMP_DIR/myapp-$SRC_VERSION/"
cp -rv data "$TMP_DIR/myapp-$SRC_VERSION/"
cd "$TMP_DIR"
# clean eventual tempory files
find . -type f -name *.*~ -exec rm {} \;
tar -zcvf "$TMP_DIR/SOURCES/myapp-$SRC_VERSION.tar.gz" \
"myapp-$SRC_VERSION"
cd -
rm -rv "$TMP_DIR/myapp-$SRC_VERSION"

# rmpbuild seems to have a bug, it can't handle a _topdir with whitespace
if [ -n "$(echo "$TMP_DIR" | grep '[ ]')" ]; then
echo "the path '$TMP_DIR' contains whitespace, rpmbuild will failed. script aborted!"
exit 1
fi

# build the rpm from our spec, specify our local build tree
rpmbuild --define="_topdir $TMP_DIR" -bb "$TMP_DIR/myapp.spec"

# and finally copy our created rpm in to into the current folder
cp -v "$TMP_DIR/RPMS/noarch/myapp-$SRC_VERSION-1.noarch.rpm" ./

Pour créer notre rpm, dans un terminal :

  • cd samples_chapter_2
  • sh ./build_rpm.sh

Et en résultat nous avons finalement le fichier myapp-1.0-1.noarch.rpm dans le dossier myapp-src :) .

Nicolas Martin

Vous pouvez télécharger tous les exemples de ce chapitre 2 (Linux) dans l'archive samples_chapter_2.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.

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

Distribute your XULRunner app - 2.4 - Package as rpm

Ce billet existe aussi en français

This post is part of a series about how to package a XULRunner application.
See the preamble for the context case.

2.4 Package as rpm

2.4.1 Prerequisites

In this part we will create a rpm package, for rpm based distributions, like Red Hat, Fedora, Suse,... (I have tested only with Fedora, please let me know if there's any problems for others).

We can create this rpm from other Linux distributions as well, Debian based for example (that's what I do), we just need the rpmbuild executable, we have just to install rpm:

apt-get install rpm

Some documentation links:

I suppose we don't have any needed compilation, but if you have some, simply do it before packaging (despite the fact this is not recommended in this guide).

2.4.2 RPM files

To build a rpm, we're supposed to setup a user configuration for all rpm creation. But it is possible to do it in a local folder, that's what we will do.

We need to create the following folders, and a myapp.spec file:

    |- samples_chapter_2/
|- myapp/
|- rpmbuild/
|- BUILD/
|- RPMS/
|- SOURCES/
|- SPECS/
|- SRPMS/
|- myapp.spec

In fact this is the same structure than the required rpm environnement, but locally.

The .spec file is the 'script' used by rpmbuild to create the rpm. It is supposed to use a source tar.gz archive, that we will build especially later.

Here's the content of our spec:

Name:           myapp
Version: 1.0
Release: 1%{?dist}
Summary: simple Hello World powered by XULRunner.

Group: Development/Tools
License: MPLv1.1 or GPLv2+ or LGPLv2+
URL: http://example.com/myapp/
Source: myapp-1.0.tar.gz
BuildArch: noarch
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id} -un)

#BuildRequires:
Requires: firefox >= 3.6

%description
simple Hello World
Powered by XULRunner.

%prep
%setup

%build

%install
rm -rf %{buildroot}
mkdir -p %{buildroot}%{_libdir}
cp -R myapp %{buildroot}%{_libdir}/
mkdir -p %{buildroot}%{_bindir}

ln -s %{_libdir}/myapp/myapp.sh %{buildroot}%{_bindir}/myapp

mkdir -p %{buildroot}%{_datadir}/applications
cp data/myapp.desktop %{buildroot}%{_datadir}/applications/

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/16x16/apps
cp data/icons/icon16.png \
%{buildroot}%{_datadir}/icons/hicolor/16x16/apps/myapp.png

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/22x22/apps
cp data/icons/icon22.png \
%{buildroot}%{_datadir}/icons/hicolor/22x22/apps/myapp.png

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/32x32/apps
cp data/icons/icon32.png \
%{buildroot}%{_datadir}/icons/hicolor/32x32/apps/myapp.png

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/48x48/apps
cp data/icons/icon48.png \
%{buildroot}%{_datadir}/icons/hicolor/48x48/apps/myapp.png

mkdir -p %{buildroot}%{_datadir}/icons/hicolor/scalable/apps
cp data/icons/icon48.svg \
%{buildroot}%{_datadir}/icons/hicolor/scalable/apps/myapp.svg

%post
touch --no-create %{_datadir}/icons/hicolor &>/dev/null || :
if [ -x %{_bindir}/gtk-update-icon-cache ]; then
%{_bindir}/gtk-update-icon-cache -q %{_datadir}/icons/hicolor;
fi

%postun
touch --no-create %{_datadir}/icons/hicolor &>/dev/null || :
if [ -x %{_bindir}/gtk-update-icon-cache ]; then
%{_bindir}/gtk-update-icon-cache -q %{_datadir}/icons/hicolor;
fi


%clean
rm -rf $RPM_BUILD_ROOT


%files
%defattr(-,root,root,-)
%{_bindir}/myapp
%{_libdir}/myapp
%{_datadir}/applications/myapp.desktop
%{_datadir}/icons/hicolor/16x16/apps/myapp.png
%{_datadir}/icons/hicolor/22x22/apps/myapp.png
%{_datadir}/icons/hicolor/32x32/apps/myapp.png
%{_datadir}/icons/hicolor/48x48/apps/myapp.png
%{_datadir}/icons/hicolor/scalable/apps/myapp.svg
#%doc

%changelog
* Fri Oct 8 2010 John Doe <johndoe@example.com> 1.0-1
- first publication

Let see in detail some entries of this file:

The Name should not contain any whitespace characters.

The Version must contain only number and digit, no string like "beta".

The Group entry must be chosen in the list in the /usr/share/doc/rpm/GROUPS file. You should have it on your computer since you have installed rpm.

The Release is the version of the rpm for this version of the app, and %{?dist} will be replaced by rpmbuild by the current distribution. It will not be completed on a non rpm distribution, you can remove it if you want.

For the License entry, you must choose a short keyword from this list.

The Source entry must match exactly the tar.gz that we will create later.

The BuildArch: noarch line specify that our app is not architecture specific. If your app contains some compiled code, you must remove this line, and a rpm specific to the current architecture will be build.

The BuildRoot value is a usual one.

The BuildRequires is commented (with a #), as we have no compilations.

Finally, the Requires entry contains the other packages that are needed by our app. You could add others, separated by comma (,). Note that you can't declare alternative dependencies like with deb. That's why we declare Firefox and not XULRunner as dependencies, in the eventual case where the Firefox of the distribution is not XULRunner based.

Later we will copy the tar.gz archive of our app into the SOURCES folder.
Then the %prep section and the %setup instruction will unpack it into the BUILD folder.
The %build section is empty, because we have no compilation to do.

In the %install section, we install our app.
These are some shell commands, the current working directory is the unpacked source tar.gz. %{buildroot} is the root of the local installation of our app, this will be the computer root / in the real installation.
%{_libdir}, %{_bindir}, and %{_datadir} corresponds respectively to /usr/lib/, /usr/bin/, and /usr/share/. I guess that's the case for main rpm based distros, If I'm wrong, please correct me, it means that some these variables should be redefined.
So in this section, we re-create the initial buildroot folder. Then we create the link to our launcher in /usr/bin/, copy our app in /usr/lib/, and copy the desktop file and our icons into the right location in /usr/share/.

The %post and %postun sections declare some actions after that our app will be installed and uninstalled. We update the icons database for the windows manager.
If the app handle some mime types, i.e. can open some type of files, this should be specified in the .desktop file. And you must add the following code in these %post and %postun sections:

if [ -x /usr/bin/update-desktop-database ]; then
update-desktop-database &> /dev/null || :
fi

The %files section is required, and contains the complete list of files/folders installed by our app.

Finally, the format of the %changelog section, is described in this doc.

2.4.3 Create the rpm

So now that we have all that we need, we will create a script to create a fake source archive for rpmbuild, which contains our app, our desktop file, and our icons. We will place this archive in the SOURCES folder, then invoke rpmbuild locally with our spec file. And in fact, we will doing all of this in a temporary folder, to let our sources clean.

Note: rmpbuild seems to bug if there's some whitespace in the path of the working folder, so be careful with the name of the (sub)directories where you place your sources.
The following script will abort if this is the case.

Here this script:

#!/bin/bash

# exit the script on errors
set -e

TMP_DIR=./tmp
SRC_VERSION=1.0

# 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 the rmpbuild tree folder
cp -rv rpmbuild/* "$TMP_DIR/"

# create a source tar.gz archive for rpmbuild into SOURCES
mkdir -v "$TMP_DIR/myapp-$SRC_VERSION"
cp -rv myapp "$TMP_DIR/myapp-$SRC_VERSION/"
cp -rv data "$TMP_DIR/myapp-$SRC_VERSION/"
cd "$TMP_DIR"
# clean eventual tempory files
find . -type f -name *.*~ -exec rm {} \;
tar -zcvf "$TMP_DIR/SOURCES/myapp-$SRC_VERSION.tar.gz" \
"myapp-$SRC_VERSION"
cd -
rm -rv "$TMP_DIR/myapp-$SRC_VERSION"

# rmpbuild seems to have a bug, it can't handle a _topdir with whitespace
if [ -n "$(echo "$TMP_DIR" | grep '[ ]')" ]; then
echo "the path '$TMP_DIR' contains whitespace, rpmbuild will failed. script aborted!"
exit 1
fi

# build the rpm from our spec, specify our local build tree
rpmbuild --define="_topdir $TMP_DIR" -bb "$TMP_DIR/myapp.spec"

# and finally copy our created rpm in to into the current folder
cp -v "$TMP_DIR/RPMS/noarch/myapp-$SRC_VERSION-1.noarch.rpm" ./

To create our rpm, in a terminal:

  • cd samples_chapter_2
  • sh ./build_rpm.sh

And as result we have finally the file myapp-1.0-1.noarch.rpm in the myapp-src folder :) .

Nicolas Martin

You can download all the samples of this chapter 2 (Linux) in the samples_chapter_2.tar.gz archive.

The myapp application, from developer.mozilla.org is in the Public Domain.

The icon used is from the Tango Desktop Project, and is in the Public Domain.

All added data, and sample files, of this chapter 2, are in the Public Domain too.

lundi, mai 23 2011

Distribuer votre appli XULRunner - 2.3 Un cas spécial, deb pour Maemo

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.

2 - Linux

2.3 Un cas spécial : deb pour Maemo

2.3.1 Maemo - généralités

Maemo est une distribution Linux basée sur Debian, pour les mobiles. Et Firefox Mobile, nom de code Fennec, y est disponible.
Fennec, comme la version standard de Firefox, peut lancer une appli XULRunner via l'argument -app.

Donc, a priori, nous pouvons lancer toutes les applications XULRunner. Bien sûr nous devrions tenir compte des spécificités des appareils mobiles, petit écran (mais avec une haute résolution), et écran tactile. Ce n'est pas le propos de ce tutoriel. Mais, telle quelle, notre appli devrait fonctionner.

Cette plateforme étant basée sur Debian, nous distribuerons un deb pour notre appli. Nous devons juste adapter quelques parties du chapitre précédent aux spécificités de Maemo.

Le Maemo Wiki contient quelques informations intéressantes.

2.3.2 Adapter le lanceur

Le changement est que nous invoquons Fennec à la place de Firefox.
Le nouveau lanceur, myapp.sh :

#!/bin/sh

CUR_DIR=$(dirname $(readlink -f "$0"))

if [ -x /usr/bin/fennec ]; then
/usr/bin/fennec -app "$CUR_DIR/application.ini" $@
elif [ -x /usr/bin/xulrunner ]; then
/usr/bin/xulrunner "$CUR_DIR/application.ini" $@
else
echo "Error: unable to find Fennec or XULRunner!"
fi
2.3.3 Nouvelle organisation des fichiers - Où placer notre appli

Sur Maemo, à cause de l'espace mémoire des appareils mobiles, les applications doivent résider dans le dossier /opt/ plutôt que dans /usr/lib/ ou /usr/share/.

Donc nous allons copier notre appli dans /opt/myapp/.
Un sous dossier serait possible également, /opt/myorganization/myapp/ par exemple.

Pour les icônes, nous utiliserons des dimensions différentes que pour le bureau, comme le fait Fennec : 1 png de 26x26px png, 1 png de 40x40px, et 1 svg de 48x48px.
Leurs destinations finales changent un peu également, elles doivent être placées dans :

  • /usr/share/icons/hicolor/26x26/hildon/myapp.png
  • /usr/share/icons/hicolor/40x40/hildon/myapp.png
  • /usr/share/icons/hicolor/scalable/hildon/myapp.svg

L'emplacement du fichier .desktop change aussi, il doit être placé dans /usr/share/applications/hildon/ .

Le fichier .desktop et les images ne sont pas trop lourdes, nous pouvons les placer ainsi, mais il serait possible de les placer dans notre dossier principal d'application et d'utiliser des liens symboliques.

2.3.4 Additions au fichier .desktop

Quelques informations peuvent être trouvées dans cette page du Wiki Maemo.

Quelques clés additionnelles peuvent être utilisées dans le fichier desktop. Certaines que j'ajoute ne sont pas documentées, mais semblent être utilisées par beaucoup de programmes, peut être pour d'anciennes version de Maemo.
Voici quelques lignes ajoutées à notre fichier desktop :

X-Icon-Path=/usr/share/icons
X-Window-Icon=myapp
X-Window-Icon-dimmed=myapp
X-Osso-Type=application/x-executable
2.3.5 Changements aux fichiers Debian

Pour éviter d'écraser le deb générique créé dans le chapitre précédent, nous allons renommer ce nouveau paquet deb pour Maemo. Nous utiliserons 'myapp-mobile'. Pour être cohérent avec le processus debhelper, voici la liste de tous ces changements :

  • Dans le fichier changelog, la ligne myapp (1.0-1) unstable; urgency=low devient myapp-mobile (1.0-1) unstable; urgency=low

  • Dans le fichier control, la valeur pour les champs Source et Package est changée en myapp-mobile

  • Le ficher myapp.links est renommé en myapp-mobile.links

Autres changements au fichier control :

D'après cette documentation, nous devons choisir une Section dans cette liste :

  • user/desktop
  • user/development
  • user/education
  • user/games
  • user/graphics
  • user/multimedia
  • user/navigation
  • user/network
  • user/office
  • user/science
  • user/system
  • user/utilities

Nous pouvons ajouter des données supplémentaires au fichier control, utilisé pour construire le deb.
le premier est XB-Maemo-Display-Name: MyApp .

Le deuxième est une icône base64, utilisée par le gestionnaire d'applications de Maemo, XB-Maemo-Icon-26.

Nous utiliserons un script bash pour créer cette valeur base64 à partir d'une icône png de 48px.

Nous avons besoin du programme uuencode, sur debian/ubuntu :

apt-get install sharutils

puis nous créons un script bash nommé build_base64.sh pour convertir un png de 48x48px en la valeur texte désirée :

#!/bin/bash

set -e

# base64 conversion
uuencode -m icon48.png icon48.png > icon48.txt

# remove the first line (begin-base64 ...)
sed -i '1d' icon48.txt

# and the last line (====)
sed -i '$d' icon48.txt

# add 4 spaces at the beginning of each lines
sed -i "s/^/ /" icon48.txt

# and add a blank line at the end of the file
echo '' >> icon48.txt

vous pouvez trouvez ce script dans le dossier data/icons de l'archive samples_chapter_2.tar.gz. Pour le lancer (depuis le dossier icons) :

sh build_base64.sh

Nous pouvons maintenant utilisé le contenu de icon48.txt comme valeur de XB-Maemo-Icon-26 dans notre fichier control.

2.3.6 Créer le deb

Et c'est tout, nous pouvons maintenant créer notre deb comme dans le chapitre précédent.

Je n'écris pas ici le script build_maemodeb.sh, c'est pratiquement le même que celui pour construire le paquet générique debian. Vous le trouverez dans l'archive jointe.

  • cd samples_chapter_2
  • sh ./build_maemodeb.sh

Note: je suppose dans ce tutoriel que nous n'avons pas de code compilé dans notre appli.
Si ce n'est pas le cas, nous devons utiliser scratchbox pour compiler, et nous devons utiliser scratchbox pour construire le deb aussi !
Et, comme dans le chapitre précédent, ne pas utiliser la ligne Architecture: all dans le fichier control.

Nicolas Martin

Vous pouvez télécharger tous les exemples de ce chapitre 2 (Linux) dans l'archive samples_chapter_2.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.

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

Distribute your XULRunner app - 2.3 - Special case, deb for Maemo

Ce billet existe aussi en français

This post is part of a series about how to package a XULRunner application.
See the preamble for the context case.

2.3 Special case : deb for Maemo

2.3.1 Maemo generalities

Maemo is a Linux, Debian based, distribution, for Mobile. And Firefox Mobile, aka Fennec, is available on this platform.
Fennec, like the desktop version of Firefox, is able to launch a XULRunner application via the -app argument.

So, a priori, we can launch any XULRunner applications. Of course, we should care of the specificities of mobile devices, small screen (but with high resolution), and touch screen. That's not the purpose of this howto. But as is, our app should work.

Because this platform is Debian based, we will distribute our app with a deb. We have just to adapt some parts of the previous chapter to the specificities of Maemo.

We can find some information in the Maemo Wiki.

2.3.2 Adapt the launcher

The change is that we invoke Fennec rather than Firefox.
The new launcher, myapp.sh:

#!/bin/sh

CUR_DIR=$(dirname $(readlink -f "$0"))

if [ -x /usr/bin/fennec ]; then
/usr/bin/fennec -app "$CUR_DIR/application.ini" $@
elif [ -x /usr/bin/xulrunner ]; then
/usr/bin/xulrunner "$CUR_DIR/application.ini" $@
else
echo "Error: unable to find Fennec or XULRunner!"
fi

2.3.3 A new file system organization - Where place our app

On Maemo, due to the memory space on mobile devices, the applications should reside in the folder /opt/ rather than in /usr/lib/ or /usr/share/.

So, we will copy our app in /opt/myapp/.
A sub-directory would possible too, /opt/myorganization/myapp/ for example.

For the icons, we will use different dimensions than for the desktop, like Fennec does itself: a 26x26px png, a 40x40px png, and a 48x48 svg.
Their final locations change a little too, they must be placed in:

  • /usr/share/icons/hicolor/26x26/hildon/myapp.png
  • /usr/share/icons/hicolor/40x40/hildon/myapp.png
  • /usr/share/icons/hicolor/scalable/hildon/myapp.svg

The location of the .desktop file change too, it must be placed into /usr/share/applications/hildon/ .

The .desktop file and the images are not so heavy, we can put them as is, but it should be possible to place them in our main app folder and use symbolic links too.

2.3.4 Additions to the .desktop file

Some information can be found in this page of the Maemo Wiki.

Some additional keys can be used in the desktop file. Not all that I add are documentated, but they seems to be used by a lot of programs, perhaps for some older version of Maemo.
Here's some lines added to our desktop file:

X-Icon-Path=/usr/share/icons
X-Window-Icon=myapp
X-Window-Icon-dimmed=myapp
X-Osso-Type=application/x-executable

2.3.5 Changes to the Debian files

To avoid to overwrite the generic deb created in the previous chapter, we will rename this new deb package for Maemo. We will use 'myapp-mobile'. To be consistent with the debhelper process, here the list of all this concerned changes:

  • In the changelog file, the line myapp (1.0-1) unstable; urgency=low becomes myapp-mobile (1.0-1) unstable; urgency=low

  • In the control file, the value for the Source and Package fields is changed to myapp-mobile

  • The file myapp.links is renamed as myapp-mobile.links

Other changes to the control file:

According to this documentation, we must choose a Section in this list:

  • user/desktop
  • user/development
  • user/education
  • user/games
  • user/graphics
  • user/multimedia
  • user/navigation
  • user/network
  • user/office
  • user/science
  • user/system
  • user/utilities

We can add extra data to the control file, used to build the deb.
The first is XB-Maemo-Display-Name: MyApp .

The second is a base64 icon, used by the application manager of Maemo, XB-Maemo-Icon-26.

We will use a bash script to create this base64 value from the 48px png icon.

First we need the uuencode program, on debian/ubuntu:

apt-get install sharutils

then, we create a Bash script named build_base64.sh to convert a 48x48px png into the desired text value:

#!/bin/bash

set -e

# base64 conversion
uuencode -m icon48.png icon48.png > icon48.txt

# remove the first line (begin-base64 ...)
sed -i '1d' icon48.txt

# and the last line (====)
sed -i '$d' icon48.txt

# add 4 spaces at the beginning of each lines
sed -i "s/^/ /" icon48.txt

# and add a blank line at the end of the file
echo '' >> icon48.txt

You can find this script into the data/icons folder of the samples_chapter_2.tar.gz archive. To launch it (into the icons folder):

sh build_base64.sh

We can now use the content of icon48.txt as the value of XB-Maemo-Icon-26 in our control file.

2.3.6 Build the deb

And, that's all, we can create our deb like in the previous chapter.

I don't write here the build_maemodeb.sh script, it's almost the same as the one to build the generic debian package. You'll find it in the joined archive.

  • cd samples_chapter_2
  • sh ./build_maemodeb.sh

Note: I suppose in this howto that we don't have compiled code in our app.
If this is not the case, we must use scratchbox to compile, and we must use scratchbox to build the deb too!
And, like in the previous chapter, don't use the line Architecture: all in the control file.

Nicolas Martin

You can download all the samples of this chapter 2 (Linux) in the samples_chapter_2.tar.gz archive.

The myapp application, from developer.mozilla.org is in the Public Domain.

The icon used is from the Tango Desktop Project, and is in the Public Domain.

All added data, and sample files, of this chapter 2, are in the Public Domain too.

- page 3 de 15 -