Distribute your XULRunner app
4 - Mac OSX
- 4.1 Icons for Mac OSX
- 4.2 Create a launcher
- 4.3 Some Mac specificities
- 4.4 Create a Application Bundle
- 4.5 Package as tar.gz and dmg
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:
- Inside Application Bundles
- Runtime Configuration Guidelines: Introduction
- Custom app bundles for Mac OS X
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:
- download the myapp.tar.gz file.
- open it. In fact the default behavior is to uncompress the archive into the same folder.
- go into the uncompressed folder
- 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.
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.