joliclic blog

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

Tag - xul

Fil des billets

vendredi, mai 27 2011

Distribuer votre appli XULRunner - 5 - Synthèse - toutes plateformes

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.

5 - Synthèse - toutes plateformes

5.1 Changements dans l'appli

Résumons les changements que nous avons apporté à l'application elle-même, pour chaque plateforme :

  • Linux
    • Nous avons ajouté quelques icônes png dans le dossier myapp/chrome/icons/default/ .
      Ceci permet d'avoir notre icône pour nos fenêtres et dans la barre de tâche.
      En details, 16x16px, 32x32px et 48x48px.

      Nous pouvons conserver ces changements globalement, ils n'auront pas d'impact sur les autres plateformes.

  • Windows
    • De manière similaire à Linux, nous avons ajouté quelques icônes .ico dans le dossier myapp/chrome/icons/default/, pour les mêmes raisons.
      Et nous avons créer cette icône depuis Linux avec le programme icotool.

      Nous pouvons conserver ces changements globalement, ils n'auront pas d'impact sur les autres plateformes.

  • Mac OSX
    • Nous avons ajouté un élément menubar principal dans toutes nos fenêtres non modales, avec quelques entrées avec un id spécifique.
      Ceci nous permet d'avoir nos propres entrées dans le Menu principal Mac.
      Ces entrées sont masquées (d'un point de vue XUL) par défaut.

      Nous pouvons conserver ces changements globalement, ils n'auront pas d'impact sur les autres plateformes, parce ces éléments XUL sont masqués pour les autres plateformes.

    • Nous avons ajouté un peu de code JavaScript à notre fichier js principal. Parce que sur un Mac l'appli ne quitte pas quand on ferme toutes les fenêtres.

      Nous pouvons conserver ces changements globalement, le code est multi-plateforme. Mieux, nous pouvons l'utiliser également sur les autres plateformes.

    • Nous avons ajouté un nouveau fichier, une fenêtre XUL masquée. Cette fenêtre est utilisée pour granir le Menu Mac principal quand toutes nos fenêtres sont fermées.

      Nous pouvons conserver ces changements globalement, ils n'auront pas d'impact sur les autres plateformes.

    • Et nous avons ajouté une préférence (browser.hiddenWindowChromeURL), pour utiliser la fenêtre cachée précédente.

      Nous pouvons conserver ces changements globalement, ils n'auront pas d'impact sur les autres plateformes.

Donc, finalement, ce sont de petits changements, et tous peuvent être ajouté globalement, car ils ne perturbent pas les plateformes non ciblées.

5.2 Données ajoutées

Pour une meilleure intégration aux différentes platformes, nous avons ajouté quelques données supplémentaires :

  • Linux
    • Un script shell utilisé comme lanceur, qui utilise de XULRunner ou de Firefox
      Un lien symbolique vers ce lanceur dans /usr/bin/ est également créé par nos installeurs.

    • Un fichier .desktop, A .desktop file, pour l'intégration au bureau, compris par tous les gestionnaires de fenêtre principaux.

    • et quelques icônes (png et svg), utilisées par les fichiers desktop.

  • Windows
    • Un vrai lanceur en C, en fait XAL (XUL App Launcher), qui permet de lancer facilement notre appli avec Firefox, sans aucune fenêtre noire de commande, et qui peut être personnalisé avec noytre icône.
      Ce lanceur peut être compilé directement depuis Linux.

    • Un script batch comme lanceur est également possible, mais avec les défauts inhérents.

  • Mac OSX
    • Pour cette platforme, nous avons en fait encapsulé notre appli dans un dossier avec une structure spéciale, avec quelques données, un Application Bundle

    • Un script shell utilisé comme lanceur, qui utilise Firefox (en fait un lien symbolique vers lui, placé dans notre bundle)

    • Uneicône icns, utilisée par le bundle pour l'exécutable et dans le dock.
      et nous avons créer cette icône depuis Linux avec le programme png2icns.

    • Un fichier Info.plist et un fichier PkgInfo, décrivant le bundle.

5.3 Installeurs créés

  • Multi-plateformes

    Pas de réelle intégration au bureau, et avec quelques limitations importante sur Mac, mais un tar.gz fonctionne et est simple.

  • Linux

    Nous avons créé un paquet deb pour les distributions basées sur Debian/Ubuntu, un deb similaire pour Maemo, et un rpm pour les distributions basées sur Red Hat/Fedora.

  • Windows

    Nous avons créer un vrai installeur/désinstalleur, directement depuis Linux, basé sur NSIS.

  • Mac OSX

    Le mode de distribution le plus convivial sur Mac, est de créer un dmg. Nous avons pu le créer depuis Linux, et avec un programme expérimental, de le compresser.

    Un simple tar.gz a aussi été créé.

5.4 Un script global et réutilisable

Dans chaque chapitres, nous avons utilisés des scripts bash pour créer tous nos installeurs et autres données.
Je vous propose maintenant un script global pour réaliser toutes ces tâches en une fois.

Plus, ce script peut être utilisé pour tout autre application XULRunner, nous avons juste à modifier un fichier de configuration.

Je n'écris ce script ici, vous le trouverez dans l'archive jointe à ce chapitre, mais voilà le fichier de configuration :

#!/bin/bash

#_______________________________________________________________________________
#
# script version: 1.0
# date: 2011-05-20
#_______________________________________________________________________________

# exit the script on errors
set -e

#_______________________________________________________________________________
#
# common configuration
#_______________________________________________________________________________

# The name of the application, will be the name of the executable for example.
# It should contain only [a-zA-Z_-.] and no whitespace.
APP_NAME=myapp

# The name of the application displayed on screen.
APP_DISPLAY_NAME=MyApp

# The absolute path to the directory containing this script. Don't modify this
# variable if you don't know what you are doing.
CUR_DIR=$(dirname "$0")
CUR_DIR=$(readlink -f -- "$CUR_DIR")

# The main folder of the sources (absolute path), containing the app, data,...
# Can be defined before, in some other scripts.
if [ ! $MAIN_DIR ]; then
MAIN_DIR=$(readlink -f -- "$CUR_DIR/..")
fi

# The folder where all resulted builds (installers, archives,...) will be copied
DIST_DIR=$MAIN_DIR/dist

# The temporary folder where all build are done
TMP_DIR=$MAIN_DIR/tmp

# Folder which contains extra useful programs for the build script
TOOLS_DIR=$MAIN_DIR/tools

# Folder which contains all build scripts, and the current config file
BUILDERS_DIR=$MAIN_DIR/builders

# The folder of the real sources of the XULRunner application
APP_SRC_DIR=$MAIN_DIR/$APP_NAME

# The folder of the extra data of the app (icons,...)
APP_DATA_DIR=$MAIN_DIR/data

# The folder of the used icons
# This folder must contains:
# icon16.png, icon22.png, icon26.png, icon32.png, icon40.png, icon48.png,
# icon128.png, icon48.svg, icon.ico, icon.icns, icon48.txt
# You can find some shell scripts in the data/icons folder to create the ico,
# icns et base64 icons.
APP_ICON_DIR=$APP_DATA_DIR/icons

# The current version of the app
APP_VERSION=1.0

# You can use a file named version in the main directory too, and uncomment the
# following line to set this variable
#APP_VERSION=$(cat "$MAIN_DIR/version")

# if the app contains some compiled code, and so it's architecture dependent,
# uncomment the following line
#ARCH_DEPENDENT=1


#_______________________________________________________________________________
#
# Linux specific
#_______________________________________________________________________________

# the folder which contains the Linux specifics data
LINUX_DIR=$APP_DATA_DIR/linux

# the desktop file, will be renamed as $APP_NAME.desktop
DESKTOP_FILE=$LINUX_DIR/desktop

# If this variable is set, the desktop file will be adapted, i.e. the strings
# @@APP_VERSION@@, @@APP_NAME@@, @@APP_DISPLAY_NAME@@, and
# @@APP_DESKTOP_CATEGORIE@@,..., will be replaced by their values. Simply
# comment the following line to not use this feature.
OPT_ADAPT_DESKTOP=1

# this value is displayed in a tooltip by the desktop
APP_DESKTOP_COMMENT="a Hello World XULRunner app"

# A value chosen in http://standards.freedesktop.org/menu-spec/latest/apa.html
# If you use multiple value, use a semicolon (;) as separator
# DON'T forget the last semicolon, even if there is only one value
APP_DESKTOP_CATEGORIE='Utility;'

# You can use this variable to put extra lines to the Desktop file, for example
# a localized comment
APP_DESKTOP_EXTRA='Comment[fr]=un Bonjour Monde pour XULRunner.\n'

# the generic Linux launcher, a shell script, for our app. Will be renamed as
# $APP_NAME.sh
LINUX_LAUNCHER=$LINUX_DIR/launcher.sh


#_______________________________________________________________________________
#
# deb package specific
#_______________________________________________________________________________

# Comment the following line if you don't want to generate a deb package
OPT_BUILD_DEB=1

# the debian folder used to build our generic deb
DEBIAN_DIR=$APP_DATA_DIR/debian

# If this variable is set, the files 'changelog', 'control', 'menu',
# 'myapp.link', and 'copyright' of the debian folder will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, @@DEB_CONTROL_VERSION@@, @@DEB_CONTROL_SECTION@@,
# @@DEB_MENU_SECTION@@, ..., will be replaced by their values. Simply comment
# the following line to not use this feature.
OPT_ADAPT_DEBIAN=1

# The version of the generated deb package.
DEB_CONTROL_VERSION=${APP_VERSION}-1

# the category of the app. See http://packages.debian.org/en/sid/
DEB_CONTROL_SECTION=Utilities

DEB_CONTROL_MAINTAINER='John Doe <johndoe@example.com>'

# author of the generated deb
DEB_CONTROL_AUTHOR=$DEB_CONTROL_MAINTAINER

DEB_CONTROL_HOMEPAGE='<http://example.com/myapp/>'

# name of the generated package
DEB_CONTROL_PACKAGE=$APP_NAME

# long description of the app for the generated deb. Can be multiline, see
# http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Description
# for the formatting (in short, add at least a space at the beginning of all
# lines apart the first)
DEB_CONTROL_DESCRIPTION='simple Hello World.
Powered by XULRunner.'

# if the app have other dependencies than Firefox, list them, separated by a
# comma. Important, this list MUST begin by a comma.
DEB_CONTROL_EXTRA_DEPENDENCIES=

# if the app contains some compiled code, and you want to use the
# ${shlibs:Depends} variable with debhelper, uncomment the following line
#DEB_CONTROL_USE_SHLIBS_DEPENDS=1

# the category for the Debian menu, see
# http://www.debian.org/doc/packaging-manuals/menu-policy/ch2.html#s2.1
DEB_MENU_SECTION='Applications/Programming'

DEB_DATE="$(date -R)"

DEB_COPYRIGHT='<Copyright (C) 2010 John Doe>'

# the short license for the generated deb
DEB_SHORT_LICENSE=$(cat "$LINUX_DIR/deb_short_license.txt")


#_______________________________________________________________________________
#
# maemo deb specific
#_______________________________________________________________________________

# Note that the debian specific variables are used as well to generate the
# maemo deb, so they must set.

# the APP_NAME specific for Maemo, can be the same as APP_NAME
MOBILE_APP_NAME=$APP_NAME

# the APP_DISPLAY_NAME specific for Maemo, can be the same as APP_DISPLAY_NAME
MOBILE_APP_DISPLAY_NAME=$APP_DISPLAY_NAME

# the name of the app for the package. Should be different than APP_NAME,
# otherwise there can be collision with the generic deb created by the other
# script
MAEMO_PKG_APPNAME=${APP_NAME}-mobile

# the desktop file for Maemo, will be renamed as $APP_NAME.desktop
MAEMO_DESKTOP_FILE=$LINUX_DIR/maemodesktop

# the launcher for Maemo, a shell script, for our app. Will be renamed as
# $APP_NAME.sh
MAEMO_LAUNCHER=$LINUX_DIR/maemolauncher.sh

# Comment the following line if you don't want to generate a deb package
OPT_BUILD_MAEMO_DEB=1

# the debian folder used to build our deb for Maemo
MAEMODEBIAN_DIR=$APP_DATA_DIR/maemodebian

# the categorie of the app for Maemo. See
# http://wiki.maemo.org/Packaging/Guidelines#Sections
DEB_CONTROL_SECTION_MAEMO=user/utilities

# the name of the generated deb for Maemo.
# Should really the same as MAEMO_PKG_APPNAME, see its comment.
DEB_CONTROL_PACKAGE_MAEMO=$MAEMO_PKG_APPNAME

# the description of the generated deb, especially for Maemo. See the comments
# for DEB_CONTROL_DESCRIPTION in this file for formatting.
DEB_CONTROL_DESCRIPTION_MAEMO=$DEB_CONTROL_DESCRIPTION

# like DEB_CONTROL_EXTRA_DEPENDENCIES, but for Maemo
DEB_CONTROL_EXTRA_DEPENDENCIES_MAEMO=

# the base64 file (text) of the icon for the Maemo deb.
# Can be generated by the script 'build_icon_base64.sh' in the data/icons dir.
MAEMO_BASE64_ICON="$APP_ICON_DIR/icon48.txt"


#_______________________________________________________________________________
#
# rpm package specific
#_______________________________________________________________________________

# Comment the following line if you don't want to generate a rpm package
OPT_BUILD_RPM=1

# path to the spec file used to generate the rpm
RPM_SPEC_FILE=$LINUX_DIR/rpmspec

# If this variable is set, the .spec file will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, @@RPM_VERSION@@, @@RPM_RELEASE@@, @@RPM_GROUP@@,
# @@RPM_LICENSE@@, @@RPM_URL@@, @@RPM_SOURCE@@, @@RPM_DATE@@, ...
# will be replaced by their values. Simply comment the following line to not use
# this feature.
OPT_ADAPT_RPM=1

# version of the app for the spec file: must contain only integer and digit, no
# strings like 'beta'
RPM_VERSION=$APP_VERSION

# the version of the generated rpm, not the app version
RPM_RELEASE='1%{?dist}'

# the short version of the generated rpm, without any string relative to the
# distribution or architecture
#RPM_RELEASE_SHORT=1
RPM_RELEASE_SHORT=$(echo $RPM_RELEASE | sed "s/%.*$//")

# breve summary of the app for the package. One line only.
RPM_SUMMARY='simple Hello World powered by XULRunner.'

# the category of the app. See /usr/share/doc/rpm/GROUPS
RPM_GROUP=Development/Tools

# keyword license
RPM_LICENSE='MPLv1.1 or GPLv2+ or LGPLv2+'

# url where the source of our app can be found
RPM_URL='http://example.com/myapp/'

# the name of the dummy tar.gz source archive, it will be built by this script.
RPM_SOURCE=${APP_NAME}-${RPM_VERSION}.tar.gz

# If the app need some build dependencies, uncomment the following line and add
# them here, separated by a comma. BUT you shouldn't have to, the logic here is
# that the built should have been done before, this script simply package the
# result.
#RPM_BUILDREQUIRES=

# if the app have other dependencies than Firefox, list them, separated by a
# comma. Important, this list MUST begin by a comma.
RPM_EXTRA_REQUIRES=

# long description of the app for the generated rpm. Can be multiline.
RPM_DESCRIPTION='simple Hello World
Powered by XULRunner.'

LC_TIME_BUFFER=$LC_TIME
export LC_TIME="en_EN.utf8"
RPM_DATE=$(date +"%a %b %e %Y")
export LC_TIME="$LC_TIME_BUFFER"

RPM_MAINTAINER='John Doe <johndoe@example.com>'


#_______________________________________________________________________________
#
# Windows specific
#_______________________________________________________________________________

# the folder which contains the Windows specifics data
WIN_DIR=$APP_DATA_DIR/win

# the version of the app used by some other variables. String.
WIN_VERSION=$APP_VERSION

# comment this line if you don't want to build xal, the launcher in C
OPT_BUILD_WIN_XAL=1

# source directory of xal
XAL_SRC_DIR=$TOOLS_DIR/xal-src

# the rc file used to build xal
WIN_RESOURCE=$WIN_DIR/resource.rc

# If this variable is set, the resource (.rc) file for xal will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, and @@WINRES_...@@
# will be replaced by their values. Simply comment the following line to not use
# this feature.
OPT_ADAPT_WINRES=1

WINRES_FILEVERSION='0,1,0,0'
# the FILEVERSION for Windows (<int,int,int,int>) will be deducted from the
# APP_VERSION. Comment this line if you don't want use this feature
OPT_CALC_WINRES_FVERSION=1

WINRES_PRODUCTVERSION='0,1,0,0'
# the PRODUCTVERSION for Windows (<int,int,int,int>) will be deducted from the
# APP_VERSION. Comment this line if you don't want use this feature
OPT_CALC_WINRES_PVERSION=1

WINRES_Comments='Published under the MPL 1.1/GPL 2.0/LGPL 2.1 licenses'
WINRES_CompanyName='John Doe Organization'
WINRES_FileDescription=$APP_DISPLAY_NAME
WINRES_FileVersion=$WIN_VERSION
WINRES_InternalName=$APP_NAME
WINRES_LegalCopyright='(c) 2010 John Doe'
WINRES_ProductName=$APP_DISPLAY_NAME
WINRES_ProductVersion=$WIN_VERSION

# Comment the following line if you don't want to generate the nsis installer
# for Windows
OPT_BUILD_WIN_INSTALLER=1

# the source folder of the nsis scripts used by this script
NSIS_SRC_DIR=$APP_DATA_DIR/nsis

# If this variable is set, the nsis script will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, and @@NSIS_...@@
# will be replaced by their values. Simply comment the following line to not use
# this feature.
OPT_ADAPT_NSIS=1

NSIS_PRODUCT_NAME=$APP_DISPLAY_NAME
NSIS_PRODUCT_INTERNAL_NAME=$APP_NAME
NSIS_PRODUCT_VERSION=$WIN_VERSION
NSIS_PRODUCT_WIN_VERSION='1.0.0.0'
# for nsis, the PRODUCTVERSION for Windows (<int,int,int,int>) will be deducted
# from the APP_VERSION. Comment this line if you don't want use this feature
OPT_CALC_NSIS_WINVERSION=1

# The license file text MUST be in the main root app folder, so in $APP_SRC_DIR.
# Otherwise, you can edit the nsis script and adapt it
NSIS_LICENCE_NAME='LICENSE.txt'

# the name of the generated nsis installer
NSIS_INSTALLER_NAME="$APP_DISPLAY_NAME-$APP_VERSION-install.exe"


#_______________________________________________________________________________
#
# Mac OSX specific
#_______________________________________________________________________________

# the folder which contains the Mac OSX specifics data
MAC_DIR=$APP_DATA_DIR/mac

# the simple and basic shell launcher for Mac. Used only for the
# multiplatform tar.gz
MAC_BASIC_LAUNCHER=$MAC_DIR/basic_launcher.sh

# the previous basic launcher will be renamed with this value
# Trick: if the extension is 'command', the file can double-clicked in the Mac
# Finder, then a terminal will be opened and the script will be launched.
MAC_BASIC_LAUNCHER_NAME=${APP_NAME}-mac.command

# Comment the following line if you don't want to generate the Application
# Bundle of the app for Mac.
OPT_BUILD_MAC_BUNDLE=1

# path to the used skeleton of the bundle
DMG_SKELET_DIR=$MAC_DIR/bundle_skelet

# If this variable is set, the nsis script will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, and @@MAC_...@@
# will be replaced by their values. Simply comment the following line to not use
# this feature.
OPT_ADAPT_MAC_INFO=1

MAC_BUNDLE_EXECUTABLE=${APP_NAME}-mac.sh
MAC_BUNDLE_INFOSTRING="$APP_DISPLAY_NAME $APP_VERSION"
MAC_BUNDLE_ICONFILE=$APP_NAME
MAC_BUNDLE_IDENTIFIER="net.yourcompany.${APP_NAME}"
MAC_BUNDLE_NAME=$APP_DISPLAY_NAME
MAC_BUNDLE_SHORTVERSIONSTRING=$APP_VERSION
MAC_BUNDLE_VERSION=$APP_VERSION

# with this option, the eventual icon 'myapp.icns' in the Contents/Resources
# folder of the used bundle skelet will be removed, and the icns icon of the
# icons folder will be added to the bundle renamed as MAC_BUNDLE_ICONFILE
MAC_BUNDLE_OVERWRITE_ICON=1

# comment the following line if you don't want to generate a dmg image
OPT_BUILD_MAC_DMG=1
# comment the following line if you don't want to customize the dmg image
OPT_CUSTOMIZE_DMG=1
# source folder of the data added to the dmg, to customize it
# Note that you have to recreate manually these customizations files if the
# DMG_VOLNAME is different than 'MyApp', in particular the background image
# will not be displayed. See the Howto.
# Comment the line to disabled this option.
DMG_CUSTOM_DATA_DIR=$MAC_DIR/dmg_extra_data

# the name of the mounted dmg disk image
DMG_VOLNAME="$APP_DISPLAY_NAME"
# the name of the generated dmg
DMG_NAME="$APP_NAME-$APP_VERSION.dmg"

# comment the following line if you don't want to compress the dmg. You need
# the <dmg> program to be able to do that
OPT_COMPRESS_DMG=1
# path to the dmg program, used to compress the dmg.
# see http://github.com/planetbeing/libdmg-hfsplus
# and http://shanemcc.co.uk/libdmg/
TOOL_DMG="$TOOLS_DIR/dmg"


#_______________________________________________________________________________
#
# Multi-platform tar.gz archive (usable on Linux, Windows, Mac OSX)
#_______________________________________________________________________________

# Note that al lot of previous variables (Linux, Windows, and Mac OSX specific)
# will be used to build this archive

# Comment the following line if you don't want to generate multi-platform tar.gz
# Note that if ARCH_DEPENDENT is active, this archive will NOT be built
OPT_BUILD_MULTI_TARGZ=1

# the name of the generated tar.gz
TARGZ_NAME=${APP_NAME}-${APP_VERSION}-multiplatform


#_______________________________________________________________________________

BUILD_CONFIG_INCLUDED=1

Pour utiliser ce script pour votre application, en raccourci vous devez remplacer toutes les données du dossier data, images, fichier... puis ouvrir et adapter le script build_config.sh pour votre appli.

En detail :

  • remplacer toutes les icônes du dossier data/icons par les votres.
    Si vous n'avez pas celles aux formats .ico et .icns, utilisez les scripts build_ico.sh et build_icns.sh pour les créer. Et utilisez le script build_base64.sh pour générer l'icône base64 pour le paquet Maemo.

  • Éditez et modifiez le fichier deb_short_license.txt dans le dossier data/linux.

  • Si vous voulez créer un dmg pour Mac OSX et le personnaliser, vous devez recréer les données utilisées, en particulier le fichier .DS_STORE, et remplacer celles du dossier data/mac/dmg_extra_data par les votres. Faites attention, il y a quelques fichiers cachés dans ce dossier sur Linux, parce que leur noms commencent par un point (.) (CTRL+H in Nautilus to show them ;).

  • Assurez vous d'avoir un fichier de licence dans le dossier principal de vottre appli, c'est nécessaire pour créer l'installeur Windows, et c'est vraiment une bonne pratique.

  • Toutes les autres données peuvent etre utilisées telles quelles, parce que leurs contenus sont parséx par les scripts et utilise le fichier de config, mais vous aussi pouvez les éditer et les modifier si vous le souhaitez.

  • Si vous voulez créer un dmg compressé pour Mac OSX, vous devez télécharger le programme libdmg depuis la page de l'auteur ou cette version modifiée, et le placer dans le dossier tools.

  • Enfin, éditer et adapter le fichier config.sh dans le dossier builders.
    Les paramètres les plus importants sont APP_NAME et APP_DISPLAY_NAME, beaucoup d'autres paramètres utilisent leurs valeurs.
    Et bien le chemin vers les sources de votre appli, çà peut être une bonne idée de les placer au même niveau que l'exemple myapp, mais ce n'est pas obligatoire.
    Remplacer bien sûr toutes les chaînes spécifiques à l'exemple myapp.

Puis dans un terminal :

  • cd myapp-src-global
  • sh ./build_all.sh

Et vous trouverez alors dans le dossier myapp-src-all/dist/ tous les installeurs créés.

5.5 Conclusions

Créer des applications XULRunner est facile, puissant et multi-plateforme, comme développer des extensions Firefox.

Vous pouvez utiliser des assistants sur mozdev.org pour générer des squelettes pour commencer une nouvelle appli (attention vous devrez adapter enregistrement chrome pour Firefox 4, l'assistant a besoin apparemment d'être mis à jour pour cette partie).

Nous avons vu dans ce tutoriel quelques petites parties auxquelles nous devons faire attention pour une meilleure intégration au bureau, et qu'il est relativement simple de créer des installeurs, depuis Linux, pour différentes plateformes cibles. Vous pouvez même réutiliser le script global, ou vous en inspirer, pour créer ces installeurs pour votre application.

Maintenant c'est à vous de faire de jolies applis, j'espère que ce tuto et le script global vous aideront.
Amusez vous bien ;) !

Nicolas Martin

Vous pouvez télécharger tous les exemples de ce chapitre 5, le script global et XAL inclus, dans l'archive myapp-src-global.tar.gz .

Ou retrouver l'intégralité de ce tuto dans sa page dédiée, disponible en téléchargement avec tous les exemples compris.

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 - 5 - Synthesis - all platforms

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.

5 - Synthesis - all platforms

5.1 Changes to the app

Let summarize the changes we have made to the application itself, for each platform:

  • Linux
    • We have added some png icons into the myapp/chrome/icons/default/ folder.
      This allows to have our icon for our windows and taskbar.
      In details, 16x16px, 32x32px and 48x48px.

      We can keep this change globally, it will have no impact on other platforms.

  • Windows
    • Similarly as for Linux, we have added some .ico icon into the myapp/chrome/icons/default/ folder, for the same reasons.
      And we have built this icon from Linux with the icotool program.

      We can keep this change globally, it will have no impact on other platforms.

  • Mac OSX
    • We have added a main menubar in all our non-modal windows, with some entries with a special id.
      This allows to have our owns entries in the Main Menu of Mac.
      These entries are hidden (in XUL sens) by default.

      We can keep this change globally, it will have no impact on other platforms, because these XUL elements are hidden for other platforms.

    • We have added some JavaScript code to our main js file, to allow quitting properly our app. Because on a Mac the app doesn't quit when we close all windows.

      We can keep this change globally, this code is multi platform. More, we can use it on the other platform as well.

    • We have added a new file, an hidden XUL window. This window is used to populate the Main Mac Menu when all our windows are closed.

      We can keep this change globally, it will have no impact on other platforms.

    • And we have added a new preference (browser.hiddenWindowChromeURL), to use the previous hidden window.

      We can keep this change globally, it will have no impact on other platforms.

So, finally these are small changes, and all of them can be added globally, because they don't disturb the untargeted platforms.

5.2 Added data

For a better integration to each platforms, we have added some specific data:

  • Linux
    • A shell script used as launcher, which relies on XULRunner or Firefox.
      A symbolic link to this launcher into /usr/bin/ is also created by our installers.

    • A .desktop file, for desktop integration, understood by all main Window Manager.

    • And some icons (png and svg), used by the desktop files.

  • Windows
    • A real launcher in C, in fact XAL (XUL App Launcher), which allows to easily launch our app with Firefox, without any black command window, and can be customized with our icon.
      This launcher can be compiled directly from Linux.

    • A batch script as launcher, is possible too, but with some inherent defaults.

  • Mac OSX
    • For this platform, we have in fact encapsulated our app into a special folder structure with some data, an Application Bundle

    • A shell script used as launcher, which uses Firefox (in fact a symbolic link to it, placed in our bundle)

    • A icns icon, used by the bundle for the executable and in the dock.
      And we have built this icon from Linux with the png2icns program.

    • A Info.plist file and a PkgInfo file, describing the bundle.

5.3 Created installers

  • Multi-platform

    No real desktop integration, and with some real limitations on Mac, but a tar.gz works and is simple.

  • Linux

    We have created a deb package for Debian/Ubuntu based distributions, a similar deb for Maemo, and a rpm for Red Hat/Fedora based distributions.

  • Windows

    We have built a real installer/uninstaller, directly from Linux, based on NSIS.

  • Mac OSX

    The easiest distribution method on Mac, is to create a dmg. We have been able to build it from Linux, and with an experimental program, to compress it.

    A simple tar.gz was created too.

5.4 A global and reusable script

In each chapters, we have used some dedicated bash scripts to create all our installers and other data.
I propose now a global script to performs all the tasks in one shot.

More, this script can be used with any other XULRunner application, we only have to adapt a config file.

I don't write this script here, you'll find it in the joined archive of this chapter, but here's the config file:

#!/bin/bash

#_______________________________________________________________________________
#
# script version: 1.0
# date: 2011-05-20
#_______________________________________________________________________________

# exit the script on errors
set -e

#_______________________________________________________________________________
#
# common configuration
#_______________________________________________________________________________

# The name of the application, will be the name of the executable for example.
# It should contain only [a-zA-Z_-.] and no whitespace.
APP_NAME=myapp

# The name of the application displayed on screen.
APP_DISPLAY_NAME=MyApp

# The absolute path to the directory containing this script. Don't modify this
# variable if you don't know what you are doing.
CUR_DIR=$(dirname "$0")
CUR_DIR=$(readlink -f -- "$CUR_DIR")

# The main folder of the sources (absolute path), containing the app, data,...
# Can be defined before, in some other scripts.
if [ ! $MAIN_DIR ]; then
MAIN_DIR=$(readlink -f -- "$CUR_DIR/..")
fi

# The folder where all resulted builds (installers, archives,...) will be copied
DIST_DIR=$MAIN_DIR/dist

# The temporary folder where all build are done
TMP_DIR=$MAIN_DIR/tmp

# Folder which contains extra useful programs for the build script
TOOLS_DIR=$MAIN_DIR/tools

# Folder which contains all build scripts, and the current config file
BUILDERS_DIR=$MAIN_DIR/builders

# The folder of the real sources of the XULRunner application
APP_SRC_DIR=$MAIN_DIR/$APP_NAME

# The folder of the extra data of the app (icons,...)
APP_DATA_DIR=$MAIN_DIR/data

# The folder of the used icons
# This folder must contains:
# icon16.png, icon22.png, icon26.png, icon32.png, icon40.png, icon48.png,
# icon128.png, icon48.svg, icon.ico, icon.icns, icon48.txt
# You can find some shell scripts in the data/icons folder to create the ico,
# icns et base64 icons.
APP_ICON_DIR=$APP_DATA_DIR/icons

# The current version of the app
APP_VERSION=1.0

# You can use a file named version in the main directory too, and uncomment the
# following line to set this variable
#APP_VERSION=$(cat "$MAIN_DIR/version")

# if the app contains some compiled code, and so it's architecture dependent,
# uncomment the following line
#ARCH_DEPENDENT=1


#_______________________________________________________________________________
#
# Linux specific
#_______________________________________________________________________________

# the folder which contains the Linux specifics data
LINUX_DIR=$APP_DATA_DIR/linux

# the desktop file, will be renamed as $APP_NAME.desktop
DESKTOP_FILE=$LINUX_DIR/desktop

# If this variable is set, the desktop file will be adapted, i.e. the strings
# @@APP_VERSION@@, @@APP_NAME@@, @@APP_DISPLAY_NAME@@, and
# @@APP_DESKTOP_CATEGORIE@@,..., will be replaced by their values. Simply
# comment the following line to not use this feature.
OPT_ADAPT_DESKTOP=1

# this value is displayed in a tooltip by the desktop
APP_DESKTOP_COMMENT="a Hello World XULRunner app"

# A value chosen in http://standards.freedesktop.org/menu-spec/latest/apa.html
# If you use multiple value, use a semicolon (;) as separator
# DON'T forget the last semicolon, even if there is only one value
APP_DESKTOP_CATEGORIE='Utility;'

# You can use this variable to put extra lines to the Desktop file, for example
# a localized comment
APP_DESKTOP_EXTRA='Comment[fr]=un Bonjour Monde pour XULRunner.\n'

# the generic Linux launcher, a shell script, for our app. Will be renamed as
# $APP_NAME.sh
LINUX_LAUNCHER=$LINUX_DIR/launcher.sh


#_______________________________________________________________________________
#
# deb package specific
#_______________________________________________________________________________

# Comment the following line if you don't want to generate a deb package
OPT_BUILD_DEB=1

# the debian folder used to build our generic deb
DEBIAN_DIR=$APP_DATA_DIR/debian

# If this variable is set, the files 'changelog', 'control', 'menu',
# 'myapp.link', and 'copyright' of the debian folder will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, @@DEB_CONTROL_VERSION@@, @@DEB_CONTROL_SECTION@@,
# @@DEB_MENU_SECTION@@, ..., will be replaced by their values. Simply comment
# the following line to not use this feature.
OPT_ADAPT_DEBIAN=1

# The version of the generated deb package.
DEB_CONTROL_VERSION=${APP_VERSION}-1

# the category of the app. See http://packages.debian.org/en/sid/
DEB_CONTROL_SECTION=Utilities

DEB_CONTROL_MAINTAINER='John Doe <johndoe@example.com>'

# author of the generated deb
DEB_CONTROL_AUTHOR=$DEB_CONTROL_MAINTAINER

DEB_CONTROL_HOMEPAGE='<http://example.com/myapp/>'

# name of the generated package
DEB_CONTROL_PACKAGE=$APP_NAME

# long description of the app for the generated deb. Can be multiline, see
# http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Description
# for the formatting (in short, add at least a space at the beginning of all
# lines apart the first)
DEB_CONTROL_DESCRIPTION='simple Hello World.
Powered by XULRunner.'

# if the app have other dependencies than Firefox, list them, separated by a
# comma. Important, this list MUST begin by a comma.
DEB_CONTROL_EXTRA_DEPENDENCIES=

# if the app contains some compiled code, and you want to use the
# ${shlibs:Depends} variable with debhelper, uncomment the following line
#DEB_CONTROL_USE_SHLIBS_DEPENDS=1

# the category for the Debian menu, see
# http://www.debian.org/doc/packaging-manuals/menu-policy/ch2.html#s2.1
DEB_MENU_SECTION='Applications/Programming'

DEB_DATE="$(date -R)"

DEB_COPYRIGHT='<Copyright (C) 2010 John Doe>'

# the short license for the generated deb
DEB_SHORT_LICENSE=$(cat "$LINUX_DIR/deb_short_license.txt")


#_______________________________________________________________________________
#
# maemo deb specific
#_______________________________________________________________________________

# Note that the debian specific variables are used as well to generate the
# maemo deb, so they must set.

# the APP_NAME specific for Maemo, can be the same as APP_NAME
MOBILE_APP_NAME=$APP_NAME

# the APP_DISPLAY_NAME specific for Maemo, can be the same as APP_DISPLAY_NAME
MOBILE_APP_DISPLAY_NAME=$APP_DISPLAY_NAME

# the name of the app for the package. Should be different than APP_NAME,
# otherwise there can be collision with the generic deb created by the other
# script
MAEMO_PKG_APPNAME=${APP_NAME}-mobile

# the desktop file for Maemo, will be renamed as $APP_NAME.desktop
MAEMO_DESKTOP_FILE=$LINUX_DIR/maemodesktop

# the launcher for Maemo, a shell script, for our app. Will be renamed as
# $APP_NAME.sh
MAEMO_LAUNCHER=$LINUX_DIR/maemolauncher.sh

# Comment the following line if you don't want to generate a deb package
OPT_BUILD_MAEMO_DEB=1

# the debian folder used to build our deb for Maemo
MAEMODEBIAN_DIR=$APP_DATA_DIR/maemodebian

# the categorie of the app for Maemo. See
# http://wiki.maemo.org/Packaging/Guidelines#Sections
DEB_CONTROL_SECTION_MAEMO=user/utilities

# the name of the generated deb for Maemo.
# Should really the same as MAEMO_PKG_APPNAME, see its comment.
DEB_CONTROL_PACKAGE_MAEMO=$MAEMO_PKG_APPNAME

# the description of the generated deb, especially for Maemo. See the comments
# for DEB_CONTROL_DESCRIPTION in this file for formatting.
DEB_CONTROL_DESCRIPTION_MAEMO=$DEB_CONTROL_DESCRIPTION

# like DEB_CONTROL_EXTRA_DEPENDENCIES, but for Maemo
DEB_CONTROL_EXTRA_DEPENDENCIES_MAEMO=

# the base64 file (text) of the icon for the Maemo deb.
# Can be generated by the script 'build_icon_base64.sh' in the data/icons dir.
MAEMO_BASE64_ICON="$APP_ICON_DIR/icon48.txt"


#_______________________________________________________________________________
#
# rpm package specific
#_______________________________________________________________________________

# Comment the following line if you don't want to generate a rpm package
OPT_BUILD_RPM=1

# path to the spec file used to generate the rpm
RPM_SPEC_FILE=$LINUX_DIR/rpmspec

# If this variable is set, the .spec file will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, @@RPM_VERSION@@, @@RPM_RELEASE@@, @@RPM_GROUP@@,
# @@RPM_LICENSE@@, @@RPM_URL@@, @@RPM_SOURCE@@, @@RPM_DATE@@, ...
# will be replaced by their values. Simply comment the following line to not use
# this feature.
OPT_ADAPT_RPM=1

# version of the app for the spec file: must contain only integer and digit, no
# strings like 'beta'
RPM_VERSION=$APP_VERSION

# the version of the generated rpm, not the app version
RPM_RELEASE='1%{?dist}'

# the short version of the generated rpm, without any string relative to the
# distribution or architecture
#RPM_RELEASE_SHORT=1
RPM_RELEASE_SHORT=$(echo $RPM_RELEASE | sed "s/%.*$//")

# breve summary of the app for the package. One line only.
RPM_SUMMARY='simple Hello World powered by XULRunner.'

# the category of the app. See /usr/share/doc/rpm/GROUPS
RPM_GROUP=Development/Tools

# keyword license
RPM_LICENSE='MPLv1.1 or GPLv2+ or LGPLv2+'

# url where the source of our app can be found
RPM_URL='http://example.com/myapp/'

# the name of the dummy tar.gz source archive, it will be built by this script.
RPM_SOURCE=${APP_NAME}-${RPM_VERSION}.tar.gz

# If the app need some build dependencies, uncomment the following line and add
# them here, separated by a comma. BUT you shouldn't have to, the logic here is
# that the built should have been done before, this script simply package the
# result.
#RPM_BUILDREQUIRES=

# if the app have other dependencies than Firefox, list them, separated by a
# comma. Important, this list MUST begin by a comma.
RPM_EXTRA_REQUIRES=

# long description of the app for the generated rpm. Can be multiline.
RPM_DESCRIPTION='simple Hello World
Powered by XULRunner.'

LC_TIME_BUFFER=$LC_TIME
export LC_TIME="en_EN.utf8"
RPM_DATE=$(date +"%a %b %e %Y")
export LC_TIME="$LC_TIME_BUFFER"

RPM_MAINTAINER='John Doe <johndoe@example.com>'


#_______________________________________________________________________________
#
# Windows specific
#_______________________________________________________________________________

# the folder which contains the Windows specifics data
WIN_DIR=$APP_DATA_DIR/win

# the version of the app used by some other variables. String.
WIN_VERSION=$APP_VERSION

# comment this line if you don't want to build xal, the launcher in C
OPT_BUILD_WIN_XAL=1

# source directory of xal
XAL_SRC_DIR=$TOOLS_DIR/xal-src

# the rc file used to build xal
WIN_RESOURCE=$WIN_DIR/resource.rc

# If this variable is set, the resource (.rc) file for xal will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, and @@WINRES_...@@
# will be replaced by their values. Simply comment the following line to not use
# this feature.
OPT_ADAPT_WINRES=1

WINRES_FILEVERSION='0,1,0,0'
# the FILEVERSION for Windows (<int,int,int,int>) will be deducted from the
# APP_VERSION. Comment this line if you don't want use this feature
OPT_CALC_WINRES_FVERSION=1

WINRES_PRODUCTVERSION='0,1,0,0'
# the PRODUCTVERSION for Windows (<int,int,int,int>) will be deducted from the
# APP_VERSION. Comment this line if you don't want use this feature
OPT_CALC_WINRES_PVERSION=1

WINRES_Comments='Published under the MPL 1.1/GPL 2.0/LGPL 2.1 licenses'
WINRES_CompanyName='John Doe Organization'
WINRES_FileDescription=$APP_DISPLAY_NAME
WINRES_FileVersion=$WIN_VERSION
WINRES_InternalName=$APP_NAME
WINRES_LegalCopyright='(c) 2010 John Doe'
WINRES_ProductName=$APP_DISPLAY_NAME
WINRES_ProductVersion=$WIN_VERSION

# Comment the following line if you don't want to generate the nsis installer
# for Windows
OPT_BUILD_WIN_INSTALLER=1

# the source folder of the nsis scripts used by this script
NSIS_SRC_DIR=$APP_DATA_DIR/nsis

# If this variable is set, the nsis script will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, and @@NSIS_...@@
# will be replaced by their values. Simply comment the following line to not use
# this feature.
OPT_ADAPT_NSIS=1

NSIS_PRODUCT_NAME=$APP_DISPLAY_NAME
NSIS_PRODUCT_INTERNAL_NAME=$APP_NAME
NSIS_PRODUCT_VERSION=$WIN_VERSION
NSIS_PRODUCT_WIN_VERSION='1.0.0.0'
# for nsis, the PRODUCTVERSION for Windows (<int,int,int,int>) will be deducted
# from the APP_VERSION. Comment this line if you don't want use this feature
OPT_CALC_NSIS_WINVERSION=1

# The license file text MUST be in the main root app folder, so in $APP_SRC_DIR.
# Otherwise, you can edit the nsis script and adapt it
NSIS_LICENCE_NAME='LICENSE.txt'

# the name of the generated nsis installer
NSIS_INSTALLER_NAME="$APP_DISPLAY_NAME-$APP_VERSION-install.exe"


#_______________________________________________________________________________
#
# Mac OSX specific
#_______________________________________________________________________________

# the folder which contains the Mac OSX specifics data
MAC_DIR=$APP_DATA_DIR/mac

# the simple and basic shell launcher for Mac. Used only for the
# multiplatform tar.gz
MAC_BASIC_LAUNCHER=$MAC_DIR/basic_launcher.sh

# the previous basic launcher will be renamed with this value
# Trick: if the extension is 'command', the file can double-clicked in the Mac
# Finder, then a terminal will be opened and the script will be launched.
MAC_BASIC_LAUNCHER_NAME=${APP_NAME}-mac.command

# Comment the following line if you don't want to generate the Application
# Bundle of the app for Mac.
OPT_BUILD_MAC_BUNDLE=1

# path to the used skeleton of the bundle
DMG_SKELET_DIR=$MAC_DIR/bundle_skelet

# If this variable is set, the nsis script will
# be adapted, i.e. the strings @@APP_VERSION@@, @@APP_NAME@@,
# @@APP_DISPLAY_NAME@@, and @@MAC_...@@
# will be replaced by their values. Simply comment the following line to not use
# this feature.
OPT_ADAPT_MAC_INFO=1

MAC_BUNDLE_EXECUTABLE=${APP_NAME}-mac.sh
MAC_BUNDLE_INFOSTRING="$APP_DISPLAY_NAME $APP_VERSION"
MAC_BUNDLE_ICONFILE=$APP_NAME
MAC_BUNDLE_IDENTIFIER="net.yourcompany.${APP_NAME}"
MAC_BUNDLE_NAME=$APP_DISPLAY_NAME
MAC_BUNDLE_SHORTVERSIONSTRING=$APP_VERSION
MAC_BUNDLE_VERSION=$APP_VERSION

# with this option, the eventual icon 'myapp.icns' in the Contents/Resources
# folder of the used bundle skelet will be removed, and the icns icon of the
# icons folder will be added to the bundle renamed as MAC_BUNDLE_ICONFILE
MAC_BUNDLE_OVERWRITE_ICON=1

# comment the following line if you don't want to generate a dmg image
OPT_BUILD_MAC_DMG=1
# comment the following line if you don't want to customize the dmg image
OPT_CUSTOMIZE_DMG=1
# source folder of the data added to the dmg, to customize it
# Note that you have to recreate manually these customizations files if the
# DMG_VOLNAME is different than 'MyApp', in particular the background image
# will not be displayed. See the Howto.
# Comment the line to disabled this option.
DMG_CUSTOM_DATA_DIR=$MAC_DIR/dmg_extra_data

# the name of the mounted dmg disk image
DMG_VOLNAME="$APP_DISPLAY_NAME"
# the name of the generated dmg
DMG_NAME="$APP_NAME-$APP_VERSION.dmg"

# comment the following line if you don't want to compress the dmg. You need
# the <dmg> program to be able to do that
OPT_COMPRESS_DMG=1
# path to the dmg program, used to compress the dmg.
# see http://github.com/planetbeing/libdmg-hfsplus
# and http://shanemcc.co.uk/libdmg/
TOOL_DMG="$TOOLS_DIR/dmg"


#_______________________________________________________________________________
#
# Multi-platform tar.gz archive (usable on Linux, Windows, Mac OSX)
#_______________________________________________________________________________

# Note that al lot of previous variables (Linux, Windows, and Mac OSX specific)
# will be used to build this archive

# Comment the following line if you don't want to generate multi-platform tar.gz
# Note that if ARCH_DEPENDENT is active, this archive will NOT be built
OPT_BUILD_MULTI_TARGZ=1

# the name of the generated tar.gz
TARGZ_NAME=${APP_NAME}-${APP_VERSION}-multiplatform


#_______________________________________________________________________________

BUILD_CONFIG_INCLUDED=1

To use this script for your own application, in short you have to overwrite all data included in the data folder, image, files... then open and adapt the build_config.sh script to your own.

In detail:

  • replace all icons in the data/icons folder by your owns.
    If you don't have the .ico and .icns ones, use the scripts build_ico.sh and build_icns.sh to build them. And use the build_base64.sh script to generate the base64 icon for the Maemo package.

  • Edit and modify the file deb_short_license.txt in the data/linux folder.

  • If you want to create a dmg for Mac OSX and customize it, you should recreate the used data, in particular the .DS_STORE file, and overwrite them in the data/mac/dmg_extra_data folder. Be careful, there're some hidden files in this folder on Linux, because their names begin with a dot (.) (CTRL+H in Nautilus to show them ;).

  • Be sure to have a license file into the main folder of your app, this is required to build the Windows installer, and this is really a good practice.

  • All other data could be used as is, because their contents will parsed by the scripts and use the config file, but you can edit and change them as well.

  • If you want to create a compressed dmg for Mac OSX, you have to download the libdmg program from the author page or this modified version, and place it in the tools folder.

  • Finally, edit and adapt the config.sh file in the builders folder.
    The most important parameters are APP_NAME and APP_DISPLAY_NAME, a lot of others parameters use them.
    And of course the path to the sources of your app, it could be a good idea to place them at the same level of the myapp example, but that's not required.
    Replace of course all strings specifics to the myapp example.

Then, in a terminal:

  • cd myapp-src-global
  • sh ./build_all.sh

And you will find into the myapp-src-all/dist/ folder all the created installers.

5.5 Conclusions

Creating XULRunner applications is easy, powerful, and multiplatform, like developing Firefox extensions.

You can use some wizards from mozdev.org to generate a skeleton to begin a new app (be careful, you will have to adapt the chrome registration for Firefox 4, the wizard need apparently to be updated for this part).

We have seen in this how-to some small parts that we need to taking care for a better desktop integration, and that it is relatively easy to create installers, from Linux, for several platforms. You can even reuse the global script, or get inspiration from it, to create these installers for your own application.

Now it's up to you to make some nice apps, I hope this how-to and the global script will help you.
Have fun ;) !

Nicolas Martin

You can download all the samples of this chapter 5, the global script and XAL included, in the myapp-src-global.tar.gz archive.

Or retrieve this entire how-to in its dedicated page, downloadable with all the examples.

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 5, are under Public Domains too.

jeudi, mai 26 2011

Distribuer votre appli XULRunner - 4 - Mac OSX

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.

4 - Mac OSX

4.1 Icônes pour Mac OSX

Nous avons besoin d'une icône icns Sur Mac les fenêtres n'ont pas d'icône, mais, plus tard, nous en utiliserons une dans le Dock. Et elle sera utilisée pour le bundle que nous créerons.

Nous pouvons générer ce fichier .icns directement depuis Linux. La restriction est que nous ne pouvons pas inclure des images avec pluieurs profondeur de couleur, mais des dimensions multiples est possible.
Nous avons besoin du programme png2icns compris dans le paquet icnsutils.
Sur Debian/Ubuntu:

apt_get install icnsutils

Nous obtenoons notre fichier icns à partir de plusieurs png avec :

png2icns icon.icns icon128.png icon48.png icon32.png icon16.png

Vous trouverez un script bash avec cette ligne de commande dans l'archive d'exemple relative à ce chapitre 4 (dans samples_chapter_4/data/icons/).

4.2 Créer un lanceur

Comme pour Linux, nous utiliserons un script shell pour notre lanceur. Mais nous avons plusieurs problèmes à résoudre.

le premier n'est pas trop difficile. Le programme readlink disponible sur Mac OSX n'est pas la version GNU, et nous ne pouvons pas utiliser l'option -f. Nous résouderons nous même cette fonctionnalité.

Un problème plus sérieux, est que nous ne pouvons pas déterminer où Firefox est installé. Donc, ici, nous imposerons une limitation, Firefox doit être installer à son emplacement par défaut, c'est à dire /Applications/ .

Note : si vous une suggestions pour déterminer où est installer Firefox, merci de me le faire savoir ;) .

Voici ce script, myapp-mac.sh, bien que ce ne soit pas exactement celui que nous utiliserons plus tard :

#!/bin/sh

set -e

# see http://stackoverflow.com/questions/7665/how-to-resolve-symbolic-links-in-a-shell-script
# get the absolute path of the executable
SELF_PATH=$(cd -P -- "$(dirname -- "$0")" && \
pwd -P) && SELF_PATH=$SELF_PATH/$(basename -- "$0")

# resolve symlinks
while [ -h $SELF_PATH ]; do
DIR=$(dirname -- "$SELF_PATH")
SYM=$(readlink $SELF_PATH)
SELF_PATH=$(cd $DIR && cd $(dirname -- "$SYM") && pwd)/$(basename -- "$SYM")
done

CUR_DIR=$(dirname "$SELF_PATH")

if [ -x /Applications/Firefox.app/Contents/MacOS/firefox-bin ]; then
/Applications/Firefox.app/Contents/MacOS/firefox-bin -app "$CUR_DIR/application.ini" $@
elif [ -x /Library/Frameworks/XUL.framework/xulrunner-bin ]; then
/Library/Frameworks/XUL.framework/xulrunner-bin "$CUR_DIR/application.ini" $@
else
echo "Error: unable to find Firefox or XULRunner!"
fi

Si nous essayons ce lanceur, nous pouvons voir 2 autres problèmes, l'icône affichée dans le dock est celui de Firefox, et le menu principal, quand nous fermons toutes les fenêtres de notre appli, est celui de Firefox.
il y a des solutions à ces problèmes, comme nous le verrons dans les prochaines parties.

Notez que si vous voulez vraiment utiliser un tel script, sans un bundle, vous pouvez vouloir l'utiliser avec l'extension .command plutôt que .sh, car alors le fichier peut être double-cliqué dans le Finder de Mac, ce qui ouvrira un terminal, puis l'appli.

4.3 Quelques spécifités Mac

Une particularité sur Mac, est le Menu principal contrôler par le système d'exploitation, et correspondant à l'application courante qui a le focus. Et quand toutes les fenêtres d'une même application sont fermées, ce menu reste présent tant que l'on ne quitte pas l'application;

Ceci est bien pris en charge dans une application XULRunner, mais nous devons ajouter un peu de code pour celà.

Quelques docuementation sur MDN:

Si notre appli utilises un élément menubar principal, XULRunner/Firefox l'utilisera (en fait le premier menubar) pour créer ce menu spécial. Si vous n'en avez pas, vous devriez en créer un, masquer par défaut pour les autres plateformes, il permettra la construction de ce menu.

les entrées dans le menu "pomme" sont construits d'après des éléments avec des id réservés. Vous devriez créer au moins quelques entrées, pour permettre de quitter l'appli par exemple.

Voici la liste des id disponibles correspondants à ces entrées (sources) :

id de l'élément item du menu correspondant
aboutName À propos de cette application
menu_preferences Préférences...
menu_mac_services Services
menu_mac_hide_app Masquer l'appli
menu_mac_hide_others Masquer les autres
menu_mac_show_all Tout afficher
menu_FileQuitItem Quitter

et voici un exemple d'un menu xul minimal pour cet usage :

  <commandset id="main-commands">
<command id="cmd:quit" oncommand="myappQuitApplication();"/>
</commandset>

<keyset id="ui-keys">
<key id="key:quitApp" key="Q" modifiers="accel" command="cmd:quit"/>
<key id="key:hideApp" key="H" modifiers="accel"/>
<key id="key:hideOthersApp" key="H" modifiers="accel,alt"/>
</keyset>

<menubar id="main-menubar" hidden="true">
<menu id="mac-menu">
<menupopup>
<menuitem id="menu_mac_hide_app" label="Hide My App" key="key:hideApp"/>
<menuitem id="menu_mac_hide_others" label="Hide Others" key="key:hideOthersApp"/>
<menuitem id="menu_mac_show_all" label="Show All"/>
<menuitem id="menu_FileQuitItem" label="Quit" key="key:quitApp" command="cmd:quit"/>
</menupopup>
</menu>
</menubar>

Bien sur, comme pour les autres parties de l'appli, les chaînes utilisées dans les labels et clés devraient être localisées. Mais ce n'est pas le propos de ce tutoriel.

La fonction myappQuitApplication a été ajoutée dans le fichier main.js de notre appli :

function myappQuitApplication() {
const Cc = Components.classes;
const Ci = Components.interfaces;

let appStartup = Cc['@mozilla.org/toolkit/app-startup;1'].
getService(Ci.nsIAppStartup);
appStartup.quit(Ci.nsIAppStartup.eAttemptQuit);

return true;
}

Enfin, le dernier problème : quand toutes les fenêtres de notre appli sont fermées, en fait l'appli ne quitte pas, et le menu principal demeure. Plus, tel quel, c'est le menu de Firefox que nous voyons.
En fait, il y a une fenêtre speciale et cachée que XULRunner utilises pour ce menu. Donc nous devons créer une telle fenêtre, et définir une préférence pour la spécifier.

Voilà la préférence ajoutée dans defaults/preferences/pref.js :

pref("browser.hiddenWindowChromeURL", "chrome://myapp/content/hiddenWindow.xul");

Et le contenu de hiddenWindow.xul, qui contient uniquement notre menubar :

<window id="hiddenWindow"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript">
<![CDATA[
function myappQuitApplication() {
const Cc = Components.classes;
const Ci = Components.interfaces;

let appStartup = Cc['@mozilla.org/toolkit/app-startup;1'].
getService(Ci.nsIAppStartup);
appStartup.quit(Ci.nsIAppStartup.eAttemptQuit);

return true;
}
]]>
</script>

<commandset id="main-commands">
<command id="cmd:quit" oncommand="myappQuitApplication();"/>
</commandset>

<keyset id="ui-keys">
<key id="key:quitApp" key="Q" modifiers="accel" command="cmd:quit"/>
<key id="key:hideApp" key="H" modifiers="accel"/>
<key id="key:hideOthersApp" key="H" modifiers="accel,alt"/>
</keyset>

<menubar id="main-menubar" hidden="true">
<menu id="mac-menu">
<menupopup>
<menuitem id="menu_mac_hide_app" label="Hide My App" key="key:hideApp"/>
<menuitem id="menu_mac_hide_others" label="Hide Others" key="key:hideOthersApp"/>
<menuitem id="menu_mac_show_all" label="Show All"/>
<menuitem id="menu_FileQuitItem" label="Quit" key="key:quitApp" command="cmd:quit"/>
</menupopup>
</menu>
</menubar>
</window>

4.4 Créer un "Application Bundle"

Sur Mac OSX, les applications sont empaquetées dans un format spécial nommé Application Bundle. C'est en fait un dossier, nommé avec l'extension .app, et avec une structure particulière.

Quelques documentations utiles :

Voici une proposition d'un dossier .app pour notre appli :

    |- MyApp.app/
|- Contents/
|- MacOS/
|- chrome/
|- defaults/
|- application.ini
|- chrome.manifest
|- foxstub
|- myapp-mac.sh
|- Resources/
|- myapp.icns
|- Info.plist
|- PkgInfo

le dossier MacOS contient en fait tous les fichiers et dossiers de notre appli XULRunner.

le dossier Resources contient uniquement notre icône au format icns.

Le fichier PkgInfo contient uniquement la chaîne APPL????. Je ne suis pas sûr que ce fichier soit réellement nécessaire, il semble important pour la compatibilité avec Mac OS 9.
La valeur spécifie que ce bundle est une application, et comme nous n'avons pas d'identifiant de 4 caractères valide, nous utilisons 4 points d'interrogation (????).

Le fichier Info.plist décrit notre appli :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>myapp-mac.sh</string>
<key>CFBundleGetInfoString</key>
<string>MyApp 1.0</string>
<key>CFBundleIconFile</key>
<string>myapp</string>
<key>CFBundleIdentifier</key>
<string>net.yourcompany.myapp</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>MyApp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0.0.0</string>
<key>NSAppleScriptEnabled</key>
<true/>
</dict>
</plist>

Quelques commentaires sur ces propriétés :

CFBundleExecutable pointe sur notre lanceur, le script shell nommé myapp-mac.sh.

la valeur de CFBundleIconFile est myapp parce que notre icône dans le dossier Resources est nommée myapp.icns. Maintenant, l'utilisateur final visualise notre bundle avec notre icône.

CFBundleIdentifier est formaté tel que domaine inversé point nom de l'application. Ce doit être un identifiant unique pour notre appli.

CFBundlePackageType spécifie que c'est un "application bundle" (APPL).

La valeur de CFBundleSignature est ???? parce que nous n'avons pas de signature. C'est supposé être un identifiant unique de 4 caractères, et supposé être obtenue après un enregistrement auprès d'Apple. Tel que je le comprend, c'est exiger essentiellement pour Mac OS 9. Ne vous inquiétez pas, je n'ai pas vu de problème avec cette valeur ;) .

Maintenant voyons une belle astuce. Tel quel, l'utilisateur final voit notre appli avec notre icône, mais quand il la lance, l'icône dans le dock est celle de Firefox.
Dans l'arborescence du dossier de notre bundle, vous pouvez voir un fichier nommé foxstub, ce fichier est en fait un lien symbolique vers l'exécutable de Firefox.

Ce lien est obtenu simplement, dans le dossier MacOS, avec :

ln -s /Applications/Firefox.app/Contents/MacOS/firefox-bin foxstub

Maintenant quand l'utilisateur lance notre appli, c'est bien notre icône qui est affichée dans le dock :) .

En fait, notre icône apparait dans le dock, puis disparait, et réapparait de nouveau, parce que notre script est lancé, puis l'exécutable lié. mais ce n'est pas un gros problème.

Voici le script modifié de notre lanceur myapp-mac.sh, qui utilise dorénavant le lien symbolique vers Firefox :

#!/bin/sh

set -e

# see http://stackoverflow.com/questions/7665/how-to-resolve-symbolic-links-in-a-shell-script
# get the absolute path of the executable
SELF_PATH=$(cd -P -- "$(dirname -- "$0")" && \
pwd -P) && SELF_PATH=$SELF_PATH/$(basename -- "$0")

# resolve symlinks
while [ -h $SELF_PATH ]; do
DIR=$(dirname -- "$SELF_PATH")
SYM=$(readlink $SELF_PATH)
SELF_PATH=$(cd $DIR && cd $(dirname -- "$SYM") && pwd)/$(basename -- "$SYM")
done

CUR_DIR=$(dirname "$SELF_PATH")

if [ -x "$CUR_DIR/foxstub" ]; then
"$CUR_DIR/foxstub" -app "$CUR_DIR/application.ini" $@
else
echo "Error: unable to find Firefox or XULRunner!"
fi

Nous allons générer notre "application bundle" à la'ide d'un script bash. Il est vraiment simple, nous avons juste à copier le contenu de notre appli dans le dossier MacOS. Voici le contenu de build_mac.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"

# create the .app folder
mkdir -v "$TMP_DIR/MyApp.app"

# copy our bundle skeleton
# (our launcher myapp-mac.sh and our symbolic link foxstub are included)
cp -rv data/bundle_skelet/. "$TMP_DIR/MyApp.app/"

# copy our app
cp -rv myapp/* "$TMP_DIR/MyApp.app/Contents/MacOS/"

# apply the correct permissions
chmod -R 755 "$TMP_DIR/MyApp.app"

# and copy the result in our main src folder
rm -frv MyApp.app
cp -Rv "$TMP_DIR/MyApp.app" ./

Ce script sera continuer par la suite. Mais pour le moment, en résultat, nous avons créer "l'Application Bundle" MyApp.app dans notre dossier source principal.

4.5 Empaqueter un tar.gz et un dmg

Pour distribuer notre bundle, nous devons créer une archive. Je propose 2 solutions, chacune avec ses avantages et inconvénients.

Créer un tar.gz

La première et la plus simple, est de créer un tar.gz. Ce format est bien pris en charge par Mac OSX. Le seul problème est l'utilisateur final risque de ne pas trouver celà très convivial. Voici ce qu'il a à faire :

  1. télécharger le fichier myapp.tar.gz.
  2. l'ouvrir. En fait le comportement par défaut est de décompresser l'archive dans le même dossier.
  3. aller dans le dossier décompressé
  4. déplacer par glisser/déposer le bundle où il le souhaite, par exemple dans /Applications/

Le problème est la phase 2, certains utilisateurs ne comprennent pas que l'archive est décompressée, et où.

pour créer ce tar.gz, nous ajoutons les lignes suivantes dans notre script build_mac.sh :

# create a sub-directory for the tar.gz creation
mkdir "$TMP_DIR/myapp-1.0"

# copy our bundle
# (It would be possible to add other files, the license for example)
cp -rv "$TMP_DIR/MyApp.app" "$TMP_DIR/myapp-1.0/"

rm -fv myapp-mac-1.0.tar.gz

cd "$TMP_DIR"
tar -zcvf "../myapp-mac-1.0.tar.gz" myapp-1.0
cd -
Créer un dmg

la seconde solution est de créer une archive dmg. En fait c'est un format d'image disque.
C'est une solution habituelle sur cette plateforme, et utilisée par Firefox par exemple.

Nous pouvons créer un dmg depuis Linux, mais avec une limitation : nous ne pouvons pas générer de dmg compressé. Enfin en fait il y a une méthode expérimentale de le faire, que je vais vous proposé ensuite, mais elle est encore expérimentale.
L'article le plus utile que j'ai trouvé sur ce sujet est ce billet de l'auteur de DMDirc. Nous allons suivre les mêmes méthodes.

La méthode la plus simple sur linux est d'utiliser mkisofs, ou genisoimage. genisoimage est un fork du premier, utilisé par certaines distributions, et comme un lien symbolique nommé mkisofs est également installé par ces distributions, j'utiliserai la commande mkisofs dans la suite du script.
Pour l'installer sur Debian/Ubuntu :

apt-get install genisoimage

Nous ajoutons dans notre script build_mac.sh :

# create the dmg
rm -fv myapp-1.0.dmg
mkisofs -V 'MyApp' -no-pad -r -apple -o "myapp-1.0.dmg" "$TMP_DIR/myapp-1.0"

Avec cette ligne, nous créons le fichier myapp-1.0.dmg, qui contient tous les fichiers et dossiers inclus dans le dossier $TMP_DIR/myapp-1.0, avec un nom de volume 'MyApp' (le titre de la fenêtre quand le dmg est monté sur Mac). Et tous les fichiers et dossiers dans le dmg ont les droits 755. the 755 rights.

Compresser ce dmg

La seule solution trouvée pour réaliser cette tâche sur Linux, mentionné dans ce billet, est d'utiliser le programme dmg du projet libdmg-hfsplus.
L'auteur dit clairement :

THE CODE HEREIN SHOULD BE CONSIDERED HIGHLY EXPERIMENTAL

traduction approximative :

CE CODE DOIT ÊTRE CONSIDÉRER COMME HAUTEMENT EXPÉRIMENTAL

Noter que le dernier commit du projet que j'ai essayé ne compilait pas (quelqu'un a rapporté le bug).
Et l'auteur de DMDirc rapporte un autre bug d'une version précédente. Mais essayer sa And the author of DMDirc report another bug in a previous version. But I have tried its version légèrement modifiée, avec succès !
Donc vous devriez l'essayer.

Voici les lignes ajoutées à notre script :

# try to compress our dmg, if the the command <dmg> is available
# see http://github.com/planetbeing/libdmg-hfsplus
# and http://shanemcc.co.uk/libdmg/
if [ -x "./tools/dmg" ]; then
# rename temporarily our dmg
mv myapp-1.0.dmg myapp-1.0.dmg.pre
# compress
"./tools/dmg" dmg myapp-1.0.dmg.pre myapp-1.0.dmg
rm -fv myapp-1.0.dmg.pre
fi

Noter que je n'inclus pas le programme dmg dans l'archive jointe, vous devrez le télécharger vous même depuis les liens précédents, et le placer dans le dossier tools.

Créer un dmg depuis Mac OSX

pour information, pour créer un dmg directement depuis Mac OSX, il existe le programme hdiutil. Pour créer un dmg compressé et s'ouvrant automatiquement, d'après un dossier nommé "myFolder", quelque chose du genre devrait marcher :

#!/bin/bash

hdiutil create -volname "myapp-1.0" -fs HFS+ -srcfolder "myFolder" -format UDRW myapp-temp.dmg

hdiutil convert myapp-temp.dmg -format UDZO -imagekey zlib-level=9 -o myapp.dmg

rm -fv myapp-temp.dmg
Personnaliser le dmg

il est possible de personnaliser notre dmg quand il est monté, c'est à dire la fenêtre affichée, par exemple : dimensions de l'icône, image d'arrière plan, dimensions de la fenêtre...

L'idée est de créer un dmg en lecture/écriture sur un mac, de le personnaliser manuellement, puis de faire une copie du fichier .DS_STORE et de toutes les autres données. Ensuite nous utiliserons ces données lors de la création de notre dmg à l'aide de notre script.
Cette partie ne peut être faite que manuellement et directement depuis un Mac.

Créer une image disque en lecture/écriture

Sur un Mac, ouvrez "Utilitaires de disque" dans le dossier "Utilitaires".
Cliquer sur "Nouvelle image".
Choisissez le même nom que le futur nom de volume de notre dmg (MyApp).
N'utilisez pas un format journalisé, pour être capable de choisir une "petite" taille pour ce dmg. La taille importe peu, elle doit juste être suffisante pour copier notre appli et les autres données.
Comme format d'image, choisissez image disque en lecture et écriture Puis cliquer sur "créer".

Ajouter nos données sur l'image disque

Maintenant montez ce dmg en cliquant dessus. Et ouvrez le.
Copiez notre appli dans la fenêtre ouverte. Nous pouvons copier le lien symbolique vers /Applications également, si il existe. Sinon nous le créerons dans un terminal plus tard.

Ouvrez un terminal, depuis les Utilitaires.
Allez dans l'image disque monté :
cd /Volumes/MyApp
puis créez un dossier nommé .background :
mkdir .background
comme ce nom commence par un point (.), ce dossier est caché, comme sur Linux.
copiez notre image d'arrière plan dans ce dossier :
cp pathToOurImage/background.png /Volumes/MyApp/.background/
Et, si besoin, créez le lien symbolique vers /Applications :
ln -s /Applications /Volumes/MyApp/Applications

Personnaliser l'image disque

Maintenant donnez le focus à la fenêtre du disque monté, et ouvrez "Afficher les options de présentation" depuis le menu "Présentation"
Sélectionner Image pour l'airrière plan, puis cliquer sur le bouton.
Nous devons sélectionner le fichier background.png dans le disque monté, mais comme ce fichier est dans un dossier caché, nous ne pouvons pas le voir. Pas d'inquiétude, taper Apple+Shift+G :
puis taper /Volumes/MyApp/.background/, puis entrée.
sélectionnez maintenant notre png.
Choissisez la taille d'icône, par exemple 128, arrangez les par aucun, puis déplacez les où vous le souhaitez, et adapter la taille de fenêtre à votre arrière-plan.

Maintenant fermer cette fenêtre, et éjecter le disque. Puis monter le de nouveau, et ouvrez le.

Copier la personnalisation créée

Dans un terminal, copiez le fichier .DS_STORE créé à la racine du disque monté :
cp /Volumes/MyApp/.DS_STORE pathToSave/myDS_STORE

Maintenant nous avons enfin tous les fichiers nécessaires pour notre script, nous avons juste à copier le fichier .DS_STORE et le dossier .background avec le fichier background.png . Le dmg que nous créerons aura la même personnalisation.
Copions ces données dans nos sources :

    |- samples_chapter_4/
|- data/
|- bundle_skelet/
|- dmg_extra_data/
|- .background/
|- background.png
|- Applications
|- .DS_STORE

Et nous ajoutons les lignes suivantes à notre script build_mac.sh, avant de créer le dmg :

# copy eventual extra data, by example a symbolic link to /Applications
if [ $(ls -A "data/dmg_extra_data" | wc -c) -ne 0 ]; then
cp -Rv data/dmg_extra_data/. "$TMP_DIR/myapp-1.0/"
fi

Notez que cette personnalisation est dépendente du nom de volume utilisé, en particulier pour l'image d'arrière plan, parce que le fichier .DS_STORE utilise des chemins absolus vers le disque monté, et le nom de volume en fait partie.
Donc si vous changer le nom de volume, vous devez recréer manuellement cette personnalisation...

Lancer notre script de build

Pour lancer notre script (sur Linux), et créer nos dmg et tar.gz, dans un terminal :

  • cd samples_chapter_4
  • sh ./build_mac.sh

Et, en résultat, nous avons finalement le dossier MyApp.app, et les fichiers myapp-1.0.dmg et myapp-1.0.tar.gz dans notre dossier de source principal.

Nicolas Martin

Vous pouvez télécharger tous les exemples de ce chapitre 4 (Mac OSX) dans l'archive samples_chapter_4.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, de ce chapitre 4, sont dans le Domain Public également.

Distribute your XULRunner app - 4 - Mac OSX

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.

4 - Mac OSX

4.1 Icons for Mac OSX

We need a icns icon. On Mac the windows doesn't have a icon, but, later, we will use one in the Dock. And it will be used for the bundle that we will create.

We can generate this .icns file directly from Linux. The restriction is that we cannot embed mutiple color depth images, but multiple sizes is possible.
We need the program png2icns from the icnsutils package.
On Debian/Ubuntu:

apt_get install icnsutils

We obtain our icns file from several png with:

png2icns icon.icns icon128.png icon48.png icon32.png icon16.png

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

4.2 Create a launcher

Like for Linux, we will use a shell script for our launcher. But we have several problems to resolve.

The first one is not too difficult. The readlink program available on Mac OSX is not the GNU one, and we can't use the -f option. We will resolve ourself this functionality.

A more serious problem, is that we can't determine where Firefox is installed. So, here, we will impose a limitation, Firefox must be installed in its default location, i.e. /Applications/ .

Note: if you have any suggestions to determine where is installed Firefox, please let me know ;) .

Here this script, myapp-mac.sh, but that's not exactly this one we will use later:

#!/bin/sh

set -e

# see http://stackoverflow.com/questions/7665/how-to-resolve-symbolic-links-in-a-shell-script
# get the absolute path of the executable
SELF_PATH=$(cd -P -- "$(dirname -- "$0")" && \
pwd -P) && SELF_PATH=$SELF_PATH/$(basename -- "$0")

# resolve symlinks
while [ -h $SELF_PATH ]; do
DIR=$(dirname -- "$SELF_PATH")
SYM=$(readlink $SELF_PATH)
SELF_PATH=$(cd $DIR && cd $(dirname -- "$SYM") && pwd)/$(basename -- "$SYM")
done

CUR_DIR=$(dirname "$SELF_PATH")

if [ -x /Applications/Firefox.app/Contents/MacOS/firefox-bin ]; then
/Applications/Firefox.app/Contents/MacOS/firefox-bin -app "$CUR_DIR/application.ini" $@
elif [ -x /Library/Frameworks/XUL.framework/xulrunner-bin ]; then
/Library/Frameworks/XUL.framework/xulrunner-bin "$CUR_DIR/application.ini" $@
else
echo "Error: unable to find Firefox or XULRunner!"
fi

If we try this launcher, we can see 2 other problems, the icon shown in the dock is the Firefox one, and the main menu, when we close all our app windows, is the Firefox one.
There is a solution to these problems, as we will see in the next parts.

Note that if you really want to use a such script, without a bundle, you may want use a .command extension rather than .sh, because then the file can be double-clicked in the Mac Finder, this will open a terminal then the app.

4.3 Some Mac specificities

There is a particularity on Mac, there is a main Menu controlled by the operating system, corresponding to the current focused application. And when all windows of a same application are closed, this menu still appeared, as long as the application quit.

This is well handled by a XULRunner application, but we must add some code for that.

Some documentation on MDN:

If your app uses a main menubar element, XULRunner/Firefox will use it (in fact the first menubar) to create this special menu. If you don't have one, you should create it, hidden by default for the other platforms, this will allow the menu construction anyway.

The entries in the apple menu are constructed from elements with reserved id. You should at least create some entries, to allow to quit the app for example.

Here's the list of available id corresponding to these entries (sources):

element id corresponding menu item
aboutName About This App
menu_preferences Preferences...
menu_mac_services Services
menu_mac_hide_app Hide App
menu_mac_hide_others Hide Others
menu_mac_show_all Show All
menu_FileQuitItem Quit

And here's an example of a minimal xul menu for this usage:

  <commandset id="main-commands">
<command id="cmd:quit" oncommand="myappQuitApplication();"/>
</commandset>

<keyset id="ui-keys">
<key id="key:quitApp" key="Q" modifiers="accel" command="cmd:quit"/>
<key id="key:hideApp" key="H" modifiers="accel"/>
<key id="key:hideOthersApp" key="H" modifiers="accel,alt"/>
</keyset>

<menubar id="main-menubar" hidden="true">
<menu id="mac-menu">
<menupopup>
<menuitem id="menu_mac_hide_app" label="Hide My App" key="key:hideApp"/>
<menuitem id="menu_mac_hide_others" label="Hide Others" key="key:hideOthersApp"/>
<menuitem id="menu_mac_show_all" label="Show All"/>
<menuitem id="menu_FileQuitItem" label="Quit" key="key:quitApp" command="cmd:quit"/>
</menupopup>
</menu>
</menubar>

Of course, like the other part of the app, the strings used in the labels and keys should be localized. But that's not the purpose of this howto.

The myappQuitApplication function has been added in the main.js file of the our app:

function myappQuitApplication() {
const Cc = Components.classes;
const Ci = Components.interfaces;

let appStartup = Cc['@mozilla.org/toolkit/app-startup;1'].
getService(Ci.nsIAppStartup);
appStartup.quit(Ci.nsIAppStartup.eAttemptQuit);

return true;
}

Finally, the last problem: when all windows of our app are closed, in fact the app doesn't quit, and the main menu remains. More, as is, this is the Firefox menu that we see.
In fact, there is a special hidden window that XULRunner uses for this menu. So we have to create a such window, and to set a preference to specify it.

Here's the added preference in defaults/preferences/pref.js:

pref("browser.hiddenWindowChromeURL", "chrome://myapp/content/hiddenWindow.xul");

And the content of hiddenWindow.xul, which contains only our main menubar:

<window id="hiddenWindow"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript">
<![CDATA[
function myappQuitApplication() {
const Cc = Components.classes;
const Ci = Components.interfaces;

let appStartup = Cc['@mozilla.org/toolkit/app-startup;1'].
getService(Ci.nsIAppStartup);
appStartup.quit(Ci.nsIAppStartup.eAttemptQuit);

return true;
}
]]>
</script>

<commandset id="main-commands">
<command id="cmd:quit" oncommand="myappQuitApplication();"/>
</commandset>

<keyset id="ui-keys">
<key id="key:quitApp" key="Q" modifiers="accel" command="cmd:quit"/>
<key id="key:hideApp" key="H" modifiers="accel"/>
<key id="key:hideOthersApp" key="H" modifiers="accel,alt"/>
</keyset>

<menubar id="main-menubar" hidden="true">
<menu id="mac-menu">
<menupopup>
<menuitem id="menu_mac_hide_app" label="Hide My App" key="key:hideApp"/>
<menuitem id="menu_mac_hide_others" label="Hide Others" key="key:hideOthersApp"/>
<menuitem id="menu_mac_show_all" label="Show All"/>
<menuitem id="menu_FileQuitItem" label="Quit" key="key:quitApp" command="cmd:quit"/>
</menupopup>
</menu>
</menubar>
</window>

4.4 Create a Application Bundle

On Mac OSX, applications are packaged in a special format named Application Bundle. This is in fact a folder, named with .app as extension, and with a special structure.

Some useful documentation:

Here's a proposal .app folder for our app:

    |- MyApp.app/
|- Contents/
|- MacOS/
|- chrome/
|- defaults/
|- application.ini
|- chrome.manifest
|- foxstub
|- myapp-mac.sh
|- Resources/
|- myapp.icns
|- Info.plist
|- PkgInfo

The MacOS folder contains in fact all the files and folders of our XULRunner app.

The Resources folder contains only our icon in icns format.

The PkgInfo file contains only the string APPL????. I'm not sure this file is really necessary, it's seems it's important for compatibilty with Mac OS 9.
The value specifies that this bundle is an application, and as we don't have a valid 4 characters identifier, we use 4 question marks (????).

The Info.plist file describe our app:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>myapp-mac.sh</string>
<key>CFBundleGetInfoString</key>
<string>MyApp 1.0</string>
<key>CFBundleIconFile</key>
<string>myapp</string>
<key>CFBundleIdentifier</key>
<string>net.yourcompany.myapp</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>MyApp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0.0.0</string>
<key>NSAppleScriptEnabled</key>
<true/>
</dict>
</plist>

Some comment on these properties:

The CFBundleExecutable point to our launcher, the shell script named myapp-mac.sh.

The value of CFBundleIconFile is myapp because our icon in the Resources folder is named myapp.icns. Now, the end user sees our bundle with our icon.

The CFBundleIdentifier is formatted as an inversed domain name dot application name. It must be a unique identifier for our app.

The CFBundlePackageType specifies that this is an application bundle (APPL).

The value of CFBundleSignature is ???? because we don't have a signature. This is supposed to be an unique 4 characters identifier, and supposed to be obtained by an Apple registration. As I understand this is required essentially for Mac OS 9. Don't worry, I have not seen any problem with our value ;) .

Now let's see a nice trick. As is, the end user sees our app with our icon, but when he launches it, the icon in the Dock is the Firefox one.
In the tree folder of our bundle, you can see a file named foxstub, this file is in fact a symbolic link to the Firefox executable.

This link is obtained simply, in the MacOS folder, with:

ln -s /Applications/Firefox.app/Contents/MacOS/firefox-bin foxstub

Now when the user launches our app, this is our icon that's displayed in the dock :) .

In fact, our icon appears in the dock, then disappears, and appears again, because our script is launched, then the linked executable. But that's not a big problem.

Here's the modified launcher script myapp-mac.sh, which now uses the symbolic link to firefox:

#!/bin/sh

set -e

# see http://stackoverflow.com/questions/7665/how-to-resolve-symbolic-links-in-a-shell-script
# get the absolute path of the executable
SELF_PATH=$(cd -P -- "$(dirname -- "$0")" && \
pwd -P) && SELF_PATH=$SELF_PATH/$(basename -- "$0")

# resolve symlinks
while [ -h $SELF_PATH ]; do
DIR=$(dirname -- "$SELF_PATH")
SYM=$(readlink $SELF_PATH)
SELF_PATH=$(cd $DIR && cd $(dirname -- "$SYM") && pwd)/$(basename -- "$SYM")
done

CUR_DIR=$(dirname "$SELF_PATH")

if [ -x "$CUR_DIR/foxstub" ]; then
"$CUR_DIR/foxstub" -app "$CUR_DIR/application.ini" $@
else
echo "Error: unable to find Firefox or XULRunner!"
fi

We will generate our application bundle with a bash script. It's very simple, we have just to copy the content of our app into the MacOS folder. Here's the content of build_mac.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"

# create the .app folder
mkdir -v "$TMP_DIR/MyApp.app"

# copy our bundle skeleton
# (our launcher myapp-mac.sh and our symbolic link foxstub are included)
cp -rv data/bundle_skelet/. "$TMP_DIR/MyApp.app/"

# copy our app
cp -rv myapp/* "$TMP_DIR/MyApp.app/Contents/MacOS/"

# apply the correct permissions
chmod -R 755 "$TMP_DIR/MyApp.app"

# and copy the result in our main src folder
rm -frv MyApp.app
cp -Rv "$TMP_DIR/MyApp.app" ./

This script will be continued in the next parts. But for the moment, as result, we have created the Application Bundle MyApp.app into our main source folder.

4.5 Package as tar.gz and dmg

To distribute our bundle, we need to create an archive. I propose here 2 solutions, each with advantages and disadvantages.

Create a tar.gz

The first and the simplest, is to create a tar.gz. This format is well handled by Mac OSX. The only problem, is that the final user can find it not really friendly. Here what's he have to do:

  1. download the myapp.tar.gz file.
  2. open it. In fact the default behavior is to uncompress the archive into the same folder.
  3. go into the uncompressed folder
  4. move by drag and drop the bundle where he wants, for example in /Applications/

The problem is phase 2, some users doesn't understand that the archive is uncompressed, and where.

To create this tar.gz, we add the following lines in our build_mac.sh script:

# create a sub-directory for the tar.gz creation
mkdir "$TMP_DIR/myapp-1.0"

# copy our bundle
# (It would be possible to add other files, the license for example)
cp -rv "$TMP_DIR/MyApp.app" "$TMP_DIR/myapp-1.0/"

rm -fv myapp-mac-1.0.tar.gz

cd "$TMP_DIR"
tar -zcvf "../myapp-mac-1.0.tar.gz" myapp-1.0
cd -
Create a dmg

The second solution is to create a dmg archive. In fact this is a disk image format.
This is a usual solution on this platform, and used by Firefox for example.

We can create a dmg from Linux, but with a limitation: we can't generate a compressed dmg. Well, in fact there's a experimental way to do it, that I will propose to you just after, but it's still experimental.
The most useful article that I have found about this subject is this post from the author of DMDirc. We will follow the same methods.

The easiest way on linux is to use mkisofs, or genisoimage. genisoimage is a fork of the former, used in some distributions, and as a symlink named mkisofs is installed as well on these distributions, I will use the command mkisofs in the following script.
To install it on Debian/Ubuntu:

apt-get install genisoimage

We add, in our build script build_mac.sh:

# create the dmg
rm -fv myapp-1.0.dmg
mkisofs -V 'MyApp' -no-pad -r -apple -o "myapp-1.0.dmg" "$TMP_DIR/myapp-1.0"

With this line, we create the file myapp-1.0.dmg, which contains all files and folders included in the $TMP_DIR/myapp-1.0 folder, with the volume name 'MyApp' (the title of the window when the dmg is mounted on Mac). And all files and folders in the dmg have the 755 rights.

Compress this dmg

The only solution found to perform this task on Linux, mentioned in this post, is to use the dmg program from the libdmg-hfsplus project.
The author clearly says:

THE CODE HEREIN SHOULD BE CONSIDERED HIGHLY EXPERIMENTAL

Note that the last commit of the project that I have tried doesn't compile (somebody have reported the bug).
And the author of DMDirc report another bug in a previous version. But I have tried its slightly modified version, with success!
So, you should give it a try.

Here the added lines to our build script:

# try to compress our dmg, if the the command <dmg> is available
# see http://github.com/planetbeing/libdmg-hfsplus
# and http://shanemcc.co.uk/libdmg/
if [ -x "./tools/dmg" ]; then
# rename temporarily our dmg
mv myapp-1.0.dmg myapp-1.0.dmg.pre
# compress
"./tools/dmg" dmg myapp-1.0.dmg.pre myapp-1.0.dmg
rm -fv myapp-1.0.dmg.pre
fi

Note that I don't include the dmg program in the joined archive, you have to download it yourself from the previous links, and place it into the tools folder.

Create a dmg from Mac OSX

For information, to create a dmg directly on Mac OSX, there's the program hdiutil. To create an auto open, compressed, dmg, from a folder named "myFolder", something like this should work:

#!/bin/bash

hdiutil create -volname "myapp-1.0" -fs HFS+ -srcfolder "myFolder" -format UDRW myapp-temp.dmg

hdiutil convert myapp-temp.dmg -format UDZO -imagekey zlib-level=9 -o myapp.dmg

rm -fv myapp-temp.dmg
Customize the dmg

It's possible to customize our dmg when it is mounted, i.e. the displayed window, for example: icon size, background image, window size...

The idea is to create a read/write dmg on a mac, customize it manually, and then make a copy of the file .DS_STORE and all the data. Later we will use these data when we will create our dmg by script.
This part can only be done directly and manually on a Mac.

create a read/write disk image

On a Mac, open "Disk Utility" from the "Utilities" folder.
Click on "New Image".
Choose the same name as the future volume name of our dmg (MyApp).
Don't use a journaled format, to be able to select a "small" size for this dmg. The size doesn't matter, it have to be big enough to copy our app and other data.
As Image Format, choose read/write disk image. Then click on "create".

Add our data on the disk image

Now mount this dmg by clicking on it. And open it.
Copy our app in the open window. We can copy the symbolic link to /Applications too, if it exists. Otherwise we will create it in a terminal later.

Open a Terminal from the Utilities.
Go into the mounted disk image:
cd /Volumes/MyApp
then create a folder named .background:
mkdir .background
because the name begin by a dot (.), this folder is hidden, like on Linux.
copy in this folder our background image:
cp pathToOurImage/background.png /Volumes/MyApp/.background/
And, if needed, create the symbolic link to /Applications:
ln -s /Applications /Volumes/MyApp/Applications

Customize the disk image

Now give the focus to the window of the mounted disk, and open "Show View Options" from the View menu.
Select picture for background, then click on the button.
We have to select the background.png file from the mounted disk, but because this png is in a hidden folder we can't see it. Don't worry, type Apple+Shift+G:
then type /Volumes/MyApp/.background/, then enter.
now select our png.
Choose the icon size, for example 128, arrange them by none, then drag them where you want, and adapt the window size to our background.

Now close this window, and eject the mounted disk. Then mount it again, and open it.

Copy the resulting customization

In a terminal, copy the created file .DS_STORE at the root of the mounted disk:
cp /Volumes/MyApp/.DS_STORE pathToSave/myDS_STORE

Now we have finally all needed files. When we create our dmg from our script, we just have to copy the file .DS_STORE and the folder .background with the file background.png. The resulting dmg will have the same customization.
Copy these data into our sources:

    |- samples_chapter_4/
|- data/
|- bundle_skelet/
|- dmg_extra_data/
|- .background/
|- background.png
|- Applications
|- .DS_STORE

And we add the following lines in our build_mac.sh script, before creating the dmg:

# copy eventual extra data, by example a symbolic link to /Applications
if [ $(ls -A "data/dmg_extra_data" | wc -c) -ne 0 ]; then
cp -Rv data/dmg_extra_data/. "$TMP_DIR/myapp-1.0/"
fi

Note that this customization is dependent of the used volume name, especially the background image, because the .DS_STORE file uses absolute paths to the mounted disk, and the volume name is part of it.
So, if you change the volume name, you have to recreate manually the customization...

Launch our build script

To launch our script (on Linux), and create our dmg and tar.gz, in a terminal:

  • cd samples_chapter_4
  • sh ./build_mac.sh

And as result, we have finally the folder MyApp.app, and the files myapp-1.0.dmg and myapp-1.0.tar.gz in our main source folder.

Nicolas Martin

You can download all the samples of this chapter 4 (Mac OSX) in the samples_chapter_4.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 other added data, and sample files, of this chapter 4, are in the Public Domain too.

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.

- page 1 de 5