joliclic code

[version française]

Distribute your XULRunner app

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:

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.

2011-06-15 - 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.