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.